Update index.html
Browse files- index.html +434 -19
index.html
CHANGED
@@ -1,19 +1,434 @@
|
|
1 |
-
<!
|
2 |
-
<html>
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>
|