UNPKG

rybitten

Version:

A color space conversion library for transforming between RGB and RYB colors.

183 lines (161 loc) 5.85 kB
export * from "./main"; import { ryb2rgb, rybHsl2rgb } from "./main"; import { RYB_ITTEN, type ColorCube } from "./cubes"; export const RYB = "ryb"; export const RYBHSL = "rybhsl"; export const ryb = (cube?: ColorCube) => ({ mode: RYB, cube }); export const rybhsl = (cube?: ColorCube) => ({ mode: RYBHSL, cube }); /** * Minimal shape of the p5 constructor we depend on. @types/p5 describes the * runtime instance, not the prototype-patching surface we use here, so this * narrow interface is clearer than trying to reuse the upstream types. */ interface P5Constructor { prototype: P5Prototype; } interface P5Prototype { colorMode: (...args: unknown[]) => unknown; fill: (...args: unknown[]) => unknown; stroke: (...args: unknown[]) => unknown; background: (...args: unknown[]) => unknown; color: (...args: unknown[]) => unknown; registerMethod: (hook: string, fn: (this: P5Instance) => void) => void; _rybExtended?: boolean; RYB?: string; RYBHSL?: string; ryb?: typeof ryb; rybhsl?: typeof rybhsl; } /** * Bookkeeping fields we attach to individual p5 instances. These are internal * to this extension — never part of the public p5 API. */ interface P5Instance { constructor: P5Constructor; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- p5's own typing here is a plain `any`; narrowing it would require module augmentation of p5 itself, which we deliberately avoid. _rybMode?: string | false | any; _rybMaxes?: [number, number, number, number]; _rybCube?: ColorCube; } /** * Extends p5.js with RYB color mode support. * Called automatically on library load. Can be invoked manually: * rybitten.extendP5(p5) */ export function extendP5(p5: P5Constructor): void { if (p5.prototype._rybExtended) return; p5.prototype._rybExtended = true; p5.prototype.RYB = RYB; p5.prototype.RYBHSL = RYBHSL; p5.prototype.ryb = ryb; p5.prototype.rybhsl = rybhsl; const originalColorMode = p5.prototype.colorMode; const originalFill = p5.prototype.fill; const originalStroke = p5.prototype.stroke; const originalBackground = p5.prototype.background; const originalColor = p5.prototype.color; function convertArgs(instance: P5Instance, args: unknown[]): unknown[] { if (instance._rybMode && typeof args[0] === "number") { const maxes = instance._rybMaxes ?? [255, 255, 255, 255]; let v1: number, v2: number, v3: number; let a: unknown; if (args.length === 1) { v1 = v2 = v3 = (args[0] as number) / maxes[0]; } else if (args.length === 2) { v1 = v2 = v3 = (args[0] as number) / maxes[0]; a = args[1]; } else if (args.length >= 3) { v1 = (args[0] as number) / maxes[0]; v2 = (args[1] as number) / maxes[1]; v3 = (args[2] as number) / maxes[2]; a = args[3]; } else { return args; } const options = { cube: instance._rybCube ?? RYB_ITTEN }; const rgb = instance._rybMode === RYBHSL ? rybHsl2rgb([v1 * 360, v2, v3], options) : ryb2rgb([v1, v2, v3], options); const newArgs: unknown[] = [rgb[0], rgb[1], rgb[2]]; if (a !== undefined) { const maxA = maxes[3] !== undefined ? maxes[3] : maxes[0]; newArgs.push((a as number) / maxA); } return newArgs; } return args; } p5.prototype.colorMode = function (this: P5Instance, ...args: unknown[]) { const first = args[0]; let actualMode: unknown = first; let cube: ColorCube | undefined; if ( first !== null && typeof first === "object" && "mode" in (first as Record<string, unknown>) ) { const modeObj = first as { mode: unknown; cube?: ColorCube }; actualMode = modeObj.mode; cube = modeObj.cube; } if (actualMode === RYB || actualMode === RYBHSL) { this._rybMode = actualMode; this._rybCube = cube; const rest = args.slice(1); if (rest.length === 0) { this._rybMaxes = actualMode === RYBHSL ? [360, 100, 100, 1] : [255, 255, 255, 255]; } else if (rest.length === 1) { const m = rest[0] as number; this._rybMaxes = [m, m, m, m]; } else { this._rybMaxes = [ rest[0] as number, rest[1] as number, rest[2] as number, rest[3] !== undefined ? (rest[3] as number) : (rest[0] as number), ]; } return originalColorMode.apply(this, ["rgb", 1]); } this._rybMode = false; delete this._rybMaxes; delete this._rybCube; return originalColorMode.apply(this, args); }; const wrap = <T extends (...a: unknown[]) => unknown>(fn: T) => function (this: P5Instance, ...args: unknown[]): unknown { if (this._rybMode) { const newArgs = convertArgs(this, args); const savedMode = this._rybMode; const savedMaxes = this._rybMaxes; const savedCube = this._rybCube; this._rybMode = false; try { return fn.apply(this, newArgs); } finally { this._rybMode = savedMode; this._rybMaxes = savedMaxes; this._rybCube = savedCube; } } return fn.apply(this, args); }; p5.prototype.fill = wrap(originalFill); p5.prototype.stroke = wrap(originalStroke); p5.prototype.background = wrap(originalBackground); p5.prototype.color = wrap(originalColor); } // Auto-extend p5 when the library loads. // eslint-disable-next-line @typescript-eslint/no-explicit-any -- p5 is an untyped global when loaded via <script>; we deliberately accept the weak type here. declare const p5: any; if (typeof p5 !== "undefined") { extendP5(p5 as P5Constructor); (p5 as P5Constructor).prototype.registerMethod( "init", function (this: P5Instance) { extendP5(this.constructor); }, ); }