import type { Color, FillChoice, GradientStops } from "/wrapper/pkg/graphite_wasm_wrapper"; // Channels can have any range (0-1, 0-255, 0-100, 0-360) in the context they are being used in, these are just containers for the numbers export type HSV = { h: number; s: number; v: number }; export type RGB = { r: number; g: number; b: number }; // COLOR FACTORY FUNCTIONS export function createColor(red: number, green: number, blue: number, alpha: number): Color { return { red, green, blue, alpha }; } export function createColorFromHSVA(h: number, s: number, v: number, a: number): Color { const convert = (n: number): number => { const k = (n + h * 6) % 6; return v - v * s * Math.max(Math.min(...[k, 4 - k, 1]), 0); }; return { red: convert(5), green: convert(3), blue: convert(1), alpha: a }; } // COLOR UTILITY FUNCTIONS export function isColor(value: unknown): value is Color { return typeof value === "object" && value !== null && "red" in value; } export function colorFromCSS(colorCode: string): Color | undefined { // Allow single-digit hex value inputs let colorValue = colorCode.trim(); if (colorValue.length === 2 && colorValue.charAt(0) === "#" && /[0-9a-f]/i.test(colorValue.charAt(1))) { const digit = colorValue.charAt(1); colorValue = `#${digit}${digit}${digit}`; } const canvas = document.createElement("canvas"); canvas.width = 1; canvas.height = 1; const context = canvas.getContext("2d", { willReadFrequently: true }); if (!context) return undefined; context.clearRect(0, 0, 1, 1); context.fillStyle = "black"; context.fillStyle = colorValue; const comparisonA = context.fillStyle; context.fillStyle = "white"; context.fillStyle = colorValue; const comparisonB = context.fillStyle; // Invalid color if (comparisonA !== comparisonB) { // If this color code didn't start with a #, add it and try again if (colorValue.trim().charAt(0) !== "#") return colorFromCSS(`#${colorValue.trim()}`); return undefined; } context.fillRect(0, 0, 1, 1); const [r, g, b, a] = [...context.getImageData(0, 0, 1, 1).data]; return createColor(r / 255, g / 255, b / 255, a / 255); } export function colorToHexNoAlpha(color: Color): string { const r = Math.round(color.red * 255) .toString(16) .padStart(2, "0"); const g = Math.round(color.green * 255) .toString(16) .padStart(2, "0"); const b = Math.round(color.blue * 255) .toString(16) .padStart(2, "0"); return `#${r}${g}${b}`; } export function colorToRgb255(color: Color): RGB { return { r: Math.round(color.red * 255), g: Math.round(color.green * 255), b: Math.round(color.blue * 255), }; } export function colorToRgbCSS(color: Color): string { const rgb = colorToRgb255(color); return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; } export function colorToRgbaCSS(color: Color): string { const rgb = colorToRgb255(color); return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${color.alpha})`; } export function colorToHSV(color: Color): HSV { const { red: r, green: g, blue: b } = color; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const d = max - min; const s = max === 0 ? 0 : d / max; const v = max; let h = 0; if (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; default: } h /= 6; } return { h, s, v }; } export function colorOpaque(color: Color): Color { return createColor(color.red, color.green, color.blue, 1); } export function colorLuminance(color: Color): number { // Convert alpha into white const r = color.red * color.alpha + (1 - color.alpha); const g = color.green * color.alpha + (1 - color.alpha); const b = color.blue * color.alpha + (1 - color.alpha); // https://stackoverflow.com/a/3943023/775283 const linearR = r <= 0.04045 ? r / 12.92 : ((r + 0.055) / 1.055) ** 2.4; const linearG = g <= 0.04045 ? g / 12.92 : ((g + 0.055) / 1.055) ** 2.4; const linearB = b <= 0.04045 ? b / 12.92 : ((b + 0.055) / 1.055) ** 2.4; return linearR * 0.2126 + linearG * 0.7152 + linearB * 0.0722; } export function colorContrastingColor(color: Color | undefined): "black" | "white" { if (!color) return "black"; const luminance = colorLuminance(color); return luminance > Math.sqrt(1.05 * 0.05) - 0.05 ? "black" : "white"; } export function contrastingOutlineFactor(value: FillChoice, proximityColor: string | [string, string], proximityRange: number): number { const pair = Array.isArray(proximityColor) ? [proximityColor[0], proximityColor[1]] : [proximityColor, proximityColor]; const [range1, range2] = pair.map((color) => colorFromCSS(window.getComputedStyle(document.body).getPropertyValue(color))); const contrast = (color: Color | undefined): number => { if (!color) return 0; const lum = colorLuminance(color); let rangeLuminance1 = range1 ? colorLuminance(range1) : 0; let rangeLuminance2 = range2 ? colorLuminance(range2) : 0; [rangeLuminance1, rangeLuminance2] = [Math.min(rangeLuminance1, rangeLuminance2), Math.max(rangeLuminance1, rangeLuminance2)]; const distance = Math.max(0, rangeLuminance1 - lum, lum - rangeLuminance2); return (1 - Math.min(distance / proximityRange, 1)) * (1 - colorToHSV(color).s); }; const gradientStops = fillChoiceGradientStops(value); if (gradientStops) { if (gradientStops.color.length === 0) return 0; const first = contrast(gradientStops.color[0]); const last = contrast(gradientStops.color[gradientStops.color.length - 1]); return Math.min(first, last); } return contrast(fillChoiceColor(value)); } // GRADIENT UTILITY FUNCTIONS export function isGradientStops(value: unknown): value is GradientStops { return typeof value === "object" && value !== null && "position" in value && "midpoint" in value && "color" in value; } // FILL CHOICE UTILITY FUNCTIONS export function fillChoiceColor(value: FillChoice): Color | undefined { if (typeof value === "object" && "Solid" in value) return value.Solid; return undefined; } export function fillChoiceGradientStops(value: FillChoice): GradientStops | undefined { if (typeof value === "object" && "Gradient" in value) return value.Gradient; return undefined; } export function parseFillChoice(value: unknown): FillChoice { if (value === "None" || value === undefined || value === null) return "None"; if (typeof value === "object" && value !== null && "Solid" in value && isColor(value.Solid)) return { Solid: value.Solid }; if (typeof value === "object" && value !== null && "Gradient" in value && isGradientStops(value.Gradient)) return { Gradient: value.Gradient }; return "None"; }