@idle-screens/schema 3.1.1 → 3.3.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.
- package/FORMAT.md +71 -2
- package/dist/index.d.ts +97 -1
- package/dist/index.js +697 -136
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +947 -189
package/dist/index.js
CHANGED
|
@@ -42,11 +42,149 @@ var LIMITS = {
|
|
|
42
42
|
maxPathPoints: 24,
|
|
43
43
|
minPathDuration: 2e3,
|
|
44
44
|
// ms — a path lap can't be faster than this
|
|
45
|
-
maxGridColumns: 100
|
|
45
|
+
maxGridColumns: 100,
|
|
46
|
+
maxTextBlockLength: 2e3,
|
|
47
|
+
minTextBlockFontSize: 0.01,
|
|
48
|
+
maxTextBlockFontSize: 0.2,
|
|
49
|
+
maxTextBlockMaxWidth: 1,
|
|
50
|
+
maxSegments: 24,
|
|
51
|
+
minSegmentDuration: 1e3,
|
|
52
|
+
// ms — flash-safety floor: segment cuts are luminance transitions
|
|
53
|
+
maxTransitionDur: 5e3,
|
|
54
|
+
minTransitionDur: 200
|
|
46
55
|
};
|
|
47
56
|
|
|
57
|
+
// src/steer.ts
|
|
58
|
+
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
59
|
+
function resolveSpecPath(spec, path) {
|
|
60
|
+
if (!spec || typeof spec !== "object" || !path) return null;
|
|
61
|
+
const parts = path.split(".");
|
|
62
|
+
if (parts.some((p) => UNSAFE_KEYS.has(p))) return null;
|
|
63
|
+
const s = spec;
|
|
64
|
+
if (parts[0] !== "layers" && parts[0] !== "background" && Array.isArray(s.layers)) {
|
|
65
|
+
const idx = s.layers.findIndex((l) => l && l.key === parts[0]);
|
|
66
|
+
if (idx === -1) return null;
|
|
67
|
+
parts.splice(0, 1, "layers", String(idx));
|
|
68
|
+
}
|
|
69
|
+
let node = spec;
|
|
70
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
71
|
+
if (node === null || typeof node !== "object") return null;
|
|
72
|
+
node = node[parts[i]];
|
|
73
|
+
}
|
|
74
|
+
if (node === null || typeof node !== "object") return null;
|
|
75
|
+
const last = parts[parts.length - 1];
|
|
76
|
+
const key = Array.isArray(node) ? Number(last) : last;
|
|
77
|
+
const exists = Array.isArray(node) ? Number.isInteger(key) && key >= 0 && key < node.length : last in node;
|
|
78
|
+
return exists ? { parent: node, key } : null;
|
|
79
|
+
}
|
|
80
|
+
function applyDeltasToSpec(spec, deltas) {
|
|
81
|
+
const copy = JSON.parse(JSON.stringify(spec));
|
|
82
|
+
for (const d of deltas) {
|
|
83
|
+
const loc = resolveSpecPath(copy, d.path);
|
|
84
|
+
if (loc) loc.parent[loc.key] = d.value;
|
|
85
|
+
}
|
|
86
|
+
return copy;
|
|
87
|
+
}
|
|
88
|
+
var HEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
89
|
+
function hexToRgb(hex) {
|
|
90
|
+
const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
|
|
91
|
+
const n = parseInt(h.slice(1), 16);
|
|
92
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
93
|
+
}
|
|
94
|
+
function lerpHex(a, b, k) {
|
|
95
|
+
const ca = hexToRgb(a);
|
|
96
|
+
const cb = hexToRgb(b);
|
|
97
|
+
const mix = ca.map((v, i) => Math.round(v + (cb[i] - v) * k));
|
|
98
|
+
return `#${mix.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
99
|
+
}
|
|
100
|
+
function lerpSpec(from, to, k) {
|
|
101
|
+
const kk = Math.max(0, Math.min(1, k));
|
|
102
|
+
const walk = (a, b, key) => {
|
|
103
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
104
|
+
const v = a + (b - a) * kk;
|
|
105
|
+
return key === "count" ? Math.max(1, Math.round(v)) : v;
|
|
106
|
+
}
|
|
107
|
+
if (typeof a === "string" && typeof b === "string" && HEX.test(a) && HEX.test(b)) {
|
|
108
|
+
return lerpHex(a, b, kk);
|
|
109
|
+
}
|
|
110
|
+
if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
|
|
111
|
+
return b.map((bv, i) => walk(a[i], bv, i));
|
|
112
|
+
}
|
|
113
|
+
if (a && b && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
|
|
114
|
+
const out = {};
|
|
115
|
+
for (const kName of Object.keys(b)) {
|
|
116
|
+
out[kName] = walk(a[kName], b[kName], kName);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
return kk > 0 ? b : a;
|
|
121
|
+
};
|
|
122
|
+
return walk(from, to);
|
|
123
|
+
}
|
|
124
|
+
function steerablePaths(spec) {
|
|
125
|
+
if (!spec || typeof spec !== "object") return [];
|
|
126
|
+
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "mode", "curve", "layer"]);
|
|
127
|
+
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops"]);
|
|
128
|
+
const out = [];
|
|
129
|
+
const walk = (node, prefix, key) => {
|
|
130
|
+
if (Array.isArray(node) && INDEXED.has(key)) {
|
|
131
|
+
node.forEach((child, i) => walk(child, `${prefix}.${i}`, ""));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(node)) {
|
|
135
|
+
if (prefix) out.push(prefix);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (node && typeof node === "object") {
|
|
139
|
+
for (const [k, v] of Object.entries(node)) {
|
|
140
|
+
if (SKIP.has(k)) continue;
|
|
141
|
+
walk(v, prefix ? `${prefix}.${k}` : k, k);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (prefix) out.push(prefix);
|
|
146
|
+
};
|
|
147
|
+
walk(spec, "", "");
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
function easeSmooth(k) {
|
|
151
|
+
const c = Math.max(0, Math.min(1, k));
|
|
152
|
+
return c * c * (3 - 2 * c);
|
|
153
|
+
}
|
|
154
|
+
function structuralSignature(spec) {
|
|
155
|
+
return JSON.stringify([
|
|
156
|
+
spec.units,
|
|
157
|
+
spec.layers.map((l) => {
|
|
158
|
+
const s = l.sprite;
|
|
159
|
+
return [
|
|
160
|
+
l.count,
|
|
161
|
+
l.size,
|
|
162
|
+
l.region,
|
|
163
|
+
l.position,
|
|
164
|
+
l.motion,
|
|
165
|
+
l.wrap,
|
|
166
|
+
l.flip,
|
|
167
|
+
l.alpha,
|
|
168
|
+
l.pulse,
|
|
169
|
+
l.spin,
|
|
170
|
+
l.grow,
|
|
171
|
+
l.layout,
|
|
172
|
+
l.sprite.kind,
|
|
173
|
+
// Dimensional draws baked into entities: radius (circle/ring), length
|
|
174
|
+
// (streak), width+aspect (rect), palette pick (all shaped sprites).
|
|
175
|
+
s.radius,
|
|
176
|
+
s.length,
|
|
177
|
+
l.sprite.kind === "rect" ? [s.width, s.aspect] : void 0,
|
|
178
|
+
Array.isArray(s.colors) ? s.colors.length : void 0,
|
|
179
|
+
s.colorWeights,
|
|
180
|
+
l.sprite.kind === "emoji" ? [l.sprite.glyphs.length, l.sprite.cycle?.period] : l.sprite.kind === "text" ? [l.sprite.strings.length, l.sprite.cycle?.period] : l.sprite.kind === "textBlock" ? [l.sprite.fontSize] : void 0
|
|
181
|
+
];
|
|
182
|
+
})
|
|
183
|
+
]);
|
|
184
|
+
}
|
|
185
|
+
|
|
48
186
|
// src/validate.ts
|
|
49
|
-
var
|
|
187
|
+
var HEX2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
50
188
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
51
189
|
var isStr = (v) => typeof v === "string";
|
|
52
190
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
@@ -78,6 +216,7 @@ var KNOWN_STREAK = /* @__PURE__ */ new Set(["kind", "length", "color", "width",
|
|
|
78
216
|
var KNOWN_RECT = /* @__PURE__ */ new Set(["kind", "width", "aspect", "color", "colors", "colorWeights"]);
|
|
79
217
|
var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
|
|
80
218
|
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle"]);
|
|
219
|
+
var KNOWN_TEXT_BLOCK = /* @__PURE__ */ new Set(["kind", "text", "maxWidth", "fontSize", "lineHeight", "align", "color"]);
|
|
81
220
|
var KNOWN_DRIFT = /* @__PURE__ */ new Set(["type", "speed", "angle", "bidirectional", "bob"]);
|
|
82
221
|
var KNOWN_RISE = /* @__PURE__ */ new Set(["type", "speed", "sway"]);
|
|
83
222
|
var KNOWN_BOUNCE = /* @__PURE__ */ new Set(["type", "speed"]);
|
|
@@ -108,6 +247,14 @@ function validateSpec(spec) {
|
|
|
108
247
|
if (spec.units !== void 0 && spec.units !== "px" && spec.units !== "viewport") {
|
|
109
248
|
err("units", "must be 'px' | 'viewport'");
|
|
110
249
|
}
|
|
250
|
+
if (spec.units === "px" && Array.isArray(spec.layers)) {
|
|
251
|
+
for (const l of spec.layers) {
|
|
252
|
+
if (isObj(l) && isObj(l.sprite) && l.sprite.kind === "textBlock") {
|
|
253
|
+
err("units", "textBlock dimensions are viewport fractions \u2014 units: 'px' is not supported with textBlock sprites");
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
111
258
|
if (spec.referenceViewport !== void 0) {
|
|
112
259
|
if (!isNum(spec.referenceViewport) || spec.referenceViewport < 100 || spec.referenceViewport > 8640) {
|
|
113
260
|
err("referenceViewport", "must be a number between 100 and 8640");
|
|
@@ -134,7 +281,7 @@ function validateSpec(spec) {
|
|
|
134
281
|
return { valid: errors.length === 0, errors, warnings };
|
|
135
282
|
}
|
|
136
283
|
function color(v, path, err) {
|
|
137
|
-
if (!isStr(v) || !
|
|
284
|
+
if (!isStr(v) || !HEX2.test(v)) err(path, "must be a hex colour like #0a3a52");
|
|
138
285
|
}
|
|
139
286
|
function validateBackground(bg, err, warn) {
|
|
140
287
|
if (!isObj(bg)) return err("background", "must be an object");
|
|
@@ -407,8 +554,28 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
407
554
|
}
|
|
408
555
|
color(sprite.color, `${path}.color`, err);
|
|
409
556
|
validatePalette(sprite, path, err);
|
|
557
|
+
} else if (sprite.kind === "textBlock") {
|
|
558
|
+
knownSet = KNOWN_TEXT_BLOCK;
|
|
559
|
+
if (typeof sprite.text !== "string" || sprite.text.length === 0) {
|
|
560
|
+
err(`${path}.text`, "must be a non-empty string");
|
|
561
|
+
} else if (sprite.text.length > LIMITS.maxTextBlockLength) {
|
|
562
|
+
err(`${path}.text`, `must be at most ${LIMITS.maxTextBlockLength} characters`);
|
|
563
|
+
}
|
|
564
|
+
if (!isNum(sprite.maxWidth) || sprite.maxWidth <= 0 || sprite.maxWidth > LIMITS.maxTextBlockMaxWidth) {
|
|
565
|
+
err(`${path}.maxWidth`, `must be a positive number up to ${LIMITS.maxTextBlockMaxWidth} (viewport fraction)`);
|
|
566
|
+
}
|
|
567
|
+
if (!isNum(sprite.fontSize) || sprite.fontSize < LIMITS.minTextBlockFontSize || sprite.fontSize > LIMITS.maxTextBlockFontSize) {
|
|
568
|
+
err(`${path}.fontSize`, `must be between ${LIMITS.minTextBlockFontSize} and ${LIMITS.maxTextBlockFontSize} (viewport fraction)`);
|
|
569
|
+
}
|
|
570
|
+
if (sprite.lineHeight !== void 0 && (!isNum(sprite.lineHeight) || sprite.lineHeight < 0.5 || sprite.lineHeight > 4)) {
|
|
571
|
+
err(`${path}.lineHeight`, "must be between 0.5 and 4 (multiplier)");
|
|
572
|
+
}
|
|
573
|
+
if (sprite.align !== void 0 && !["left", "center", "right"].includes(sprite.align)) {
|
|
574
|
+
err(`${path}.align`, "must be 'left' | 'center' | 'right'");
|
|
575
|
+
}
|
|
576
|
+
if (sprite.color !== void 0) color(sprite.color, `${path}.color`, err);
|
|
410
577
|
} else {
|
|
411
|
-
err(`${path}.kind`, "must be emoji | text | circle | ring | streak | rect");
|
|
578
|
+
err(`${path}.kind`, "must be emoji | text | circle | ring | streak | rect | textBlock");
|
|
412
579
|
return;
|
|
413
580
|
}
|
|
414
581
|
for (const k of unknownKeys(sprite, knownSet)) {
|
|
@@ -576,6 +743,107 @@ ${r.errors.map((e) => ` ${e.path || "<root>"}: ${e.message}`).join("\n")}`);
|
|
|
576
743
|
}
|
|
577
744
|
return spec;
|
|
578
745
|
}
|
|
746
|
+
function validateSequence(seq) {
|
|
747
|
+
const errors = [];
|
|
748
|
+
const warnings = [];
|
|
749
|
+
const err = (path, message) => void errors.push({ path, message });
|
|
750
|
+
if (!isObj(seq)) return { valid: false, errors: [{ path: "", message: "sequence must be an object" }], warnings: [] };
|
|
751
|
+
if (seq.format !== "idle-sequence") err("format", "must be 'idle-sequence'");
|
|
752
|
+
if (seq.schemaVersion !== SCHEMA_VERSION) err("schemaVersion", `must be ${SCHEMA_VERSION}`);
|
|
753
|
+
if (!isStr(seq.id) || seq.id.trim() === "") err("id", "must be a non-empty string");
|
|
754
|
+
if (!isStr(seq.label) || seq.label.trim() === "") err("label", "must be a non-empty string");
|
|
755
|
+
if (seq.seed !== void 0 && !isNum(seq.seed)) err("seed", "must be a number");
|
|
756
|
+
if (typeof seq.loop !== "boolean") err("loop", "must be a boolean");
|
|
757
|
+
if (!Array.isArray(seq.segments) || seq.segments.length === 0) {
|
|
758
|
+
err("segments", "must be a non-empty array");
|
|
759
|
+
return { valid: false, errors, warnings };
|
|
760
|
+
}
|
|
761
|
+
if (seq.segments.length > LIMITS.maxSegments) {
|
|
762
|
+
err("segments", `at most ${LIMITS.maxSegments} segments`);
|
|
763
|
+
}
|
|
764
|
+
const keys = /* @__PURE__ */ new Set();
|
|
765
|
+
let hasDurationless = false;
|
|
766
|
+
for (let i = 0; i < seq.segments.length; i++) {
|
|
767
|
+
const s = seq.segments[i];
|
|
768
|
+
const p = `segments[${i}]`;
|
|
769
|
+
if (!isObj(s)) {
|
|
770
|
+
err(p, "must be an object");
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
if (!isStr(s.key) || s.key.trim() === "") {
|
|
774
|
+
err(`${p}.key`, "must be a non-empty string");
|
|
775
|
+
} else if (keys.has(s.key)) {
|
|
776
|
+
err(`${p}.key`, `duplicate key '${s.key}'`);
|
|
777
|
+
} else {
|
|
778
|
+
keys.add(s.key);
|
|
779
|
+
}
|
|
780
|
+
if (s.duration !== void 0) {
|
|
781
|
+
if (hasDurationless) {
|
|
782
|
+
err(`${p}.duration`, "a segment after a durationless segment may not have a duration");
|
|
783
|
+
}
|
|
784
|
+
if (!isNum(s.duration) || s.duration < LIMITS.minSegmentDuration) {
|
|
785
|
+
err(`${p}.duration`, `must be >= ${LIMITS.minSegmentDuration} ms`);
|
|
786
|
+
}
|
|
787
|
+
} else {
|
|
788
|
+
if (i < seq.segments.length - 1) {
|
|
789
|
+
err(`${p}.duration`, "only the final segment may omit duration");
|
|
790
|
+
}
|
|
791
|
+
hasDurationless = true;
|
|
792
|
+
}
|
|
793
|
+
if (s.advance !== void 0 && !["auto", "input", "either"].includes(s.advance)) {
|
|
794
|
+
err(`${p}.advance`, "must be 'auto' | 'input' | 'either'");
|
|
795
|
+
}
|
|
796
|
+
if (s.transition !== void 0) {
|
|
797
|
+
if (!isObj(s.transition)) {
|
|
798
|
+
err(`${p}.transition`, "must be an object");
|
|
799
|
+
} else if (s.transition.type === "morph") {
|
|
800
|
+
if (!isNum(s.transition.dur) || s.transition.dur < LIMITS.minTransitionDur || s.transition.dur > LIMITS.maxTransitionDur) {
|
|
801
|
+
err(`${p}.transition.dur`, `must be a number between ${LIMITS.minTransitionDur} and ${LIMITS.maxTransitionDur}`);
|
|
802
|
+
}
|
|
803
|
+
} else if (s.transition.type !== "cut") {
|
|
804
|
+
err(`${p}.transition.type`, "must be 'cut' or 'morph'");
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (isObj(s.scene)) {
|
|
808
|
+
const sceneResult = validateSpec(s.scene);
|
|
809
|
+
for (const e of sceneResult.errors) errors.push({ path: `${p}.scene.${e.path}`, message: e.message });
|
|
810
|
+
if (sceneResult.warnings) {
|
|
811
|
+
for (const w of sceneResult.warnings) warnings.push({ path: `${p}.scene.${w.path}`, code: w.code, message: w.message });
|
|
812
|
+
}
|
|
813
|
+
} else {
|
|
814
|
+
err(`${p}.scene`, "must be a valid SaverSpec object");
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (seq.loop === true && hasDurationless) {
|
|
818
|
+
err("loop", "loop: true requires all segments to have a duration");
|
|
819
|
+
}
|
|
820
|
+
if (errors.length === 0 && Array.isArray(seq.segments)) {
|
|
821
|
+
for (let i = 0; i < seq.segments.length; i++) {
|
|
822
|
+
const s = seq.segments[i];
|
|
823
|
+
if (!isObj(s) || !isObj(s.transition) || s.transition.type !== "morph") continue;
|
|
824
|
+
const next = seq.segments[i + 1];
|
|
825
|
+
if (!next || !isObj(next) || !isObj(s.scene) || !isObj(next.scene)) continue;
|
|
826
|
+
const sigA = structuralSignature(s.scene);
|
|
827
|
+
const sigB = structuralSignature(next.scene);
|
|
828
|
+
if (sigA !== sigB) {
|
|
829
|
+
warnings.push({
|
|
830
|
+
path: `segments[${i}].transition`,
|
|
831
|
+
code: "morph-structural-mismatch",
|
|
832
|
+
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
838
|
+
}
|
|
839
|
+
function assertValidSequence(seq) {
|
|
840
|
+
const r = validateSequence(seq);
|
|
841
|
+
if (!r.valid) {
|
|
842
|
+
throw new Error(`invalid sequence:
|
|
843
|
+
${r.errors.map((e) => ` ${e.path || "<root>"}: ${e.message}`).join("\n")}`);
|
|
844
|
+
}
|
|
845
|
+
return seq;
|
|
846
|
+
}
|
|
579
847
|
|
|
580
848
|
// src/simulate.ts
|
|
581
849
|
var WARP_NEAR = 0.08;
|
|
@@ -660,7 +928,7 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
660
928
|
}
|
|
661
929
|
const out = [];
|
|
662
930
|
for (let i = 0; i < effectiveCount; i++) {
|
|
663
|
-
const size = sprite.kind === "circle" || sprite.kind === "ring" ? rng.range(sprite.radius[0], sprite.radius[1]) * 2 * scale : sprite.kind === "streak" ? rng.range(sprite.length[0], sprite.length[1]) * scale : sprite.kind === "rect" ? rng.range(sprite.width[0], sprite.width[1]) * scale : rng.range(smin, smax) * scale;
|
|
931
|
+
const size = sprite.kind === "textBlock" ? sprite.fontSize * scale : sprite.kind === "circle" || sprite.kind === "ring" ? rng.range(sprite.radius[0], sprite.radius[1]) * 2 * scale : sprite.kind === "streak" ? rng.range(sprite.length[0], sprite.length[1]) * scale : sprite.kind === "rect" ? rng.range(sprite.width[0], sprite.width[1]) * scale : rng.range(smin, smax) * scale;
|
|
664
932
|
const size2 = sprite.kind === "rect" ? size * (sprite.aspect ? rng.range(sprite.aspect[0], sprite.aspect[1]) : 1) : void 0;
|
|
665
933
|
let vx = 0;
|
|
666
934
|
let vy = 0;
|
|
@@ -1044,139 +1312,84 @@ function linkPairs(positions, k, maxDist, doWrap, w, h) {
|
|
|
1044
1312
|
}
|
|
1045
1313
|
return out;
|
|
1046
1314
|
}
|
|
1315
|
+
var CHAR_NARROW = new Set(`iIljtf1!|.,;:'"()[]{}`);
|
|
1316
|
+
var CHAR_WIDE = new Set("mwMWGOQD@%");
|
|
1317
|
+
function charWidthEm(ch) {
|
|
1318
|
+
if (ch === " " || ch === " ") return 0.3;
|
|
1319
|
+
if (CHAR_NARROW.has(ch)) return 0.35;
|
|
1320
|
+
if (CHAR_WIDE.has(ch)) return 0.72;
|
|
1321
|
+
return 0.55;
|
|
1322
|
+
}
|
|
1323
|
+
function textWidthEm(str) {
|
|
1324
|
+
let w = 0;
|
|
1325
|
+
for (let i = 0; i < str.length; i++) w += charWidthEm(str[i]);
|
|
1326
|
+
return w;
|
|
1327
|
+
}
|
|
1328
|
+
function breakTextBlock(text, maxWidthEm) {
|
|
1329
|
+
const paragraphs = text.split("\n");
|
|
1330
|
+
const lines = [];
|
|
1331
|
+
for (const para of paragraphs) {
|
|
1332
|
+
if (para.length === 0) {
|
|
1333
|
+
lines.push({ text: "", widthEm: 0 });
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
const words = para.split(/\s+/).filter((w) => w.length > 0);
|
|
1337
|
+
if (words.length === 0) {
|
|
1338
|
+
lines.push({ text: "", widthEm: 0 });
|
|
1339
|
+
continue;
|
|
1340
|
+
}
|
|
1341
|
+
let lineText = words[0];
|
|
1342
|
+
let lineW = textWidthEm(lineText);
|
|
1343
|
+
const spaceW = charWidthEm(" ");
|
|
1344
|
+
for (let i = 1; i < words.length; i++) {
|
|
1345
|
+
const word = words[i];
|
|
1346
|
+
const wordW = textWidthEm(word);
|
|
1347
|
+
if (lineW + spaceW + wordW <= maxWidthEm) {
|
|
1348
|
+
lineText += " " + word;
|
|
1349
|
+
lineW += spaceW + wordW;
|
|
1350
|
+
} else {
|
|
1351
|
+
lines.push({ text: lineText, widthEm: lineW });
|
|
1352
|
+
lineText = word;
|
|
1353
|
+
lineW = wordW;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
lines.push({ text: lineText, widthEm: lineW });
|
|
1357
|
+
}
|
|
1358
|
+
return lines;
|
|
1359
|
+
}
|
|
1047
1360
|
|
|
1048
1361
|
// src/compile.ts
|
|
1049
1362
|
import {
|
|
1050
1363
|
createRng
|
|
1051
1364
|
} from "@idle-screens/core";
|
|
1052
1365
|
|
|
1053
|
-
// src/
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
if (
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
const idx = s.layers.findIndex((l) => l && l.key === parts[0]);
|
|
1062
|
-
if (idx === -1) return null;
|
|
1063
|
-
parts.splice(0, 1, "layers", String(idx));
|
|
1366
|
+
// src/sequence.ts
|
|
1367
|
+
function resolveSegment(seq, T) {
|
|
1368
|
+
const { segments, loop } = seq;
|
|
1369
|
+
if (segments.length === 0) return { index: 0, localT: 0, startT: 0 };
|
|
1370
|
+
let totalTimed = 0;
|
|
1371
|
+
for (const seg of segments) {
|
|
1372
|
+
if (seg.duration != null) totalTimed += seg.duration;
|
|
1373
|
+
else break;
|
|
1064
1374
|
}
|
|
1065
|
-
let
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
node = node[parts[i]];
|
|
1069
|
-
}
|
|
1070
|
-
if (node === null || typeof node !== "object") return null;
|
|
1071
|
-
const last = parts[parts.length - 1];
|
|
1072
|
-
const key = Array.isArray(node) ? Number(last) : last;
|
|
1073
|
-
const exists = Array.isArray(node) ? Number.isInteger(key) && key >= 0 && key < node.length : last in node;
|
|
1074
|
-
return exists ? { parent: node, key } : null;
|
|
1075
|
-
}
|
|
1076
|
-
function applyDeltasToSpec(spec, deltas) {
|
|
1077
|
-
const copy = JSON.parse(JSON.stringify(spec));
|
|
1078
|
-
for (const d of deltas) {
|
|
1079
|
-
const loc = resolveSpecPath(copy, d.path);
|
|
1080
|
-
if (loc) loc.parent[loc.key] = d.value;
|
|
1375
|
+
let t = Math.max(0, T);
|
|
1376
|
+
if (loop && totalTimed > 0) {
|
|
1377
|
+
t = t % totalTimed;
|
|
1081
1378
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
const n = parseInt(h.slice(1), 16);
|
|
1088
|
-
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
1089
|
-
}
|
|
1090
|
-
function lerpHex(a, b, k) {
|
|
1091
|
-
const ca = hexToRgb(a);
|
|
1092
|
-
const cb = hexToRgb(b);
|
|
1093
|
-
const mix = ca.map((v, i) => Math.round(v + (cb[i] - v) * k));
|
|
1094
|
-
return `#${mix.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
1095
|
-
}
|
|
1096
|
-
function lerpSpec(from, to, k) {
|
|
1097
|
-
const kk = Math.max(0, Math.min(1, k));
|
|
1098
|
-
const walk = (a, b, key) => {
|
|
1099
|
-
if (typeof a === "number" && typeof b === "number") {
|
|
1100
|
-
const v = a + (b - a) * kk;
|
|
1101
|
-
return key === "count" ? Math.max(1, Math.round(v)) : v;
|
|
1102
|
-
}
|
|
1103
|
-
if (typeof a === "string" && typeof b === "string" && HEX2.test(a) && HEX2.test(b)) {
|
|
1104
|
-
return lerpHex(a, b, kk);
|
|
1105
|
-
}
|
|
1106
|
-
if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
|
|
1107
|
-
return b.map((bv, i) => walk(a[i], bv, i));
|
|
1379
|
+
let cumulative = 0;
|
|
1380
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1381
|
+
const seg = segments[i];
|
|
1382
|
+
if (seg.duration == null) {
|
|
1383
|
+
return { index: i, localT: t - cumulative, startT: cumulative };
|
|
1108
1384
|
}
|
|
1109
|
-
if (
|
|
1110
|
-
|
|
1111
|
-
for (const kName of Object.keys(b)) {
|
|
1112
|
-
out[kName] = walk(a[kName], b[kName], kName);
|
|
1113
|
-
}
|
|
1114
|
-
return out;
|
|
1385
|
+
if (t < cumulative + seg.duration) {
|
|
1386
|
+
return { index: i, localT: t - cumulative, startT: cumulative };
|
|
1115
1387
|
}
|
|
1116
|
-
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
if (!spec || typeof spec !== "object") return [];
|
|
1122
|
-
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "mode", "curve", "layer"]);
|
|
1123
|
-
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops"]);
|
|
1124
|
-
const out = [];
|
|
1125
|
-
const walk = (node, prefix, key) => {
|
|
1126
|
-
if (Array.isArray(node) && INDEXED.has(key)) {
|
|
1127
|
-
node.forEach((child, i) => walk(child, `${prefix}.${i}`, ""));
|
|
1128
|
-
return;
|
|
1129
|
-
}
|
|
1130
|
-
if (Array.isArray(node)) {
|
|
1131
|
-
if (prefix) out.push(prefix);
|
|
1132
|
-
return;
|
|
1133
|
-
}
|
|
1134
|
-
if (node && typeof node === "object") {
|
|
1135
|
-
for (const [k, v] of Object.entries(node)) {
|
|
1136
|
-
if (SKIP.has(k)) continue;
|
|
1137
|
-
walk(v, prefix ? `${prefix}.${k}` : k, k);
|
|
1138
|
-
}
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1141
|
-
if (prefix) out.push(prefix);
|
|
1142
|
-
};
|
|
1143
|
-
walk(spec, "", "");
|
|
1144
|
-
return out;
|
|
1145
|
-
}
|
|
1146
|
-
function easeSmooth(k) {
|
|
1147
|
-
const c = Math.max(0, Math.min(1, k));
|
|
1148
|
-
return c * c * (3 - 2 * c);
|
|
1149
|
-
}
|
|
1150
|
-
function structuralSignature(spec) {
|
|
1151
|
-
return JSON.stringify([
|
|
1152
|
-
spec.units,
|
|
1153
|
-
spec.layers.map((l) => {
|
|
1154
|
-
const s = l.sprite;
|
|
1155
|
-
return [
|
|
1156
|
-
l.count,
|
|
1157
|
-
l.size,
|
|
1158
|
-
l.region,
|
|
1159
|
-
l.position,
|
|
1160
|
-
l.motion,
|
|
1161
|
-
l.wrap,
|
|
1162
|
-
l.flip,
|
|
1163
|
-
l.alpha,
|
|
1164
|
-
l.pulse,
|
|
1165
|
-
l.spin,
|
|
1166
|
-
l.grow,
|
|
1167
|
-
l.layout,
|
|
1168
|
-
l.sprite.kind,
|
|
1169
|
-
// Dimensional draws baked into entities: radius (circle/ring), length
|
|
1170
|
-
// (streak), width+aspect (rect), palette pick (all shaped sprites).
|
|
1171
|
-
s.radius,
|
|
1172
|
-
s.length,
|
|
1173
|
-
l.sprite.kind === "rect" ? [s.width, s.aspect] : void 0,
|
|
1174
|
-
Array.isArray(s.colors) ? s.colors.length : void 0,
|
|
1175
|
-
s.colorWeights,
|
|
1176
|
-
l.sprite.kind === "emoji" ? [l.sprite.glyphs.length, l.sprite.cycle?.period] : l.sprite.kind === "text" ? [l.sprite.strings.length, l.sprite.cycle?.period] : void 0
|
|
1177
|
-
];
|
|
1178
|
-
})
|
|
1179
|
-
]);
|
|
1388
|
+
cumulative += seg.duration;
|
|
1389
|
+
}
|
|
1390
|
+
const last = segments.length - 1;
|
|
1391
|
+
const lastStart = cumulative - (segments[last].duration ?? 0);
|
|
1392
|
+
return { index: last, localT: t - lastStart, startT: lastStart };
|
|
1180
1393
|
}
|
|
1181
1394
|
|
|
1182
1395
|
// src/compile.ts
|
|
@@ -1226,12 +1439,14 @@ var SpecInstance = class {
|
|
|
1226
1439
|
let canvas;
|
|
1227
1440
|
if (ctx.surface) {
|
|
1228
1441
|
canvas = ctx.surface;
|
|
1442
|
+
this.ownsCanvas = false;
|
|
1229
1443
|
} else {
|
|
1230
1444
|
const el = document.createElement("canvas");
|
|
1231
1445
|
el.style.cssText = "display:block;width:100%;height:100%";
|
|
1232
1446
|
el.setAttribute("aria-hidden", "true");
|
|
1233
1447
|
ctx.host.appendChild(el);
|
|
1234
1448
|
canvas = el;
|
|
1449
|
+
this.ownsCanvas = true;
|
|
1235
1450
|
}
|
|
1236
1451
|
this.canvas = canvas;
|
|
1237
1452
|
const c2d = canvas.getContext("2d", { alpha: false });
|
|
@@ -1247,6 +1462,7 @@ var SpecInstance = class {
|
|
|
1247
1462
|
}
|
|
1248
1463
|
spec;
|
|
1249
1464
|
canvas;
|
|
1465
|
+
ownsCanvas;
|
|
1250
1466
|
ctx;
|
|
1251
1467
|
saverCtx;
|
|
1252
1468
|
seed;
|
|
@@ -1365,7 +1581,7 @@ var SpecInstance = class {
|
|
|
1365
1581
|
const headAlpha = alphaAt(e, t) * lifeA;
|
|
1366
1582
|
const headSize = sizeAt(e, t);
|
|
1367
1583
|
const sprite = built.layer.sprite;
|
|
1368
|
-
const resolvedColor = sprite.kind === "circle" || sprite.kind === "ring" || sprite.kind === "streak" || sprite.kind === "rect" ? sprite.colors?.[e.colorIndex] ?? sprite.color : sprite.kind === "text" ? sprite.color ?? "#e6e8ef" : "#e6e8ef";
|
|
1584
|
+
const resolvedColor = sprite.kind === "circle" || sprite.kind === "ring" || sprite.kind === "streak" || sprite.kind === "rect" ? sprite.colors?.[e.colorIndex] ?? sprite.color : sprite.kind === "text" || sprite.kind === "textBlock" ? sprite.color ?? "#e6e8ef" : "#e6e8ef";
|
|
1369
1585
|
const isSoft = sprite.kind === "circle" && sprite.soft;
|
|
1370
1586
|
const wrap2 = built.layer.wrap !== false;
|
|
1371
1587
|
const head = this.entityPos(e, t, parentE);
|
|
@@ -1472,6 +1688,28 @@ var SpecInstance = class {
|
|
|
1472
1688
|
ctx.restore();
|
|
1473
1689
|
return;
|
|
1474
1690
|
}
|
|
1691
|
+
if (sprite.kind === "textBlock") {
|
|
1692
|
+
const unitScale2 = Math.min(this.w, this.h);
|
|
1693
|
+
const fsPx = sprite.fontSize * unitScale2;
|
|
1694
|
+
const lh = (sprite.lineHeight ?? 1.4) * fsPx;
|
|
1695
|
+
const maxWPx = sprite.maxWidth * unitScale2;
|
|
1696
|
+
const maxWEm = maxWPx / fsPx;
|
|
1697
|
+
const lines = breakTextBlock(sprite.text, maxWEm);
|
|
1698
|
+
const align = sprite.align ?? "left";
|
|
1699
|
+
ctx.save();
|
|
1700
|
+
ctx.translate(p.x, p.y);
|
|
1701
|
+
if (rot) ctx.rotate(rot);
|
|
1702
|
+
ctx.font = `${fsPx}px system-ui, sans-serif`;
|
|
1703
|
+
ctx.fillStyle = sprite.color ?? "#e6e8ef";
|
|
1704
|
+
ctx.textBaseline = "top";
|
|
1705
|
+
ctx.textAlign = align;
|
|
1706
|
+
const xOff = align === "center" ? maxWPx / 2 : align === "right" ? maxWPx : 0;
|
|
1707
|
+
for (let li = 0; li < lines.length; li++) {
|
|
1708
|
+
ctx.fillText(lines[li].text, xOff, li * lh);
|
|
1709
|
+
}
|
|
1710
|
+
ctx.restore();
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1475
1713
|
ctx.save();
|
|
1476
1714
|
ctx.translate(p.x, p.y);
|
|
1477
1715
|
if (rot) ctx.rotate(rot);
|
|
@@ -1639,9 +1877,29 @@ var SpecInstance = class {
|
|
|
1639
1877
|
this.rebuild();
|
|
1640
1878
|
if (this.paused) this.renderFrame(0, this.seed);
|
|
1641
1879
|
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Replace the rendered spec without triggering a transition glide.
|
|
1882
|
+
* Checks structural signature — use `hotSwapPaint` when the caller
|
|
1883
|
+
* already knows the signature is unchanged (e.g. mid-morph lerp).
|
|
1884
|
+
*/
|
|
1885
|
+
hotSwapSpec(spec) {
|
|
1886
|
+
this.effSpec = spec;
|
|
1887
|
+
const sig = structuralSignature(this.effSpec);
|
|
1888
|
+
if (sig !== this.lastStructural) this.rebuild();
|
|
1889
|
+
else this.layers.forEach((b, i) => {
|
|
1890
|
+
b.layer = this.effSpec.layers[i] ?? b.layer;
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
/** Paint-only hot-swap: skips structuralSignature (caller guarantees match). */
|
|
1894
|
+
hotSwapPaint(spec) {
|
|
1895
|
+
this.effSpec = spec;
|
|
1896
|
+
this.layers.forEach((b, i) => {
|
|
1897
|
+
b.layer = this.effSpec.layers[i] ?? b.layer;
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1642
1900
|
dispose() {
|
|
1643
1901
|
this.stop();
|
|
1644
|
-
if (typeof HTMLCanvasElement !== "undefined" && this.canvas instanceof HTMLCanvasElement) this.canvas.remove();
|
|
1902
|
+
if (this.ownsCanvas && typeof HTMLCanvasElement !== "undefined" && this.canvas instanceof HTMLCanvasElement) this.canvas.remove();
|
|
1645
1903
|
}
|
|
1646
1904
|
};
|
|
1647
1905
|
function compileSaver(spec) {
|
|
@@ -1652,6 +1910,192 @@ function compileSaver(spec) {
|
|
|
1652
1910
|
spec: valid
|
|
1653
1911
|
};
|
|
1654
1912
|
}
|
|
1913
|
+
var SequenceInstance = class {
|
|
1914
|
+
seq;
|
|
1915
|
+
childCtx;
|
|
1916
|
+
canvas;
|
|
1917
|
+
children;
|
|
1918
|
+
activeIndex = -1;
|
|
1919
|
+
paused = false;
|
|
1920
|
+
/**
|
|
1921
|
+
* When a morph is active, the child at `morphFromIndex` renders with a
|
|
1922
|
+
* lerped spec. The child stays keyed to the *outgoing* segment's slot so
|
|
1923
|
+
* its seed (and entity placement) is continuous.
|
|
1924
|
+
*/
|
|
1925
|
+
morphFromIndex = -1;
|
|
1926
|
+
constructor(seq, ctx) {
|
|
1927
|
+
this.seq = seq;
|
|
1928
|
+
let surface = ctx.surface ?? null;
|
|
1929
|
+
let canvas = null;
|
|
1930
|
+
if (!surface && typeof document !== "undefined") {
|
|
1931
|
+
canvas = document.createElement("canvas");
|
|
1932
|
+
canvas.style.cssText = "display:block;width:100%;height:100%";
|
|
1933
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
1934
|
+
ctx.host.appendChild(canvas);
|
|
1935
|
+
surface = canvas;
|
|
1936
|
+
}
|
|
1937
|
+
this.canvas = canvas;
|
|
1938
|
+
this.childCtx = { ...ctx, surface, reducedMotion: true };
|
|
1939
|
+
this.children = new Array(seq.segments.length).fill(null);
|
|
1940
|
+
}
|
|
1941
|
+
childSeed(index, fallback) {
|
|
1942
|
+
return this.seq.segments[index].scene.seed ?? this.seq.seed ?? fallback;
|
|
1943
|
+
}
|
|
1944
|
+
childScene(index) {
|
|
1945
|
+
const seg = this.seq.segments[index];
|
|
1946
|
+
if (seg.scene.seed != null) return seg.scene;
|
|
1947
|
+
if (this.seq.seed != null) return { ...seg.scene, seed: this.seq.seed + index };
|
|
1948
|
+
return seg.scene;
|
|
1949
|
+
}
|
|
1950
|
+
/** Whether the boundary from `from` to `from+1` should morph. */
|
|
1951
|
+
canMorph(from) {
|
|
1952
|
+
const seg = this.seq.segments[from];
|
|
1953
|
+
if (!seg || seg.transition?.type !== "morph") return false;
|
|
1954
|
+
const next = this.seq.segments[from + 1];
|
|
1955
|
+
if (!next) return false;
|
|
1956
|
+
return structuralSignature(seg.scene) === structuralSignature(next.scene);
|
|
1957
|
+
}
|
|
1958
|
+
/** Walk back through consecutive morph boundaries to find the chain origin. */
|
|
1959
|
+
morphChainRoot(index) {
|
|
1960
|
+
let i = index;
|
|
1961
|
+
while (i > 0 && this.canMorph(i - 1)) i--;
|
|
1962
|
+
return i;
|
|
1963
|
+
}
|
|
1964
|
+
morphDur(from) {
|
|
1965
|
+
const tr = this.seq.segments[from]?.transition;
|
|
1966
|
+
return tr?.type === "morph" ? tr.dur : 0;
|
|
1967
|
+
}
|
|
1968
|
+
ensureChild(index) {
|
|
1969
|
+
if (index < 0 || index >= this.seq.segments.length) index = 0;
|
|
1970
|
+
let child = this.children[index];
|
|
1971
|
+
if (!child) {
|
|
1972
|
+
child = new SpecInstance(this.childScene(index), this.childCtx);
|
|
1973
|
+
this.children[index] = child;
|
|
1974
|
+
if (this.paused) child.setPaused(true);
|
|
1975
|
+
}
|
|
1976
|
+
return child;
|
|
1977
|
+
}
|
|
1978
|
+
releaseChild(index) {
|
|
1979
|
+
const child = this.children[index];
|
|
1980
|
+
if (child) {
|
|
1981
|
+
child.dispose();
|
|
1982
|
+
this.children[index] = null;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
renderFrame(T, seed) {
|
|
1986
|
+
const resolved = resolveSegment(this.seq, T);
|
|
1987
|
+
const { index, localT } = resolved;
|
|
1988
|
+
const prevIdx = index > 0 ? index - 1 : -1;
|
|
1989
|
+
const morphActive = prevIdx >= 0 && this.canMorph(prevIdx) && localT < this.morphDur(prevIdx);
|
|
1990
|
+
if (morphActive) {
|
|
1991
|
+
const chainRoot = this.morphChainRoot(index);
|
|
1992
|
+
this.morphFromIndex = prevIdx;
|
|
1993
|
+
this.activeIndex = index;
|
|
1994
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
1995
|
+
if (i !== chainRoot) this.releaseChild(i);
|
|
1996
|
+
}
|
|
1997
|
+
const child = this.ensureChild(chainRoot);
|
|
1998
|
+
const dur = this.morphDur(prevIdx);
|
|
1999
|
+
const k = easeSmooth(localT / dur);
|
|
2000
|
+
const specA = this.childScene(prevIdx);
|
|
2001
|
+
const specB = this.childScene(index);
|
|
2002
|
+
child.hotSwapPaint(lerpSpec(specA, specB, k));
|
|
2003
|
+
child.renderFrame(localT, this.childSeed(chainRoot, seed));
|
|
2004
|
+
} else {
|
|
2005
|
+
if (this.morphFromIndex >= 0) {
|
|
2006
|
+
const oldRoot = this.morphChainRoot(this.morphFromIndex);
|
|
2007
|
+
this.releaseChild(oldRoot);
|
|
2008
|
+
if (oldRoot !== this.morphFromIndex) this.releaseChild(this.morphFromIndex);
|
|
2009
|
+
this.morphFromIndex = -1;
|
|
2010
|
+
}
|
|
2011
|
+
if (index !== this.activeIndex) {
|
|
2012
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
2013
|
+
if (i !== index) this.releaseChild(i);
|
|
2014
|
+
}
|
|
2015
|
+
this.activeIndex = index;
|
|
2016
|
+
}
|
|
2017
|
+
const chainRoot = this.morphChainRoot(index);
|
|
2018
|
+
const useMorphSeed = chainRoot < index;
|
|
2019
|
+
const effSeed = useMorphSeed ? this.childSeed(chainRoot, seed) : this.childSeed(index, seed);
|
|
2020
|
+
let child;
|
|
2021
|
+
if (useMorphSeed && !this.children[index]) {
|
|
2022
|
+
const rootScene = this.childScene(chainRoot);
|
|
2023
|
+
child = new SpecInstance(rootScene, this.childCtx);
|
|
2024
|
+
this.children[index] = child;
|
|
2025
|
+
if (this.paused) child.setPaused(true);
|
|
2026
|
+
child.hotSwapSpec(this.childScene(index));
|
|
2027
|
+
} else {
|
|
2028
|
+
child = this.ensureChild(index);
|
|
2029
|
+
if (useMorphSeed) child.hotSwapSpec(this.childScene(index));
|
|
2030
|
+
}
|
|
2031
|
+
child.renderFrame(localT, effSeed);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
setPaused(paused) {
|
|
2035
|
+
this.paused = paused;
|
|
2036
|
+
for (const child of this.children) {
|
|
2037
|
+
child?.setPaused(paused);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
resize(width, height, dpr) {
|
|
2041
|
+
for (const child of this.children) {
|
|
2042
|
+
child?.resize(width, height, dpr);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
applyTrack(track) {
|
|
2046
|
+
const deltas = track?.deltas ?? [];
|
|
2047
|
+
const segDelta = deltas.find((d) => d.path === "sequence.segment");
|
|
2048
|
+
if (segDelta !== void 0 && typeof segDelta.value === "number") {
|
|
2049
|
+
const idx = Math.max(0, Math.min(this.seq.segments.length - 1, Math.round(segDelta.value)));
|
|
2050
|
+
if (idx !== this.activeIndex) {
|
|
2051
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
2052
|
+
if (i !== idx) this.releaseChild(i);
|
|
2053
|
+
}
|
|
2054
|
+
this.activeIndex = idx;
|
|
2055
|
+
const child = this.ensureChild(idx);
|
|
2056
|
+
child.renderFrame(0, this.seq.segments[idx].scene.seed ?? this.seq.seed ?? 0);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
const childDeltas = deltas.filter((d) => d.path !== "sequence.segment");
|
|
2060
|
+
if (childDeltas.length > 0 && this.activeIndex >= 0) {
|
|
2061
|
+
const child = this.children[this.activeIndex];
|
|
2062
|
+
child?.applyTrack({ ...track, deltas: childDeltas });
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
dispose() {
|
|
2066
|
+
for (let i = 0; i < this.children.length; i++) {
|
|
2067
|
+
this.releaseChild(i);
|
|
2068
|
+
}
|
|
2069
|
+
if (this.canvas) this.canvas.remove();
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2072
|
+
function sequenceManifest(seq) {
|
|
2073
|
+
const maxTotal = seq.segments.reduce((max, s) => {
|
|
2074
|
+
const t = s.scene.layers.reduce((n, l) => n + l.count, 0);
|
|
2075
|
+
return Math.max(max, t);
|
|
2076
|
+
}, 0);
|
|
2077
|
+
const costTier = maxTotal < 30 ? "idle" : maxTotal < 150 ? "low" : maxTotal < 400 ? "medium" : "high";
|
|
2078
|
+
return {
|
|
2079
|
+
id: seq.id,
|
|
2080
|
+
label: seq.label,
|
|
2081
|
+
timeModel: "closed-form",
|
|
2082
|
+
passthrough: false,
|
|
2083
|
+
minBackend: "canvas2d",
|
|
2084
|
+
costTier,
|
|
2085
|
+
motionIntensity: "moderate",
|
|
2086
|
+
reducedMotionFallback: "static",
|
|
2087
|
+
a11y: { flashSafe: true },
|
|
2088
|
+
workerReady: false
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
function compileSequence(spec) {
|
|
2092
|
+
const valid = assertValidSequence(spec);
|
|
2093
|
+
return {
|
|
2094
|
+
manifest: sequenceManifest(valid),
|
|
2095
|
+
mount: (ctx) => new SequenceInstance(valid, ctx),
|
|
2096
|
+
spec: valid
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
1655
2099
|
|
|
1656
2100
|
// src/advise.ts
|
|
1657
2101
|
import { createRng as createRng2 } from "@idle-screens/core";
|
|
@@ -1671,7 +2115,7 @@ function spriteLuma(layer, e) {
|
|
|
1671
2115
|
if (s.kind === "circle" || s.kind === "ring" || s.kind === "streak" || s.kind === "rect") {
|
|
1672
2116
|
return hexLuma(s.colors?.[e.colorIndex] ?? s.color);
|
|
1673
2117
|
}
|
|
1674
|
-
if (s.kind === "text") return hexLuma(s.color ?? "#e6e8ef");
|
|
2118
|
+
if (s.kind === "text" || s.kind === "textBlock") return hexLuma(s.color ?? "#e6e8ef");
|
|
1675
2119
|
return 0.75;
|
|
1676
2120
|
}
|
|
1677
2121
|
function spriteHex(layer, e) {
|
|
@@ -1679,7 +2123,7 @@ function spriteHex(layer, e) {
|
|
|
1679
2123
|
if (s.kind === "circle" || s.kind === "ring" || s.kind === "streak" || s.kind === "rect") {
|
|
1680
2124
|
return s.colors?.[e.colorIndex] ?? s.color;
|
|
1681
2125
|
}
|
|
1682
|
-
if (s.kind === "text") return s.color ?? "#e6e8ef";
|
|
2126
|
+
if (s.kind === "text" || s.kind === "textBlock") return s.color ?? "#e6e8ef";
|
|
1683
2127
|
return null;
|
|
1684
2128
|
}
|
|
1685
2129
|
function backgroundLuma(spec) {
|
|
@@ -1734,7 +2178,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
1734
2178
|
const layer = spec.layers[li];
|
|
1735
2179
|
const entities = allEntities[li];
|
|
1736
2180
|
totalEntities += entities.length;
|
|
1737
|
-
const isText = layer.sprite.kind === "text";
|
|
2181
|
+
const isText = layer.sprite.kind === "text" || layer.sprite.kind === "textBlock";
|
|
1738
2182
|
const isStaticText = isText && layer.motion.type === "static";
|
|
1739
2183
|
if (isStaticText) textLayerCount++;
|
|
1740
2184
|
if (layer.motion.type !== "static") motionLayerCount++;
|
|
@@ -1793,7 +2237,18 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
1793
2237
|
const entities = allEntities[li];
|
|
1794
2238
|
for (const e of entities) {
|
|
1795
2239
|
const r = e.size / 2;
|
|
1796
|
-
|
|
2240
|
+
let pixArea;
|
|
2241
|
+
if (layer.sprite.kind === "circle") {
|
|
2242
|
+
pixArea = Math.PI * r * r;
|
|
2243
|
+
} else if (layer.sprite.kind === "textBlock") {
|
|
2244
|
+
const fsPx = layer.sprite.fontSize * scale;
|
|
2245
|
+
const lh = (layer.sprite.lineHeight ?? 1.4) * fsPx;
|
|
2246
|
+
const maxWPx = layer.sprite.maxWidth * scale;
|
|
2247
|
+
const lines = breakTextBlock(layer.sprite.text, maxWPx / fsPx);
|
|
2248
|
+
pixArea = maxWPx * lines.length * lh * 0.55;
|
|
2249
|
+
} else {
|
|
2250
|
+
pixArea = e.size * e.size;
|
|
2251
|
+
}
|
|
1797
2252
|
totalCoverage += pixArea * e.alpha / (w * h);
|
|
1798
2253
|
}
|
|
1799
2254
|
if (layer.links) {
|
|
@@ -1880,6 +2335,40 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
1880
2335
|
}
|
|
1881
2336
|
return warnings;
|
|
1882
2337
|
}
|
|
2338
|
+
function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
2339
|
+
const warnings = [];
|
|
2340
|
+
for (let i = 0; i < seq.segments.length; i++) {
|
|
2341
|
+
const segWarnings = adviseSpec(seq.segments[i].scene, viewport);
|
|
2342
|
+
for (const w of segWarnings) {
|
|
2343
|
+
warnings.push({ path: `segments[${i}].scene.${w.path}`, code: w.code, message: w.message });
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
for (let i = 0; i < seq.segments.length - 1; i++) {
|
|
2347
|
+
const lumaA = backgroundLuma(seq.segments[i].scene);
|
|
2348
|
+
const lumaB = backgroundLuma(seq.segments[i + 1].scene);
|
|
2349
|
+
const delta = Math.abs(lumaA - lumaB);
|
|
2350
|
+
if (delta > 0.5) {
|
|
2351
|
+
warnings.push({
|
|
2352
|
+
path: `segments[${i}]`,
|
|
2353
|
+
code: "boundary-luminance-jump",
|
|
2354
|
+
message: `background luminance jumps ${delta.toFixed(2)} at boundary ${i}\u2192${i + 1} \u2014 may flash on cut`
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
const tr = seq.segments[i].transition;
|
|
2358
|
+
if (tr?.type === "morph") {
|
|
2359
|
+
const sigA = structuralSignature(seq.segments[i].scene);
|
|
2360
|
+
const sigB = structuralSignature(seq.segments[i + 1].scene);
|
|
2361
|
+
if (sigA !== sigB) {
|
|
2362
|
+
warnings.push({
|
|
2363
|
+
path: `segments[${i}].transition`,
|
|
2364
|
+
code: "morph-structural-mismatch",
|
|
2365
|
+
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
return warnings;
|
|
2371
|
+
}
|
|
1883
2372
|
|
|
1884
2373
|
// src/describe.ts
|
|
1885
2374
|
import { createRng as createRng3 } from "@idle-screens/core";
|
|
@@ -2021,6 +2510,24 @@ function textBox(s, e, p) {
|
|
|
2021
2510
|
halfY: fh * 0.6
|
|
2022
2511
|
};
|
|
2023
2512
|
}
|
|
2513
|
+
function textBlockBox(s, _e, p, w, h) {
|
|
2514
|
+
const unitScale = Math.min(w, h);
|
|
2515
|
+
const fsPx = s.fontSize * unitScale;
|
|
2516
|
+
const lh = (s.lineHeight ?? 1.4) * fsPx;
|
|
2517
|
+
const maxWPx = s.maxWidth * unitScale;
|
|
2518
|
+
const maxWEm = maxWPx / fsPx;
|
|
2519
|
+
const lines = breakTextBlock(s.text, maxWEm);
|
|
2520
|
+
const totalH = lines.length * lh;
|
|
2521
|
+
const maxLineW = lines.reduce((m, l) => Math.max(m, l.widthEm), 0) * fsPx;
|
|
2522
|
+
const align = s.align ?? "left";
|
|
2523
|
+
const cx = align === "center" ? p.x + maxWPx / 2 : align === "right" ? p.x + maxWPx - maxLineW / 2 : p.x + maxLineW / 2;
|
|
2524
|
+
return {
|
|
2525
|
+
cx,
|
|
2526
|
+
cy: p.y + totalH / 2,
|
|
2527
|
+
halfX: maxLineW / 2,
|
|
2528
|
+
halfY: totalH / 2
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2024
2531
|
function luminanceGrid(spec, opts = {}) {
|
|
2025
2532
|
const scene = buildScene(spec, opts);
|
|
2026
2533
|
const t = opts.t ?? 5e3;
|
|
@@ -2142,6 +2649,12 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
2142
2649
|
centerY = box.cy;
|
|
2143
2650
|
halfX = box.halfX;
|
|
2144
2651
|
halfY = box.halfY;
|
|
2652
|
+
} else if (s.kind === "textBlock") {
|
|
2653
|
+
const box = textBlockBox(s, e, p, w, h);
|
|
2654
|
+
centerX = box.cx;
|
|
2655
|
+
centerY = box.cy;
|
|
2656
|
+
halfX = box.halfX;
|
|
2657
|
+
halfY = box.halfY;
|
|
2145
2658
|
}
|
|
2146
2659
|
const circular = s.kind === "circle" || s.kind === "ring";
|
|
2147
2660
|
const soft = s.kind === "circle" && !!s.soft;
|
|
@@ -2153,7 +2666,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
2153
2666
|
const c1 = Math.min(cols - 1, Math.floor((centerX + reachX) / cellW));
|
|
2154
2667
|
const r0 = Math.max(0, Math.floor((centerY - reachY) / cellH));
|
|
2155
2668
|
const r1 = Math.min(rows - 1, Math.floor((centerY + reachY) / cellH));
|
|
2156
|
-
const inkWeight = s.kind === "text" || s.kind === "emoji" ? 0.55 : 1;
|
|
2669
|
+
const inkWeight = s.kind === "text" || s.kind === "emoji" || s.kind === "textBlock" ? 0.55 : 1;
|
|
2157
2670
|
for (let r = r0; r <= r1; r++) {
|
|
2158
2671
|
for (let c = c0; c <= c1; c++) {
|
|
2159
2672
|
const dx = (c + 0.5) * cellW - centerX;
|
|
@@ -2312,6 +2825,13 @@ function textSprites(spec, opts = {}) {
|
|
|
2312
2825
|
const t = opts.t ?? 5e3;
|
|
2313
2826
|
const out = [];
|
|
2314
2827
|
scene.layers.forEach(({ layer, entities }, layerIndex) => {
|
|
2828
|
+
if (layer.sprite.kind === "textBlock") {
|
|
2829
|
+
const s2 = layer.sprite;
|
|
2830
|
+
const unitScale = Math.min(scene.w, scene.h);
|
|
2831
|
+
const sizePx2 = s2.fontSize * unitScale;
|
|
2832
|
+
out.push({ layerIndex, key: layer.key, strings: [s2.text], count: entities.length, sizePx: Math.round(sizePx2) });
|
|
2833
|
+
return;
|
|
2834
|
+
}
|
|
2315
2835
|
if (layer.sprite.kind !== "text") return;
|
|
2316
2836
|
const s = layer.sprite;
|
|
2317
2837
|
const m = s.font ? /(\d+(?:\.\d+)?)px/.exec(s.font) : null;
|
|
@@ -2362,6 +2882,9 @@ function dominanceRanking(spec, opts = {}) {
|
|
|
2362
2882
|
} else if (s.kind === "text") {
|
|
2363
2883
|
const box = textBox(s, e, { x: 0, y: 0 });
|
|
2364
2884
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
|
|
2885
|
+
} else if (s.kind === "textBlock") {
|
|
2886
|
+
const box = textBlockBox(s, e, { x: 0, y: 0 }, w, h);
|
|
2887
|
+
entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
|
|
2365
2888
|
} else entArea = sz * sz * 0.55;
|
|
2366
2889
|
area += entArea * a;
|
|
2367
2890
|
if (layer.trail) {
|
|
@@ -3213,6 +3736,36 @@ var DASHBOARD_SPEC = {
|
|
|
3213
3736
|
]
|
|
3214
3737
|
};
|
|
3215
3738
|
|
|
3739
|
+
// src/examples/haiku.ts
|
|
3740
|
+
var HAIKU_SPEC = {
|
|
3741
|
+
schemaVersion: 1,
|
|
3742
|
+
id: "haiku",
|
|
3743
|
+
label: "Haiku",
|
|
3744
|
+
seed: 42,
|
|
3745
|
+
background: { type: "solid", color: "#080812" },
|
|
3746
|
+
layers: [
|
|
3747
|
+
{
|
|
3748
|
+
count: 1,
|
|
3749
|
+
sprite: {
|
|
3750
|
+
kind: "textBlock",
|
|
3751
|
+
text: "An old silent pond\nA frog jumps into the pond \u2014\nSplash! Silence again.",
|
|
3752
|
+
maxWidth: 0.8,
|
|
3753
|
+
fontSize: 0.05,
|
|
3754
|
+
lineHeight: 1.8,
|
|
3755
|
+
align: "center",
|
|
3756
|
+
color: "#c8cad4"
|
|
3757
|
+
},
|
|
3758
|
+
motion: { type: "static" },
|
|
3759
|
+
position: { x: 0.1, y: 0.35 }
|
|
3760
|
+
},
|
|
3761
|
+
{
|
|
3762
|
+
count: 30,
|
|
3763
|
+
sprite: { kind: "circle", radius: [5e-4, 15e-4], color: "#3a3a5c" },
|
|
3764
|
+
motion: { type: "drift", speed: [2e-3, 6e-3], angle: 270 }
|
|
3765
|
+
}
|
|
3766
|
+
]
|
|
3767
|
+
};
|
|
3768
|
+
|
|
3216
3769
|
// src/examples/lanterns.ts
|
|
3217
3770
|
var LANTERNS_SPEC = {
|
|
3218
3771
|
schemaVersion: 1,
|
|
@@ -3906,7 +4459,8 @@ var SCHEMA_EXAMPLES = [
|
|
|
3906
4459
|
{ id: "polygons", label: "Polygons", spec: POLYGONS_SPEC },
|
|
3907
4460
|
{ id: "matrix-rain", label: "Matrix Rain", spec: MATRIX_RAIN_SPEC },
|
|
3908
4461
|
{ id: "procession", label: "Night Procession", spec: PROCESSION_SPEC },
|
|
3909
|
-
{ id: "nostalghia-candle", label: "Nostalghia's Candle", spec: NOSTALGHIA_CANDLE_SPEC }
|
|
4462
|
+
{ id: "nostalghia-candle", label: "Nostalghia's Candle", spec: NOSTALGHIA_CANDLE_SPEC },
|
|
4463
|
+
{ id: "haiku", label: "Haiku", spec: HAIKU_SPEC }
|
|
3910
4464
|
];
|
|
3911
4465
|
var EXAMPLE_SPECS = SCHEMA_EXAMPLES.map((e) => e.spec);
|
|
3912
4466
|
var EXAMPLE_BY_ID = Object.fromEntries(
|
|
@@ -3920,6 +4474,7 @@ export {
|
|
|
3920
4474
|
DASHBOARD_SPEC,
|
|
3921
4475
|
EXAMPLE_BY_ID,
|
|
3922
4476
|
EXAMPLE_SPECS,
|
|
4477
|
+
HAIKU_SPEC,
|
|
3923
4478
|
LANTERNS_SPEC,
|
|
3924
4479
|
LIMITS,
|
|
3925
4480
|
MATRIX_RAIN_SPEC,
|
|
@@ -3934,12 +4489,16 @@ export {
|
|
|
3934
4489
|
SNOWFALL_SPEC,
|
|
3935
4490
|
WARP_NEAR,
|
|
3936
4491
|
WARP_TUNNEL_SPEC,
|
|
4492
|
+
adviseSequence,
|
|
3937
4493
|
adviseSpec,
|
|
3938
4494
|
alphaAt,
|
|
3939
4495
|
applyDeltasToSpec,
|
|
4496
|
+
assertValidSequence,
|
|
3940
4497
|
assertValidSpec,
|
|
4498
|
+
breakTextBlock,
|
|
3941
4499
|
buildEntities,
|
|
3942
4500
|
compileSaver,
|
|
4501
|
+
compileSequence,
|
|
3943
4502
|
describeScene,
|
|
3944
4503
|
diffScenes,
|
|
3945
4504
|
dominanceRanking,
|
|
@@ -3956,6 +4515,7 @@ export {
|
|
|
3956
4515
|
positionAt,
|
|
3957
4516
|
renderBrailleMap,
|
|
3958
4517
|
renderDensityMap,
|
|
4518
|
+
resolveSegment,
|
|
3959
4519
|
resolveSpecPath,
|
|
3960
4520
|
rotationAt,
|
|
3961
4521
|
sizeAt,
|
|
@@ -3964,6 +4524,7 @@ export {
|
|
|
3964
4524
|
steerablePaths,
|
|
3965
4525
|
structuralSignature,
|
|
3966
4526
|
textSprites,
|
|
4527
|
+
validateSequence,
|
|
3967
4528
|
validateSpec
|
|
3968
4529
|
};
|
|
3969
4530
|
//# sourceMappingURL=index.js.map
|