UNPKG

@fimbul-works/vec-color

Version:

A comprehensive, type-safe color manipulation library for TypeScript that provides a wide range of color space conversions, blending operations, and accessibility utilities.

255 lines (254 loc) 9.45 kB
import { estimateColorTemperature, kelvinToRGB } from "./kelvin"; import { hslToRGB, rgbToHSL } from "./hsl"; import { labToRGB, rgbToLAB } from "./lab"; import { Vec3 } from "@fimbul-works/vec"; import { mix } from "./blend"; /** * Converts a color to grayscale using luminance weights * @param color - Input color as Vec3 RGB (each channel from 0 to 1) * @returns Vec3 containing grayscale RGB values */ export function grayscale(color) { const gray = 0.299 * color.x + 0.587 * color.y + 0.114 * color.z; return new Vec3(gray, gray, gray); } /** * Inverts a color * @param color - Input color as Vec3 RGB (each channel from 0 to 1) * @returns Vec3 containing inverted RGB values */ export function invert(color) { return new Vec3(1 - color.x, 1 - color.y, 1 - color.z); } /** * Adjusts the brightness of a color * @param color - Input color as Vec3 RGB (each channel from 0 to 1) * @param amount - Amount to adjust brightness (-1 to 1) * @returns Vec3 containing adjusted RGB values */ export function adjustBrightness(color, amount) { const hsl = rgbToHSL(color); return hslToRGB(new Vec3(hsl.x, hsl.y, Math.max(0, Math.min(1, hsl.z + amount)))); } /** * Adjusts the saturation of a color * @param color - Input color as Vec3 RGB (each channel from 0 to 1) * @param amount - Amount to adjust saturation (-1 to 1) * @returns Vec3 containing adjusted RGB values */ export function adjustSaturation(color, amount) { const hsl = rgbToHSL(color); return hslToRGB(new Vec3(hsl.x, Math.max(0, Math.min(1, hsl.y + amount)), hsl.z)); } /** * Adjusts local contrast using a combination of luminance and saturation * @param color Input color as Vec3 RGB * @param amount Amount of contrast adjustment (-1 to 1) * @returns Contrast adjusted color as Vec3 RGB */ export function adjustContrast(color, amount) { const lab = rgbToLAB(color); const midpoint = 50; const factor = 1 + amount; return labToRGB(new Vec3(midpoint + (lab.x - midpoint) * factor, lab.y * factor, lab.z * factor)); } /** * Adjusts the gamma of a color * @param color - Input color as Vec3 RGB (each channel from 0 to 1) * @param amount - Amount of gamma adjustment * @returns Vec3 containing adjusted gamma values */ export function adjustGamma(color, gamma) { return new Vec3(color.x ** gamma, color.y ** gamma, color.z ** gamma); } /** * Selectively adjusts shadows and highlights * @param color Input color as Vec3 RGB * @param shadows Adjustment for shadows (-1 to 1) * @param highlights Adjustment for highlights (-1 to 1) * @returns Adjusted color as Vec3 RGB */ export function adjustTonalRange(color, shadows, highlights) { const lab = rgbToLAB(color); const l = lab.x; if (l < 50) { lab.x = lab.x * (1 + shadows); } else { lab.x = lab.x + (100 - lab.x) * highlights; } return labToRGB(lab); } /** * Shifts colors towards or away from pure grays * @param color Input color as Vec3 RGB * @param amount Amount of neutralization (-1 to 1, negative makes colors more neutral) * @returns Adjusted color as Vec3 RGB */ export function adjustNeutrality(color, amount) { const lab = rgbToLAB(color); const factor = 1 + amount; // Adjust a* and b* components to move towards/away from neutral const newA = lab.y * factor; const newB = lab.z * factor; return labToRGB(new Vec3(lab.x, newA, newB)); } /** * Adjusts color based on its complementary color * @param color Input color as Vec3 RGB * @param amount Amount of complementary influence (-1 to 1) * @returns Adjusted color as Vec3 RGB */ export function adjustComplementary(color, amount) { const hsl = rgbToHSL(color); const complementHue = (hsl.x + 0.5) % 1; const complement = hslToRGB(new Vec3(complementHue, hsl.y, hsl.z)); return mix(color, complement, amount * 0.5); } /** * Adds white to create a tint of the color * @param color Input color as Vec3 RGB * @param amount Amount of white to add (0 to 1) * @returns Tinted color as Vec3 RGB */ export function tint(color, amount) { return mix(color, new Vec3(1, 1, 1), amount); } /** * Adds black to create a shade of the color * @param color Input color as Vec3 RGB * @param amount Amount of black to add (0 to 1) * @returns Shaded color as Vec3 RGB */ export function shade(color, amount) { return mix(color, new Vec3(0, 0, 0), amount); } /** * Adds gray to create a tone of the color * @param color Input color as Vec3 RGB * @param amount Amount of gray to add (0 to 1) * @returns Toned color as Vec3 RGB */ export function tone(color, amount) { return mix(color, new Vec3(0.5, 0.5, 0.5), amount); } /** * Adjusts the color temperature * @param color Input color as Vec3 RGB * @param adjustment Temperature adjustment in Kelvin (-10000 to 10000) * @returns Temperature adjusted color as Vec3 RGB */ export function adjustTemperature(color, adjustment) { const warm = kelvinToRGB(2000); // Very warm (orange) const cool = kelvinToRGB(12000); // Very cool (blue) if (adjustment > 0) { return mix(color, warm, Math.min(1, adjustment / 10000)); } return mix(color, cool, Math.min(1, -adjustment / 10000)); } /** * Rotates the hue of a color * @param color Input color as Vec3 RGB * @param degrees Degrees to rotate the hue (-360 to 360) * @returns Color with rotated hue as Vec3 RGB */ export function rotateHue(color, degrees) { const hsl = rgbToHSL(color); const newHue = (hsl.x + degrees / 360 + 1) % 1; return hslToRGB(new Vec3(newHue, hsl.y, hsl.z)); } /** * Adjusts color vibrance (saturates colors while preserving skin tones) * @param color Input color as Vec3 RGB * @param amount Amount to adjust vibrance (-1 to 1) * @returns Vibrance adjusted color as Vec3 RGB */ export function adjustVibrance(color, amount) { const lab = rgbToLAB(color); const saturation = Math.sqrt(lab.y * lab.y + lab.z * lab.z); const isSkinTone = color.x > color.y && color.y > color.z && color.x > 0.4 && color.x < 0.9 && color.y > 0.2 && color.y < 0.7; const skinToneFactor = isSkinTone ? 0.3 : 1.0; const saturationFactor = Math.max(0, 1 - saturation / 100); const adjustmentFactor = 1 + amount * skinToneFactor * saturationFactor; const finalFactor = amount < 0 ? 1 + amount * skinToneFactor : adjustmentFactor; return labToRGB(new Vec3(lab.x, lab.y * finalFactor, lab.z * finalFactor)); } /** * Creates a sepia tone effect * @param color Input color as Vec3 RGB * @param amount Amount of sepia effect (0 to 1) * @returns Sepia-toned color as Vec3 RGB */ export function sepia(color, amount) { const r = color.x * 0.393 + color.y * 0.769 + color.z * 0.189; const g = color.x * 0.349 + color.y * 0.686 + color.z * 0.168; const b = color.x * 0.272 + color.y * 0.534 + color.z * 0.131; return mix(color, new Vec3(Math.min(1, r), Math.min(1, g), Math.min(1, b)), amount); } /** * Adjusts RGB channels independently * @param color Input color as Vec3 RGB * @param adjustments Vec3 containing adjustment values for each channel (-1 to 1) * @returns Color balanced color as Vec3 RGB */ export function colorBalance(color, adjustments) { return new Vec3(Math.max(0, Math.min(1, color.x * (1 + adjustments.x))), Math.max(0, Math.min(1, color.y * (1 + adjustments.y))), Math.max(0, Math.min(1, color.z * (1 + adjustments.z)))); } /** * Adjusts color based on time of day lighting * @param color Input color as Vec3 RGB * @param timeOfDay Hour in 24-hour format (0-23) * @returns Adjusted color as Vec3 RGB */ export function adjustTimeOfDay(color, timeOfDay) { const temperatures = { 0: 2700, // Night 6: 3500, // Dawn 8: 5500, // Morning 12: 6500, // Noon 16: 5500, // Afternoon 18: 4000, // Sunset 20: 2700, // Evening }; const hours = Object.keys(temperatures).map(Number); const hour = hours.reduce((prev, curr) => Math.abs(curr - timeOfDay) < Math.abs(prev - timeOfDay) ? curr : prev); const targetTemp = temperatures[hour]; const currentTemp = estimateColorTemperature(color) || 6500; const adjustment = targetTemp - currentTemp; return adjustTemperature(color, adjustment); } /** * Harmonizes a color by adjusting it to the nearest harmonic relationship * with a reference color while maintaining its character * @param color Color to harmonize * @param referenceColor Reference color to harmonize against * @returns Harmonized color */ export function harmonizeColor(color, referenceColor) { const colorHSL = rgbToHSL(color); const refHSL = rgbToHSL(referenceColor); const colorHue = colorHSL.x * 360; const refHue = refHSL.x * 360; const harmonicIntervals = [ 0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330, ]; let minDiff = 360; let harmonicHue = colorHue; for (const interval of harmonicIntervals) { const targetHue = (refHue + interval) % 360; const diff = Math.min(Math.abs(colorHue - targetHue), Math.abs(colorHue - (targetHue + 360)), Math.abs(colorHue + 360 - targetHue)); if (diff < minDiff) { minDiff = diff; harmonicHue = targetHue; } } if (minDiff <= 5) { return color; } return hslToRGB(new Vec3(harmonicHue / 360, colorHSL.y * 0.8 + refHSL.y * 0.2, colorHSL.z * 0.8 + refHSL.z * 0.2)); }