DotSlashGabut commited on
Commit
7e19f0d
Β·
verified Β·
1 Parent(s): f81a2b3

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +434 -19
index.html CHANGED
@@ -1,19 +1,434 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Color Palette Generator Plus</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script>
9
+ tailwind.config = { darkMode: 'class' };
10
+ </script>
11
+ <script>
12
+ (function() {
13
+ const saved = localStorage.getItem('theme');
14
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
15
+ if (saved ? saved === 'dark' : prefersDark) {
16
+ document.documentElement.classList.add('dark');
17
+ }
18
+ })();
19
+ </script>
20
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/color-thief/2.3.0/color-thief.umd.js"></script>
21
+ <style>
22
+ .color-box {
23
+ width: calc(20% - 0.5rem);
24
+ aspect-ratio: 1 / 1;
25
+ margin: 0.25rem;
26
+ position: relative;
27
+ flex-grow: 0;
28
+ flex-shrink: 0;
29
+ cursor: pointer;
30
+ }
31
+ @media (max-width: 640px) {
32
+ .color-box { width: calc(50% - 0.5rem); }
33
+ }
34
+ .color-box-content {
35
+ position: absolute;
36
+ inset: 0;
37
+ display: flex;
38
+ justify-content: center;
39
+ align-items: flex-end;
40
+ }
41
+ .hsl-sliders { display: none; margin-top: 1rem; }
42
+ .hsl-sliders.active { display: block; }
43
+ .slider-container {
44
+ display: flex;
45
+ align-items: center;
46
+ margin-bottom: 0.5rem;
47
+ }
48
+ .slider-container input[type="range"] { flex-grow: 1; margin-right: 0.5rem; }
49
+ .hsl-value { width: 28px; text-align: right; font-family: monospace; }
50
+ .color-box.selected {
51
+ outline: 2.5px solid #0f75fd;
52
+ outline-offset: 3px;
53
+ z-index: 10;
54
+ }
55
+ </style>
56
+ </head>
57
+
58
+ <body class="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen flex flex-col items-center py-4 px-4 transition-colors">
59
+ <!-- Theme toggle -->
60
+ <button id="themeToggle"
61
+ class="fixed top-4 right-4 z-50 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-100 rounded-full w-10 h-10 flex items-center justify-center shadow-md focus:outline-none focus:ring-2 focus:ring-blue-500"
62
+ aria-label="Toggle dark mode">
63
+ <span id="themeIcon">πŸŒ™</span>
64
+ </button>
65
+
66
+ <div class="w-full max-w-2xl mx-auto mt-12">
67
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-md overflow-hidden">
68
+ <div class="p-4 sm:p-8">
69
+ <h1 class="text-2xl sm:text-3xl font-bold mb-6 text-center">Color Palette Generator +</h1>
70
+
71
+ <!-- Source radios -->
72
+ <div class="flex flex-wrap justify-center gap-4 mb-4">
73
+ <label><input type="radio" name="colorSource" value="random" checked>
74
+ <span class="ml-1">Random Palette</span></label>
75
+ <label><input type="radio" name="colorSource" value="image">
76
+ <span class="ml-1">From File or URL</span></label>
77
+ </div>
78
+
79
+ <!-- Image inputs + preview -->
80
+ <div id="imageInputs" class="flex-col items-center space-y-4 hidden mb-4 ">
81
+ <input type="file" id="imageUpload" accept="image/*"
82
+ class="border rounded px-2 py-1 w-full dark:bg-gray-700 dark:border-gray-600">
83
+ <input type="text" id="imageUrl" placeholder="Enter image URL"
84
+ class="border rounded px-2 py-1 w-full dark:bg-gray-700 dark:border-gray-600">
85
+ <div id="imagePreviewWrapper"
86
+ class="relative w-full max-w-xs border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden">
87
+ <img id="imagePreview" class="w-full h-auto" alt="Preview" style="display:none;">
88
+ <div id="previewError" class="text-red-500 text-sm p-2 hidden">❌ Could not load image</div>
89
+ </div>
90
+ </div>
91
+
92
+ <!-- Generate / Download -->
93
+ <div class="text-center flex flex-wrap justify-center gap-4 items-center mb-4">
94
+ <button id="generateBtn" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded">Generate</button>
95
+ <div class="flex items-center">
96
+ <button id="downloadBtn" class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded">Download</button>
97
+ <label class="ml-2 flex items-center">
98
+ <input type="checkbox" id="includeHexCodes" class="form-checkbox h-5 w-5 text-blue-600" checked>
99
+ <span class="ml-2">Include hex codes</span>
100
+ </label>
101
+ </div>
102
+ </div>
103
+
104
+ <!-- Palette & codes -->
105
+ <div class="flex flex-wrap justify-center bg-gray-200 dark:bg-gray-700 p-1 rounded mb-4 text-sm">
106
+ Click any color box to adjust HSL.
107
+ </div>
108
+ <div id="palette" class="flex flex-wrap justify-center mb-6"></div>
109
+ <div id="colorCodes" class="text-sm font-mono text-center break-words mb-6"></div>
110
+
111
+ <!-- HSL sliders -->
112
+ <div id="hslSliders" class="hsl-sliders">
113
+ <div class="slider-container">
114
+ <label class="block text-sm font-medium w-6">H</label>
115
+ <input type="range" id="hueSlider" min="0" max="360" value="0">
116
+ <span id="hueValue" class="hsl-value">000</span>
117
+ </div>
118
+ <div class="slider-container">
119
+ <label class="block text-sm font-medium w-6">S</label>
120
+ <input type="range" id="saturationSlider" min="0" max="100" value="100">
121
+ <span id="saturationValue" class="hsl-value">100</span>
122
+ </div>
123
+ <div class="slider-container mb-6">
124
+ <label class="block text-sm font-medium w-6">L</label>
125
+ <input type="range" id="luminanceSlider" min="0" max="100" value="50">
126
+ <span id="luminanceValue" class="hsl-value">050</span>
127
+ </div>
128
+ </div>
129
+
130
+ <div class="space-y-6">
131
+ <!-- Color count -->
132
+ <div class="flex items-center justify-center space-x-4">
133
+ <label for="colorCount">Colors:</label>
134
+ <input type="number" id="colorCount" min="1" max="100" value="5"
135
+ class="border rounded px-2 py-1 w-16 dark:bg-gray-700 dark:border-gray-600">
136
+ </div>
137
+
138
+ <!-- Color categories -->
139
+ <div class="flex flex-wrap justify-center gap-x-3 gap-y-2">
140
+ <label><input type="radio" name="colorOption" value="random" checked> <span class="ml-1">Default</span></label>
141
+ <label><input type="radio" name="colorOption" value="vivid"> <span class="ml-1">Vivid</span></label>
142
+ <label><input type="radio" name="colorOption" value="light"> <span class="ml-1">Light</span></label>
143
+ <label><input type="radio" name="colorOption" value="dark"> <span class="ml-1">Dark</span></label>
144
+ <label><input type="radio" name="colorOption" value="earth"> <span class="ml-1">Earth</span></label>
145
+ <label><input type="radio" name="colorOption" value="ocean"> <span class="ml-1">Ocean</span></label>
146
+ <label><input type="radio" name="colorOption" value="sunset"> <span class="ml-1">Sunset</span></label>
147
+ <label><input type="radio" name="colorOption" value="forest"> <span class="ml-1">Forest</span></label>
148
+ </div>
149
+
150
+ <!-- Scheme radios -->
151
+ <div class="flex flex-wrap justify-center gap-4 bg-gray-200 dark:bg-gray-700 p-4 rounded">
152
+ <label><input type="radio" name="colorScheme" value="random" checked> <span class="ml-1">Random</span></label>
153
+ <label><input type="radio" name="colorScheme" value="harmonize"> <span class="ml-1">Harmonize</span></label>
154
+ <label><input type="radio" name="colorScheme" value="monotone"> <span class="ml-1">Monotone</span></label>
155
+ <label><input type="radio" name="colorScheme" value="gradient"> <span class="ml-1">Gradient</span></label>
156
+ </div>
157
+
158
+ </div>
159
+ </div>
160
+ </div>
161
+ </div>
162
+
163
+ <script>
164
+ /* ---------- THEME SWITCHER ---------- */
165
+ const html = document.documentElement;
166
+ const toggleBtn = document.getElementById('themeToggle');
167
+ const icon = document.getElementById('themeIcon');
168
+ function setTheme(dark) {
169
+ html.classList.toggle('dark', dark);
170
+ icon.textContent = dark ? 'β˜€οΈ' : 'πŸŒ™';
171
+ localStorage.setItem('theme', dark ? 'dark' : 'light');
172
+ }
173
+ const saved = localStorage.getItem('theme');
174
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
175
+ setTheme(saved ? saved === 'dark' : prefersDark);
176
+ toggleBtn.addEventListener('click', () => setTheme(!html.classList.contains('dark')));
177
+
178
+ /* ---------- UTILITIES ---------- */
179
+ function hslToRgb(h, s, l) {
180
+ h /= 360; s /= 100; l /= 100;
181
+ let r, g, b;
182
+ if (s === 0) { r = g = b = l; } else {
183
+ const hue2rgb = (p, q, t) => {
184
+ if (t < 0) t += 1; if (t > 1) t -= 1;
185
+ if (t < 1/6) return p + (q - p) * 6 * t;
186
+ if (t < 1/2) return q;
187
+ if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
188
+ return p;
189
+ };
190
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
191
+ const p = 2 * l - q;
192
+ r = hue2rgb(p, q, h + 1/3);
193
+ g = hue2rgb(p, q, h);
194
+ b = hue2rgb(p, q, h - 1/3);
195
+ }
196
+ return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
197
+ }
198
+ function rgbToHsl(r, g, b) {
199
+ r /= 255; g /= 255; b /= 255;
200
+ const max = Math.max(r, g, b), min = Math.min(r, g, b);
201
+ let h, s, l = (max + min) / 2;
202
+ if (max === min) { h = s = 0; } else {
203
+ const d = max - min;
204
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
205
+ switch (max) {
206
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
207
+ case g: h = (b - r) / d + 2; break;
208
+ case b: h = (r - g) / d + 4; break;
209
+ }
210
+ h /= 6;
211
+ }
212
+ return [h * 360, s * 100, l * 100];
213
+ }
214
+ function rgbToHex(r, g, b) {
215
+ return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
216
+ }
217
+
218
+ /* ---------- COLOR GENERATORS ---------- */
219
+ const generators = {
220
+ random: () => [Math.floor(Math.random()*256), Math.floor(Math.random()*256), Math.floor(Math.random()*256)],
221
+ vivid: () => hslToRgb(Math.random()*360, 80+Math.random()*20, 50+Math.random()*10),
222
+ light: () => hslToRgb(Math.random()*360, 60+Math.random()*20, 80+Math.random()*15),
223
+ dark: () => hslToRgb(Math.random()*360, 60+Math.random()*20, 10+Math.random()*20),
224
+ earth: () => hslToRgb(20+Math.random()*25, 26+Math.random()*15, 36+Math.random()*41),
225
+ ocean: () => hslToRgb(180+Math.random()*40, 40+Math.random()*30, 30+Math.random()*50),
226
+ sunset: () => hslToRgb(Math.random()*40, 60+Math.random()*30, 40+Math.random()*50),
227
+ forest: () => hslToRgb(80+Math.random()*70, 30+Math.random()*40, 20+Math.random()*50)
228
+ };
229
+
230
+ function harmonize(baseColor, count) {
231
+ const [h, s, l] = rgbToHsl(...baseColor);
232
+ const colors = [baseColor];
233
+ for (let i = 1; i < count; i++) colors.push(hslToRgb((h + i * (360 / count)) % 360, s, l));
234
+ return colors;
235
+ }
236
+ function monotone(baseColor, count) {
237
+ const [h, s, l] = rgbToHsl(...baseColor);
238
+ const colors = [];
239
+ for (let i = 0; i < count; i++) {
240
+ const newL = l + (i - Math.floor(count / 2)) * (30 / count);
241
+ colors.push(hslToRgb(h, s, Math.max(0, Math.min(100, newL))));
242
+ }
243
+ return colors;
244
+ }
245
+ function gradient(startColor, endColor, count) {
246
+ const colors = [];
247
+ for (let i = 0; i < count; i++) {
248
+ const t = i / (count - 1);
249
+ colors.push([
250
+ Math.round(startColor[0] + (endColor[0] - startColor[0]) * t),
251
+ Math.round(startColor[1] + (endColor[1] - startColor[1]) * t),
252
+ Math.round(startColor[2] + (endColor[2] - startColor[2]) * t)
253
+ ]);
254
+ }
255
+ return colors;
256
+ }
257
+
258
+ /* ---------- DOM ---------- */
259
+ const palette = document.getElementById('palette');
260
+ const colorCodes = document.getElementById('colorCodes');
261
+ const colorCount = document.getElementById('colorCount');
262
+ const generateBtn = document.getElementById('generateBtn');
263
+ const downloadBtn = document.getElementById('downloadBtn');
264
+ const imageInputs = document.getElementById('imageInputs');
265
+ const colorThief = new ColorThief();
266
+
267
+ /* ---------- IMAGE PREVIEW ---------- */
268
+ const imageUpload = document.getElementById('imageUpload');
269
+ const imageUrl = document.getElementById('imageUrl');
270
+ const imagePreview = document.getElementById('imagePreview');
271
+ const previewError = document.getElementById('previewError');
272
+
273
+ function showPreview(src) {
274
+ imagePreview.style.display = 'block';
275
+ previewError.classList.add('hidden');
276
+ imagePreview.src = src;
277
+ }
278
+ function clearPreview() {
279
+ imagePreview.style.display = 'none';
280
+ previewError.classList.add('hidden');
281
+ imagePreview.src = '';
282
+ }
283
+ imageUpload.addEventListener('change', e => {
284
+ if (e.target.files && e.target.files[0]) showPreview(URL.createObjectURL(e.target.files[0]));
285
+ });
286
+ imageUrl.addEventListener('change', e => {
287
+ if (e.target.value.trim()) showPreview(e.target.value.trim());
288
+ else clearPreview();
289
+ });
290
+ imagePreview.addEventListener('error', () => {
291
+ imagePreview.style.display = 'none';
292
+ previewError.classList.remove('hidden');
293
+ });
294
+
295
+ /* ---------- DISPLAY ---------- */
296
+ function displayColors(colors) {
297
+ palette.innerHTML = '';
298
+ colorCodes.innerHTML = '';
299
+ colors.forEach((color, idx) => {
300
+ const box = document.createElement('div');
301
+ box.className = 'color-box rounded';
302
+ box.style.backgroundColor = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
303
+ box.dataset.rgb = color.join(','); // πŸ‘ˆ store exact RGB
304
+ const content = document.createElement('div');
305
+ content.className = 'color-box-content';
306
+ const hex = rgbToHex(...color);
307
+ const txt = document.createElement('div');
308
+ txt.className = 'text-xs sm:text-sm text-center bg-black/50 text-white py-1 w-full';
309
+ txt.textContent = hex;
310
+ content.appendChild(txt); box.appendChild(content); palette.appendChild(box);
311
+ colorCodes.innerHTML += `${hex} `;
312
+ box.addEventListener('click', () => {
313
+ activateHSLSliders(color, idx);
314
+ highlightSelectedColor(idx);
315
+ });
316
+ });
317
+ const boxes = palette.querySelectorAll('.color-box');
318
+ const isMobile = window.innerWidth <= 640;
319
+ const boxWidth = isMobile ? 32 : (100 / Math.min(5, colors.length));
320
+ boxes.forEach(b => b.style.width = `calc(${boxWidth}% - 0.5rem)`);
321
+ }
322
+ function highlightSelectedColor(idx) {
323
+ document.querySelectorAll('.color-box').forEach((b, i) => b.classList.toggle('selected', i === idx));
324
+ }
325
+ function activateHSLSliders(color, idx) {
326
+ const sliders = document.getElementById('hslSliders');
327
+ sliders.classList.add('active');
328
+ const hueS = document.getElementById('hueSlider');
329
+ const satS = document.getElementById('saturationSlider');
330
+ const lumS = document.getElementById('luminanceSlider');
331
+ const hueV = document.getElementById('hueValue');
332
+ const satV = document.getElementById('saturationValue');
333
+ const lumV = document.getElementById('luminanceValue');
334
+
335
+ // πŸ‘‡ use exact stored RGB instead of computed CSS
336
+ const [r, g, b] = palette.children[idx].dataset.rgb.split(',').map(Number);
337
+ const [h, s, l] = rgbToHsl(r, g, b);
338
+ hueS.value = Math.round(h);
339
+ satS.value = Math.round(s);
340
+ lumS.value = Math.round(l);
341
+
342
+ function update() {
343
+ const newColor = hslToRgb(+hueS.value, +satS.value, +lumS.value);
344
+ const box = palette.children[idx];
345
+ box.style.backgroundColor = `rgb(${newColor[0]}, ${newColor[1]}, ${newColor[2]})`;
346
+ box.dataset.rgb = newColor.join(','); // update stored RGB
347
+ const hex = rgbToHex(...newColor);
348
+ box.querySelector('.color-box-content div').textContent = hex;
349
+ const codes = colorCodes.textContent.split(' ');
350
+ codes[idx] = hex;
351
+ colorCodes.textContent = codes.join(' ');
352
+ hueV.textContent = hueS.value.padStart(3, '0');
353
+ satV.textContent = satS.value.padStart(3, '0');
354
+ lumV.textContent = lumS.value.padStart(3, '0');
355
+ }
356
+ hueS.oninput = satS.oninput = lumS.oninput = update;
357
+ update();
358
+ }
359
+
360
+ /* ---------- GENERATE ---------- */
361
+ async function generateColors() {
362
+ const count = +colorCount.value;
363
+ const option = document.querySelector('input[name="colorOption"]:checked').value;
364
+ const scheme = document.querySelector('input[name="colorScheme"]:checked').value;
365
+ const source = document.querySelector('input[name="colorSource"]:checked').value;
366
+ let colors = [];
367
+ if (source === 'image') {
368
+ let img;
369
+ if (imageUpload.files.length) {
370
+ img = new Image();
371
+ img.src = URL.createObjectURL(imageUpload.files[0]);
372
+ await new Promise(r => img.onload = r);
373
+ } else if (imageUrl.value.trim()) {
374
+ img = new Image(); img.crossOrigin = 'Anonymous'; img.src = imageUrl.value.trim();
375
+ await new Promise(r => img.onload = r);
376
+ } else { alert('Please upload an image or enter a URL.'); return; }
377
+ colors = colorThief.getPalette(img, count);
378
+ } else {
379
+ const base = generators[option]();
380
+ switch (scheme) {
381
+ case 'harmonize': colors = harmonize(base, count); break;
382
+ case 'monotone': colors = monotone(base, count); break;
383
+ case 'gradient': colors = gradient(base, generators[option](), count); break;
384
+ default: colors = Array.from({ length: count }, generators[option]);
385
+ }
386
+ }
387
+ displayColors(colors);
388
+ }
389
+
390
+ /* ---------- DOWNLOAD ---------- */
391
+ function downloadPalette() {
392
+ const colors = Array.from(palette.children).map(c => getComputedStyle(c).backgroundColor);
393
+ const count = colors.length;
394
+ const includeHex = document.getElementById('includeHexCodes').checked;
395
+ const gridSize = Math.ceil(Math.sqrt(count));
396
+ const canvasSize = 1000;
397
+ const cell = canvasSize / gridSize;
398
+ const canvas = document.createElement('canvas');
399
+ canvas.width = canvas.height = canvasSize;
400
+ const ctx = canvas.getContext('2d');
401
+ ctx.fillStyle = 'white'; ctx.fillRect(0, 0, canvasSize, canvasSize);
402
+ colors.forEach((color, idx) => {
403
+ const row = Math.floor(idx / gridSize);
404
+ const col = idx % gridSize;
405
+ ctx.fillStyle = color;
406
+ ctx.fillRect(col * cell, row * cell, cell, cell);
407
+ if (includeHex) {
408
+ const rgb = color.match(/\d+/g).map(Number);
409
+ const hex = rgbToHex(...rgb);
410
+ ctx.fillStyle = (rgb[0]*299 + rgb[1]*587 + rgb[2]*114) > 128000 ? 'black' : 'white';
411
+ ctx.font = `${cell/10}px Arial`;
412
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
413
+ ctx.fillText(hex, (col + 0.5) * cell, (row + 0.5) * cell);
414
+ }
415
+ });
416
+ const link = document.createElement('a');
417
+ link.download = 'color_palette_grid.png';
418
+ link.href = canvas.toDataURL('image/png');
419
+ link.click();
420
+ }
421
+
422
+ /* ---------- EVENT LISTENERS ---------- */
423
+ generateBtn.addEventListener('click', generateColors);
424
+ downloadBtn.addEventListener('click', downloadPalette);
425
+ document.querySelectorAll('input[name="colorSource"]').forEach(r =>
426
+ r.addEventListener('change', e => {
427
+ imageInputs.style.display = e.target.value === 'image' ? 'flex' : 'none';
428
+ if (e.target.value !== 'image') clearPreview();
429
+ })
430
+ );
431
+ generateColors();
432
+ </script>
433
+ </body>
434
+ </html>