import React, { useState, useEffect, useRef } from 'react';
// Utility functions for color conversion and manipulation
// HSL to Hex conversion (simplified for demonstration)
const hslToHex = (h, s, l) => {
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = n => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0');
};
return `#${f(0)}${f(8)}${f(4)}`;
};
// Hex to HSL conversion (simplified for demonstration)
const hexToHsl = (hex) => {
let r = 0, g = 0, b = 0;
// Handle short hex codes (e.g., #abc)
if (hex.length === 4) {
r = parseInt(hex[1] + hex[1], 16);
g = parseInt(hex[2] + hex[2], 16);
b = parseInt(hex[3] + hex[3], 16);
} else if (hex.length === 7) {
r = parseInt(hex.substring(1, 3), 16);
g = parseInt(hex.substring(3, 5), 16);
b = parseInt(hex.substring(5, 7), 16);
}
r /= 255;
g /= 255;
b /= 255;
let cmin = Math.min(r, g, b),
cmax = Math.max(r, g, b),
delta = cmax - cmin,
h = 0,
s = 0,
l = 0;
if (delta === 0) h = 0;
else if (cmax === r) h = ((g - b) / delta) % 6;
else if (cmax === g) h = (b - r) / delta + 2;
else h = (r - g) / delta + 4;
h = Math.round(h * 60);
if (h < 0) h += 360;
l = (cmax + cmin) / 2;
s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
s = +(s * 100).toFixed(1);
l = +(l * 100).toFixed(1);
return { h: h, s: s, l: l };
};
// Generates a random HSL color
const getRandomHsl = () => ({
h: Math.floor(Math.random() * 360),
s: Math.floor(Math.random() * 70) + 30, // 30-100% saturation
l: Math.floor(Math.random() * 50) + 20, // 20-70% lightness
});
// Applies a color harmony rule to a base color (simplified)
const applyHarmonyRule = (baseHsl, rule) => {
const palette = [];
const { h, s, l } = baseHsl;
switch (rule) {
case 'monochromatic':
// Generate variations of lightness and saturation
palette.push(baseHsl);
palette.push({ h, s: Math.max(0, s - 20), l: Math.min(100, l + 15) });
palette.push({ h, s: Math.min(100, s + 15), l: Math.max(0, l - 10) });
palette.push({ h, s: Math.max(0, s - 10), l: Math.min(100, l + 30) });
palette.push({ h, s: Math.min(100, s + 20), l: Math.max(0, l - 20) });
break;
case 'complementary':
palette.push(baseHsl);
palette.push({ h: (h + 180) % 360, s, l }); // Complementary
palette.push({ h: (h + 180) % 360, s: Math.max(0, s - 20), l: Math.min(100, l + 15) }); // Tint of complementary
palette.push({ h, s: Math.max(0, s - 20), l: Math.min(100, l + 15) }); // Tint of base
palette.push({ h: (h + 180) % 360, s: Math.min(100, s + 15), l: Math.max(0, l - 10) }); // Shade of complementary
break;
case 'analogous':
palette.push(baseHsl);
palette.push({ h: (h + 30) % 360, s, l }); // +30 degrees
palette.push({ h: (h - 30 + 360) % 360, s, l }); // -30 degrees
palette.push({ h: (h + 60) % 360, s: Math.min(100, s + 10), l: l });
palette.push({ h: (h - 60 + 360) % 360, s: Math.min(100, s + 10), l: l });
break;
case 'triadic':
palette.push(baseHsl);
palette.push({ h: (h + 120) % 360, s, l });
palette.push({ h: (h + 240) % 360, s, l });
palette.push({ h: (h + 120) % 360, s: Math.max(0, s - 20), l: Math.min(100, l + 15) });
palette.push({ h: (h + 240) % 360, s: Math.min(100, s + 15), l: Math.max(0, l - 10) });
break;
default: // Random
return Array.from({ length: 5 }, getRandomHsl);
}
return palette.map(color => ({ ...color, hex: hslToHex(color.h, color.s, color.l) }));
};
// Main App Component
const App = () => {
// State variables
const [genreInput, setGenreInput] = useState('');
const [uploadedImage, setUploadedImage] = useState(null);
const [colorPalette, setColorPalette] = useState([]);
const [lockedColors, setLockedColors] = useState({}); // { index: hexColor, ... }
const [harmonyRule, setHarmonyRule] = useState('random'); // monochromatic, complementary, analogous, triadic, random
const [hslAdjustments, setHslAdjustments] = useState({ hue: 0, saturation: 0, lightness: 0 });
const [loading, setLoading] = useState(false);
const fileInputRef = useRef(null);
// Simulated AI for Text-to-Palette
const genreToPaletteMap = {
mystery: [
'#2c3e50', '#34495e', '#617d8a', '#95a5a6', '#c0392b'
], // Dark blues, grays, a hint of deep red
fantasy: [
'#8e44ad', '#9b59b6', '#2980b9', '#3498db', '#f1c40f'
], // Purples, blues, gold
sci_fi: [
'#1abc9c', '#16a085', '#2ecc71', '#27ae60', '#ecf0f1'
], // Teal, greens, light gray (futuristic)
romance: [
'#e74c3c', '#c0392b', '#e67e22', '#f39c12', '#fce4ec'
], // Reds, oranges, light pink (warm)
thriller: [
'#000000', '#333333', '#666666', '#bdbdbd', '#e74c3c'
], // Black, dark grays, stark red
horror: [
'#0a0a0a', '#1a1a1a', '#4a0000', '#8a0000', '#bb0000'
], // Deep blacks, dark reds
};
// Initial palette generation on component mount
useEffect(() => {
generatePalette('initial');
}, []);
// Helper to generate a palette based on input
const generatePalette = async (source = 'random', input = '') => {
setLoading(true);
let newPalette = [];
let baseColorHsl = getRandomHsl();
// Incorporate locked colors first
let currentLockedColors = Object.values(lockedColors).filter(Boolean).map(hexToHsl);
if (currentLockedColors.length > 0) {
baseColorHsl = currentLockedColors[0]; // Use first locked color as base
}
// --- Simulate AI Logic ---
if (source === 'genre' && input) {
const lowerInput = input.toLowerCase().replace(/\s/g, '_');
if (genreToPaletteMap[lowerInput]) {
newPalette = genreToPaletteMap[lowerInput].map(hex => ({ ...hexToHsl(hex), hex }));
} else {
// If genre not found, generate a random one and show a message
console.log(`Genre "${input}" not found in AI knowledge base. Generating random palette.`);
newPalette = applyHarmonyRule(baseColorHsl, 'random');
}
} else if (source === 'image' && uploadedImage) {
newPalette = await extractColorsFromImage(uploadedImage);
} else if (source === 'initial') {
newPalette = applyHarmonyRule(baseColorHsl, 'random');
} else {
// Default or random generation
newPalette = applyHarmonyRule(baseColorHsl, harmonyRule);
}
// Apply locked colors to the new palette (replace slots or add if not enough)
const finalPalette = [];
for (let i = 0; i < 5; i++) {
if (lockedColors[i]) {
finalPalette.push({ ...hexToHsl(lockedColors[i]), hex: lockedColors[i] });
} else if (newPalette[i]) {
finalPalette.push(newPalette[i]);
} else {
// If fewer colors from harmony rule, fill with random
finalPalette.push(applyHarmonyRule(getRandomHsl(), 'random')[0]);
}
}
setColorPalette(finalPalette);
setLoading(false);
};
// Client-side image color extraction (simplified)
const extractColorsFromImage = (imageFile) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, img.width, img.height);
const pixels = ctx.getImageData(0, 0, img.width, img.height).data;
const colorCounts = {};
// Sample every 100th pixel for performance
for (let i = 0; i < pixels.length; i += 400) {
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
colorCounts[hex] = (colorCounts[hex] || 0) + 1;
}
// Sort by count to find dominant colors
const sortedColors = Object.entries(colorCounts).sort(([, a], [, b]) => b - a);
// Take top 5 dominant colors
const dominantHexColors = sortedColors.slice(0, 5).map(([hex]) => hex);
// Convert to HSL and resolve
const palette = dominantHexColors.map(hex => ({ ...hexToHsl(hex), hex }));
resolve(palette.length > 0 ? palette : Array.from({ length: 5 }, getRandomHsl)); // Fallback if no colors found
};
img.src = e.target.result;
};
reader.readAsDataURL(imageFile);
});
};
// Handlers
const handleGenreChange = (e) => setGenreInput(e.target.value);
const handleImageUpload = (e) => {
const file = e.target.files[0];
if (file) {
setUploadedImage(file);
generatePalette('image', file); // Generate palette from image
}
};
const handleGeneratePalette = () => {
if (genreInput) {
generatePalette('genre', genreInput);
} else if (uploadedImage) {
generatePalette('image', uploadedImage);
} else {
generatePalette('random');
}
};
const toggleLockColor = (index) => {
setLockedColors(prev => {
const newLockedColors = { ...prev };
if (newLockedColors[index]) {
delete newLockedColors[index]; // Unlock
} else {
newLockedColors[index] = colorPalette[index].hex; // Lock current color
}
return newLockedColors;
});
};
const handleHslAdjust = (e, type, index) => {
const value = parseInt(e.target.value, 10);
const updatedPalette = [...colorPalette];
const targetColor = updatedPalette[index];
if (targetColor) {
let { h, s, l } = targetColor;
if (type === 'hue') h = (h + value) % 360;
if (type === 'saturation') s = Math.max(0, Math.min(100, s + value));
if (type === 'lightness') l = Math.max(0, Math.min(100, l + value));
updatedPalette[index] = { h, s, l, hex: hslToHex(h, s, l) };
setColorPalette(updatedPalette);
}
};
// Function to apply HSL adjustments to a single color
const applySingleHslAdjustment = (colorHsl, adjust) => {
let { h, s, l } = colorHsl;
h = (h + adjust.hue + 360) % 360; // Ensure hue stays within 0-359
s = Math.max(0, Math.min(100, s + adjust.saturation));
l = Math.max(0, Math.min(100, l + adjust.lightness));
return { h, s, l, hex: hslToHex(h, s, l) };
};
// Applies HSL adjustments to unlocked colors
useEffect(() => {
if (colorPalette.length > 0 && (hslAdjustments.hue !== 0 || hslAdjustments.saturation !== 0 || hslAdjustments.lightness !== 0)) {
const adjustedPalette = colorPalette.map((color, index) => {
if (lockedColors[index]) {
return color; // Keep locked colors as is
}
return applySingleHslAdjustment(color, hslAdjustments);
});
setColorPalette(adjustedPalette);
setHslAdjustments({ hue: 0, saturation: 0, lightness: 0 }); // Reset adjustments after applying
}
}, [hslAdjustments.hue, hslAdjustments.saturation, hslAdjustments.lightness]); // Only run when manual HSL changes
const handleHarmonyRuleChange = (rule) => {
setHarmonyRule(rule);
// Regenerate palette based on new rule. If there are locked colors, use one as base.
if (Object.keys(lockedColors).length > 0) {
const baseColorHex = Object.values(lockedColors)[0];
const baseHsl = hexToHsl(baseColorHex);
const newPalette = applyHarmonyRule(baseHsl, rule);
const finalPalette = newPalette.map((color, index) => lockedColors[index] ? { ...hexToHsl(lockedColors[index]), hex: lockedColors[index] } : color);
setColorPalette(finalPalette);
} else {
generatePalette('harmony', rule);
}
};
// Helper for contrast checking (simplified)
const getContrastTextColor = (hexColor) => {
if (!hexColor) return '#000000'; // Default to black
const hsl = hexToHsl(hexColor);
return hsl.l > 50 ? '#000000' : '#FFFFFF'; // Black text for light colors, white for dark
};
return (
{/* Input Section */}
{/* Palette Display */}
Generated Color Palette
{loading && (
)}
{!loading && colorPalette.length > 0 && (
{colorPalette.map((color, index) => (
toggleLockColor(index)}
>
{/* Lock Button */}
{ e.stopPropagation(); toggleLockColor(index); }}
>
{lockedColors[index] ? : }
{!lockedColors[index] && }
{lockedColors[index] && }
{lockedColors[index] && }
{lockedColors[index] && }
{/* Hex Code Display */}
{color.hex.toUpperCase()}
{/* HSL values for reference/debugging */}
{/*
H:{color.h}° S:{color.s}% L:{color.l}%
*/}
))}
)}
{/* Adjustment Tools */}
Fine-Tune Your Palette
{/* Color Harmony Rules */}
Color Harmony Rules (Applies to unlocked colors)
{['random', 'monochromatic', 'complementary', 'analogous', 'triadic'].map(rule => (
handleHarmonyRuleChange(rule)}
className={`px-5 py-2 rounded-full font-medium shadow-md transition-all duration-200 ${harmonyRule === rule ? 'bg-blue-600 text-white transform scale-105' : 'bg-gray-200 text-gray-800 dark:bg-gray-600 dark:text-gray-100 hover:bg-blue-100 dark:hover:bg-blue-800'}`}
>
{rule.charAt(0).toUpperCase() + rule.slice(1)}
))}
{/* HSL Adjustment Sliders (Applies to the whole palette for demonstration) */}
Adjust HSL (Applies to unlocked colors)
{['hue', 'saturation', 'lightness'].map((type) => (
{type}
setHslAdjustments(prev => ({ ...prev, [type]: parseInt(e.target.value) }))}
onMouseUp={handleGeneratePalette} // Regenerate when done adjusting
onTouchEnd={handleGeneratePalette} // For touch devices
className="w-full h-2 bg-gray-300 dark:bg-gray-600 rounded-lg appearance-none cursor-pointer range-sm accent-blue-500"
/>
Delta: {hslAdjustments[type]}
))}
Note: HSL adjustments are applied to unlocked colors on release of the slider.
© {new Date().getFullYear()} AI Palette Generator. All rights reserved.
This app simulates AI functionality for demonstration purposes. Real AI models would involve complex backend processing.
Social Media: Facebook | Twitter | Newsletter: Subscribe
);
};
export default App;