@idle-screens/schema 3.2.0 → 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 +8 -2
- package/dist/index.d.ts +14 -3
- package/dist/index.js +277 -138
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +20 -6
package/FORMAT.md
CHANGED
|
@@ -313,8 +313,14 @@ the final segment may omit duration (holds indefinitely).
|
|
|
313
313
|
**Advance mode:** `auto` (default), `input`, or `either`. Validated but not
|
|
314
314
|
wired to runtime behavior — timer/input drivers are planned for a follow-up.
|
|
315
315
|
|
|
316
|
-
**Transitions:** `{ type: 'cut' }`
|
|
317
|
-
|
|
316
|
+
**Transitions:** `{ type: 'cut' }` (default) performs a hard switch.
|
|
317
|
+
`{ type: 'morph', dur: number }` smoothly interpolates paint properties
|
|
318
|
+
(colors, opacity values) over `dur` ms when crossing into the next segment.
|
|
319
|
+
Morph requires structurally identical adjacent segments (same
|
|
320
|
+
`structuralSignature`); if they differ, the engine falls back to cut and the
|
|
321
|
+
validator emits a `morph-structural-mismatch` warning. During a morph, entity
|
|
322
|
+
placement inherits the outgoing segment's seed — the incoming segment's own
|
|
323
|
+
seed is unused. `dur` must be between 200 and 5000 ms.
|
|
318
324
|
|
|
319
325
|
**Time mapping:** global clock `T` maps to `(segmentIndex, localT)` via prefix
|
|
320
326
|
sums of durations. Half-open segments: `[start, start+duration)`. With
|
package/dist/index.d.ts
CHANGED
|
@@ -431,9 +431,12 @@ declare const LIMITS: {
|
|
|
431
431
|
readonly maxTransitionDur: 5000;
|
|
432
432
|
readonly minTransitionDur: 200;
|
|
433
433
|
};
|
|
434
|
-
|
|
434
|
+
type SequenceTransition = {
|
|
435
435
|
type: 'cut';
|
|
436
|
-
}
|
|
436
|
+
} | {
|
|
437
|
+
type: 'morph';
|
|
438
|
+
dur: number;
|
|
439
|
+
};
|
|
437
440
|
interface SequenceSegment {
|
|
438
441
|
key: string;
|
|
439
442
|
scene: SaverSpec;
|
|
@@ -681,6 +684,14 @@ declare function adviseSpec(spec: SaverSpec, viewport?: {
|
|
|
681
684
|
width: number;
|
|
682
685
|
height: number;
|
|
683
686
|
}): SpecWarning[];
|
|
687
|
+
/**
|
|
688
|
+
* Non-blocking advisory warnings for a validated sequence envelope.
|
|
689
|
+
* Checks cross-segment luminance jumps at boundaries and per-segment advisories.
|
|
690
|
+
*/
|
|
691
|
+
declare function adviseSequence(seq: IdleSequence, viewport?: {
|
|
692
|
+
width: number;
|
|
693
|
+
height: number;
|
|
694
|
+
}): SpecWarning[];
|
|
684
695
|
|
|
685
696
|
interface LayerSnapshot {
|
|
686
697
|
key: string | undefined;
|
|
@@ -1075,4 +1086,4 @@ declare const EXAMPLE_SPECS: SaverSpec[];
|
|
|
1075
1086
|
/** Lookup by saver id. */
|
|
1076
1087
|
declare const EXAMPLE_BY_ID: Readonly<Record<string, SaverSpec>>;
|
|
1077
1088
|
|
|
1078
|
-
export { AQUARIUM_SPEC, AURORA_SPEC, type BackgroundDrift, type BackgroundSpec, type BandSpec, COMETS_SPEC, CONSTELLATION_SPEC, type CycleSpec, DASHBOARD_SPEC, type DominanceEntry, EXAMPLE_BY_ID, EXAMPLE_SPECS, type Entity, type GradientStop, HAIKU_SPEC, type IdleSequence, LANTERNS_SPEC, LIMITS, type LayerMotionStats, type LayerSnapshot, type LayerSpec, type LinkEdge, type LuminanceGrid, type LuminanceGridOptions, MATRIX_RAIN_SPEC, type MotionSpec, NOSTALGHIA_CANDLE_SPEC, ORRERY_SPEC, POLYGONS_SPEC, PROCESSION_SPEC, type PerceiveOptions, type Placed, RAIN_SPEC, type ResolvedSegment, SAKURA_SPEC, SCHEMA_EXAMPLES, SCHEMA_VERSION, SNOWFALL_SPEC, type SaverSpec, type SceneDescription, type SceneDiff, type ScenePerception, type SceneSnapshot, type SchemaExample, type SequenceSegment, type SequenceTransition, type SpecError, type SpecWarning, type SpriteSpec, type SteerDelta, type TextBlockLine, type TextSpriteInfo, type ValidationResult, WARP_NEAR, WARP_TUNNEL_SPEC, type WanderOsc, adviseSpec, alphaAt, applyDeltasToSpec, assertValidSequence, assertValidSpec, breakTextBlock, buildEntities, compileSaver, compileSequence, describeScene, diffScenes, dominanceRanking, easeSmooth, headingAt, lerpSpec, lifeAlphaAt, linkEdges, linkPairs, luminanceGrid, manifestFor, motionStats, perceiveScene, positionAt, renderBrailleMap, renderDensityMap, resolveSegment, resolveSpecPath, rotationAt, sizeAt, spriteIndexAt, spriteVariants, steerablePaths, structuralSignature, textSprites, validateSequence, validateSpec };
|
|
1089
|
+
export { AQUARIUM_SPEC, AURORA_SPEC, type BackgroundDrift, type BackgroundSpec, type BandSpec, COMETS_SPEC, CONSTELLATION_SPEC, type CycleSpec, DASHBOARD_SPEC, type DominanceEntry, EXAMPLE_BY_ID, EXAMPLE_SPECS, type Entity, type GradientStop, HAIKU_SPEC, type IdleSequence, LANTERNS_SPEC, LIMITS, type LayerMotionStats, type LayerSnapshot, type LayerSpec, type LinkEdge, type LuminanceGrid, type LuminanceGridOptions, MATRIX_RAIN_SPEC, type MotionSpec, NOSTALGHIA_CANDLE_SPEC, ORRERY_SPEC, POLYGONS_SPEC, PROCESSION_SPEC, type PerceiveOptions, type Placed, RAIN_SPEC, type ResolvedSegment, SAKURA_SPEC, SCHEMA_EXAMPLES, SCHEMA_VERSION, SNOWFALL_SPEC, type SaverSpec, type SceneDescription, type SceneDiff, type ScenePerception, type SceneSnapshot, type SchemaExample, type SequenceSegment, type SequenceTransition, type SpecError, type SpecWarning, type SpriteSpec, type SteerDelta, type TextBlockLine, type TextSpriteInfo, type ValidationResult, WARP_NEAR, WARP_TUNNEL_SPEC, type WanderOsc, adviseSequence, adviseSpec, alphaAt, applyDeltasToSpec, assertValidSequence, assertValidSpec, breakTextBlock, buildEntities, compileSaver, compileSequence, describeScene, diffScenes, dominanceRanking, easeSmooth, headingAt, lerpSpec, lifeAlphaAt, linkEdges, linkPairs, luminanceGrid, manifestFor, motionStats, perceiveScene, positionAt, renderBrailleMap, renderDensityMap, resolveSegment, resolveSpecPath, rotationAt, sizeAt, spriteIndexAt, spriteVariants, steerablePaths, structuralSignature, textSprites, validateSequence, validateSpec };
|
package/dist/index.js
CHANGED
|
@@ -54,8 +54,137 @@ var LIMITS = {
|
|
|
54
54
|
minTransitionDur: 200
|
|
55
55
|
};
|
|
56
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
|
+
|
|
57
186
|
// src/validate.ts
|
|
58
|
-
var
|
|
187
|
+
var HEX2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
59
188
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
60
189
|
var isStr = (v) => typeof v === "string";
|
|
61
190
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
@@ -152,7 +281,7 @@ function validateSpec(spec) {
|
|
|
152
281
|
return { valid: errors.length === 0, errors, warnings };
|
|
153
282
|
}
|
|
154
283
|
function color(v, path, err) {
|
|
155
|
-
if (!isStr(v) || !
|
|
284
|
+
if (!isStr(v) || !HEX2.test(v)) err(path, "must be a hex colour like #0a3a52");
|
|
156
285
|
}
|
|
157
286
|
function validateBackground(bg, err, warn) {
|
|
158
287
|
if (!isObj(bg)) return err("background", "must be an object");
|
|
@@ -667,8 +796,12 @@ function validateSequence(seq) {
|
|
|
667
796
|
if (s.transition !== void 0) {
|
|
668
797
|
if (!isObj(s.transition)) {
|
|
669
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
|
+
}
|
|
670
803
|
} else if (s.transition.type !== "cut") {
|
|
671
|
-
err(`${p}.transition.type`, "must be 'cut'
|
|
804
|
+
err(`${p}.transition.type`, "must be 'cut' or 'morph'");
|
|
672
805
|
}
|
|
673
806
|
}
|
|
674
807
|
if (isObj(s.scene)) {
|
|
@@ -684,6 +817,23 @@ function validateSequence(seq) {
|
|
|
684
817
|
if (seq.loop === true && hasDurationless) {
|
|
685
818
|
err("loop", "loop: true requires all segments to have a duration");
|
|
686
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
|
+
}
|
|
687
837
|
return { valid: errors.length === 0, errors, warnings };
|
|
688
838
|
}
|
|
689
839
|
function assertValidSequence(seq) {
|
|
@@ -1213,135 +1363,6 @@ import {
|
|
|
1213
1363
|
createRng
|
|
1214
1364
|
} from "@idle-screens/core";
|
|
1215
1365
|
|
|
1216
|
-
// src/steer.ts
|
|
1217
|
-
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1218
|
-
function resolveSpecPath(spec, path) {
|
|
1219
|
-
if (!spec || typeof spec !== "object" || !path) return null;
|
|
1220
|
-
const parts = path.split(".");
|
|
1221
|
-
if (parts.some((p) => UNSAFE_KEYS.has(p))) return null;
|
|
1222
|
-
const s = spec;
|
|
1223
|
-
if (parts[0] !== "layers" && parts[0] !== "background" && Array.isArray(s.layers)) {
|
|
1224
|
-
const idx = s.layers.findIndex((l) => l && l.key === parts[0]);
|
|
1225
|
-
if (idx === -1) return null;
|
|
1226
|
-
parts.splice(0, 1, "layers", String(idx));
|
|
1227
|
-
}
|
|
1228
|
-
let node = spec;
|
|
1229
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
1230
|
-
if (node === null || typeof node !== "object") return null;
|
|
1231
|
-
node = node[parts[i]];
|
|
1232
|
-
}
|
|
1233
|
-
if (node === null || typeof node !== "object") return null;
|
|
1234
|
-
const last = parts[parts.length - 1];
|
|
1235
|
-
const key = Array.isArray(node) ? Number(last) : last;
|
|
1236
|
-
const exists = Array.isArray(node) ? Number.isInteger(key) && key >= 0 && key < node.length : last in node;
|
|
1237
|
-
return exists ? { parent: node, key } : null;
|
|
1238
|
-
}
|
|
1239
|
-
function applyDeltasToSpec(spec, deltas) {
|
|
1240
|
-
const copy = JSON.parse(JSON.stringify(spec));
|
|
1241
|
-
for (const d of deltas) {
|
|
1242
|
-
const loc = resolveSpecPath(copy, d.path);
|
|
1243
|
-
if (loc) loc.parent[loc.key] = d.value;
|
|
1244
|
-
}
|
|
1245
|
-
return copy;
|
|
1246
|
-
}
|
|
1247
|
-
var HEX2 = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
1248
|
-
function hexToRgb(hex) {
|
|
1249
|
-
const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
|
|
1250
|
-
const n = parseInt(h.slice(1), 16);
|
|
1251
|
-
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
1252
|
-
}
|
|
1253
|
-
function lerpHex(a, b, k) {
|
|
1254
|
-
const ca = hexToRgb(a);
|
|
1255
|
-
const cb = hexToRgb(b);
|
|
1256
|
-
const mix = ca.map((v, i) => Math.round(v + (cb[i] - v) * k));
|
|
1257
|
-
return `#${mix.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
1258
|
-
}
|
|
1259
|
-
function lerpSpec(from, to, k) {
|
|
1260
|
-
const kk = Math.max(0, Math.min(1, k));
|
|
1261
|
-
const walk = (a, b, key) => {
|
|
1262
|
-
if (typeof a === "number" && typeof b === "number") {
|
|
1263
|
-
const v = a + (b - a) * kk;
|
|
1264
|
-
return key === "count" ? Math.max(1, Math.round(v)) : v;
|
|
1265
|
-
}
|
|
1266
|
-
if (typeof a === "string" && typeof b === "string" && HEX2.test(a) && HEX2.test(b)) {
|
|
1267
|
-
return lerpHex(a, b, kk);
|
|
1268
|
-
}
|
|
1269
|
-
if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
|
|
1270
|
-
return b.map((bv, i) => walk(a[i], bv, i));
|
|
1271
|
-
}
|
|
1272
|
-
if (a && b && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
|
|
1273
|
-
const out = {};
|
|
1274
|
-
for (const kName of Object.keys(b)) {
|
|
1275
|
-
out[kName] = walk(a[kName], b[kName], kName);
|
|
1276
|
-
}
|
|
1277
|
-
return out;
|
|
1278
|
-
}
|
|
1279
|
-
return kk > 0 ? b : a;
|
|
1280
|
-
};
|
|
1281
|
-
return walk(from, to);
|
|
1282
|
-
}
|
|
1283
|
-
function steerablePaths(spec) {
|
|
1284
|
-
if (!spec || typeof spec !== "object") return [];
|
|
1285
|
-
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "mode", "curve", "layer"]);
|
|
1286
|
-
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops"]);
|
|
1287
|
-
const out = [];
|
|
1288
|
-
const walk = (node, prefix, key) => {
|
|
1289
|
-
if (Array.isArray(node) && INDEXED.has(key)) {
|
|
1290
|
-
node.forEach((child, i) => walk(child, `${prefix}.${i}`, ""));
|
|
1291
|
-
return;
|
|
1292
|
-
}
|
|
1293
|
-
if (Array.isArray(node)) {
|
|
1294
|
-
if (prefix) out.push(prefix);
|
|
1295
|
-
return;
|
|
1296
|
-
}
|
|
1297
|
-
if (node && typeof node === "object") {
|
|
1298
|
-
for (const [k, v] of Object.entries(node)) {
|
|
1299
|
-
if (SKIP.has(k)) continue;
|
|
1300
|
-
walk(v, prefix ? `${prefix}.${k}` : k, k);
|
|
1301
|
-
}
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
|
-
if (prefix) out.push(prefix);
|
|
1305
|
-
};
|
|
1306
|
-
walk(spec, "", "");
|
|
1307
|
-
return out;
|
|
1308
|
-
}
|
|
1309
|
-
function easeSmooth(k) {
|
|
1310
|
-
const c = Math.max(0, Math.min(1, k));
|
|
1311
|
-
return c * c * (3 - 2 * c);
|
|
1312
|
-
}
|
|
1313
|
-
function structuralSignature(spec) {
|
|
1314
|
-
return JSON.stringify([
|
|
1315
|
-
spec.units,
|
|
1316
|
-
spec.layers.map((l) => {
|
|
1317
|
-
const s = l.sprite;
|
|
1318
|
-
return [
|
|
1319
|
-
l.count,
|
|
1320
|
-
l.size,
|
|
1321
|
-
l.region,
|
|
1322
|
-
l.position,
|
|
1323
|
-
l.motion,
|
|
1324
|
-
l.wrap,
|
|
1325
|
-
l.flip,
|
|
1326
|
-
l.alpha,
|
|
1327
|
-
l.pulse,
|
|
1328
|
-
l.spin,
|
|
1329
|
-
l.grow,
|
|
1330
|
-
l.layout,
|
|
1331
|
-
l.sprite.kind,
|
|
1332
|
-
// Dimensional draws baked into entities: radius (circle/ring), length
|
|
1333
|
-
// (streak), width+aspect (rect), palette pick (all shaped sprites).
|
|
1334
|
-
s.radius,
|
|
1335
|
-
s.length,
|
|
1336
|
-
l.sprite.kind === "rect" ? [s.width, s.aspect] : void 0,
|
|
1337
|
-
Array.isArray(s.colors) ? s.colors.length : void 0,
|
|
1338
|
-
s.colorWeights,
|
|
1339
|
-
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
|
|
1340
|
-
];
|
|
1341
|
-
})
|
|
1342
|
-
]);
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
1366
|
// src/sequence.ts
|
|
1346
1367
|
function resolveSegment(seq, T) {
|
|
1347
1368
|
const { segments, loop } = seq;
|
|
@@ -1856,6 +1877,26 @@ var SpecInstance = class {
|
|
|
1856
1877
|
this.rebuild();
|
|
1857
1878
|
if (this.paused) this.renderFrame(0, this.seed);
|
|
1858
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
|
+
}
|
|
1859
1900
|
dispose() {
|
|
1860
1901
|
this.stop();
|
|
1861
1902
|
if (this.ownsCanvas && typeof HTMLCanvasElement !== "undefined" && this.canvas instanceof HTMLCanvasElement) this.canvas.remove();
|
|
@@ -1876,6 +1917,12 @@ var SequenceInstance = class {
|
|
|
1876
1917
|
children;
|
|
1877
1918
|
activeIndex = -1;
|
|
1878
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;
|
|
1879
1926
|
constructor(seq, ctx) {
|
|
1880
1927
|
this.seq = seq;
|
|
1881
1928
|
let surface = ctx.surface ?? null;
|
|
@@ -1891,12 +1938,33 @@ var SequenceInstance = class {
|
|
|
1891
1938
|
this.childCtx = { ...ctx, surface, reducedMotion: true };
|
|
1892
1939
|
this.children = new Array(seq.segments.length).fill(null);
|
|
1893
1940
|
}
|
|
1941
|
+
childSeed(index, fallback) {
|
|
1942
|
+
return this.seq.segments[index].scene.seed ?? this.seq.seed ?? fallback;
|
|
1943
|
+
}
|
|
1894
1944
|
childScene(index) {
|
|
1895
1945
|
const seg = this.seq.segments[index];
|
|
1896
1946
|
if (seg.scene.seed != null) return seg.scene;
|
|
1897
1947
|
if (this.seq.seed != null) return { ...seg.scene, seed: this.seq.seed + index };
|
|
1898
1948
|
return seg.scene;
|
|
1899
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
|
+
}
|
|
1900
1968
|
ensureChild(index) {
|
|
1901
1969
|
if (index < 0 || index >= this.seq.segments.length) index = 0;
|
|
1902
1970
|
let child = this.children[index];
|
|
@@ -1917,15 +1985,51 @@ var SequenceInstance = class {
|
|
|
1917
1985
|
renderFrame(T, seed) {
|
|
1918
1986
|
const resolved = resolveSegment(this.seq, T);
|
|
1919
1987
|
const { index, localT } = resolved;
|
|
1920
|
-
|
|
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;
|
|
1921
1994
|
for (let i = 0; i < this.children.length; i++) {
|
|
1922
|
-
if (i !==
|
|
1995
|
+
if (i !== chainRoot) this.releaseChild(i);
|
|
1923
1996
|
}
|
|
1924
|
-
|
|
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);
|
|
1925
2032
|
}
|
|
1926
|
-
const child = this.ensureChild(index);
|
|
1927
|
-
const childSeed = this.seq.segments[index].scene.seed ?? this.seq.seed ?? seed;
|
|
1928
|
-
child.renderFrame(localT, childSeed);
|
|
1929
2033
|
}
|
|
1930
2034
|
setPaused(paused) {
|
|
1931
2035
|
this.paused = paused;
|
|
@@ -2231,6 +2335,40 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
2231
2335
|
}
|
|
2232
2336
|
return warnings;
|
|
2233
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
|
+
}
|
|
2234
2372
|
|
|
2235
2373
|
// src/describe.ts
|
|
2236
2374
|
import { createRng as createRng3 } from "@idle-screens/core";
|
|
@@ -4351,6 +4489,7 @@ export {
|
|
|
4351
4489
|
SNOWFALL_SPEC,
|
|
4352
4490
|
WARP_NEAR,
|
|
4353
4491
|
WARP_TUNNEL_SPEC,
|
|
4492
|
+
adviseSequence,
|
|
4354
4493
|
adviseSpec,
|
|
4355
4494
|
alphaAt,
|
|
4356
4495
|
applyDeltasToSpec,
|