@homie-rocks/studio 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +55 -0
  3. package/assets/assets.ts +500 -0
  4. package/assets/render-page.ts +629 -0
  5. package/assets/safety.mjs +194 -0
  6. package/bin/homie-studio.mjs +47 -1
  7. package/lib/art-cli.mjs +296 -0
  8. package/lib/art-tools.mjs +331 -0
  9. package/lib/asset-check.mjs +189 -0
  10. package/lib/asset-import.mjs +136 -0
  11. package/lib/asset-manifest.mjs +318 -0
  12. package/lib/build.mjs +25 -2
  13. package/lib/codex.mjs +43 -1
  14. package/lib/colour.mjs +160 -0
  15. package/lib/decisions.mjs +800 -0
  16. package/lib/directory.mjs +9 -1
  17. package/lib/library.mjs +169 -0
  18. package/lib/mcp-tools.mjs +22 -4
  19. package/lib/mcp.mjs +6 -3
  20. package/lib/optimise.mjs +380 -0
  21. package/lib/remix-assets.mjs +84 -0
  22. package/lib/render3d.mjs +81 -0
  23. package/lib/scaffold.mjs +26 -0
  24. package/lib/site.mjs +6 -1
  25. package/lib/studio.mjs +136 -9
  26. package/lib/style-board.mjs +227 -0
  27. package/lib/style-presets.mjs +121 -0
  28. package/lib/template-history.json +38 -0
  29. package/mcp/ui/card.css +15 -0
  30. package/mcp/ui/cast.js +62 -0
  31. package/mcp/ui/decision.js +61 -0
  32. package/mcp/ui/lineup.js +68 -0
  33. package/mcp/ui/rights.js +30 -0
  34. package/mcp/ui/style.js +154 -0
  35. package/package.json +14 -2
  36. package/starters/gem-rush-3d/assets/RIGHTS.md +203 -0
  37. package/starters/gem-rush-3d/assets/manifest.json +2318 -0
  38. package/starters/gem-rush-3d/credits.json +31 -0
  39. package/starters/gem-rush-3d/game.json +11 -0
  40. package/starters/gem-rush-3d/index.html +20 -0
  41. package/starters/gem-rush-3d/lab.json +19 -0
  42. package/starters/gem-rush-3d/src/main.ts +2086 -0
  43. package/starters/gem-rush-3d/style.json +66 -0
  44. package/starters/gem-rush-3d/tunables.json +13 -0
  45. package/worker/index.mjs +5 -3
  46. package/worker/pages.mjs +13 -5
  47. package/worker/qr.mjs +11 -0
  48. package/worker/site.mjs +5 -3
  49. package/worker/version.mjs +1 -1
package/lib/colour.mjs ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * COLOUR ARITHMETIC for art direction: hex and RGB, HSL nudges (a palette steered "warmer" or "less saturated"),
3
+ * CIE Lab and the CIEDE2000 difference (is an asset's albedo still in the locked palette?), and a small k-means for
4
+ * the dominant colours of a picture. No dependencies; numbers, never a file.
5
+ */
6
+
7
+ const clamp = (v, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, v));
8
+ export const HEX = /^#([0-9a-f]{6}|[0-9a-f]{3})$/i;
9
+
10
+ /** "#ffcf5a" (or "#fc5") as [r, g, b] in 0..255; null when it is not a hex colour. */
11
+ export function hexToRgb(hex) {
12
+ const m = HEX.exec(String(hex ?? '').trim());
13
+ if (!m) return null;
14
+ const h = m[1].length === 3 ? m[1].replace(/./g, '$&$&') : m[1];
15
+ return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
16
+ }
17
+
18
+ export function rgbToHex([r, g, b]) {
19
+ return `#${[r, g, b].map((v) => Math.round(clamp(v, 0, 255)).toString(16).padStart(2, '0')).join('')}`;
20
+ }
21
+
22
+ export function rgbToHsl([r, g, b]) {
23
+ const R = r / 255; const G = g / 255; const B = b / 255;
24
+ const max = Math.max(R, G, B); const min = Math.min(R, G, B);
25
+ const l = (max + min) / 2;
26
+ if (max === min) return [0, 0, l];
27
+ const d = max - min;
28
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
29
+ let h = max === R ? (G - B) / d + (G < B ? 6 : 0) : max === G ? (B - R) / d + 2 : (R - G) / d + 4;
30
+ h *= 60;
31
+ return [h, s, l];
32
+ }
33
+
34
+ export function hslToRgb([h, s, l]) {
35
+ const H = (((h % 360) + 360) % 360) / 360;
36
+ if (s === 0) return [l * 255, l * 255, l * 255];
37
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
38
+ const p = 2 * l - q;
39
+ const f = (t) => { let x = t; if (x < 0) x += 1; if (x > 1) x -= 1; if (x < 1 / 6) return p + (q - p) * 6 * x; if (x < 1 / 2) return q; if (x < 2 / 3) return p + (q - p) * (2 / 3 - x) * 6; return p; };
40
+ return [f(H + 1 / 3) * 255, f(H) * 255, f(H - 1 / 3) * 255];
41
+ }
42
+
43
+ /** The shortest signed angle from hue a to hue b, in degrees. */
44
+ const hueStep = (a, b) => ((((b - a) % 360) + 540) % 360) - 180;
45
+
46
+ /**
47
+ * One colour nudged: { hue: degrees toward `toward` (or a plain rotation), sat: multiply, light: add (-1..1),
48
+ * warm: 0..1 (toward orange) or cool (toward blue) }. Greys stay grey.
49
+ */
50
+ export function nudge(hex, { warm = 0, cool = 0, sat = 1, light = 0, hue = 0 } = {}) {
51
+ const rgb = hexToRgb(hex);
52
+ if (!rgb) return hex;
53
+ let [h, s, l] = rgbToHsl(rgb);
54
+ if (s > 0.06) {
55
+ if (warm) h += hueStep(h, 32) * clamp(warm);
56
+ if (cool) h += hueStep(h, 215) * clamp(cool);
57
+ h += hue;
58
+ }
59
+ s = clamp(s * sat);
60
+ l = clamp(l + light, 0.02, 0.98);
61
+ return rgbToHex(hslToRgb([h, s, l]));
62
+ }
63
+
64
+ /* ------------------------------------------------------------------ Lab and CIEDE2000 */
65
+
66
+ function srgbToLinear(c) { const v = c / 255; return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; }
67
+
68
+ /** sRGB [0..255] to CIE L*a*b* (D65). */
69
+ export function rgbToLab([r, g, b]) {
70
+ const R = srgbToLinear(r); const G = srgbToLinear(g); const B = srgbToLinear(b);
71
+ const X = (R * 0.4124 + G * 0.3576 + B * 0.1805) / 0.95047;
72
+ const Y = R * 0.2126 + G * 0.7152 + B * 0.0722;
73
+ const Z = (R * 0.0193 + G * 0.1192 + B * 0.9505) / 1.08883;
74
+ const f = (t) => (t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116);
75
+ const fx = f(X); const fy = f(Y); const fz = f(Z);
76
+ return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
77
+ }
78
+
79
+ /** CIEDE2000 between two Lab colours (Sharma, Wu and Dalal 2005). About 2.3 is "just noticeable". */
80
+ export function deltaE2000([L1, a1, b1], [L2, a2, b2]) {
81
+ const rad = Math.PI / 180;
82
+ const C1 = Math.hypot(a1, b1); const C2 = Math.hypot(a2, b2);
83
+ const Cb = (C1 + C2) / 2;
84
+ const G = 0.5 * (1 - Math.sqrt(Cb ** 7 / (Cb ** 7 + 25 ** 7)));
85
+ const ap1 = (1 + G) * a1; const ap2 = (1 + G) * a2;
86
+ const Cp1 = Math.hypot(ap1, b1); const Cp2 = Math.hypot(ap2, b2);
87
+ const hp = (b, ap) => { if (b === 0 && ap === 0) return 0; const h = Math.atan2(b, ap) / rad; return h >= 0 ? h : h + 360; };
88
+ const hp1 = hp(b1, ap1); const hp2 = hp(b2, ap2);
89
+ const dL = L2 - L1; const dC = Cp2 - Cp1;
90
+ let dh = 0;
91
+ if (Cp1 * Cp2 !== 0) { dh = hp2 - hp1; if (dh > 180) dh -= 360; else if (dh < -180) dh += 360; }
92
+ const dH = 2 * Math.sqrt(Cp1 * Cp2) * Math.sin((dh / 2) * rad);
93
+ const Lb = (L1 + L2) / 2; const Cpb = (Cp1 + Cp2) / 2;
94
+ let hb = hp1 + hp2;
95
+ if (Cp1 * Cp2 !== 0) { hb = Math.abs(hp1 - hp2) <= 180 ? (hp1 + hp2) / 2 : hp1 + hp2 < 360 ? (hp1 + hp2 + 360) / 2 : (hp1 + hp2 - 360) / 2; }
96
+ const T = 1 - 0.17 * Math.cos((hb - 30) * rad) + 0.24 * Math.cos(2 * hb * rad) + 0.32 * Math.cos((3 * hb + 6) * rad) - 0.2 * Math.cos((4 * hb - 63) * rad);
97
+ const dTheta = 30 * Math.exp(-(((hb - 275) / 25) ** 2));
98
+ const Rc = 2 * Math.sqrt(Cpb ** 7 / (Cpb ** 7 + 25 ** 7));
99
+ const Sl = 1 + (0.015 * (Lb - 50) ** 2) / Math.sqrt(20 + (Lb - 50) ** 2);
100
+ const Sc = 1 + 0.045 * Cpb; const Sh = 1 + 0.015 * Cpb * T;
101
+ const Rt = -Math.sin(2 * dTheta * rad) * Rc;
102
+ return Math.sqrt((dL / Sl) ** 2 + (dC / Sc) ** 2 + (dH / Sh) ** 2 + Rt * (dC / Sc) * (dH / Sh));
103
+ }
104
+
105
+ /** ΔE2000 between two hex colours. */
106
+ export const deltaHex = (a, b) => { const x = hexToRgb(a); const y = hexToRgb(b); return x && y ? deltaE2000(rgbToLab(x), rgbToLab(y)) : Infinity; };
107
+
108
+ /** The nearest palette colour to `hex`: { hex, delta }. */
109
+ export function nearest(hex, palette) {
110
+ let best = { hex: null, delta: Infinity };
111
+ for (const p of palette) { const d = deltaHex(hex, p); if (d < best.delta) best = { hex: p, delta: d }; }
112
+ return best;
113
+ }
114
+
115
+ /**
116
+ * Dominant colours of RGB samples ([[r,g,b], ...], or a flat Uint8Array of RGB with `weights`), by k-means in Lab:
117
+ * [{ hex, share }] biggest share first. Deterministic: the first centres are the most distant samples.
118
+ */
119
+ export function dominant(samples, k = 5, { iterations = 12 } = {}) {
120
+ const pts = samples.map((s) => ({ rgb: s.rgb ?? s, w: s.w ?? 1, lab: rgbToLab(s.rgb ?? s) }));
121
+ if (!pts.length) return [];
122
+ const K = Math.min(k, pts.length);
123
+ // Farthest-point seeding from the heaviest sample.
124
+ const centres = [pts.slice().sort((a, b) => b.w - a.w)[0].lab];
125
+ while (centres.length < K) {
126
+ let far = null; let fd = -1;
127
+ for (const p of pts) { const d = Math.min(...centres.map((c) => (c[0] - p.lab[0]) ** 2 + (c[1] - p.lab[1]) ** 2 + (c[2] - p.lab[2]) ** 2)); if (d > fd) { fd = d; far = p; } }
128
+ centres.push(far.lab);
129
+ }
130
+ let assign = new Array(pts.length).fill(0);
131
+ for (let it = 0; it < iterations; it++) {
132
+ assign = pts.map((p) => { let bi = 0; let bd = Infinity; centres.forEach((c, i) => { const d = (c[0] - p.lab[0]) ** 2 + (c[1] - p.lab[1]) ** 2 + (c[2] - p.lab[2]) ** 2; if (d < bd) { bd = d; bi = i; } }); return bi; });
133
+ for (let i = 0; i < K; i++) {
134
+ let w = 0; const s = [0, 0, 0];
135
+ pts.forEach((p, j) => { if (assign[j] === i) { w += p.w; s[0] += p.lab[0] * p.w; s[1] += p.lab[1] * p.w; s[2] += p.lab[2] * p.w; } });
136
+ if (w) centres[i] = [s[0] / w, s[1] / w, s[2] / w];
137
+ }
138
+ }
139
+ const total = pts.reduce((n, p) => n + p.w, 0);
140
+ return centres.map((c, i) => {
141
+ let w = 0; const rgb = [0, 0, 0];
142
+ pts.forEach((p, j) => { if (assign[j] === i) { w += p.w; rgb[0] += p.rgb[0] * p.w; rgb[1] += p.rgb[1] * p.w; rgb[2] += p.rgb[2] * p.w; } });
143
+ return w ? { hex: rgbToHex(rgb.map((x) => x / w)), share: +(w / total).toFixed(3) } : null;
144
+ }).filter(Boolean).sort((a, b) => b.share - a.share);
145
+ }
146
+
147
+ /** Relative luminance (0..1) of a hex colour, for "is this a light palette". */
148
+ export function luminance(hex) {
149
+ const rgb = hexToRgb(hex);
150
+ if (!rgb) return 0;
151
+ const [r, g, b] = rgb.map(srgbToLinear);
152
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
153
+ }
154
+
155
+ /** Interpolate two hex colours (t 0..1). */
156
+ export function mix(a, b, t) {
157
+ const x = hexToRgb(a); const y = hexToRgb(b);
158
+ if (!x || !y) return a;
159
+ return rgbToHex(x.map((v, i) => v + (y[i] - v) * t));
160
+ }