Canvas-only 2D game editor/runtime built on PixiJS v8 + Vite + TypeScript. - core: asset model (Scene/Camera/Sprite), scene objects with sparse overrides, property schema + resolve, command stack (undo/redo), serialization and project store interface (16 unit tests) - ui: custom in-canvas widget toolkit (panels, fields, color picker, list/scroll, popup) with a hidden <input> keyboard bridge - editor: asset/hierarchy panels, schema-driven inspector, toolbar - viewport: box rendering, camera frame, selection gizmo, drag/zoom/pan - runtime: world/entity instantiation for play mode, input, camera follow - api: PHP JSON project store with revision counter (409 on conflict) - tools: CDP-based e2e smoke test Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
64 lines
1.9 KiB
TypeScript
64 lines
1.9 KiB
TypeScript
/** Color helpers. Colors are 0xRRGGBB numbers throughout the application. */
|
|
|
|
export interface HSV {
|
|
h: number; // 0..360
|
|
s: number; // 0..1
|
|
v: number; // 0..1
|
|
}
|
|
|
|
export function toHex(color: number): string {
|
|
return '#' + (color & 0xffffff).toString(16).padStart(6, '0');
|
|
}
|
|
|
|
/** Parses "#rgb", "#rrggbb", "rrggbb". Returns null when invalid. */
|
|
export function parseHex(text: string): number | null {
|
|
const t = text.trim().replace(/^#/, '');
|
|
if (/^[0-9a-f]{3}$/i.test(t)) {
|
|
const [r, g, b] = t.split('') as [string, string, string];
|
|
return parseInt(r + r + g + g + b + b, 16);
|
|
}
|
|
if (/^[0-9a-f]{6}$/i.test(t)) return parseInt(t, 16);
|
|
return null;
|
|
}
|
|
|
|
export function rgbToHsv(color: number): HSV {
|
|
const r = ((color >> 16) & 0xff) / 255;
|
|
const g = ((color >> 8) & 0xff) / 255;
|
|
const b = (color & 0xff) / 255;
|
|
const max = Math.max(r, g, b);
|
|
const min = Math.min(r, g, b);
|
|
const d = max - min;
|
|
let h = 0;
|
|
if (d > 0) {
|
|
if (max === r) h = ((g - b) / d) % 6;
|
|
else if (max === g) h = (b - r) / d + 2;
|
|
else h = (r - g) / d + 4;
|
|
h *= 60;
|
|
if (h < 0) h += 360;
|
|
}
|
|
return { h, s: max === 0 ? 0 : d / max, v: max };
|
|
}
|
|
|
|
export function hsvToRgb({ h, s, v }: HSV): number {
|
|
const c = v * s;
|
|
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
const m = v - c;
|
|
let r = 0, g = 0, b = 0;
|
|
if (h < 60) [r, g, b] = [c, x, 0];
|
|
else if (h < 120) [r, g, b] = [x, c, 0];
|
|
else if (h < 180) [r, g, b] = [0, c, x];
|
|
else if (h < 240) [r, g, b] = [0, x, c];
|
|
else if (h < 300) [r, g, b] = [x, 0, c];
|
|
else [r, g, b] = [c, 0, x];
|
|
const to = (n: number) => Math.round((n + m) * 255);
|
|
return (to(r) << 16) | (to(g) << 8) | to(b);
|
|
}
|
|
|
|
/** Perceived brightness 0..1, used to pick readable text on a swatch. */
|
|
export function luminance(color: number): number {
|
|
const r = (color >> 16) & 0xff;
|
|
const g = (color >> 8) & 0xff;
|
|
const b = color & 0xff;
|
|
return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
}
|