@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
JavaScript
'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