import { NeedlescriptError } from './errors.ts'; import { didYouMean } from './suggestions.ts'; export const DEFAULT_PALETTE = [ '#c8472f', '#31604f', '#3a4e8c', '#d9a441', '#8c4a6b', '#2b2b2b', '#5e8f8c', '#b8651b', ] as const; export const DEFAULT_BACKGROUND = '#f5efe4'; const COLOR_RGB: Record = { aliceblue: [240, 248, 255], antiquewhite: [250, 235, 215], aqua: [0, 255, 255], aquamarine: [127, 255, 212], azure: [240, 255, 255], beige: [245, 245, 220], bisque: [255, 228, 196], black: [0, 0, 0], blanchedalmond: [255, 235, 205], blue: [0, 0, 255], blueviolet: [138, 43, 226], brown: [165, 42, 42], burlywood: [222, 184, 135], cadetblue: [95, 158, 160], chartreuse: [127, 255, 0], chocolate: [210, 105, 30], coral: [255, 127, 80], cornflowerblue: [100, 149, 237], cornsilk: [255, 248, 220], crimson: [220, 20, 60], cyan: [0, 255, 255], darkblue: [0, 0, 139], darkcyan: [0, 139, 139], darkgoldenrod: [184, 134, 11], darkgray: [169, 169, 169], darkgreen: [0, 100, 0], darkgrey: [169, 169, 169], darkkhaki: [189, 183, 107], darkmagenta: [139, 0, 139], darkolivegreen: [85, 107, 47], darkorange: [255, 140, 0], darkorchid: [153, 50, 204], darkred: [139, 0, 0], darksalmon: [233, 150, 122], darkseagreen: [143, 188, 143], darkslateblue: [72, 61, 139], darkslategray: [47, 79, 79], darkslategrey: [47, 79, 79], darkturquoise: [0, 206, 209], darkviolet: [148, 0, 211], deeppink: [255, 20, 147], deepskyblue: [0, 191, 255], dimgray: [105, 105, 105], dimgrey: [105, 105, 105], dodgerblue: [30, 144, 255], firebrick: [178, 34, 34], floralwhite: [255, 250, 240], forestgreen: [34, 139, 34], fuchsia: [255, 0, 255], gainsboro: [220, 220, 220], ghostwhite: [248, 248, 255], gold: [255, 215, 0], goldenrod: [218, 165, 32], gray: [128, 128, 128], green: [0, 128, 0], greenyellow: [173, 255, 47], grey: [128, 128, 128], honeydew: [240, 255, 240], hotpink: [255, 105, 180], indianred: [205, 92, 92], indigo: [75, 0, 130], ivory: [255, 255, 240], khaki: [240, 230, 140], lavender: [230, 230, 250], lavenderblush: [255, 240, 245], lawngreen: [124, 252, 0], lemonchiffon: [255, 250, 205], lightblue: [173, 216, 230], lightcoral: [240, 128, 128], lightcyan: [224, 255, 255], lightgoldenrodyellow: [250, 250, 210], lightgray: [211, 211, 211], lightgreen: [144, 238, 144], lightgrey: [211, 211, 211], lightpink: [255, 182, 193], lightsalmon: [255, 160, 122], lightseagreen: [32, 178, 170], lightskyblue: [135, 206, 250], lightslategray: [119, 136, 153], lightslategrey: [119, 136, 153], lightsteelblue: [176, 196, 222], lightyellow: [255, 255, 224], lime: [0, 255, 0], limegreen: [50, 205, 50], linen: [250, 240, 230], magenta: [255, 0, 255], maroon: [128, 0, 0], mediumaquamarine: [102, 205, 170], mediumblue: [0, 0, 205], mediumorchid: [186, 85, 211], mediumpurple: [147, 112, 219], mediumseagreen: [60, 179, 113], mediumslateblue: [123, 104, 238], mediumspringgreen: [0, 250, 154], mediumturquoise: [72, 209, 204], mediumvioletred: [199, 21, 133], midnightblue: [25, 25, 112], mintcream: [245, 255, 250], mistyrose: [255, 228, 225], moccasin: [255, 228, 181], navajowhite: [255, 222, 173], navy: [0, 0, 128], oldlace: [253, 245, 230], olive: [128, 128, 0], olivedrab: [107, 142, 35], orange: [255, 165, 0], orangered: [255, 69, 0], orchid: [218, 112, 214], palegoldenrod: [238, 232, 170], palegreen: [152, 251, 152], paleturquoise: [175, 238, 238], palevioletred: [219, 112, 147], papayawhip: [255, 239, 213], peachpuff: [255, 218, 185], peru: [205, 133, 63], pink: [255, 192, 203], plum: [221, 160, 221], powderblue: [176, 224, 230], purple: [128, 0, 128], rebeccapurple: [102, 51, 153], red: [255, 0, 0], rosybrown: [188, 143, 143], royalblue: [65, 105, 225], saddlebrown: [139, 69, 19], salmon: [250, 128, 114], sandybrown: [244, 164, 96], seagreen: [46, 139, 87], seashell: [255, 245, 238], sienna: [160, 82, 45], silver: [192, 192, 192], skyblue: [135, 206, 235], slateblue: [106, 90, 205], slategray: [112, 128, 144], slategrey: [112, 128, 144], snow: [255, 250, 250], springgreen: [0, 255, 127], steelblue: [70, 130, 180], tan: [210, 180, 140], teal: [0, 128, 128], thistle: [216, 191, 216], tomato: [255, 99, 71], turquoise: [64, 224, 208], violet: [238, 130, 238], wheat: [245, 222, 179], white: [255, 255, 255], whitesmoke: [245, 245, 245], yellow: [255, 255, 0], yellowgreen: [154, 205, 50], }; const channelHex = (n: number) => Math.round(Math.min(255, Math.max(0, n))) .toString(16) .padStart(2, '0'); const fromRgb = ([r, g, b]: readonly number[]) => `#${channelHex(r)}${channelHex(g)}${channelHex(b)}`; export const COLOR_NAMES: Readonly> = Object.fromEntries( Object.entries(COLOR_RGB).map(([name, channels]) => [name, fromRgb(channels)]), ); export interface ParsedColor { hex: string; name?: string; } export function parseColor(value: string, line?: number): string { return parseColorDetails(value, line).hex; } export function parseColorDetails(value: string, line?: number): ParsedColor { if (value.startsWith('#')) { if (/^#[0-9a-f]{8}$/i.test(value)) throw new NeedlescriptError( `Color '${value}' has an alpha channel, but thread is opaque`, line, ); if (!/^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i.test(value)) throw new NeedlescriptError( `Malformed color '${value}' — use #rgb or #rrggbb (no alpha)`, line, ); const raw = value.slice(1).toLowerCase(); return { hex: raw.length === 3 ? `#${[...raw].map((c) => c + c).join('')}` : `#${raw}` }; } const name = value.toLowerCase(); const hex = COLOR_NAMES[name]; if (!hex) throw new NeedlescriptError( `Unknown color '${value}'${didYouMean(name, Object.keys(COLOR_NAMES))} — hex colors start with #`, line, ); return { hex, name }; } export function hexParts(color: string, line?: number): [number, number, number] { const hex = parseColor(color, line); return [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)) as [number, number, number]; } export function rgb(r: number, g: number, b: number): string { return fromRgb([r, g, b]); } export function hsl(h: number, s: number, l: number): string { h = ((h % 360) + 360) % 360; s = Math.min(1, Math.max(0, s)); l = Math.min(1, Math.max(0, l)); const c = (1 - Math.abs(2 * l - 1)) * s; const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); const m = l - c / 2; const [r, g, b] = h < 60 ? [c, x, 0] : h < 120 ? [x, c, 0] : h < 180 ? [0, c, x] : h < 240 ? [0, x, c] : h < 300 ? [x, 0, c] : [c, 0, x]; return rgb((r + m) * 255, (g + m) * 255, (b + m) * 255); } export type OKLab = [number, number, number]; const linear = (v: number) => (v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)); const gamma = (v: number) => (v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055); export function oklab(color: string, line?: number): OKLab { const [r8, g8, b8] = hexParts(color, line); const r = linear(r8 / 255), g = linear(g8 / 255), b = linear(b8 / 255); const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b); const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b); const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b); return [ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s, 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s, ]; } export function unoklab([L, a, b]: OKLab): string { const l = Math.pow(L + 0.3963377774 * a + 0.2158037573 * b, 3); const m = Math.pow(L - 0.1055613458 * a - 0.0638541728 * b, 3); const s = Math.pow(L - 0.0894841775 * a - 1.291485548 * b, 3); return rgb( gamma(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s) * 255, gamma(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s) * 255, gamma(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s) * 255, ); } export function colorDist(a: string, b: string, line?: number): number { const x = oklab(a, line), y = oklab(b, line); return Math.hypot(x[0] - y[0], x[1] - y[1], x[2] - y[2]); } export function lerpColor(a: string, b: string, t: number, mode = 'oklab', line?: number): string { t = Math.min(1, Math.max(0, t)); if (mode === 'rgb') { const x = hexParts(a, line), y = hexParts(b, line); return rgb(x[0] + (y[0] - x[0]) * t, x[1] + (y[1] - x[1]) * t, x[2] + (y[2] - x[2]) * t); } if (mode !== 'oklab') throw new NeedlescriptError(`lerpcolor mode must be 'oklab' or 'rgb'`, line); const x = oklab(a, line), y = oklab(b, line); return unoklab([x[0] + (y[0] - x[0]) * t, x[1] + (y[1] - x[1]) * t, x[2] + (y[2] - x[2]) * t]); } export function defaultSlotColor(index: number): string { return DEFAULT_PALETTE[ ((index % DEFAULT_PALETTE.length) + DEFAULT_PALETTE.length) % DEFAULT_PALETTE.length ]; }