PluginProbe
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor / 2.0.7
Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor v2.0.7
2.0.13 2.0.12 2.0.11 2.0.10 2.0.9 trunk 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8
blockenberg / blocks / color-thief / index.js

index.js in Blockenberg — 600+ Advanced Gutenberg Blocks & AI Agent for WordPress Block Editor 2.0.7, at blocks/color-thief/index.js

275 lines 17.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 ( function () {
2 var el = React.createElement;
3 var __ = wp.i18n.__;
4 var registerBlockType = wp.blocks.registerBlockType;
5 var useBlockProps = wp.blockEditor.useBlockProps;
6 var InspectorControls = wp.blockEditor.InspectorControls;
7 var MediaUpload = wp.blockEditor.MediaUpload;
8 var MediaUploadCheck = wp.blockEditor.MediaUploadCheck;
9 var PanelBody = wp.components.PanelBody;
10 var PanelRow = wp.components.PanelRow;
11 var ToggleControl = wp.components.ToggleControl;
12 var RangeControl = wp.components.RangeControl;
13 var SelectControl = wp.components.SelectControl;
14 var Button = wp.components.Button;
15 var PanelColorSettings = wp.blockEditor.PanelColorSettings;
16
17 function getTypographyControl() {
18 return (window.bkbgTypographyControl || function () { return null; });
19 }
20
21 // ── Editor-side k-means palette extractor (simplified) ──────────
22 function rgbToHex(r, g, b) {
23 return '#' + [r, g, b].map(function (v) {
24 return ('0' + Math.round(v).toString(16)).slice(-2);
25 }).join('');
26 }
27
28 function hexToRgb(hex) {
29 var r = parseInt(hex.slice(1, 3), 16);
30 var g = parseInt(hex.slice(3, 5), 16);
31 var b = parseInt(hex.slice(5, 7), 16);
32 return [r, g, b];
33 }
34
35 function rgbToHsl(r, g, b) {
36 r /= 255; g /= 255; b /= 255;
37 var max = Math.max(r, g, b), min = Math.min(r, g, b);
38 var h, s, l = (max + min) / 2;
39 if (max === min) { h = s = 0; }
40 else {
41 var d = max - min;
42 s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
43 switch (max) {
44 case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
45 case g: h = ((b - r) / d + 2) / 6; break;
46 default: h = ((r - g) / d + 4) / 6; break;
47 }
48 }
49 return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
50 }
51
52 // Sample pixels and run k-means quantization on canvas
53 function extractPalette(imgEl, numColors, quality) {
54 var canvas = document.createElement('canvas');
55 var scale = Math.min(1, 100 / Math.max(imgEl.naturalWidth || imgEl.width, imgEl.naturalHeight || imgEl.height));
56 canvas.width = Math.max(1, Math.floor((imgEl.naturalWidth || imgEl.width) * scale));
57 canvas.height = Math.max(1, Math.floor((imgEl.naturalHeight || imgEl.height) * scale));
58 var ctx = canvas.getContext('2d');
59 ctx.drawImage(imgEl, 0, 0, canvas.width, canvas.height);
60 var data;
61 try { data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; }
62 catch (e) { return null; }
63
64 // Collect sample pixels
65 var pixels = [];
66 for (var i = 0; i < data.length; i += 4 * quality) {
67 var a = data[i + 3];
68 if (a < 128) continue;
69 pixels.push([data[i], data[i + 1], data[i + 2]]);
70 }
71 if (pixels.length === 0) return null;
72
73 // Simple k-means (5 iterations)
74 var k = Math.min(numColors, pixels.length);
75 var centroids = pixels.slice(0, k).map(function (p) { return p.slice(); });
76 for (var iter = 0; iter < 5; iter++) {
77 var clusters = centroids.map(function () { return []; });
78 pixels.forEach(function (p) {
79 var best = 0, bestD = Infinity;
80 centroids.forEach(function (c, ci) {
81 var d = Math.pow(p[0]-c[0],2)+Math.pow(p[1]-c[1],2)+Math.pow(p[2]-c[2],2);
82 if (d < bestD) { bestD = d; best = ci; }
83 });
84 clusters[best].push(p);
85 });
86 clusters.forEach(function (cl, ci) {
87 if (cl.length === 0) return;
88 centroids[ci] = [
89 cl.reduce(function (s, p) { return s + p[0]; }, 0) / cl.length,
90 cl.reduce(function (s, p) { return s + p[1]; }, 0) / cl.length,
91 cl.reduce(function (s, p) { return s + p[2]; }, 0) / cl.length
92 ];
93 });
94 }
95 return centroids.map(function (c) {
96 return { r: Math.round(c[0]), g: Math.round(c[1]), b: Math.round(c[2]) };
97 });
98 }
99
100 // ── Sample swatches for editor preview when no image ────────────
101 var SAMPLE_SWATCHES = ['#e74c3c','#3498db','#2ecc71','#f39c12','#9b59b6','#1abc9c'];
102
103 // ── Swatch grid component ────────────────────────────────────────
104 function SwatchGrid(props) {
105 var attr = props.attr;
106 var swatches = props.swatches || SAMPLE_SWATCHES;
107 var size = attr.swatchSize;
108 var radius = attr.swatchRadius;
109 var gap = attr.swatchGap;
110
111 var gridStyle = { display: 'flex', flexWrap: 'wrap', gap: gap + 'px', justifyContent: 'center', padding: '12px 0' };
112 if (attr.layout === 'grid') {
113 gridStyle = { display: 'grid', gridTemplateColumns: 'repeat(' + attr.swatchColumns + ', 1fr)', gap: gap + 'px', padding: '12px 0' };
114 } else if (attr.layout === 'list') {
115 gridStyle = { display: 'flex', flexDirection: 'column', gap: gap + 'px', padding: '12px 0' };
116 }
117
118 return el('div', { style: gridStyle },
119 swatches.map(function (hex, i) {
120 var rgb = hexToRgb(hex.length === 7 ? hex : rgbToHex(hex.r || 0, hex.g || 0, hex.b || 0));
121 var hsl = rgbToHsl(rgb[0], rgb[1], rgb[2]);
122 var hexVal = hex.length === 7 ? hex : rgbToHex(rgb[0], rgb[1], rgb[2]);
123 var label = attr.showHexValues ? hexVal :
124 attr.showRgbValues ? 'rgb(' + rgb.join(',') + ')' :
125 attr.showHslValues ? 'hsl(' + hsl[0] + ',' + hsl[1] + '%,' + hsl[2] + '%)' : '';
126
127 return el('div', { key: i, className: 'bkbg-ct-swatch', style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px' } },
128 el('div', { style: { width: size + 'px', height: size + 'px', background: hexVal, borderRadius: radius + 'px', border: '1px solid rgba(0,0,0,.1)', boxShadow: '0 2px 8px rgba(0,0,0,.15)', cursor: attr.showCopyButton ? 'pointer' : 'default' }, title: attr.showCopyButton ? 'Copy: ' + hexVal : hexVal }),
129 (attr.showHexValues || attr.showRgbValues || attr.showHslValues) &&
130 el('span', { className: 'bkbg-ct-swatch-label', style: { color: attr.labelColor, maxWidth: size + 'px' } }, label)
131 );
132 })
133 );
134 }
135
136 // ── Register block ───────────────────────────────────────────────
137 registerBlockType('blockenberg/color-thief', {
138 title: 'Color Thief',
139 icon: 'art',
140 category: 'bkbg-dev',
141
142 edit: function (props) {
143 var attr = props.attributes;
144 var setAttr = props.setAttributes;
145
146 var imgRef = React.useRef(null);
147 var _state = React.useState(null);
148 var swatches = _state[0]; var setSwatches = _state[1];
149
150 // Extract when imageUrl changes
151 React.useEffect(function () {
152 if (!attr.imageUrl) { setSwatches(null); return; }
153 var img = new window.Image();
154 img.crossOrigin = 'anonymous';
155 img.onload = function () {
156 var palette = extractPalette(img, attr.paletteSize, attr.quality);
157 if (palette) {
158 setSwatches(palette.map(function (c) { return rgbToHex(c.r, c.g, c.b); }));
159 }
160 };
161 img.onerror = function () { setSwatches(null); };
162 img.src = attr.imageUrl;
163 }, [attr.imageUrl, attr.paletteSize, attr.quality]);
164
165 var displaySwatches = swatches || SAMPLE_SWATCHES;
166
167 return el(React.Fragment, null,
168 el(InspectorControls, null,
169 // Image
170 el(PanelBody, { title: 'Image', initialOpen: true },
171 el(MediaUploadCheck, null,
172 el(MediaUpload, {
173 onSelect: function (media) {
174 setAttr({ imageUrl: media.url, imageId: media.id, imageAlt: media.alt || '' });
175 },
176 allowedTypes: ['image'],
177 value: attr.imageId,
178 render: function (r) {
179 return el('div', null,
180 attr.imageUrl
181 ? el(Button, { variant: 'secondary', isSmall: true, onClick: r.open }, 'Change Image')
182 : el(Button, { variant: 'primary', onClick: r.open }, 'Select Image'),
183 attr.imageUrl && el(Button, { variant: 'tertiary', isSmall: true, isDestructive: true, style: { marginLeft: 8 }, onClick: function () { setAttr({ imageUrl: '', imageId: 0, imageAlt: '' }); setSwatches(null); } }, 'Remove')
184 );
185 }
186 })
187 ),
188 el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Show Image Preview', checked: attr.showImagePreview, onChange: function (v) { setAttr({ showImagePreview: v }); } }),
189 attr.showImagePreview && el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Preview Height (px)', value: attr.imageHeight, min: 80, max: 600, onChange: function (v) { setAttr({ imageHeight: v }); } }),
190 attr.showImagePreview && el(SelectControl, { __nextHasNoMarginBottom: true, label: 'Object Fit', value: attr.imageObjectFit, options: [{value:'cover',label:'Cover'},{value:'contain',label:'Contain'},{value:'fill',label:'Fill'}], onChange: function (v) { setAttr({ imageObjectFit: v }); } })
191 ),
192 // Palette
193 el(PanelBody, { title: 'Palette Settings', initialOpen: true },
194 el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Number of Colors', value: attr.paletteSize, min: 2, max: 12, onChange: function (v) { setAttr({ paletteSize: v }); } }),
195 el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Sampling Quality', help: 'Lower = more accurate, slower', value: attr.quality, min: 1, max: 20, onChange: function (v) { setAttr({ quality: v }); } }),
196 el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Show Dominant Color Banner', checked: attr.showDominant, onChange: function (v) { setAttr({ showDominant: v }); } }),
197 el(SelectControl, { __nextHasNoMarginBottom: true, label: 'Layout', value: attr.layout, options: [{value:'row',label:'Row (flex)'},{value:'grid',label:'Grid'},{value:'list',label:'List'}], onChange: function (v) { setAttr({ layout: v }); } }),
198 attr.layout === 'grid' && el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Grid Columns', value: attr.swatchColumns, min: 2, max: 12, onChange: function (v) { setAttr({ swatchColumns: v }); } })
199 ),
200 // Swatches
201 el(PanelBody, { title: 'Swatch Appearance', initialOpen: false },
202 el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Swatch Size (px)', value: attr.swatchSize, min: 24, max: 150, onChange: function (v) { setAttr({ swatchSize: v }); } }),
203 el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Corner Radius (px)', value: attr.swatchRadius, min: 0, max: 60, onChange: function (v) { setAttr({ swatchRadius: v }); } }),
204 el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Gap (px)', value: attr.swatchGap, min: 2, max: 40, onChange: function (v) { setAttr({ swatchGap: v }); } }),
205 el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Show Copy Button', checked: attr.showCopyButton, onChange: function (v) { setAttr({ showCopyButton: v }); } }),
206 attr.showCopyButton && el(SelectControl, { __nextHasNoMarginBottom: true, label: 'Copy Format', value: attr.copyFormat, options: [{value:'hex',label:'HEX (#rrggbb)'},{value:'rgb',label:'RGB'},{value:'hsl',label:'HSL'}], onChange: function (v) { setAttr({ copyFormat: v }); } })
207 ),
208 // Labels
209 el(PanelBody, { title: 'Color Labels', initialOpen: false },
210 el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Show HEX Values', checked: attr.showHexValues, onChange: function (v) { setAttr({ showHexValues: v }); } }),
211 el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Show RGB Values', checked: attr.showRgbValues, onChange: function (v) { setAttr({ showRgbValues: v }); } }),
212 el(ToggleControl, { __nextHasNoMarginBottom: true, label: 'Show HSL Values', checked: attr.showHslValues, onChange: function (v) { setAttr({ showHslValues: v }); } })
213 ),
214 // Typography
215 el(PanelBody, { title: 'Typography', initialOpen: false },
216 (function () {
217 var TC = getTypographyControl();
218 return el(TC, { label: 'Label', value: attr.typoLabel || {}, onChange: function (v) { setAttr({ typoLabel: v }); } });
219 })()
220 ),
221 // Style
222 el(PanelBody, { title: 'Style', initialOpen: false },
223 el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Block Border Radius (px)', value: attr.borderRadius, min: 0, max: 40, onChange: function (v) { setAttr({ borderRadius: v }); } }),
224 el(RangeControl, { __nextHasNoMarginBottom: true, label: 'Padding (px)', value: attr.padding, min: 0, max: 60, onChange: function (v) { setAttr({ padding: v }); } })
225 ),
226 el(PanelColorSettings, {
227 title: 'Colors',
228 initialOpen: false,
229 colorSettings: [
230 { value: attr.bgColor, onChange: function (v) { setAttr({ bgColor: v || '#ffffff' }); }, label: 'Background' },
231 { value: attr.labelColor, onChange: function (v) { setAttr({ labelColor: v || '#374151' }); }, label: 'Label Text' },
232 { value: attr.borderColor, onChange: function (v) { setAttr({ borderColor: v || '#e5e7eb' }); }, label: 'Border' }
233 ]
234 })
235 ),
236
237 // ── Editor preview ───────────────────────────────────
238 el('div', useBlockProps({ className: 'bkbg-ct-editor' }),
239 el('div', {
240 style: {
241 backgroundColor: attr.bgColor,
242 borderRadius: attr.borderRadius + 'px',
243 border: '1px solid ' + attr.borderColor,
244 padding: attr.padding + 'px',
245 overflow: 'hidden'
246 }
247 },
248 attr.showImagePreview && el('div', { style: { marginBottom: '12px', borderRadius: (attr.borderRadius > 4 ? attr.borderRadius - 4 : 0) + 'px', overflow: 'hidden' } },
249 attr.imageUrl
250 ? el('img', { src: attr.imageUrl, alt: attr.imageAlt, style: { width: '100%', height: attr.imageHeight + 'px', objectFit: attr.imageObjectFit, display: 'block' } })
251 : el('div', { style: { width: '100%', height: attr.imageHeight + 'px', background: '#f3f4f6', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#9ca3af' } }, '📷 Select an image to extract its palette')
252 ),
253 attr.showDominant && displaySwatches.length > 0 &&
254 el('div', { style: { height: '32px', background: displaySwatches[0], borderRadius: '6px', marginBottom: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center' } },
255 el('span', { style: { color: '#fff', fontSize: '11px', fontWeight: 700, textShadow: '0 1px 2px rgba(0,0,0,.5)' } }, 'DOMINANT')
256 ),
257 !attr.imageUrl && el('div', { style: { textAlign: 'center', fontSize: '12px', color: '#9ca3af', marginBottom: '8px' } }, 'Sample palette — upload an image to see real colors'),
258 el(SwatchGrid, { attr: attr, swatches: displaySwatches })
259 )
260 )
261 );
262 },
263
264 save: function (props) {
265 var attr = props.attributes;
266 return el('div', useBlockProps.save(),
267 el('div', {
268 className: 'bkbg-ct-app',
269 'data-opts': JSON.stringify(attr)
270 })
271 );
272 }
273 });
274 }() );
275