UNPKG

@14ch/color-palette-generator

Version:

A comprehensive color palette generation library with support for color scales, combinations, and transparency

1,362 lines (1,349 loc) 49.2 kB
'use strict'; // logger.ts // Unified log utility - provides consistency for error handling /** * Output log message */ const outputLog = (level, context, message, data) => { // Output nothing in production environment if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") { return; } // Production detection in browser environment (common bundler configuration) if (typeof window !== "undefined" && window.__PRODUCTION__) { return; } const prefix = `[ColorPalette:${context}]`; const logMessage = `${prefix} ${message}`; switch (level) { case "error": if (data !== undefined) { console.error(logMessage, data); } else { console.error(logMessage); } break; case "warn": if (data !== undefined) { console.warn(logMessage, data); } else { console.warn(logMessage); } break; case "info": if (data !== undefined) { console.info(logMessage, data); } else { console.info(logMessage); } break; } }; /** * Unified log interface */ const logger = { /** * Output warning log */ warn: (context, message, data) => { outputLog("warn", context, message, data); }, /** * Output error log */ error: (context, message, data) => { outputLog("error", context, message, data); }, /** * Output info log */ info: (context, message, data) => { outputLog("info", context, message, data); }, }; /** * Helper for detailed logging in development environment */ const createContextLogger = (context) => ({ warn: (message, data) => logger.warn(context, message, data), error: (message, data) => logger.error(context, message, data), info: (message, data) => logger.info(context, message, data), }); // colorUtils.ts const log$2 = createContextLogger("ColorUtils"); // ============================================================================= // Validate HEX Color // ============================================================================= const validateHexColor = (color) => { try { const cleanColor = String(color).trim(); if (!cleanColor || cleanColor.length === 0) { return false; } const normalizedColor = cleanColor.startsWith("#") ? cleanColor : `#${cleanColor}`; if (normalizedColor.length !== 7) { return false; } // Check basic hex format const hexPattern = /^#[0-9a-fA-F]{6}$/; if (!hexPattern.test(normalizedColor)) { return false; } // Parse RGB values directly const r = parseInt(normalizedColor.slice(1, 3), 16); const g = parseInt(normalizedColor.slice(3, 5), 16); const b = parseInt(normalizedColor.slice(5, 7), 16); // Validate each component is within valid range if (isNaN(r) || !isFinite(r) || r < 0 || r > 255 || isNaN(g) || !isFinite(g) || g < 0 || g > 255 || isNaN(b) || !isFinite(b) || b < 0 || b > 255) { return false; } // Validate the color can be correctly represented in hex const convertedBack = `#${[r, g, b] .map((c) => c.toString(16).padStart(2, "0")) .join("")}`; return convertedBack.toLowerCase() === normalizedColor.toLowerCase(); } catch (error) { return false; } }; // ============================================================================= // RGB ⇔ HEX Conversion // ============================================================================= const rgbToHex = ({ r, g, b }) => { const clamp = (value) => { if (isNaN(value) || !isFinite(value)) return 0; return Math.max(0, Math.min(255, Math.round(value))); }; const clampedR = clamp(r); const clampedG = clamp(g); const clampedB = clamp(b); return `#${[clampedR, clampedG, clampedB] .map((c) => c.toString(16).padStart(2, "0")) .join("")}`; }; const hexToRGB = (hex) => { const cleanHex = String(hex).trim(); const normalizedHex = cleanHex.startsWith("#") ? cleanHex : `#${cleanHex}`; // Simple validation without using validateHexColor if (normalizedHex.length !== 7 || !/^#[0-9a-fA-F]{6}$/.test(normalizedHex)) { log$2.warn("Invalid hex color detected, using black fallback", { hex }); return { r: 0, g: 0, b: 0 }; } const r = parseInt(normalizedHex.slice(1, 3), 16); const g = parseInt(normalizedHex.slice(3, 5), 16); const b = parseInt(normalizedHex.slice(5, 7), 16); return { r, g, b }; }; // ============================================================================= // RGB ⇔ HSL Conversion // ============================================================================= const rgbToHSL = ({ r, g, b }) => { const clamp = (value) => { if (isNaN(value) || !isFinite(value)) return 0; return Math.max(0, Math.min(255, value)); }; r = clamp(r) / 255; g = clamp(g) / 255; b = clamp(b) / 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); let h = 0; let s = 0; const l = (max + min) / 2; if (max !== min) { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return { h: h * 360, s: s * 100, l: l * 100 }; }; const hslToRGB = ({ h, s, l }) => { h = isFinite(h) ? ((h % 360) + 360) % 360 : 0; s = isFinite(s) ? Math.max(0, Math.min(100, s)) / 100 : 0; l = isFinite(l) ? Math.max(0, Math.min(100, l)) / 100 : 0; const c = (1 - Math.abs(2 * l - 1)) * s; const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); const m = l - c / 2; let r = 0, g = 0, b = 0; if (0 <= h && h < 60) { r = c; g = x; b = 0; } else if (60 <= h && h < 120) { r = x; g = c; b = 0; } else if (120 <= h && h < 180) { r = 0; g = c; b = x; } else if (180 <= h && h < 240) { r = 0; g = x; b = c; } else if (240 <= h && h < 300) { r = x; g = 0; b = c; } else { r = c; g = 0; b = x; } return { r: Math.round((r + m) * 255), g: Math.round((g + m) * 255), b: Math.round((b + m) * 255), }; }; // ============================================================================= // HEX ⇔ HSL Conversion // ============================================================================= const hexToHSL = (hex) => { const rgb = hexToRGB(hex); return rgbToHSL(rgb); }; // constants.ts /** * Level definitions */ const SCALE_LEVELS = [ 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950, ]; const MIN_LEVEL = 50; const MAX_LEVEL = 950; /** * Lightness scale definitions */ const STANDARD_LIGHTNESS_SCALE = { 50: 96, 100: 92, 200: 83, 300: 74, 400: 65, 500: 56, 600: 47, 700: 38, 800: 29, 900: 20, 950: 16, }; const MAX_LIGHTNESS = 96; const MIN_LIGHTNESS = 16; /** * Alpha value definitions */ const MIN_ALPHA = 0.1; const MAX_ALPHA = 1.0; /** * Default settings */ const DEFAULT_LIGHTNESS_METHOD = "hybrid"; const DEFAULT_HUE_SHIFT_MODE = "natural"; const DEFAULT_COLOR_CONFIG = { lightnessMethod: "hybrid", hueShiftMode: "natural", includeTransparent: false, includeTextColors: false, bgColorLight: "#ffffff", bgColorDark: "#000000", transparentOriginLevel: 500, }; const DEFAULT_BASE_COLOR_CONFIG = { lightnessMethod: "hybrid", hueShiftMode: "fixed", includeTransparent: false, includeTextColors: false, bgColorLight: "#ffffff", bgColorDark: "#000000", transparentOriginLevel: 950, }; /** * Default options */ const DEFAULT_RANDOM_COLOR_CONFIG = { saturationRange: [35, 75], // Moderate saturation lightnessRange: [ STANDARD_LIGHTNESS_SCALE[300], STANDARD_LIGHTNESS_SCALE[700], ], // Specified lightness lightnessMethod: "hybrid", // Balanced lightness hueRange: [0, 360], // All hues }; // lightness.ts // ============================================================================= // Lightness Calculation Functions // ============================================================================= /** * Get lightness value from color according to lightness calculation method */ const getLightness = ({ color, lightnessMethod = "hybrid", }) => { const rgb = hexToRGB(color); switch (lightnessMethod) { case "hsl": return getHSLLightness(rgb); case "perceptual": return getPerceptualLightness(rgb); case "average": return getAverageLightness(rgb); case "hybrid": default: return getHybridLightness(rgb); } }; /** * Calculate perceptual lightness from RGB (CIE Lab* based) */ const getPerceptualLightness = ({ r, g, b, }) => { const toLinear = ({ c }) => { if (isNaN(c) || !isFinite(c)) c = 0; const normalized = Math.max(0, Math.min(255, c)) / 255; return normalized <= 0.04045 ? normalized / 12.92 : Math.pow((normalized + 0.055) / 1.055, 2.4); }; const rLinear = toLinear({ c: r }); const gLinear = toLinear({ c: g }); const bLinear = toLinear({ c: b }); const luminance = 0.2126 * rLinear + 0.7152 * gLinear + 0.0722 * bLinear; // Accurate CIE L* calculation const threshold = 216 / 24389; const multiplier = 24389 / 27; const result = luminance > threshold ? Math.pow(luminance, 1 / 3) * 116 - 16 : luminance * multiplier; return isFinite(result) ? result : 0; }; /** * Get HSL lightness */ const getHSLLightness = ({ r, g, b, }) => { const hsl = rgbToHSL({ r, g, b }); return hsl.l; }; /** * Get RGB average lightness */ const getAverageLightness = ({ r, g, b, }) => { const average = (r + g + b) / 3; return (average / 255) * 100; }; /** * Get hybrid lightness (weighted average of perceptual lightness + HSL lightness) */ const getHybridLightness = ({ r, g, b, }) => { const perceptual = getPerceptualLightness({ r, g, b }); const hsl = rgbToHSL({ r, g, b }); // Weighted average of perceptual lightness and HSL lightness return perceptual * 0.3 + hsl.l * 0.7; }; // ============================================================================= // Lightness Adjustment Functions // ============================================================================= /** * Adjust color to achieve specified lightness */ const adjustToLightness = ({ h, s, targetLightness, lightnessMethod = "hybrid", }) => { h = isFinite(h) ? ((h % 360) + 360) % 360 : 0; s = isFinite(s) ? Math.max(0, Math.min(100, s)) : 0; targetLightness = isFinite(targetLightness) ? targetLightness : 50; switch (lightnessMethod) { case "hsl": return adjustToHSLLightness({ h, s, targetLightness }); default: return adjustToLightnessByBinarySearch({ h, s, targetLightness, lightnessMethod, }); } }; /** * Direct adjustment by HSL lightness (100% round-trip consistency guaranteed) */ const adjustToHSLLightness = ({ h, s, targetLightness, }) => { const hsl = { h, s, l: targetLightness }; const rgb = hslToRGB(hsl); return rgbToHex(rgb); }; /** * Lightness adjustment by binary search */ const adjustToLightnessByBinarySearch = ({ h, s, targetLightness, lightnessMethod = "hybrid", }) => { const MAX_ITERATIONS = 100; const PRECISION_THRESHOLD = 0.001; let low = 0; let high = 100; let bestL = 50; let bestDiff = Infinity; for (let i = 0; i < MAX_ITERATIONS; i++) { const mid = (low + high) / 2; const rgb = hslToRGB({ h, s, l: mid }); const currentLightness = getLightness({ color: rgbToHex(rgb), lightnessMethod: lightnessMethod, }); const diff = Math.abs(currentLightness - targetLightness); // Record L value with minimum error if (diff < bestDiff) { bestDiff = diff; bestL = mid; } // Exit if sufficient precision is reached if (diff < PRECISION_THRESHOLD) break; // Exit if range becomes sufficiently small if (high - low < PRECISION_THRESHOLD) break; if (currentLightness < targetLightness) { low = mid; } else { high = mid; } } const finalRgb = hslToRGB({ h, s, l: bestL }); return rgbToHex(finalRgb); }; // ============================================================================= // Scale Generation Functions // ============================================================================= /** * Find the closest lightness level to the specified color */ const findClosestLevel = ({ inputLightness, lightnessMethod = "hybrid", }) => { if (!isFinite(inputLightness)) inputLightness = 50; return SCALE_LEVELS.reduce((closestLevel, current) => { const lightness = lightnessMethod !== "perceptual" ? getAdjustedLightness({ level: current, lightnessMethod }) : STANDARD_LIGHTNESS_SCALE[current]; const currentDiff = Math.abs(inputLightness - lightness); const closestDiff = Math.abs(inputLightness - (lightnessMethod !== "perceptual" ? getAdjustedLightness({ level: closestLevel, lightnessMethod }) : STANDARD_LIGHTNESS_SCALE[closestLevel])); return currentDiff < closestDiff ? current : closestLevel; }); }; /** * Calculate even scale based on the specified color */ const calculateEvenScale = ({ inputLightness, baseLevel, }) => { if (!isFinite(inputLightness)) inputLightness = 50; const clampedInputLightness = Math.max(MIN_LIGHTNESS, Math.min(MAX_LIGHTNESS, inputLightness)); if (!SCALE_LEVELS.includes(baseLevel)) baseLevel = 500; const STEP_SIZE = 50; const baseIndex = (baseLevel - MIN_LEVEL) / STEP_SIZE; const totalSteps = (MAX_LEVEL - MIN_LEVEL) / STEP_SIZE; const upwardSteps = baseIndex; const downwardSteps = totalSteps - baseIndex; const availableUpward = MAX_LIGHTNESS - clampedInputLightness; const availableDownward = clampedInputLightness - MIN_LIGHTNESS; const upwardInterval = upwardSteps > 0 ? availableUpward / upwardSteps : 0; const downwardInterval = downwardSteps > 0 ? availableDownward / downwardSteps : 0; const evenScale = {}; evenScale[baseLevel] = clampedInputLightness; // Upper levels (bright direction) for (let i = 1; i <= upwardSteps; i++) { const level = baseLevel - i * STEP_SIZE; const lightness = Math.min(clampedInputLightness + upwardInterval * i, MAX_LIGHTNESS); evenScale[level] = lightness; } // Lower levels (dark direction) for (let i = 1; i <= downwardSteps; i++) { const level = baseLevel + i * STEP_SIZE; const lightness = Math.max(clampedInputLightness - downwardInterval * i, MIN_LIGHTNESS); evenScale[level] = lightness; } // Return clamped results const adjustedLightnessScale = {}; SCALE_LEVELS.forEach((level) => { if (evenScale[level] !== undefined) { adjustedLightnessScale[level] = Math.max(MIN_LIGHTNESS, Math.min(MAX_LIGHTNESS, evenScale[level])); } }); return adjustedLightnessScale; }; // ============================================================================= // Helper Functions // ============================================================================= /** * Get adjusted lightness according to method */ const getAdjustedLightness = ({ level, lightnessMethod, }) => { const normalizedLevel = (level - MIN_LEVEL) / (MAX_LEVEL - MIN_LEVEL); switch (lightnessMethod) { case "hsl": case "average": return MAX_LIGHTNESS - normalizedLevel * (MAX_LIGHTNESS - MIN_LIGHTNESS); case "hybrid": const perceptualLightness = STANDARD_LIGHTNESS_SCALE[level]; const linearLightness = MAX_LIGHTNESS - normalizedLevel * (MAX_LIGHTNESS - MIN_LIGHTNESS); return perceptualLightness * 0.3 + linearLightness * 0.7; case "perceptual": default: return STANDARD_LIGHTNESS_SCALE[level]; } }; // hueShift.ts // ============================================================================= // Hue Shift Calculation Functions // ============================================================================= /** * Calculate hue shift */ const calculateHueShift = ({ baseHue, baseLightness, targetLightness, adjustedLightnessScale, hueShiftMode, }) => { const MAX_HUE_SHIFT = 30; // No change in fixed mode if (hueShiftMode === "fixed") { return baseHue; } const lightnessDiff = targetLightness - baseLightness; // Use perception-based dynamic calculation const hueBasedIntensity = calculateHueIntensityByHue(baseHue); const lightnessBasedIntensity = calculateHueIntensityByLightness(lightnessDiff, adjustedLightnessScale); // Get shift amount & direction let hueShift = hueBasedIntensity * lightnessBasedIntensity * MAX_HUE_SHIFT; // Reverse direction in unnatural mode if (hueShiftMode === "unnatural") { hueShift = -hueShift; } const newHue = baseHue + hueShift; return normalizeHue(newHue); }; /** * Calculate hue shift intensity based on hue */ const calculateHueIntensityByHue = (hue) => { const normalizedHue = normalizeHue(hue); const radians = (normalizedHue * Math.PI) / 180; // Approximation of perceptual sensitivity curve based on MacAdam ellipse // High at red (0°) and blue (240°), low around yellow-green (60°) const perceptualSensitivity = 0.3 + (0.5 * (1 + Math.cos(radians - Math.PI / 3))) / 2; // Direction of hue shift due to temperature change // Warm colors (0-180°): bright→yellow (+), dark→magenta (-) // Cool colors (180-360°): bright→green (-), dark→blue/purple (+) const temperatureDirection = Math.cos(radians); // Combine perceptual sensitivity and temperature direction return perceptualSensitivity * temperatureDirection; }; /** * Calculate hue shift intensity based on lightness difference */ const calculateHueIntensityByLightness = (lightnessDiff, adjustedLightnessScale) => { const minLightness = Math.min(...Object.values(adjustedLightnessScale)); const maxLightness = Math.max(...Object.values(adjustedLightnessScale)); const actualRange = maxLightness - minLightness; // Normalize while preserving sign (-1 to +1 range) const normalizedDiff = lightnessDiff / actualRange; return Math.max(-1, Math.min(normalizedDiff, 1)); }; /** * Normalize hue to 0-360 range */ const normalizeHue = (hue) => { while (hue < 0) hue += 360; while (hue >= 360) hue -= 360; return hue; }; // transparentColor.ts const log$1 = createContextLogger("TransparentColor"); // ============================================================================= // Transparent Color Palette Generation // ============================================================================= /** * Generate transparent color palette */ const setTransparentPalette = ({ colorConfig, palette, }) => { if (!colorConfig.transparentOriginLevel) return; SCALE_LEVELS.forEach((level) => { const transparentOriginLevel = colorConfig.transparentOriginLevel; const targetSolidColor = palette[`--${colorConfig.prefix}-${level}`]; if (!targetSolidColor) return; // Normalize and validate input values const normalizedColor = targetSolidColor.trim(); if (!normalizedColor || normalizedColor === "" || normalizedColor === "undefined") { log$1.warn(`Invalid target solid color for level ${level}`, { targetSolidColor, }); return; } // Calculate transparency const fixedAlpha = getAlphaForLevel({ level, transparentOriginLevel: transparentOriginLevel, }); // Determine background color (bright background for levels below origin, dark background for levels above) const backgroundColor = level <= transparentOriginLevel ? colorConfig.bgColorLight : colorConfig.bgColorDark; if (!backgroundColor) return; // Calculate transparent color const transparentColor = calculateTransparentColor({ targetSolidColor: normalizedColor, backgroundColor, fixedAlpha, }); palette[`--${colorConfig.prefix}-${level}-transparent`] = transparentColor; }); }; // ============================================================================= // Transparency Calculation // ============================================================================= /** * Calculate transparency based on level */ const getAlphaForLevel = ({ level, transparentOriginLevel, }) => { // originLevel itself is always MAX_ALPHA if (level === transparentOriginLevel) { return MAX_ALPHA; } const alphaDifference = MAX_ALPHA - MIN_ALPHA; // 0.9 const STEP_SIZE = 50; if (level < transparentOriginLevel) { // Bright direction (from 50 to transparentOriginLevel) const totalSteps = (transparentOriginLevel - MIN_LEVEL) / STEP_SIZE; if (totalSteps === 0) { return MIN_ALPHA; } const currentStep = (level - MIN_LEVEL) / STEP_SIZE; const stepAlpha = alphaDifference / totalSteps; return MIN_ALPHA + stepAlpha * currentStep; } else { // Dark direction (from transparentOriginLevel to 950) const totalSteps = (MAX_LEVEL - transparentOriginLevel) / STEP_SIZE; if (totalSteps === 0) { return MIN_ALPHA; } const currentStep = (level - transparentOriginLevel) / STEP_SIZE; const stepAlpha = alphaDifference / totalSteps; return MAX_ALPHA - stepAlpha * currentStep; } }; // ============================================================================= // Transparent Color Calculation // ============================================================================= /** * Reverse calculate transparent color from fixed transparency */ const calculateTransparentColor = ({ targetSolidColor, backgroundColor, fixedAlpha, }) => { // RGB conversion and error handling let target; let bg; try { target = hexToRGB(targetSolidColor); if (isNaN(target.r) || isNaN(target.g) || isNaN(target.b)) { log$1.error(`Invalid RGB from target color`, { targetSolidColor, target }); return `rgba(0, 0, 0, ${fixedAlpha.toFixed(3)})`; } } catch (error) { log$1.error(`Error converting target color`, { targetSolidColor, error }); return `rgba(0, 0, 0, ${fixedAlpha.toFixed(3)})`; } try { bg = hexToRGB(backgroundColor); if (isNaN(bg.r) || isNaN(bg.g) || isNaN(bg.b)) { log$1.error(`Invalid RGB from background color`, { backgroundColor, bg }); return `rgba(0, 0, 0, ${fixedAlpha.toFixed(3)})`; } } catch (error) { log$1.error(`Error converting background color`, { backgroundColor, error }); return `rgba(0, 0, 0, ${fixedAlpha.toFixed(3)})`; } // Prevent division by zero if (fixedAlpha === 0) { log$1.warn("Alpha is 0, returning background color"); return `rgba(${bg.r}, ${bg.g}, ${bg.b}, 0.000)`; } // Reverse calculate RGB values of transparent color const backgroundMultiplier = 1 - fixedAlpha; const transparentR = (target.r - bg.r * backgroundMultiplier) / fixedAlpha; const transparentG = (target.g - bg.g * backgroundMultiplier) / fixedAlpha; const transparentB = (target.b - bg.b * backgroundMultiplier) / fixedAlpha; // Clamp to 0-255 range const clampedR = clampRGBValue(transparentR); const clampedG = clampRGBValue(transparentG); const clampedB = clampRGBValue(transparentB); return `rgba(${clampedR}, ${clampedG}, ${clampedB}, ${fixedAlpha.toFixed(3)})`; }; /** * Clamp RGB value to 0-255 range */ const clampRGBValue = (value) => { return Math.max(0, Math.min(255, Math.round(value))); }; // palette.ts const log = createContextLogger("Palette"); // ============================================================================= // Main Functions // ============================================================================= /** * Generate versatile color palette(s) from specified color(s) */ const generateColorPalette = (input) => { // Handle multiple configurations if (Array.isArray(input)) { const allPalette = {}; input.forEach((config) => { const palette = generateColorPalette(config); Object.assign(allPalette, palette); }); return allPalette; } // Handle single configuration const colorConfig = input; const inputRGB = hexToRGB(colorConfig.color); const normalizedColor = rgbToHex(inputRGB); const inputHSL = rgbToHSL(inputRGB); // Detect invalid color input and log output if (inputRGB.r === 0 && inputRGB.g === 0 && inputRGB.b === 0 && colorConfig.color !== "#000000") { log.warn("Invalid color input detected, using black fallback", { originalColor: colorConfig.color, fallbackColor: normalizedColor, prefix: colorConfig.prefix, }); } const normalizedConfig = { ...colorConfig, color: normalizedColor, lightnessMethod: colorConfig.lightnessMethod || DEFAULT_COLOR_CONFIG.lightnessMethod, hueShiftMode: colorConfig.hueShiftMode || DEFAULT_COLOR_CONFIG.hueShiftMode, includeTransparent: colorConfig.includeTransparent ?? DEFAULT_COLOR_CONFIG.includeTransparent, includeTextColors: colorConfig.includeTextColors ?? DEFAULT_COLOR_CONFIG.includeTextColors, bgColorLight: colorConfig.bgColorLight || DEFAULT_COLOR_CONFIG.bgColorLight, bgColorDark: colorConfig.bgColorDark || DEFAULT_COLOR_CONFIG.bgColorDark, transparentOriginLevel: colorConfig.transparentOriginLevel || DEFAULT_COLOR_CONFIG.transparentOriginLevel, }; const inputLightness = getLightness({ color: normalizedColor, lightnessMethod: normalizedConfig.lightnessMethod, }); const closestLevel = findClosestLevel({ inputLightness, lightnessMethod: normalizedConfig.lightnessMethod, }); const adjustedLightnessScale = calculateEvenScale({ inputLightness, baseLevel: closestLevel, }); const palette = generateOriginalPalette({ colorConfig: normalizedConfig, inputHSL, closestLevel, adjustedLightnessScale, }); setVariationColors({ colorConfig: normalizedConfig, closestLevel, palette, }); if (colorConfig.includeTransparent) { setTransparentPalette({ palette, colorConfig: normalizedConfig, }); } // Generate text colors last to ensure proper order setTextColor({ colorConfig: normalizedConfig, inputColor: normalizedColor, palette, }); return palette; }; // ============================================================================= // Palette Generation Logic // ============================================================================= /** * Generate basic color palette */ const generateOriginalPalette = ({ colorConfig, inputHSL, closestLevel, adjustedLightnessScale, }) => { const palette = {}; const originalLightness = adjustedLightnessScale[closestLevel]; Object.entries(adjustedLightnessScale).forEach(([key, targetLightness]) => { if (parseInt(key) === closestLevel) { palette[`--${colorConfig.prefix}-${key}`] = colorConfig.color; } else { const adjustedHue = calculateHueShift({ baseHue: inputHSL.h, baseLightness: originalLightness, targetLightness, adjustedLightnessScale, hueShiftMode: colorConfig.hueShiftMode, }); const generatedColor = adjustToLightness({ h: adjustedHue, s: inputHSL.s, targetLightness, lightnessMethod: colorConfig.lightnessMethod, }); palette[`--${colorConfig.prefix}-${key}`] = generatedColor; } }); return palette; }; /** * Set Variation Colors */ const setVariationColors = ({ colorConfig, closestLevel, palette, }) => { palette[`--${colorConfig.prefix}-color`] = `var(--${colorConfig.prefix}-${closestLevel})`; const currentIndex = SCALE_LEVELS.indexOf(closestLevel); const variations = [ { name: "lighter", offset: -2 }, { name: "light", offset: -1 }, { name: "dark", offset: 1 }, { name: "darker", offset: 2 }, ]; variations.forEach(({ name, offset }) => { const targetIndex = Math.max(0, Math.min(SCALE_LEVELS.length - 1, currentIndex + offset)); const targetLevel = SCALE_LEVELS[targetIndex]; palette[`--${colorConfig.prefix}-${name}`] = `var(--${colorConfig.prefix}-${targetLevel})`; }); }; /** * Set Text Color * Generate appropriate text colors for both light and dark backgrounds * Only generate text colors if includeTextColors is enabled */ const setTextColor = ({ colorConfig, inputColor, palette, }) => { // Only generate text colors if includeTextColors is enabled if (!colorConfig.includeTextColors) { return; } const inputRGB = hexToRGB(inputColor); const normalizedColor = rgbToHex(inputRGB); const inputPerceptualLightness = getLightness({ color: normalizedColor, lightnessMethod: "perceptual", }); // Find the primary color level (the level closest to input color) const primaryLevel = findClosestLevel({ inputLightness: inputPerceptualLightness, lightnessMethod: colorConfig.lightnessMethod, }); // Get primary color and its lightness const primaryColor = palette[`--${colorConfig.prefix}-${primaryLevel}`]; if (!primaryColor) { return; } const primaryLightness = getLightness({ color: primaryColor, lightnessMethod: "perceptual", }); // Find text color for light background (dark text on light background) const textColorForLightBackground = findTextColorLevel({ primaryLevel, primaryLightness, palette, prefix: colorConfig.prefix, targetLightness: 60, isLighter: false, // Find darker color }); // Find text color for dark background (light text on dark background) const textColorForDarkBackground = findTextColorLevel({ primaryLevel, primaryLightness, palette, prefix: colorConfig.prefix, targetLightness: 50, isLighter: true, // Find lighter color }); // Set light theme text color (dark text on light background) palette[`--${colorConfig.prefix}-text-color-on-light`] = `var(--${colorConfig.prefix}-${textColorForLightBackground})`; // Set dark theme text color (light text on dark background) palette[`--${colorConfig.prefix}-text-color-on-dark`] = `var(--${colorConfig.prefix}-${textColorForDarkBackground})`; }; /** * Find appropriate text color level based on lightness criteria */ const findTextColorLevel = ({ primaryLevel, primaryLightness, palette, prefix, targetLightness, isLighter, }) => { // Check if primary color meets the criteria const meetsCriteria = isLighter ? primaryLightness >= targetLightness : primaryLightness <= targetLightness; if (meetsCriteria) { return primaryLevel; } // Search for appropriate color level const primaryIndex = SCALE_LEVELS.indexOf(primaryLevel); const startIndex = isLighter ? primaryIndex - 1 : primaryIndex + 1; const endIndex = isLighter ? 0 : SCALE_LEVELS.length; const step = isLighter ? -1 : 1; for (let i = startIndex; isLighter ? i >= endIndex : i < endIndex; i += step) { const level = SCALE_LEVELS[i]; const levelColor = palette[`--${prefix}-${level}`]; if (levelColor) { const levelLightness = getLightness({ color: levelColor, lightnessMethod: "perceptual", }); const levelMeetsCriteria = isLighter ? levelLightness >= targetLightness : levelLightness <= targetLightness; if (levelMeetsCriteria) { return level; } } } // Fallback to extreme level return isLighter ? 50 : 950; }; // ============================================================================= // Palette Utility Functions // ============================================================================= /** * Resolve CSS variable to its final HEX value by following all variable references */ const resolveVariable = ({ variableName, palette, fallback = "#000000", }) => { const visited = new Set(); const resolve = (varName) => { // Ensure variable name starts with -- const normalizedName = varName.startsWith("--") ? varName : `--${varName}`; // Check for circular reference if (visited.has(normalizedName)) { return fallback; } // Mark as visited visited.add(normalizedName); // Get value from palette const value = palette[normalizedName]; // Early return if no value found if (!value) return fallback; // Return HEX color directly if (value.startsWith("#")) return value; // Resolve CSS variable reference recursively if (value.startsWith("var(")) { const innerVariable = value.slice(4, -1); // Remove var() wrapper return resolve(innerVariable); } return fallback; }; return resolve(variableName); }; // hue.ts // ============================================================================= // Hue Change Functions // ============================================================================= /** * Adjust color hue while maintaining the same tone (saturation/lightness) */ const adjustColorToSameTone = ({ color, targetHue, lightnessMethod = "hybrid", }) => { // Normalize target hue to 0-360 range targetHue = isFinite(targetHue) ? ((targetHue % 360) + 360) % 360 : 0; // Check if the color is valid if (!validateHexColor(color)) { // Fallback for invalid color return color; } // Convert input color to HSL const hsl = hexToHSL(color); // Calculate perceived lightness of original color using specified method const originalPerceivedLightness = getLightness({ color: color, lightnessMethod, }); // Use existing adjustToLightness function to maintain perceived lightness return adjustToLightness({ h: targetHue, s: hsl.s, targetLightness: originalPerceivedLightness, lightnessMethod, }); }; // ============================================================================= // Hue Palette Generation // ============================================================================= /** * Named hue positions on the color wheel (24 divisions) */ const HUE_NAMES = { 0: "red", 15: "scarlet", 30: "orange", 45: "amber", 60: "yellow", 75: "peridot", 90: "lime", 105: "sage", 120: "green", 135: "jade", 150: "emerald", 165: "turquoise", 180: "cyan", 195: "cerulean", 210: "azure", 225: "cobalt", 240: "blue", 255: "violet", 270: "purple", 285: "orchid", 300: "magenta", 315: "rose", 330: "crimson", 345: "ruby", }; /** * Generate complete color palettes for each hue division */ const generateHuePalette = ({ color, divisions = 24, lightnessMethod = "hybrid", hueShiftMode = "natural", includeTransparent = false, bgColorLight = "#ffffff", bgColorDark = "#000000", transparentOriginLevel = 500, includeTextColors = false, }) => { // Get base colors for each hue const baseColors = generateHueColors({ color, divisions, lightnessMethod }); // Create ColorConfig array for all base colors const colorConfigs = baseColors.map(({ name, color }) => ({ id: name.toLowerCase(), prefix: name.toLowerCase(), color, lightnessMethod, hueShiftMode, includeTransparent, bgColorLight, bgColorDark, transparentOriginLevel, includeTextColors, })); // Let generateColorPalette handle the palette generation return generateColorPalette(colorConfigs); }; /** * Generate evenly spaced base colors for each hue division */ const generateHueColors = ({ color, divisions = 24, lightnessMethod = "hybrid", }) => { if (!validateHexColor(color)) { return []; } const hueStep = 360 / divisions; const colors = []; for (let i = 0; i < divisions; i++) { const hue = i * hueStep; const normalizedHue = Math.round(hue); const adjustedColor = adjustColorToSameTone({ color, targetHue: hue, lightnessMethod, }); // Get name from predefined names or generate generic name const name = HUE_NAMES[normalizedHue] || `hue-${normalizedHue}`; colors.push({ name, hue: normalizedHue, color: adjustedColor, }); } return colors; }; // combination.ts // ============================================================================= // Color Combination Generation // ============================================================================= /** * Generate harmonious color combination from primary color */ const generateCombination = (config) => { const combinationType = config.combinationType || "complementary"; const primaryHSL = hexToHSL(config.primaryColor); const lightnessMethod = config.lightnessMethod || "hybrid"; const baseColorStrategy = config.baseColorStrategy || "harmonic"; const baseColorConfig = generateBaseColorConfig({ primaryHSL, lightnessMethod, strategy: baseColorStrategy, config, }); const primaryColorConfig = { lightnessMethod, hueShiftMode: "natural", includeTransparent: config.includeTransparent ?? DEFAULT_COLOR_CONFIG.includeTransparent, includeTextColors: config.includeTextColors ?? DEFAULT_COLOR_CONFIG.includeTextColors, bgColorLight: config.bgColorLight ?? DEFAULT_COLOR_CONFIG.bgColorLight, bgColorDark: config.bgColorDark ?? DEFAULT_COLOR_CONFIG.bgColorDark, transparentOriginLevel: config.transparentOriginLevel ?? DEFAULT_COLOR_CONFIG.transparentOriginLevel, id: "primary", prefix: "primary", color: config.primaryColor, }; const secondaryColorConfigs = generateSecondaryColorConfigs({ primaryHSL, combinationType, lightnessMethod, primaryColor: config.primaryColor, config, }); return [baseColorConfig, primaryColorConfig, ...secondaryColorConfigs]; }; // ============================================================================= // ColorConfig Construction // ============================================================================= /** * Generate base color Config */ const generateBaseColorConfig = ({ primaryHSL, lightnessMethod = "hybrid", strategy = "harmonic", config, }) => { const baseColor = getBaseColor({ primaryHSL, lightnessMethod, strategy }); return { lightnessMethod, hueShiftMode: "fixed", includeTransparent: config.includeTransparent ?? DEFAULT_BASE_COLOR_CONFIG.includeTransparent, includeTextColors: config.includeTextColors ?? DEFAULT_BASE_COLOR_CONFIG.includeTextColors, bgColorLight: config.bgColorLight ?? DEFAULT_BASE_COLOR_CONFIG.bgColorLight, bgColorDark: config.bgColorDark ?? DEFAULT_BASE_COLOR_CONFIG.bgColorDark, transparentOriginLevel: config.baseTransparentOriginLevel ?? DEFAULT_BASE_COLOR_CONFIG.transparentOriginLevel, id: "base", prefix: "base", color: baseColor, }; }; /** * Generate secondary color group Configs */ const generateSecondaryColorConfigs = ({ primaryHSL, combinationType, lightnessMethod, primaryColor, config, }) => { if (combinationType === "monochromatic") { return []; } const secondaryColors = getSecondaryColors({ primaryHSL, combinationType, lightnessMethod, primaryColor, }); const configs = []; const secondaryColorMap = [ { id: "secondary", prefix: "secondary", color: secondaryColors.secondary }, // Second { id: "secondary2", prefix: "secondary2", color: secondaryColors.secondary2, }, // Third { id: "secondary3", prefix: "secondary3", color: secondaryColors.secondary3, }, // Fourth ]; for (const { id, color, prefix } of secondaryColorMap) { if (color) { configs.push({ lightnessMethod, hueShiftMode: "natural", includeTransparent: config.includeTransparent ?? DEFAULT_COLOR_CONFIG.includeTransparent, includeTextColors: config.includeTextColors ?? DEFAULT_COLOR_CONFIG.includeTextColors, bgColorLight: config.bgColorLight ?? DEFAULT_COLOR_CONFIG.bgColorLight, bgColorDark: config.bgColorDark ?? DEFAULT_COLOR_CONFIG.bgColorDark, transparentOriginLevel: config.transparentOriginLevel ?? DEFAULT_COLOR_CONFIG.transparentOriginLevel, id, prefix, color, }); } } return configs; }; // ============================================================================= // Color Generation // ============================================================================= /** * Get base color (final color string) */ const getBaseColor = ({ primaryHSL, lightnessMethod = "hybrid", strategy = "harmonic", }) => { const targetLightness = STANDARD_LIGHTNESS_SCALE[500]; // 500 level equivalent const baseSaturation = Math.max(5, Math.min(15, primaryHSL.s * 0.1)); const strategyMap = { harmonic: { baseHue: primaryHSL.h, finalSaturation: baseSaturation }, contrasting: { baseHue: normalizeHue(primaryHSL.h + 180), finalSaturation: baseSaturation, }, neutral: { baseHue: 0, finalSaturation: 0 }, }; const { baseHue, finalSaturation } = strategyMap[strategy] || strategyMap.harmonic; return adjustToLightness({ h: baseHue, s: finalSaturation, targetLightness, lightnessMethod: lightnessMethod, }); }; /** * Get secondary colors (final color strings) */ const getSecondaryColors = ({ primaryHSL, combinationType, lightnessMethod, primaryColor, }) => { const { h: primaryHue } = primaryHSL; const combinationMap = { monochromatic: { secondary: primaryHSL, }, analogous: { secondary: { ...primaryHSL, h: normalizeHue(primaryHue + 30) }, secondary2: { ...primaryHSL, h: normalizeHue(primaryHue - 30) }, }, complementary: { secondary: { ...primaryHSL, h: normalizeHue(primaryHue + 180) }, }, splitComplementary: { secondary: { ...primaryHSL, h: normalizeHue(primaryHue + 150) }, secondary2: { ...primaryHSL, h: normalizeHue(primaryHue + 210) }, }, doubleComplementary: { secondary: { ...primaryHSL, h: normalizeHue(primaryHue + 30) }, secondary2: { ...primaryHSL, h: normalizeHue(primaryHue + 180) }, secondary3: { ...primaryHSL, h: normalizeHue(primaryHue + 210) }, }, doubleComplementaryReverse: { secondary: { ...primaryHSL, h: normalizeHue(primaryHue - 30) }, secondary2: { ...primaryHSL, h: normalizeHue(primaryHue + 180) }, secondary3: { ...primaryHSL, h: normalizeHue(primaryHue + 150) }, }, triadic: { secondary: { ...primaryHSL, h: normalizeHue(primaryHue + 120) }, secondary2: { ...primaryHSL, h: normalizeHue(primaryHue + 240) }, }, tetradic: { secondary: { ...primaryHSL, h: normalizeHue(primaryHue + 90) }, secondary2: { ...primaryHSL, h: normalizeHue(primaryHue + 180) }, secondary3: { ...primaryHSL, h: normalizeHue(primaryHue + 270) }, }, }; const hslValues = combinationMap[combinationType] || combinationMap.complementary; const result = {}; const keys = ["secondary", "secondary2", "secondary3"]; for (const key of keys) { const hsl = hslValues[key]; if (hsl) { result[key] = adjustColorToSameTone({ color: primaryColor, targetHue: hsl.h, lightnessMethod, }); } } return result; }; // ============================================================================= // Color Adjustment // ============================================================================= // randomColor.ts // ============================================================================= // Random Primary Color Generation Feature // ============================================================================= /** * Generate random primary color * @param options Generation options * @returns HEX string */ function generateRandomPrimaryColor(config = {}) { const perfectConfig = { ...DEFAULT_RANDOM_COLOR_CONFIG, ...config }; // Generate random hue const [minHue, maxHue] = perfectConfig.hueRange; const hue = Math.random() * (maxHue - minHue) + minHue; // Generate random lightness const [minLightness, maxLightness] = perfectConfig.lightnessRange; const lightness = Math.random() * (maxLightness - minLightness) + minLightness; // Generate random saturation const [minSat, maxSat] = perfectConfig.saturationRange; const saturation = Math.random() * (maxSat - minSat) + minSat; // Adjust to specified lightness and return HEX string return adjustToLightness({ h: hue, s: saturation, targetLightness: lightness, lightnessMethod: perfectConfig.lightnessMethod, }); } // applyToDom.ts /** * Apply CSS custom properties to DOM */ const applyColorPaletteToDom = (palette) => { if (typeof document !== "undefined") { Object.entries(palette).forEach(([key, value]) => { document.documentElement.style.setProperty(key, value); }); } }; exports.DEFAULT_BASE_COLOR_CONFIG = DEFAULT_BASE_COLOR_CONFIG; exports.DEFAULT_COLOR_CONFIG = DEFAULT_COLOR_CONFIG; exports.DEFAULT_HUE_SHIFT_MODE = DEFAULT_HUE_SHIFT_MODE; exports.DEFAULT_LIGHTNESS_METHOD = DEFAULT_LIGHTNESS_METHOD; exports.HUE_NAMES = HUE_NAMES; exports.MAX_LEVEL = MAX_LEVEL; exports.MAX_LIGHTNESS = MAX_LIGHTNESS; exports.MIN_LEVEL = MIN_LEVEL; exports.MIN_LIGHTNESS = MIN_LIGHTNESS; exports.SCALE_LEVELS = SCALE_LEVELS; exports.STANDARD_LIGHTNESS_SCALE = STANDARD_LIGHTNESS_SCALE; exports.adjustColorToSameTone = adjustColorToSameTone; exports.adjustToLightness = adjustToLightness; exports.applyColorPaletteToDom = applyColorPaletteToDom; exports.generateColorPalette = generateColorPalette; exports.generateCombination = generateCombination; exports.generateHuePalette = generateHuePalette; exports.generateRandomPrimaryColor = generateRandomPrimaryColor; exports.getLightness = getLightness; exports.hexToHSL = hexToHSL; exports.hexToRGB = hexToRGB; exports.hslToRGB = hslToRGB; exports.resolveVariable = resolveVariable; exports.rgbToHSL = rgbToHSL; exports.rgbToHex = rgbToHex; //# sourceMappingURL=index.js.map