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.

183 lines (182 loc) 7.8 kB
import { Vec3, Vec4 } from "@fimbul-works/vec"; import { toPremultipliedAlpha } from "./alpha"; /** * Type guard to check if a color is Vec4 */ function isVec4(color) { return color instanceof Vec4; } /** * Mix two colors with a given ratio * Handles both RGB and RGBA colors * @param color1 First color (Vec3 or Vec4) * @param color2 Second color (Vec3 or Vec4) * @param ratio Mix ratio (0 to 1) * @returns Mixed color in same format as inputs */ export function mix(color1, color2, ratio) { if (isVec4(color1) && isVec4(color2)) { const outAlpha = color1.w * (1 - ratio) + color2.w * ratio; if (outAlpha === 0) return new Vec4(0, 0, 0, 0); return new Vec4(color1.x * (1 - ratio) + color2.x * ratio, color1.y * (1 - ratio) + color2.y * ratio, color1.z * (1 - ratio) + color2.z * ratio, outAlpha); } return new Vec3(color1.x * (1 - ratio) + color2.x * ratio, color1.y * (1 - ratio) + color2.y * ratio, color1.z * (1 - ratio) + color2.z * ratio); } /** * Blends two colors with alpha using premultiplied alpha blending * @param bottom - Bottom color as Vec4 RGBA * @param top - Top color as Vec4 RGBA * @returns Vec4 containing the blended RGBA color */ export function blendWithAlpha(bottom, top) { const outAlpha = top.w + bottom.w * (1 - top.w); if (outAlpha === 0) return new Vec4(0, 0, 0, 0); return new Vec4(top.x + bottom.x * (1 - top.w), top.y + bottom.y * (1 - top.w), top.z + bottom.z * (1 - top.w), outAlpha); } /** * Applies a blend operation to two colors * Handles both RGB and RGBA colors * @param base Base color (Vec3 or Vec4) * @param blend Blend color (Vec3 or Vec4) * @param blendFn Blend function to apply to RGB components * @returns Blended color in same format as inputs */ function applyBlend(base, blend, blendFn) { if (isVec4(base) && isVec4(blend)) { const premulBase = toPremultipliedAlpha(new Vec3(base.x, base.y, base.z), base.w); const premulBlend = toPremultipliedAlpha(new Vec3(blend.x, blend.y, blend.z), blend.w); const baseVec3 = new Vec3(premulBase.x, premulBase.y, premulBase.z); const blendVec3 = new Vec3(premulBlend.x, premulBlend.y, premulBlend.z); const result = blendFn(baseVec3, blendVec3); const outAlpha = blend.w + base.w * (1 - blend.w); if (outAlpha === 0) return new Vec4(0, 0, 0, 0); return new Vec4(result.x, result.y, result.z, outAlpha); } return blendFn(base, blend); } /** * Multiplies two colors together * Supports both RGB and RGBA colors * @param base Base color (Vec3 or Vec4) * @param blend Blend color (Vec3 or Vec4) * @returns Blended color in same format as inputs */ export function blendMultiply(base, blend) { return applyBlend(base, blend, (b, bl) => new Vec3(b.x * bl.x, b.y * bl.y, b.z * bl.z)); } /** * Screens two colors together * Supports both RGB and RGBA colors * @param base Base color (Vec3 or Vec4) * @param blend Blend color (Vec3 or Vec4) * @returns Blended color in same format as inputs */ export function blendScreen(base, blend) { return applyBlend(base, blend, (b, bl) => new Vec3(1 - (1 - b.x) * (1 - bl.x), 1 - (1 - b.y) * (1 - bl.y), 1 - (1 - b.z) * (1 - bl.z))); } /** * Applies overlay blend mode * Supports both RGB and RGBA colors * @param base Base color (Vec3 or Vec4) * @param blend Blend color (Vec3 or Vec4) * @returns Blended color in same format as inputs */ export function blendOverlay(base, blend) { return applyBlend(base, blend, (b, bl) => new Vec3(b.x < 0.5 ? 2 * b.x * bl.x : 1 - 2 * (1 - b.x) * (1 - bl.x), b.y < 0.5 ? 2 * b.y * bl.y : 1 - 2 * (1 - b.y) * (1 - bl.y), b.z < 0.5 ? 2 * b.z * bl.z : 1 - 2 * (1 - b.z) * (1 - bl.z))); } /** * Selects the darker color between base and blend colors * Supports both RGB and RGBA colors * @param base Base color (Vec3 or Vec4) * @param blend Blend color (Vec3 or Vec4) * @returns Blended color in same format as inputs */ export function blendDarken(base, blend) { return applyBlend(base, blend, (b, bl) => new Vec3(Math.min(b.x, bl.x), Math.min(b.y, bl.y), Math.min(b.z, bl.z))); } /** * Selects the lighter color between base and blend colors * Supports both RGB and RGBA colors * @param base Base color (Vec3 or Vec4) * @param blend Blend color (Vec3 or Vec4) * @returns Blended color in same format as inputs */ export function blendLighten(base, blend) { return applyBlend(base, blend, (b, bl) => new Vec3(Math.max(b.x, bl.x), Math.max(b.y, bl.y), Math.max(b.z, bl.z))); } /** * Brightens the base color based on the blend color using color dodge blend mode * Supports both RGB and RGBA colors * @param base Base color (Vec3 or Vec4) * @param blend Blend color (Vec3 or Vec4) * @returns Blended color in same format as inputs */ export function blendColorDodge(base, blend) { return applyBlend(base, blend, (b, bl) => new Vec3(b.x === 0 ? 0 : bl.x === 1 ? 1 : Math.min(1, b.x / (1 - bl.x)), b.y === 0 ? 0 : bl.y === 1 ? 1 : Math.min(1, b.y / (1 - bl.y)), b.z === 0 ? 0 : bl.z === 1 ? 1 : Math.min(1, b.z / (1 - bl.z)))); } /** * Darkens the base color based on the blend color using color burn blend mode * Supports both RGB and RGBA colors * @param base Base color (Vec3 or Vec4) * @param blend Blend color (Vec3 or Vec4) * @returns Blended color in same format as inputs */ export function blendColorBurn(base, blend) { return applyBlend(base, blend, (b, bl) => new Vec3(b.x === 1 ? 1 : bl.x === 0 ? 0 : 1 - Math.min(1, (1 - b.x) / bl.x), b.y === 1 ? 1 : bl.y === 0 ? 0 : 1 - Math.min(1, (1 - b.y) / bl.y), b.z === 1 ? 1 : bl.z === 0 ? 0 : 1 - Math.min(1, (1 - b.z) / bl.z))); } /** * Combines colors using hard light blend mode * Supports both RGB and RGBA colors * @param base Base color (Vec3 or Vec4) * @param blend Blend color (Vec3 or Vec4) * @returns Blended color in same format as inputs */ export function blendHardLight(base, blend) { return applyBlend(base, blend, (b, bl) => new Vec3(blend.x < 0.5 ? 2 * base.x * blend.x : 1 - 2 * (1 - base.x) * (1 - blend.x), blend.y < 0.5 ? 2 * base.y * blend.y : 1 - 2 * (1 - base.y) * (1 - blend.y), blend.z < 0.5 ? 2 * base.z * blend.z : 1 - 2 * (1 - base.z) * (1 - blend.z))); } /** * Combines colors using soft light blend mode for a more subtle effect * Supports both RGB and RGBA colors * @param base Base color (Vec3 or Vec4) * @param blend Blend color (Vec3 or Vec4) * @returns Blended color in same format as inputs */ export function blendSoftLight(base, blend) { const softlight = (b, l) => { if (l <= 0.5) { return b - (1 - 2 * l) * b * (1 - b); } const d = b <= 0.25 ? ((16 * b - 12) * b + 4) * b : Math.sqrt(b); return b + (2 * l - 1) * (d - b); }; return applyBlend(base, blend, (b, bl) => new Vec3(softlight(b.x, bl.x), softlight(b.y, bl.y), softlight(b.z, bl.z))); } /** * Calculates the absolute difference between colors * Supports both RGB and RGBA colors * @param base Base color (Vec3 or Vec4) * @param blend Blend color (Vec3 or Vec4) * @returns Blended color in same format as inputs */ export function blendDifference(base, blend) { return applyBlend(base, blend, (b, bl) => new Vec3(Math.abs(b.x - bl.x), Math.abs(b.y - bl.y), Math.abs(b.z - bl.z))); } /** * Similar to difference blend mode but with lower contrast * Supports both RGB and RGBA colors * @param base Base color (Vec3 or Vec4) * @param blend Blend color (Vec3 or Vec4) * @returns Blended color in same format as inputs */ export function blendExclusion(base, blend) { return applyBlend(base, blend, (b, bl) => new Vec3(b.x + bl.x - 2 * b.x * bl.x, b.y + bl.y - 2 * b.y * bl.y, b.z + bl.z - 2 * b.z * bl.z)); }