@idle-screens/schema 3.9.0 → 3.10.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 +128 -6
- package/dist/index.d.ts +243 -15
- package/dist/index.js +1121 -199
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +63 -2
package/dist/index.js
CHANGED
|
@@ -71,6 +71,20 @@ var LIMITS = {
|
|
|
71
71
|
minTransitionDur: 200,
|
|
72
72
|
minEmitEvery: 1e3,
|
|
73
73
|
// ms — at most one event per second per entity (flash safety)
|
|
74
|
+
maxTimelineKeys: 256,
|
|
75
|
+
minTimelineKeyInterval: 200,
|
|
76
|
+
// ms — distinct key times on one path (flash safety; the timeline design's floor)
|
|
77
|
+
maxTimelineKeyDur: 3e4,
|
|
78
|
+
// ms — one key's glide
|
|
79
|
+
maxTimelineDuration: 864e5,
|
|
80
|
+
// ms — a looping timeline's lap (a day)
|
|
81
|
+
maxLayerTransformOffset: 2,
|
|
82
|
+
// min(w,h) units — transform x/y and position dx/dy
|
|
83
|
+
maxLayerTransformOffsetPx: 17280,
|
|
84
|
+
// px, under `units: 'px'` — twice the largest referenceViewport
|
|
85
|
+
maxLayerTransformScale: 8,
|
|
86
|
+
maxLayerTransformRotate: 3600,
|
|
87
|
+
// degrees
|
|
74
88
|
maxEmitEvery: 6e5,
|
|
75
89
|
// ms — ten minutes; longer silences than that are `life.enter`
|
|
76
90
|
minEmitLife: 500,
|
|
@@ -121,6 +135,79 @@ function resolveSpecPath(spec, path) {
|
|
|
121
135
|
const exists = Array.isArray(node) ? Number.isInteger(key) && key >= 0 && key < node.length : last in node;
|
|
122
136
|
return exists ? { parent: node, key } : null;
|
|
123
137
|
}
|
|
138
|
+
function canonicalSpecPath(spec, path) {
|
|
139
|
+
if (!resolveSpecPath(spec, path)) return null;
|
|
140
|
+
const parts = path.split(".");
|
|
141
|
+
const s = spec;
|
|
142
|
+
if (parts[0] !== "layers" && parts[0] !== "background" && Array.isArray(s.layers)) {
|
|
143
|
+
const idx = s.layers.findIndex((l) => l && l.key === parts[0]);
|
|
144
|
+
if (idx !== -1) parts.splice(0, 1, "layers", String(idx));
|
|
145
|
+
}
|
|
146
|
+
return parts.join(".");
|
|
147
|
+
}
|
|
148
|
+
function readSpecPath(spec, path) {
|
|
149
|
+
const loc = resolveSpecPath(spec, path);
|
|
150
|
+
return loc ? loc.parent[loc.key] : void 0;
|
|
151
|
+
}
|
|
152
|
+
var TRANSFORM_IDENTITY = { x: 0, y: 0, scale: 1, scaleX: 1, rotate: 0 };
|
|
153
|
+
function alignTransform(a, b) {
|
|
154
|
+
const isObj2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
155
|
+
if (!isObj2(a) || !isObj2(b)) return [a, b];
|
|
156
|
+
let fa = null;
|
|
157
|
+
let fb = null;
|
|
158
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
159
|
+
const id = TRANSFORM_IDENTITY[key];
|
|
160
|
+
if (id === void 0) continue;
|
|
161
|
+
if (a[key] === void 0) (fa ??= { ...a })[key] = id;
|
|
162
|
+
if (b[key] === void 0) (fb ??= { ...b })[key] = id;
|
|
163
|
+
}
|
|
164
|
+
return [fa ?? a, fb ?? b];
|
|
165
|
+
}
|
|
166
|
+
function alignPaintFields(a, b) {
|
|
167
|
+
const la = a?.layers;
|
|
168
|
+
const lb = b?.layers;
|
|
169
|
+
if (!Array.isArray(la) || !Array.isArray(lb)) return [a, b];
|
|
170
|
+
let outA = null;
|
|
171
|
+
let outB = null;
|
|
172
|
+
const n = Math.min(la.length, lb.length);
|
|
173
|
+
for (let i = 0; i < n; i++) {
|
|
174
|
+
const x = la[i];
|
|
175
|
+
const y = lb[i];
|
|
176
|
+
if (!x || !y || typeof x !== "object" || typeof y !== "object") continue;
|
|
177
|
+
let nx = x;
|
|
178
|
+
let ny = y;
|
|
179
|
+
if (x.opacity === void 0 !== (y.opacity === void 0)) {
|
|
180
|
+
if (x.opacity === void 0) nx = { ...nx, opacity: 1 };
|
|
181
|
+
else ny = { ...ny, opacity: 1 };
|
|
182
|
+
}
|
|
183
|
+
const tx = x.transform;
|
|
184
|
+
const ty = y.transform;
|
|
185
|
+
if (tx || ty) {
|
|
186
|
+
const fx = { ...tx ?? {} };
|
|
187
|
+
const fy = { ...ty ?? {} };
|
|
188
|
+
let changed = !tx || !ty;
|
|
189
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(fx), ...Object.keys(fy)])) {
|
|
190
|
+
const id = TRANSFORM_IDENTITY[key];
|
|
191
|
+
if (id === void 0) continue;
|
|
192
|
+
if (fx[key] === void 0) {
|
|
193
|
+
fx[key] = id;
|
|
194
|
+
changed = true;
|
|
195
|
+
}
|
|
196
|
+
if (fy[key] === void 0) {
|
|
197
|
+
fy[key] = id;
|
|
198
|
+
changed = true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (changed) {
|
|
202
|
+
nx = { ...nx, transform: fx };
|
|
203
|
+
ny = { ...ny, transform: fy };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (nx !== x) (outA ??= la.slice())[i] = nx;
|
|
207
|
+
if (ny !== y) (outB ??= lb.slice())[i] = ny;
|
|
208
|
+
}
|
|
209
|
+
return [outA ? { ...a, layers: outA } : a, outB ? { ...b, layers: outB } : b];
|
|
210
|
+
}
|
|
124
211
|
function applyDeltasToSpec(spec, deltas) {
|
|
125
212
|
const copy = JSON.parse(JSON.stringify(spec));
|
|
126
213
|
for (const d of deltas) {
|
|
@@ -143,7 +230,14 @@ function lerpHex(a, b, k) {
|
|
|
143
230
|
}
|
|
144
231
|
function lerpSpec(from, to, k) {
|
|
145
232
|
const kk = Math.max(0, Math.min(1, k));
|
|
146
|
-
const
|
|
233
|
+
const out = lerpValue(from, to, kk, true);
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
function lerpValue(from, to, k, specRoot = false, rootKey) {
|
|
237
|
+
if (specRoot) [from, to] = alignPaintFields(from, to);
|
|
238
|
+
if (rootKey === "transform") [from, to] = alignTransform(from, to);
|
|
239
|
+
const kk = Math.max(0, Math.min(1, k));
|
|
240
|
+
const walk = (a, b, key, root = false) => {
|
|
147
241
|
if (typeof a === "number" && typeof b === "number") {
|
|
148
242
|
const v = a + (b - a) * kk;
|
|
149
243
|
return key === "count" ? Math.max(1, Math.round(v)) : v;
|
|
@@ -157,13 +251,15 @@ function lerpSpec(from, to, k) {
|
|
|
157
251
|
if (a && b && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
|
|
158
252
|
const out = {};
|
|
159
253
|
for (const kName of Object.keys(b)) {
|
|
160
|
-
|
|
254
|
+
const av = a[kName];
|
|
255
|
+
const bv = b[kName];
|
|
256
|
+
out[kName] = root && kName === "timeline" ? kk > 0 ? bv : av : kName === "transform" ? walk(...alignTransform(av, bv), kName) : walk(av, bv, kName);
|
|
161
257
|
}
|
|
162
258
|
return out;
|
|
163
259
|
}
|
|
164
260
|
return kk > 0 ? b : a;
|
|
165
261
|
};
|
|
166
|
-
return walk(from, to);
|
|
262
|
+
return walk(from, to, rootKey, specRoot);
|
|
167
263
|
}
|
|
168
264
|
function textStringsOf(sprite) {
|
|
169
265
|
if (sprite.kind === "text") return sprite.strings;
|
|
@@ -246,7 +342,9 @@ function steerablePaths(spec) {
|
|
|
246
342
|
if (prefix) out.push(prefix);
|
|
247
343
|
};
|
|
248
344
|
walk(spec, "", "");
|
|
249
|
-
|
|
345
|
+
const ignored = ignoredPropertyPaths(spec);
|
|
346
|
+
if (ignored.length === 0) return out;
|
|
347
|
+
return out.filter((p) => !ignored.some((ig) => p === ig || p.startsWith(`${ig}.`)));
|
|
250
348
|
}
|
|
251
349
|
function sequenceSteerablePaths(seq) {
|
|
252
350
|
const out = /* @__PURE__ */ new Set(["sequence.segment"]);
|
|
@@ -299,13 +397,244 @@ function structuralSignature(spec) {
|
|
|
299
397
|
]);
|
|
300
398
|
}
|
|
301
399
|
|
|
400
|
+
// src/sequence.ts
|
|
401
|
+
function segmentStart(seq, index) {
|
|
402
|
+
let t = 0;
|
|
403
|
+
const n = Math.max(0, Math.min(index, seq.segments.length - 1));
|
|
404
|
+
for (let i = 0; i < n; i++) t += seq.segments[i].duration ?? 0;
|
|
405
|
+
return t;
|
|
406
|
+
}
|
|
407
|
+
function resolveSegment(seq, T, opts = {}) {
|
|
408
|
+
const { segments, loop } = seq;
|
|
409
|
+
if (segments.length === 0) return { index: 0, localT: 0, startT: 0 };
|
|
410
|
+
let totalTimed = 0;
|
|
411
|
+
for (const seg of segments) {
|
|
412
|
+
if (seg.duration != null) totalTimed += seg.duration;
|
|
413
|
+
else break;
|
|
414
|
+
}
|
|
415
|
+
const t0 = Math.max(0, T);
|
|
416
|
+
const walk = (t, releasedBelow) => {
|
|
417
|
+
let cumulative = 0;
|
|
418
|
+
for (let i = 0; i < segments.length; i++) {
|
|
419
|
+
const seg = segments[i];
|
|
420
|
+
if (seg.duration == null) {
|
|
421
|
+
return loop ? null : { index: i, localT: t - cumulative, startT: cumulative };
|
|
422
|
+
}
|
|
423
|
+
if (t < cumulative + seg.duration) {
|
|
424
|
+
return { index: i, localT: t - cumulative, startT: cumulative };
|
|
425
|
+
}
|
|
426
|
+
if (seg.advance === "input" && i >= releasedBelow) {
|
|
427
|
+
return { index: i, localT: t - cumulative, startT: cumulative, held: true };
|
|
428
|
+
}
|
|
429
|
+
cumulative += seg.duration;
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
};
|
|
433
|
+
const first = walk(t0, opts.releasedBelow ?? 0);
|
|
434
|
+
if (first) return first;
|
|
435
|
+
if (loop && totalTimed > 0) {
|
|
436
|
+
const wrapped = walk(t0 % totalTimed, 0);
|
|
437
|
+
if (wrapped) return wrapped;
|
|
438
|
+
}
|
|
439
|
+
const last = segments.length - 1;
|
|
440
|
+
const lastStart = totalTimed - (segments[last].duration ?? 0);
|
|
441
|
+
return { index: last, localT: t0 - lastStart, startT: lastStart };
|
|
442
|
+
}
|
|
443
|
+
function canMorph(seq, from) {
|
|
444
|
+
const seg = seq.segments[from];
|
|
445
|
+
if (!seg || seg.transition?.type !== "morph") return false;
|
|
446
|
+
const next = seq.segments[from + 1];
|
|
447
|
+
if (!next) return false;
|
|
448
|
+
return structuralSignature(seg.scene) === structuralSignature(next.scene);
|
|
449
|
+
}
|
|
450
|
+
function morphChainRoot(seq, index) {
|
|
451
|
+
let i = index;
|
|
452
|
+
while (i > 0 && canMorph(seq, i - 1)) i--;
|
|
453
|
+
return i;
|
|
454
|
+
}
|
|
455
|
+
function canWrapMorph(seq) {
|
|
456
|
+
if (seq.wrapMorph !== true || seq.loop !== true) return false;
|
|
457
|
+
const last = seq.segments.length - 1;
|
|
458
|
+
if (last < 1) return false;
|
|
459
|
+
const seg = seq.segments[last];
|
|
460
|
+
if (seg.transition?.type !== "morph" || seg.duration == null) return false;
|
|
461
|
+
if (morphChainRoot(seq, last) !== 0) return false;
|
|
462
|
+
return structuralSignature(seg.scene) === structuralSignature(seq.segments[0].scene);
|
|
463
|
+
}
|
|
464
|
+
function normalizeSeed(seed) {
|
|
465
|
+
return seed >>> 0 || 1;
|
|
466
|
+
}
|
|
467
|
+
function segmentRenderSeed(seq, index) {
|
|
468
|
+
const root = morphChainRoot(seq, index);
|
|
469
|
+
const scene = seq.segments[root]?.scene;
|
|
470
|
+
if (!scene) return void 0;
|
|
471
|
+
const raw = scene.seed ?? (seq.seed !== void 0 ? seq.seed + root : void 0);
|
|
472
|
+
return raw === void 0 ? void 0 : normalizeSeed(raw);
|
|
473
|
+
}
|
|
474
|
+
function bedRenderSeed(seq) {
|
|
475
|
+
if (!seq.bed) return void 0;
|
|
476
|
+
const raw = seq.bed.seed ?? (seq.seed !== void 0 ? seq.seed + LIMITS.maxSegments : void 0);
|
|
477
|
+
return raw === void 0 ? void 0 : normalizeSeed(raw);
|
|
478
|
+
}
|
|
479
|
+
function sequenceWantsFinish(seq) {
|
|
480
|
+
return seq.finish !== void 0 || seq.segments.some((s) => s.scene.finish !== void 0);
|
|
481
|
+
}
|
|
482
|
+
function sequenceSwapCompatible(prev, next) {
|
|
483
|
+
if (prev.segments.length !== next.segments.length) return false;
|
|
484
|
+
if (prev.finish !== void 0 || next.finish !== void 0) {
|
|
485
|
+
if (prev.seed === void 0 !== (next.seed === void 0)) return false;
|
|
486
|
+
if (prev.seed !== void 0 && next.seed !== void 0 && normalizeSeed(prev.seed) !== normalizeSeed(next.seed)) return false;
|
|
487
|
+
}
|
|
488
|
+
if (sequenceWantsFinish(prev) !== sequenceWantsFinish(next)) return false;
|
|
489
|
+
if (prev.loop !== next.loop) return false;
|
|
490
|
+
if ((prev.sync ?? "mount") !== (next.sync ?? "mount")) return false;
|
|
491
|
+
if (!!prev.bed !== !!next.bed) return false;
|
|
492
|
+
if (prev.bed && next.bed) {
|
|
493
|
+
if (structuralSignature(prev.bed) !== structuralSignature(next.bed)) return false;
|
|
494
|
+
if (bedRenderSeed(prev) !== bedRenderSeed(next)) return false;
|
|
495
|
+
}
|
|
496
|
+
for (let i = 0; i < prev.segments.length; i++) {
|
|
497
|
+
const a = prev.segments[i];
|
|
498
|
+
const b = next.segments[i];
|
|
499
|
+
if (a.duration !== b.duration) return false;
|
|
500
|
+
if ((a.advance ?? "auto") !== (b.advance ?? "auto")) return false;
|
|
501
|
+
if (transitionKey(a.transition) !== transitionKey(b.transition)) return false;
|
|
502
|
+
if (structuralSignature(a.scene) !== structuralSignature(b.scene)) return false;
|
|
503
|
+
if (segmentRenderSeed(prev, i) !== segmentRenderSeed(next, i)) return false;
|
|
504
|
+
}
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
function transitionKey(tr) {
|
|
508
|
+
if (!tr || tr.type === "cut") return "cut";
|
|
509
|
+
if (tr.type === "morph") return `morph:${tr.dur}:${tr.text ?? "step"}`;
|
|
510
|
+
return `fade:${tr.dur}`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/timeline.ts
|
|
514
|
+
var DEFAULT_KEY_DUR = 1e3;
|
|
515
|
+
var tracksCache = /* @__PURE__ */ new WeakMap();
|
|
516
|
+
function withoutTimeline(spec) {
|
|
517
|
+
if (!spec.timeline) return spec;
|
|
518
|
+
const rest = { ...spec };
|
|
519
|
+
delete rest.timeline;
|
|
520
|
+
return rest;
|
|
521
|
+
}
|
|
522
|
+
function timelineTracks(spec) {
|
|
523
|
+
const tl = spec.timeline;
|
|
524
|
+
if (!tl) return /* @__PURE__ */ new Map();
|
|
525
|
+
const hit = tracksCache.get(tl);
|
|
526
|
+
if (hit && hit.spec === spec) return hit.tracks;
|
|
527
|
+
const base = withoutTimeline(spec);
|
|
528
|
+
const tracks = /* @__PURE__ */ new Map();
|
|
529
|
+
for (const k of tl.keys ?? []) {
|
|
530
|
+
const cp = canonicalSpecPath(base, k.path);
|
|
531
|
+
if (!cp) continue;
|
|
532
|
+
const list = tracks.get(cp) ?? [];
|
|
533
|
+
list.push(k);
|
|
534
|
+
tracks.set(cp, list);
|
|
535
|
+
}
|
|
536
|
+
for (const list of tracks.values()) list.sort((a, b) => a.t - b.t);
|
|
537
|
+
tracksCache.set(tl, { spec, tracks });
|
|
538
|
+
return tracks;
|
|
539
|
+
}
|
|
540
|
+
function timelineLocalTime(tl, t) {
|
|
541
|
+
const d = tl.duration ?? 0;
|
|
542
|
+
if (tl.loop && d > 0) return (t % d + d) % d;
|
|
543
|
+
return t;
|
|
544
|
+
}
|
|
545
|
+
function ease(k, x) {
|
|
546
|
+
const c = x <= 0 ? 0 : x >= 1 ? 1 : x;
|
|
547
|
+
if (k.ease === "step") return c >= 1 ? 1 : 0;
|
|
548
|
+
if (k.ease === "linear") return c;
|
|
549
|
+
return easeSmooth(c);
|
|
550
|
+
}
|
|
551
|
+
function evalTrack(keys, start, tt, leaf) {
|
|
552
|
+
let from = start;
|
|
553
|
+
let key = null;
|
|
554
|
+
const at = (tau) => {
|
|
555
|
+
if (!key) return from;
|
|
556
|
+
const dur = key.dur ?? DEFAULT_KEY_DUR;
|
|
557
|
+
if (dur <= 0 || tau >= key.t + dur) return key.value;
|
|
558
|
+
return lerpValue(from, key.value, ease(key, (tau - key.t) / dur), false, leaf);
|
|
559
|
+
};
|
|
560
|
+
for (const k of keys) {
|
|
561
|
+
if (k.t > tt) break;
|
|
562
|
+
from = at(k.t);
|
|
563
|
+
key = k;
|
|
564
|
+
}
|
|
565
|
+
return at(tt);
|
|
566
|
+
}
|
|
567
|
+
function resolveTimelineAt(spec, t) {
|
|
568
|
+
const tl = spec.timeline;
|
|
569
|
+
if (!tl) return spec;
|
|
570
|
+
const base = withoutTimeline(spec);
|
|
571
|
+
const tracks = timelineTracks(spec);
|
|
572
|
+
if (tracks.size === 0) return base;
|
|
573
|
+
const tt = timelineLocalTime(tl, t);
|
|
574
|
+
const deltas = [];
|
|
575
|
+
for (const [path, keys] of tracks) {
|
|
576
|
+
const start = tl.loop ? keys[keys.length - 1].value : readSpecPath(base, path);
|
|
577
|
+
deltas.push({ t: 0, path, value: evalTrack(keys, start, tt, path.slice(path.lastIndexOf(".") + 1)) });
|
|
578
|
+
}
|
|
579
|
+
return applyDeltasToSpec(base, deltas);
|
|
580
|
+
}
|
|
581
|
+
function nextKeyAfter(spec, path, t) {
|
|
582
|
+
const tl = spec.timeline;
|
|
583
|
+
if (!tl) return null;
|
|
584
|
+
const cp = canonicalSpecPath(withoutTimeline(spec), path);
|
|
585
|
+
const keys = cp ? timelineTracks(spec).get(cp) : void 0;
|
|
586
|
+
if (!keys || keys.length === 0) return null;
|
|
587
|
+
const tt = timelineLocalTime(tl, t);
|
|
588
|
+
const next = keys.find((k) => k.t > tt);
|
|
589
|
+
if (next) return { at: t + (next.t - tt), dur: next.dur ?? DEFAULT_KEY_DUR };
|
|
590
|
+
if (tl.loop && (tl.duration ?? 0) > 0) {
|
|
591
|
+
const first = keys[0];
|
|
592
|
+
return { at: t + (tl.duration - tt) + first.t, dur: first.dur ?? DEFAULT_KEY_DUR };
|
|
593
|
+
}
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
function timelineSampleTimes(spec, dense = true, cap = 120, only) {
|
|
597
|
+
const tl = spec.timeline;
|
|
598
|
+
if (!tl) return [];
|
|
599
|
+
const d = tl.duration ?? 0;
|
|
600
|
+
const fold = (t) => tl.loop && d > 0 ? (t % d + d) % d : t;
|
|
601
|
+
const bounds = /* @__PURE__ */ new Set([0]);
|
|
602
|
+
const extras = /* @__PURE__ */ new Set();
|
|
603
|
+
const keys = [];
|
|
604
|
+
for (const [path, list] of timelineTracks(spec)) if (!only || only(path)) keys.push(...list);
|
|
605
|
+
for (const k of keys) {
|
|
606
|
+
if (!Number.isFinite(k.t)) continue;
|
|
607
|
+
const dur = k.dur ?? DEFAULT_KEY_DUR;
|
|
608
|
+
bounds.add(fold(k.t));
|
|
609
|
+
bounds.add(fold(k.t + dur));
|
|
610
|
+
if (dense) for (const f of [0.25, 0.5, 0.75]) extras.add(fold(k.t + dur * f));
|
|
611
|
+
}
|
|
612
|
+
let extra = [...extras].filter((t) => !bounds.has(t)).sort((a, b) => a - b);
|
|
613
|
+
if (extra.length > cap) {
|
|
614
|
+
const step = extra.length / cap;
|
|
615
|
+
extra = Array.from({ length: cap }, (_, i) => extra[Math.floor(i * step)]);
|
|
616
|
+
}
|
|
617
|
+
return [.../* @__PURE__ */ new Set([...bounds, ...extra])].filter(Number.isFinite).sort((a, b) => a - b);
|
|
618
|
+
}
|
|
619
|
+
function timelineSampleTimesAfter(spec, now, dense = false, only) {
|
|
620
|
+
const tl = spec.timeline;
|
|
621
|
+
if (!tl) return [];
|
|
622
|
+
const local = timelineSampleTimes(spec, dense, 120, only);
|
|
623
|
+
const d = tl.duration ?? 0;
|
|
624
|
+
if (tl.loop && d > 0) {
|
|
625
|
+
const phase = (now % d + d) % d;
|
|
626
|
+
return local.map((s) => now + ((s - phase) % d + d) % d).sort((a, b) => a - b);
|
|
627
|
+
}
|
|
628
|
+
return local.filter((s) => s > now);
|
|
629
|
+
}
|
|
630
|
+
|
|
302
631
|
// src/validate.ts
|
|
303
632
|
var HEX2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
304
633
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
305
634
|
var isStr = (v) => typeof v === "string";
|
|
306
635
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
307
636
|
var isRange = (v) => Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && v[0] <= v[1];
|
|
308
|
-
var KNOWN_TOP = /* @__PURE__ */ new Set(["schemaVersion", "id", "label", "seed", "motionIntensity", "density", "units", "referenceViewport", "background", "layers", "ghosting", "finish"]);
|
|
637
|
+
var KNOWN_TOP = /* @__PURE__ */ new Set(["schemaVersion", "id", "label", "seed", "motionIntensity", "density", "units", "referenceViewport", "background", "layers", "ghosting", "finish", "timeline"]);
|
|
309
638
|
var KNOWN_FINISH = /* @__PURE__ */ new Set(["grain", "dither", "animate"]);
|
|
310
639
|
var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
311
640
|
"count",
|
|
@@ -328,8 +657,14 @@ var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
|
328
657
|
"life",
|
|
329
658
|
"emit",
|
|
330
659
|
"clock",
|
|
331
|
-
"rotate"
|
|
660
|
+
"rotate",
|
|
661
|
+
"opacity",
|
|
662
|
+
"transform"
|
|
332
663
|
]);
|
|
664
|
+
var KNOWN_TRANSFORM = /* @__PURE__ */ new Set(["x", "y", "scale", "scaleX", "rotate"]);
|
|
665
|
+
var KNOWN_POSITION = /* @__PURE__ */ new Set(["x", "y", "dx", "dy"]);
|
|
666
|
+
var KNOWN_TIMELINE = /* @__PURE__ */ new Set(["loop", "duration", "keys"]);
|
|
667
|
+
var KNOWN_TIMELINE_KEY = /* @__PURE__ */ new Set(["t", "path", "value", "ease", "dur"]);
|
|
333
668
|
var KNOWN_CIRCLE = /* @__PURE__ */ new Set(["kind", "radius", "color", "soft", "colors", "colorWeights"]);
|
|
334
669
|
var KNOWN_RING = /* @__PURE__ */ new Set(["kind", "radius", "color", "width", "colors", "colorWeights"]);
|
|
335
670
|
var KNOWN_STREAK = /* @__PURE__ */ new Set(["kind", "length", "color", "width", "colors", "colorWeights"]);
|
|
@@ -361,6 +696,19 @@ var LAYER_PROPS_ON_SPRITE = /* @__PURE__ */ new Set(["blend", "trail", "alpha",
|
|
|
361
696
|
function unknownKeys(obj, known) {
|
|
362
697
|
return Object.keys(obj).filter((k) => !known.has(k));
|
|
363
698
|
}
|
|
699
|
+
function dimUnit(spec) {
|
|
700
|
+
return isObj(spec) && spec.units === "px" ? "px" : "fractions of min(width, height)";
|
|
701
|
+
}
|
|
702
|
+
function offsetBound(spec) {
|
|
703
|
+
return isObj(spec) && spec.units === "px" ? [LIMITS.maxLayerTransformOffsetPx, "px"] : [LIMITS.maxLayerTransformOffset, "min(w, h) units"];
|
|
704
|
+
}
|
|
705
|
+
function rangeMsg(spec, note) {
|
|
706
|
+
return `must be a [min,max] range of positive ${dimUnit(spec)}${note ? ` (${note})` : ""}`;
|
|
707
|
+
}
|
|
708
|
+
function positiveScalar(v, path, err, msg = "must be > 0") {
|
|
709
|
+
if (Array.isArray(v)) err(path, "must be a single number > 0, not a [min,max] range");
|
|
710
|
+
else if (!isNum(v) || v <= 0) err(path, msg);
|
|
711
|
+
}
|
|
364
712
|
function normalizeColors(spec) {
|
|
365
713
|
if (!isObj(spec) || !Array.isArray(spec.layers)) return;
|
|
366
714
|
for (const layer of spec.layers) {
|
|
@@ -375,6 +723,45 @@ function normalizeColors(spec) {
|
|
|
375
723
|
}
|
|
376
724
|
function validateSpec(spec) {
|
|
377
725
|
normalizeColors(spec);
|
|
726
|
+
return validateSpecCore(spec);
|
|
727
|
+
}
|
|
728
|
+
function ignoredPropertyPaths(spec) {
|
|
729
|
+
if (!isObj(spec)) return [];
|
|
730
|
+
return (validateSpecCore(spec).warnings ?? []).filter((w) => w.code === "unknown-property" || w.code === "misplaced-property").map((w) => w.path.replace(/\[(\d+)\]/g, ".$1"));
|
|
731
|
+
}
|
|
732
|
+
function validateSpecPaths(spec, paths) {
|
|
733
|
+
const layers = /* @__PURE__ */ new Set();
|
|
734
|
+
let background = false;
|
|
735
|
+
let finish = false;
|
|
736
|
+
for (const p of paths) {
|
|
737
|
+
if (/^layers\.\d+(\.(key|count|motion)(\.|$)|$)/.test(p)) return validateSpec(spec).valid;
|
|
738
|
+
const [head, idx] = p.split(".");
|
|
739
|
+
if (head === "layers" && idx !== void 0 && /^\d+$/.test(idx)) layers.add(Number(idx));
|
|
740
|
+
else if (head === "background") background = true;
|
|
741
|
+
else if (head === "finish") finish = true;
|
|
742
|
+
else return validateSpec(spec).valid;
|
|
743
|
+
}
|
|
744
|
+
let ok = true;
|
|
745
|
+
const err = () => {
|
|
746
|
+
ok = false;
|
|
747
|
+
};
|
|
748
|
+
const warn = () => void 0;
|
|
749
|
+
if (!Array.isArray(spec.layers)) return false;
|
|
750
|
+
for (const i of layers) {
|
|
751
|
+
const layer = spec.layers[i];
|
|
752
|
+
if (layer === void 0) return false;
|
|
753
|
+
validateLayer(layer, `layers[${i}]`, err, warn, spec);
|
|
754
|
+
if (spec.units === "px" && isObj(layer) && isObj(layer.sprite) && layer.sprite.kind === "textBlock") ok = false;
|
|
755
|
+
}
|
|
756
|
+
if (background && spec.background !== void 0) validateBackground(spec.background, err, warn);
|
|
757
|
+
if (finish && spec.finish !== void 0) validateFinish(spec.finish, "finish", err, warn);
|
|
758
|
+
if (layers.size > 0) {
|
|
759
|
+
const total = spec.layers.reduce((n, l) => n + (isObj(l) && isNum(l.count) ? l.count : 0), 0);
|
|
760
|
+
if (total > LIMITS.maxTotal) ok = false;
|
|
761
|
+
}
|
|
762
|
+
return ok;
|
|
763
|
+
}
|
|
764
|
+
function validateSpecCore(spec) {
|
|
378
765
|
const errors = [];
|
|
379
766
|
const warnings = [];
|
|
380
767
|
const err = (path, message) => void errors.push({ path, message });
|
|
@@ -425,8 +812,107 @@ function validateSpec(spec) {
|
|
|
425
812
|
});
|
|
426
813
|
if (total > LIMITS.maxTotal) err("layers", `total entities ${total} exceeds cap ${LIMITS.maxTotal}`);
|
|
427
814
|
}
|
|
815
|
+
if (spec.timeline !== void 0 && errors.length === 0) validateTimeline(spec, err, warn);
|
|
428
816
|
return { valid: errors.length === 0, errors, warnings };
|
|
429
817
|
}
|
|
818
|
+
function validateTimeline(spec, errOuter, warn) {
|
|
819
|
+
let failed = false;
|
|
820
|
+
const err = (p, m) => {
|
|
821
|
+
failed = true;
|
|
822
|
+
errOuter(p, m);
|
|
823
|
+
};
|
|
824
|
+
const tl = spec.timeline;
|
|
825
|
+
if (!isObj(tl)) return err("timeline", "must be an object {loop?, duration?, keys}");
|
|
826
|
+
for (const k of unknownKeys(tl, KNOWN_TIMELINE)) warn(`timeline.${k}`, "unknown-property", `unknown timeline property '${k}' \u2014 will be ignored`);
|
|
827
|
+
if (tl.loop !== void 0 && typeof tl.loop !== "boolean") err("timeline.loop", "must be a boolean");
|
|
828
|
+
if (tl.duration !== void 0 && (!isNum(tl.duration) || tl.duration <= 0 || tl.duration > LIMITS.maxTimelineDuration)) {
|
|
829
|
+
err("timeline.duration", `must be a number of ms, 0 < duration <= ${LIMITS.maxTimelineDuration}`);
|
|
830
|
+
}
|
|
831
|
+
if (tl.loop === true && tl.duration === void 0) err("timeline.duration", "loop: true needs a duration (the lap length in ms)");
|
|
832
|
+
if (!Array.isArray(tl.keys) || tl.keys.length === 0) return err("timeline.keys", "must be a non-empty array");
|
|
833
|
+
if (tl.keys.length > LIMITS.maxTimelineKeys) return err("timeline.keys", `at most ${LIMITS.maxTimelineKeys} keys`);
|
|
834
|
+
const base = withoutTimeline(spec);
|
|
835
|
+
const baseSig = structuralSignature(base);
|
|
836
|
+
const times = /* @__PURE__ */ new Map();
|
|
837
|
+
let latest = 0;
|
|
838
|
+
tl.keys.forEach((key, i) => {
|
|
839
|
+
const p = `timeline.keys[${i}]`;
|
|
840
|
+
if (!isObj(key)) return err(p, "must be an object {t, path, value, ease?, dur?}");
|
|
841
|
+
for (const k of unknownKeys(key, KNOWN_TIMELINE_KEY)) warn(`${p}.${k}`, "unknown-property", `unknown key property '${k}' \u2014 will be ignored`);
|
|
842
|
+
let ok = true;
|
|
843
|
+
if (!isNum(key.t) || key.t < 0) {
|
|
844
|
+
err(`${p}.t`, "must be a number of ms >= 0");
|
|
845
|
+
ok = false;
|
|
846
|
+
}
|
|
847
|
+
if (key.ease !== void 0 && key.ease !== "step" && key.ease !== "linear" && key.ease !== "smooth") err(`${p}.ease`, "must be 'step' | 'linear' | 'smooth'");
|
|
848
|
+
if (key.dur !== void 0 && (!isNum(key.dur) || key.dur < 0 || key.dur > LIMITS.maxTimelineKeyDur)) {
|
|
849
|
+
err(`${p}.dur`, `must be a number of ms, 0..${LIMITS.maxTimelineKeyDur}`);
|
|
850
|
+
}
|
|
851
|
+
if (!("value" in key)) {
|
|
852
|
+
err(`${p}.value`, "is required");
|
|
853
|
+
ok = false;
|
|
854
|
+
}
|
|
855
|
+
const cp = isStr(key.path) ? canonicalSpecPath(base, key.path) : null;
|
|
856
|
+
if (!isStr(key.path) || key.path.trim() === "") {
|
|
857
|
+
err(`${p}.path`, "must be a non-empty dot-path");
|
|
858
|
+
ok = false;
|
|
859
|
+
} else if (!cp) {
|
|
860
|
+
err(`${p}.path`, `'${key.path}' does not resolve on the spec \u2014 a key can only animate a field the spec already has`);
|
|
861
|
+
ok = false;
|
|
862
|
+
} else if (cp.split(".").includes("timeline") || cp === "schemaVersion" || cp === "id" || cp === "label" || cp === "seed") {
|
|
863
|
+
err(`${p}.path`, `'${key.path}' is not animatable`);
|
|
864
|
+
ok = false;
|
|
865
|
+
}
|
|
866
|
+
if (!ok || !cp) return;
|
|
867
|
+
const t = key.t;
|
|
868
|
+
latest = Math.max(latest, t);
|
|
869
|
+
const applied = applyDeltasToSpec(base, [{ t: 0, path: cp, value: key.value }]);
|
|
870
|
+
const r = validateSpecPaths(applied, [cp]) ? { valid: true, errors: [] } : validateSpec(applied);
|
|
871
|
+
if (!r.valid) {
|
|
872
|
+
for (const e of r.errors) err(`${p}.value`, `applied at '${key.path}' \u2192 ${e.path || "<root>"}: ${e.message}`);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (structuralSignature(applied) !== baseSig) {
|
|
876
|
+
warn(`${p}.path`, "timeline-structural-key", `'${key.path}' is structural (placement, count, motion, size\u2026): the scene rebuilds at this key and entities re-seed \u2014 a pop, not a glide. Animate paint (colour, opacity, transform, polygon points) instead`);
|
|
877
|
+
}
|
|
878
|
+
const list = times.get(cp) ?? [];
|
|
879
|
+
list.push(t);
|
|
880
|
+
times.set(cp, list);
|
|
881
|
+
const dur = isNum(key.dur) ? key.dur : DEFAULT_KEY_DUR;
|
|
882
|
+
if (tl.loop === true && isNum(tl.duration) && t + dur > tl.duration) {
|
|
883
|
+
warn(`${p}.dur`, "timeline-glide-overruns-lap", `this key's glide ends at ${t + dur} ms, after the ${tl.duration} ms lap \u2014 the wrap cuts it short`);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
if (tl.loop === true && isNum(tl.duration) && latest >= tl.duration) {
|
|
887
|
+
err("timeline.duration", `must be greater than the latest key's t (${latest} ms) under loop`);
|
|
888
|
+
}
|
|
889
|
+
const minGap = LIMITS.minTimelineKeyInterval;
|
|
890
|
+
for (const [path, list] of times) {
|
|
891
|
+
const distinct = [...new Set(list)].sort((a, b) => a - b);
|
|
892
|
+
for (let i = 1; i < distinct.length; i++) {
|
|
893
|
+
if (distinct[i] - distinct[i - 1] < minGap) {
|
|
894
|
+
err("timeline.keys", `keys on '${path}' at ${distinct[i - 1]} and ${distinct[i]} ms are closer than ${minGap} ms (flash safety)`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (tl.loop === true && isNum(tl.duration) && distinct.length > 1) {
|
|
898
|
+
const wrapGap = tl.duration - distinct[distinct.length - 1] + distinct[0];
|
|
899
|
+
if (wrapGap < minGap) err("timeline.keys", `keys on '${path}' at ${distinct[distinct.length - 1]} ms and ${distinct[0]} ms (next lap) are closer than ${minGap} ms across the loop wrap (flash safety)`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (failed) return;
|
|
903
|
+
const keyed = [...timelineTracks(spec).keys()];
|
|
904
|
+
const seen = /* @__PURE__ */ new Set();
|
|
905
|
+
for (const t of timelineSampleTimes(spec, true, 120)) {
|
|
906
|
+
const r = resolveTimelineAt(spec, t);
|
|
907
|
+
const state = JSON.stringify(keyed.map((p) => readSpecPath(r, p)));
|
|
908
|
+
if (seen.has(state)) continue;
|
|
909
|
+
seen.add(state);
|
|
910
|
+
if (validateSpecPaths(r, keyed)) continue;
|
|
911
|
+
const e = validateSpec(r).errors[0];
|
|
912
|
+
err("timeline", `at t = ${Math.round(t)} ms the keys combine into an invalid scene \u2014 ${e ? `${e.path || "<root>"}: ${e.message}` : "invalid"}`);
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
430
916
|
function color(v, path, err) {
|
|
431
917
|
if (!isStr(v) || !HEX2.test(v)) err(path, "must be a hex colour like #0a3a52");
|
|
432
918
|
}
|
|
@@ -471,7 +957,7 @@ function validateBackground(bg, err, warn) {
|
|
|
471
957
|
if (!isObj(bg.band)) err("background.band", "must be an object");
|
|
472
958
|
else {
|
|
473
959
|
color(bg.band.color, "background.band.color", err);
|
|
474
|
-
|
|
960
|
+
positiveScalar(bg.band.height, "background.band.height", err);
|
|
475
961
|
}
|
|
476
962
|
}
|
|
477
963
|
for (const k of unknownKeys(bg, KNOWN_BG_GRADIENT)) {
|
|
@@ -537,7 +1023,7 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
537
1023
|
err(`${path}.count`, `at most ${LIMITS.maxPerLayer} per layer`);
|
|
538
1024
|
}
|
|
539
1025
|
if (layer.size !== void 0 && (!isRange(layer.size) || layer.size[0] <= 0)) {
|
|
540
|
-
err(`${path}.size`,
|
|
1026
|
+
err(`${path}.size`, rangeMsg(spec));
|
|
541
1027
|
}
|
|
542
1028
|
if (layer.wrap !== void 0 && typeof layer.wrap !== "boolean") err(`${path}.wrap`, "must be a boolean");
|
|
543
1029
|
if (layer.flip !== void 0 && typeof layer.flip !== "boolean") err(`${path}.flip`, "must be a boolean");
|
|
@@ -550,12 +1036,40 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
550
1036
|
if (layer.key !== void 0 && (!isStr(layer.key) || layer.key.trim() === "")) {
|
|
551
1037
|
err(`${path}.key`, "must be a non-empty string");
|
|
552
1038
|
}
|
|
1039
|
+
if (layer.opacity !== void 0 && (!isNum(layer.opacity) || layer.opacity < 0 || layer.opacity > 1)) {
|
|
1040
|
+
err(`${path}.opacity`, "must be a number 0..1");
|
|
1041
|
+
}
|
|
1042
|
+
if (layer.transform !== void 0) {
|
|
1043
|
+
const tf = layer.transform;
|
|
1044
|
+
if (!isObj(tf)) err(`${path}.transform`, "must be an object {x?, y?, scale?, scaleX?, rotate?}");
|
|
1045
|
+
else {
|
|
1046
|
+
for (const k of unknownKeys(tf, KNOWN_TRANSFORM)) warn(`${path}.transform.${k}`, "unknown-property", `unknown transform property '${k}' \u2014 will be ignored`);
|
|
1047
|
+
const [off, unit] = offsetBound(spec);
|
|
1048
|
+
const sc = LIMITS.maxLayerTransformScale;
|
|
1049
|
+
for (const axis of ["x", "y"]) {
|
|
1050
|
+
if (tf[axis] !== void 0 && (!isNum(tf[axis]) || Math.abs(tf[axis]) > off)) err(`${path}.transform.${axis}`, `must be a number within \xB1${off} (${unit})`);
|
|
1051
|
+
}
|
|
1052
|
+
if (tf.scale !== void 0 && (!isNum(tf.scale) || tf.scale < 0 || tf.scale > sc)) err(`${path}.transform.scale`, `must be a number 0..${sc}`);
|
|
1053
|
+
if (tf.scaleX !== void 0 && (!isNum(tf.scaleX) || Math.abs(tf.scaleX) > sc)) err(`${path}.transform.scaleX`, `must be a number within \xB1${sc} (negative mirrors)`);
|
|
1054
|
+
if (tf.rotate !== void 0 && (!isNum(tf.rotate) || Math.abs(tf.rotate) > LIMITS.maxLayerTransformRotate)) {
|
|
1055
|
+
err(`${path}.transform.rotate`, `must be a number of degrees within \xB1${LIMITS.maxLayerTransformRotate}`);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
553
1059
|
if (layer.position !== void 0) {
|
|
554
1060
|
if (!isObj(layer.position) || !isNum(layer.position.x) || !isNum(layer.position.y)) {
|
|
555
1061
|
err(`${path}.position`, "must be {x, y} with numbers 0..1");
|
|
556
1062
|
} else {
|
|
557
1063
|
if (layer.position.x < 0 || layer.position.x > 1) err(`${path}.position.x`, "must be 0..1");
|
|
558
1064
|
if (layer.position.y < 0 || layer.position.y > 1) err(`${path}.position.y`, "must be 0..1");
|
|
1065
|
+
const [off, unit] = offsetBound(spec);
|
|
1066
|
+
for (const axis of ["dx", "dy"]) {
|
|
1067
|
+
const v = layer.position[axis];
|
|
1068
|
+
if (v !== void 0 && (!isNum(v) || Math.abs(v) > off)) {
|
|
1069
|
+
err(`${path}.position.${axis}`, `must be a number within \xB1${off} (${unit})`);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
for (const k of unknownKeys(layer.position, KNOWN_POSITION)) warn(`${path}.position.${k}`, "unknown-property", `unknown position property '${k}' \u2014 will be ignored`);
|
|
559
1073
|
const dataLayout = isObj(layer.layout) && (layer.layout.type === "list" || layer.layout.type === "table");
|
|
560
1074
|
if (isNum(layer.count) && layer.count !== 1 && !dataLayout) {
|
|
561
1075
|
err(`${path}.position`, "position requires count: 1 \u2014 or a layout of type 'list' / 'table', where it anchors the whole block");
|
|
@@ -634,16 +1148,12 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
634
1148
|
if (!isNum(layer.links.k) || !Number.isInteger(layer.links.k) || layer.links.k < 1 || layer.links.k > LIMITS.maxLinksK) {
|
|
635
1149
|
err(`${path}.links.k`, `must be an integer 1..${LIMITS.maxLinksK}`);
|
|
636
1150
|
}
|
|
637
|
-
|
|
638
|
-
err(`${path}.links.maxDist`, "must be > 0");
|
|
639
|
-
}
|
|
1151
|
+
positiveScalar(layer.links.maxDist, `${path}.links.maxDist`, err);
|
|
640
1152
|
if (layer.links.color !== void 0) color(layer.links.color, `${path}.links.color`, err);
|
|
641
1153
|
if (layer.links.alpha !== void 0 && (!isNum(layer.links.alpha) || layer.links.alpha < 0 || layer.links.alpha > 1)) {
|
|
642
1154
|
err(`${path}.links.alpha`, "must be 0..1");
|
|
643
1155
|
}
|
|
644
|
-
if (layer.links.width !== void 0
|
|
645
|
-
err(`${path}.links.width`, "must be > 0");
|
|
646
|
-
}
|
|
1156
|
+
if (layer.links.width !== void 0) positiveScalar(layer.links.width, `${path}.links.width`, err);
|
|
647
1157
|
if (layer.links.mode !== void 0 && !["nearest", "chain", "random"].includes(layer.links.mode)) {
|
|
648
1158
|
err(`${path}.links.mode`, "must be 'nearest' | 'chain' | 'random'");
|
|
649
1159
|
}
|
|
@@ -797,10 +1307,10 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
797
1307
|
}
|
|
798
1308
|
}
|
|
799
1309
|
}
|
|
800
|
-
validateSprite(layer.sprite, `${path}.sprite`, err, warn);
|
|
1310
|
+
validateSprite(layer.sprite, `${path}.sprite`, err, warn, spec);
|
|
801
1311
|
validateMotion(layer.motion, `${path}.motion`, err, warn, spec);
|
|
802
1312
|
}
|
|
803
|
-
function validateSprite(sprite, path, err, warn) {
|
|
1313
|
+
function validateSprite(sprite, path, err, warn, spec) {
|
|
804
1314
|
if (!isObj(sprite)) return err(path, "must be an object");
|
|
805
1315
|
let knownSet;
|
|
806
1316
|
if (sprite.kind === "emoji") {
|
|
@@ -824,32 +1334,30 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
824
1334
|
if (sprite.baseline !== void 0 && !["top", "middle", "bottom"].includes(sprite.baseline)) {
|
|
825
1335
|
err(`${path}.baseline`, "must be top | middle | bottom");
|
|
826
1336
|
}
|
|
827
|
-
if (sprite.maxWidth !== void 0
|
|
828
|
-
err(`${path}.maxWidth`, "must be a positive number");
|
|
829
|
-
}
|
|
1337
|
+
if (sprite.maxWidth !== void 0) positiveScalar(sprite.maxWidth, `${path}.maxWidth`, err, "must be a positive number");
|
|
830
1338
|
validateTextRole(sprite, path, err);
|
|
831
1339
|
validateCycle(sprite, path, err);
|
|
832
1340
|
} else if (sprite.kind === "circle") {
|
|
833
1341
|
knownSet = KNOWN_CIRCLE;
|
|
834
|
-
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`,
|
|
1342
|
+
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec));
|
|
835
1343
|
color(sprite.color, `${path}.color`, err);
|
|
836
1344
|
if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
|
|
837
1345
|
validatePalette(sprite, path, err);
|
|
838
1346
|
} else if (sprite.kind === "ring") {
|
|
839
1347
|
knownSet = KNOWN_RING;
|
|
840
|
-
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`,
|
|
1348
|
+
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec));
|
|
841
1349
|
color(sprite.color, `${path}.color`, err);
|
|
842
|
-
if (sprite.width !== void 0
|
|
1350
|
+
if (sprite.width !== void 0) positiveScalar(sprite.width, `${path}.width`, err);
|
|
843
1351
|
validatePalette(sprite, path, err);
|
|
844
1352
|
} else if (sprite.kind === "streak") {
|
|
845
1353
|
knownSet = KNOWN_STREAK;
|
|
846
|
-
if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`,
|
|
1354
|
+
if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`, rangeMsg(spec));
|
|
847
1355
|
color(sprite.color, `${path}.color`, err);
|
|
848
|
-
if (sprite.width !== void 0
|
|
1356
|
+
if (sprite.width !== void 0) positiveScalar(sprite.width, `${path}.width`, err);
|
|
849
1357
|
validatePalette(sprite, path, err);
|
|
850
1358
|
} else if (sprite.kind === "rect") {
|
|
851
1359
|
knownSet = KNOWN_RECT;
|
|
852
|
-
if (!isRange(sprite.width) || sprite.width[0] <= 0) err(`${path}.width`,
|
|
1360
|
+
if (!isRange(sprite.width) || sprite.width[0] <= 0) err(`${path}.width`, rangeMsg(spec));
|
|
853
1361
|
if (sprite.aspect !== void 0 && (!isRange(sprite.aspect) || sprite.aspect[0] <= 0)) {
|
|
854
1362
|
err(`${path}.aspect`, "must be a [min,max] range of positive height/width ratios");
|
|
855
1363
|
}
|
|
@@ -863,9 +1371,9 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
863
1371
|
if (!Array.isArray(sprite.values) || sprite.values.length === 0 || sprite.values.length > LIMITS.maxPerLayer || !sprite.values.every((v) => isNum(v) && v >= 0)) {
|
|
864
1372
|
err(`${path}.values`, `must be 1..${LIMITS.maxPerLayer} numbers >= 0`);
|
|
865
1373
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
if (sprite.max !== void 0
|
|
1374
|
+
positiveScalar(sprite.length, `${path}.length`, err, "must be > 0 (full-scale bar length)");
|
|
1375
|
+
positiveScalar(sprite.thickness, `${path}.thickness`, err);
|
|
1376
|
+
if (sprite.max !== void 0) positiveScalar(sprite.max, `${path}.max`, err);
|
|
869
1377
|
if (sprite.direction !== void 0 && !["right", "left", "up", "down"].includes(sprite.direction)) {
|
|
870
1378
|
err(`${path}.direction`, "must be 'right' | 'left' | 'up' | 'down'");
|
|
871
1379
|
}
|
|
@@ -873,7 +1381,7 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
873
1381
|
validatePalette(sprite, path, err);
|
|
874
1382
|
} else if (sprite.kind === "polygon") {
|
|
875
1383
|
knownSet = KNOWN_POLYGON;
|
|
876
|
-
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`,
|
|
1384
|
+
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec, "circumradius"));
|
|
877
1385
|
if (sprite.sides !== void 0 && sprite.points !== void 0) {
|
|
878
1386
|
err(`${path}.sides`, "use sides (a regular polygon) or points (a custom one), not both");
|
|
879
1387
|
}
|
|
@@ -886,9 +1394,9 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
886
1394
|
validatePalette(sprite, path, err);
|
|
887
1395
|
} else if (sprite.kind === "stroke") {
|
|
888
1396
|
knownSet = KNOWN_STROKE;
|
|
889
|
-
if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`,
|
|
1397
|
+
if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`, rangeMsg(spec, "the mark's bounding size"));
|
|
890
1398
|
validateShapePoints(sprite.points, `${path}.points`, 2, err);
|
|
891
|
-
if (sprite.width !== void 0
|
|
1399
|
+
if (sprite.width !== void 0) positiveScalar(sprite.width, `${path}.width`, err);
|
|
892
1400
|
if (sprite.curve !== void 0 && sprite.curve !== "smooth" && sprite.curve !== "linear") err(`${path}.curve`, "must be 'smooth' | 'linear'");
|
|
893
1401
|
if (sprite.taper !== void 0 && typeof sprite.taper !== "boolean") err(`${path}.taper`, "must be a boolean");
|
|
894
1402
|
if (sprite.orient !== void 0 && typeof sprite.orient !== "boolean") err(`${path}.orient`, "must be a boolean");
|
|
@@ -901,10 +1409,14 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
901
1409
|
} else if (sprite.text.length > LIMITS.maxTextBlockLength) {
|
|
902
1410
|
err(`${path}.text`, `must be at most ${LIMITS.maxTextBlockLength} characters`);
|
|
903
1411
|
}
|
|
904
|
-
if (
|
|
1412
|
+
if (Array.isArray(sprite.maxWidth)) {
|
|
1413
|
+
err(`${path}.maxWidth`, "must be a single number (viewport fraction), not a [min,max] range");
|
|
1414
|
+
} else if (!isNum(sprite.maxWidth) || sprite.maxWidth <= 0 || sprite.maxWidth > LIMITS.maxTextBlockMaxWidth) {
|
|
905
1415
|
err(`${path}.maxWidth`, `must be a positive number up to ${LIMITS.maxTextBlockMaxWidth} (viewport fraction)`);
|
|
906
1416
|
}
|
|
907
|
-
if (
|
|
1417
|
+
if (Array.isArray(sprite.fontSize)) {
|
|
1418
|
+
err(`${path}.fontSize`, "must be a single number (viewport fraction), not a [min,max] range");
|
|
1419
|
+
} else if (!isNum(sprite.fontSize) || sprite.fontSize < LIMITS.minTextBlockFontSize || sprite.fontSize > LIMITS.maxTextBlockFontSize) {
|
|
908
1420
|
err(`${path}.fontSize`, `must be between ${LIMITS.minTextBlockFontSize} and ${LIMITS.maxTextBlockFontSize} (viewport fraction)`);
|
|
909
1421
|
}
|
|
910
1422
|
if (sprite.lineHeight !== void 0 && (!isNum(sprite.lineHeight) || sprite.lineHeight < 0.5 || sprite.lineHeight > 4)) {
|
|
@@ -1052,7 +1564,7 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
1052
1564
|
warn(`${path}.speed`, "near-zero-speed", `orbit speed is near zero \u2014 entities will appear frozen. Typical range: 5\u201360 deg/sec`);
|
|
1053
1565
|
}
|
|
1054
1566
|
}
|
|
1055
|
-
if (!isRange(motion.radius) || motion.radius[0] <= 0) err(`${path}.radius`,
|
|
1567
|
+
if (!isRange(motion.radius) || motion.radius[0] <= 0) err(`${path}.radius`, rangeMsg(spec));
|
|
1056
1568
|
if (motion.center !== void 0) {
|
|
1057
1569
|
if (!isObj(motion.center)) {
|
|
1058
1570
|
err(`${path}.center`, "must be {x, y} (0..1) or { layer: key }");
|
|
@@ -1144,14 +1656,14 @@ function validateShapePoints(points, path, min, err) {
|
|
|
1144
1656
|
}
|
|
1145
1657
|
}
|
|
1146
1658
|
}
|
|
1147
|
-
function validateEase(
|
|
1148
|
-
if (
|
|
1149
|
-
if (!isObj(
|
|
1150
|
-
if (
|
|
1151
|
-
if (!isNum(
|
|
1659
|
+
function validateEase(ease2, path, err, warn) {
|
|
1660
|
+
if (ease2 === void 0) return;
|
|
1661
|
+
if (!isObj(ease2)) return err(path, "must be an object { type: 'settle' | 'buoyant', tau }");
|
|
1662
|
+
if (ease2.type !== "settle" && ease2.type !== "buoyant") err(`${path}.type`, "must be 'settle' | 'buoyant'");
|
|
1663
|
+
if (!isNum(ease2.tau) || ease2.tau < LIMITS.minEaseTau || ease2.tau > LIMITS.maxEaseTau) {
|
|
1152
1664
|
err(`${path}.tau`, `must be ${LIMITS.minEaseTau}..${LIMITS.maxEaseTau} ms`);
|
|
1153
1665
|
}
|
|
1154
|
-
for (const k of unknownKeys(
|
|
1666
|
+
for (const k of unknownKeys(ease2, /* @__PURE__ */ new Set(["type", "tau"]))) {
|
|
1155
1667
|
warn(`${path}.${k}`, "unknown-property", `unknown ease property '${k}' \u2014 will be ignored`);
|
|
1156
1668
|
}
|
|
1157
1669
|
}
|
|
@@ -1183,6 +1695,7 @@ function validateSequence(seq) {
|
|
|
1183
1695
|
if (seq.seed !== void 0 && !isNum(seq.seed)) err("seed", "must be a number");
|
|
1184
1696
|
if (typeof seq.loop !== "boolean") err("loop", "must be a boolean");
|
|
1185
1697
|
if (seq.sync !== void 0 && seq.sync !== "mount" && seq.sync !== "epoch") err("sync", "must be 'mount' | 'epoch'");
|
|
1698
|
+
if (seq.wrapMorph !== void 0 && typeof seq.wrapMorph !== "boolean") err("wrapMorph", "must be a boolean");
|
|
1186
1699
|
if (seq.finish !== void 0) validateFinish(seq.finish, "finish", err, (p, code, message) => void warnings.push({ path: p, code, message }));
|
|
1187
1700
|
if (!Array.isArray(seq.segments) || seq.segments.length === 0) {
|
|
1188
1701
|
err("segments", "must be a non-empty array");
|
|
@@ -1236,8 +1749,8 @@ function validateSequence(seq) {
|
|
|
1236
1749
|
if (isObj(s.transition) && s.transition.text !== void 0) {
|
|
1237
1750
|
if (s.transition.type !== "morph") {
|
|
1238
1751
|
err(`${p}.transition.text`, "is a morph option (fade already cross-fades whole frames; cut has no window)");
|
|
1239
|
-
} else if (s.transition.text !== "step" && s.transition.text !== "crossfade") {
|
|
1240
|
-
err(`${p}.transition.text`, "must be 'step' | 'crossfade'");
|
|
1752
|
+
} else if (s.transition.text !== "step" && s.transition.text !== "crossfade" && s.transition.text !== "dip") {
|
|
1753
|
+
err(`${p}.transition.text`, "must be 'step' | 'crossfade' | 'dip'");
|
|
1241
1754
|
}
|
|
1242
1755
|
}
|
|
1243
1756
|
}
|
|
@@ -1294,7 +1807,7 @@ function validateSequence(seq) {
|
|
|
1294
1807
|
code: "morph-structural-mismatch",
|
|
1295
1808
|
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
1296
1809
|
});
|
|
1297
|
-
} else if (morphNothingMorphable(s.scene, next.scene, { textCrossfade: s.transition.text === "crossfade" })) {
|
|
1810
|
+
} else if (s.scene.timeline === void 0 && next.scene.timeline === void 0 && morphNothingMorphable(s.scene, next.scene, { textCrossfade: s.transition.text === "crossfade" || s.transition.text === "dip" })) {
|
|
1298
1811
|
warnings.push({
|
|
1299
1812
|
path: `segments[${i}].transition`,
|
|
1300
1813
|
code: "morph-nothing-morphable",
|
|
@@ -1303,6 +1816,20 @@ function validateSequence(seq) {
|
|
|
1303
1816
|
}
|
|
1304
1817
|
}
|
|
1305
1818
|
}
|
|
1819
|
+
if (errors.length === 0 && seq.wrapMorph === true) {
|
|
1820
|
+
const sq = seq;
|
|
1821
|
+
const last = sq.segments.length - 1;
|
|
1822
|
+
const lastTr = sq.segments[last]?.transition;
|
|
1823
|
+
let why = null;
|
|
1824
|
+
if (sq.loop !== true) why = "the sequence does not loop";
|
|
1825
|
+
else if (last < 1) why = "there is only one segment";
|
|
1826
|
+
else if (lastTr?.type !== "morph") why = "the last segment declares no morph transition";
|
|
1827
|
+
else if (structuralSignature(sq.segments[last].scene) !== structuralSignature(sq.segments[0].scene)) why = `segment ${last} and segment 0 differ structurally`;
|
|
1828
|
+
else if (morphChainRoot(sq, last) !== 0) why = `the lap is not one morph chain (segment ${morphChainRoot(sq, last)} starts a new chain), so the wrap would jump seeds`;
|
|
1829
|
+
if (why || !canWrapMorph(sq)) {
|
|
1830
|
+
warnings.push({ path: "wrapMorph", code: "wrap-morph-inactive", message: `wrapMorph has no effect: ${why ?? "the wrap cannot morph"} \u2014 the wrap stays a cut` });
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1306
1833
|
return { valid: errors.length === 0, errors, warnings };
|
|
1307
1834
|
}
|
|
1308
1835
|
function assertValidSequence(seq) {
|
|
@@ -1494,7 +2021,7 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1494
2021
|
if (mSpec.type === "path") {
|
|
1495
2022
|
pathPts = mSpec.points.map((p) => ({ x: p.x * w, y: p.y * h }));
|
|
1496
2023
|
}
|
|
1497
|
-
const
|
|
2024
|
+
const ease2 = mSpec.type === "drift" || mSpec.type === "rise" || mSpec.type === "wander" ? mSpec.ease : void 0;
|
|
1498
2025
|
const clockPhase = layer.clock ? (layer.clock.phase ?? 0) % 1 * Math.PI * 2 : 0;
|
|
1499
2026
|
const layout = layer.layout;
|
|
1500
2027
|
const ordered = layout?.type === "list" || layout?.type === "table";
|
|
@@ -1513,6 +2040,8 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1513
2040
|
if (layer.position) {
|
|
1514
2041
|
listX0 = layer.position.x * w;
|
|
1515
2042
|
listY0 = layer.position.y * h;
|
|
2043
|
+
if (layer.position.dx) listX0 += layer.position.dx * scale;
|
|
2044
|
+
if (layer.position.dy) listY0 += layer.position.dy * scale;
|
|
1516
2045
|
} else {
|
|
1517
2046
|
const [rx0, rx1] = layer.region?.x ?? [0, 1];
|
|
1518
2047
|
const [ry0, ry1] = layer.region?.y ?? [0, 1];
|
|
@@ -1607,6 +2136,8 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1607
2136
|
if (layer.position && layer.count === 1) {
|
|
1608
2137
|
x0 = layer.position.x * w;
|
|
1609
2138
|
y0 = layer.position.y * h;
|
|
2139
|
+
if (layer.position.dx) x0 += layer.position.dx * scale;
|
|
2140
|
+
if (layer.position.dy) y0 += layer.position.dy * scale;
|
|
1610
2141
|
} else if (layout?.type === "list" || layout?.type === "table") {
|
|
1611
2142
|
rng.next();
|
|
1612
2143
|
rng.next();
|
|
@@ -1688,7 +2219,7 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1688
2219
|
} : {},
|
|
1689
2220
|
// Time-structure fields (2026-09): ease draws nothing; emit's one draw
|
|
1690
2221
|
// per entity happens in a pass AFTER this loop (see below).
|
|
1691
|
-
...
|
|
2222
|
+
...ease2 ? { ease: { type: ease2.type, tau: ease2.tau } } : {},
|
|
1692
2223
|
// Static rotation (2026-09-10): a scalar draws nothing; the range form
|
|
1693
2224
|
// draws one value here, last, only when declared.
|
|
1694
2225
|
...typeof layer.rotate === "number" ? { rotate: layer.rotate * Math.PI / 180 } : Array.isArray(layer.rotate) ? { rotate: rng.range(layer.rotate[0], layer.rotate[1]) * Math.PI / 180 } : {}
|
|
@@ -2405,108 +2936,6 @@ function screen(fp, pat, alpha, off, w, h) {
|
|
|
2405
2936
|
p.restore();
|
|
2406
2937
|
}
|
|
2407
2938
|
|
|
2408
|
-
// src/sequence.ts
|
|
2409
|
-
function segmentStart(seq, index) {
|
|
2410
|
-
let t = 0;
|
|
2411
|
-
const n = Math.max(0, Math.min(index, seq.segments.length - 1));
|
|
2412
|
-
for (let i = 0; i < n; i++) t += seq.segments[i].duration ?? 0;
|
|
2413
|
-
return t;
|
|
2414
|
-
}
|
|
2415
|
-
function resolveSegment(seq, T, opts = {}) {
|
|
2416
|
-
const { segments, loop } = seq;
|
|
2417
|
-
if (segments.length === 0) return { index: 0, localT: 0, startT: 0 };
|
|
2418
|
-
let totalTimed = 0;
|
|
2419
|
-
for (const seg of segments) {
|
|
2420
|
-
if (seg.duration != null) totalTimed += seg.duration;
|
|
2421
|
-
else break;
|
|
2422
|
-
}
|
|
2423
|
-
const t0 = Math.max(0, T);
|
|
2424
|
-
const walk = (t, releasedBelow) => {
|
|
2425
|
-
let cumulative = 0;
|
|
2426
|
-
for (let i = 0; i < segments.length; i++) {
|
|
2427
|
-
const seg = segments[i];
|
|
2428
|
-
if (seg.duration == null) {
|
|
2429
|
-
return loop ? null : { index: i, localT: t - cumulative, startT: cumulative };
|
|
2430
|
-
}
|
|
2431
|
-
if (t < cumulative + seg.duration) {
|
|
2432
|
-
return { index: i, localT: t - cumulative, startT: cumulative };
|
|
2433
|
-
}
|
|
2434
|
-
if (seg.advance === "input" && i >= releasedBelow) {
|
|
2435
|
-
return { index: i, localT: t - cumulative, startT: cumulative, held: true };
|
|
2436
|
-
}
|
|
2437
|
-
cumulative += seg.duration;
|
|
2438
|
-
}
|
|
2439
|
-
return null;
|
|
2440
|
-
};
|
|
2441
|
-
const first = walk(t0, opts.releasedBelow ?? 0);
|
|
2442
|
-
if (first) return first;
|
|
2443
|
-
if (loop && totalTimed > 0) {
|
|
2444
|
-
const wrapped = walk(t0 % totalTimed, 0);
|
|
2445
|
-
if (wrapped) return wrapped;
|
|
2446
|
-
}
|
|
2447
|
-
const last = segments.length - 1;
|
|
2448
|
-
const lastStart = totalTimed - (segments[last].duration ?? 0);
|
|
2449
|
-
return { index: last, localT: t0 - lastStart, startT: lastStart };
|
|
2450
|
-
}
|
|
2451
|
-
function canMorph(seq, from) {
|
|
2452
|
-
const seg = seq.segments[from];
|
|
2453
|
-
if (!seg || seg.transition?.type !== "morph") return false;
|
|
2454
|
-
const next = seq.segments[from + 1];
|
|
2455
|
-
if (!next) return false;
|
|
2456
|
-
return structuralSignature(seg.scene) === structuralSignature(next.scene);
|
|
2457
|
-
}
|
|
2458
|
-
function morphChainRoot(seq, index) {
|
|
2459
|
-
let i = index;
|
|
2460
|
-
while (i > 0 && canMorph(seq, i - 1)) i--;
|
|
2461
|
-
return i;
|
|
2462
|
-
}
|
|
2463
|
-
function normalizeSeed(seed) {
|
|
2464
|
-
return seed >>> 0 || 1;
|
|
2465
|
-
}
|
|
2466
|
-
function segmentRenderSeed(seq, index) {
|
|
2467
|
-
const root = morphChainRoot(seq, index);
|
|
2468
|
-
const scene = seq.segments[root]?.scene;
|
|
2469
|
-
if (!scene) return void 0;
|
|
2470
|
-
const raw = scene.seed ?? (seq.seed !== void 0 ? seq.seed + root : void 0);
|
|
2471
|
-
return raw === void 0 ? void 0 : normalizeSeed(raw);
|
|
2472
|
-
}
|
|
2473
|
-
function bedRenderSeed(seq) {
|
|
2474
|
-
if (!seq.bed) return void 0;
|
|
2475
|
-
const raw = seq.bed.seed ?? (seq.seed !== void 0 ? seq.seed + LIMITS.maxSegments : void 0);
|
|
2476
|
-
return raw === void 0 ? void 0 : normalizeSeed(raw);
|
|
2477
|
-
}
|
|
2478
|
-
function sequenceWantsFinish(seq) {
|
|
2479
|
-
return seq.finish !== void 0 || seq.segments.some((s) => s.scene.finish !== void 0);
|
|
2480
|
-
}
|
|
2481
|
-
function sequenceSwapCompatible(prev, next) {
|
|
2482
|
-
if (prev.segments.length !== next.segments.length) return false;
|
|
2483
|
-
if (prev.seed === void 0 !== (next.seed === void 0)) return false;
|
|
2484
|
-
if (prev.seed !== void 0 && next.seed !== void 0 && normalizeSeed(prev.seed) !== normalizeSeed(next.seed)) return false;
|
|
2485
|
-
if (sequenceWantsFinish(prev) !== sequenceWantsFinish(next)) return false;
|
|
2486
|
-
if (prev.loop !== next.loop) return false;
|
|
2487
|
-
if ((prev.sync ?? "mount") !== (next.sync ?? "mount")) return false;
|
|
2488
|
-
if (!!prev.bed !== !!next.bed) return false;
|
|
2489
|
-
if (prev.bed && next.bed) {
|
|
2490
|
-
if (structuralSignature(prev.bed) !== structuralSignature(next.bed)) return false;
|
|
2491
|
-
if (bedRenderSeed(prev) !== bedRenderSeed(next)) return false;
|
|
2492
|
-
}
|
|
2493
|
-
for (let i = 0; i < prev.segments.length; i++) {
|
|
2494
|
-
const a = prev.segments[i];
|
|
2495
|
-
const b = next.segments[i];
|
|
2496
|
-
if (a.duration !== b.duration) return false;
|
|
2497
|
-
if ((a.advance ?? "auto") !== (b.advance ?? "auto")) return false;
|
|
2498
|
-
if (transitionKey(a.transition) !== transitionKey(b.transition)) return false;
|
|
2499
|
-
if (structuralSignature(a.scene) !== structuralSignature(b.scene)) return false;
|
|
2500
|
-
if (segmentRenderSeed(prev, i) !== segmentRenderSeed(next, i)) return false;
|
|
2501
|
-
}
|
|
2502
|
-
return true;
|
|
2503
|
-
}
|
|
2504
|
-
function transitionKey(tr) {
|
|
2505
|
-
if (!tr || tr.type === "cut") return "cut";
|
|
2506
|
-
if (tr.type === "morph") return `morph:${tr.dur}:${tr.text ?? "step"}`;
|
|
2507
|
-
return `fade:${tr.dur}`;
|
|
2508
|
-
}
|
|
2509
|
-
|
|
2510
2939
|
// src/compile.ts
|
|
2511
2940
|
var DEFAULT_STEER_DUR = 1e3;
|
|
2512
2941
|
function applyRetainedDeltas(spec, deltas) {
|
|
@@ -2554,10 +2983,62 @@ function manifestFor(spec) {
|
|
|
2554
2983
|
workerReady: true
|
|
2555
2984
|
};
|
|
2556
2985
|
}
|
|
2986
|
+
var clamp013 = (v) => v <= 0 ? 0 : v >= 1 ? 1 : v;
|
|
2987
|
+
var EXPIRED = /* @__PURE__ */ Symbol("expired");
|
|
2988
|
+
function overrideHeldAt(o, t) {
|
|
2989
|
+
const k = o.dur > 0 ? easeSmooth(clamp013((t - o.t0) / o.dur)) : 1;
|
|
2990
|
+
return k >= 1 ? o.to : lerpValue(o.from, o.to, k, false, o.leaf);
|
|
2991
|
+
}
|
|
2992
|
+
function overrideValueAt(o, t, timelineValue) {
|
|
2993
|
+
if (t < o.until) return overrideHeldAt(o, t);
|
|
2994
|
+
if (o.outDur > 0 && t < o.until + o.outDur) return lerpValue(overrideHeldAt(o, o.until), timelineValue(), easeSmooth((t - o.until) / o.outDur), false, o.leaf);
|
|
2995
|
+
return EXPIRED;
|
|
2996
|
+
}
|
|
2997
|
+
var steerId = (d) => `${d.t}|${JSON.stringify(d.value)}`;
|
|
2998
|
+
function armOverride(authored, path, from, to, now, glide) {
|
|
2999
|
+
const next = nextKeyAfter(authored, path, now);
|
|
3000
|
+
return { leaf: path.slice(path.lastIndexOf(".") + 1), from, to, t0: now, dur: glide, until: next ? next.at : Number.POSITIVE_INFINITY, outDur: next ? next.dur : 0 };
|
|
3001
|
+
}
|
|
3002
|
+
function applyOverridesAt(resolved, overrides, t, expired) {
|
|
3003
|
+
if (overrides.size === 0) return resolved;
|
|
3004
|
+
const deltas = [];
|
|
3005
|
+
for (const [path, o] of overrides) {
|
|
3006
|
+
const v = overrideValueAt(o, t, () => readSpecPath(resolved, path));
|
|
3007
|
+
if (v === EXPIRED) {
|
|
3008
|
+
expired?.push(path);
|
|
3009
|
+
continue;
|
|
3010
|
+
}
|
|
3011
|
+
deltas.push({ t: 0, path, value: v });
|
|
3012
|
+
}
|
|
3013
|
+
return deltas.length > 0 ? applyDeltasToSpec(resolved, deltas) : resolved;
|
|
3014
|
+
}
|
|
3015
|
+
function overridesStayValid(authored, overrides, now) {
|
|
3016
|
+
const paths = [...overrides.keys()];
|
|
3017
|
+
if (paths.length === 0) return true;
|
|
3018
|
+
const groupOf = (p) => p.split(".").slice(0, p.startsWith("layers.") ? 2 : 1).join(".");
|
|
3019
|
+
const groups = [...new Set(paths.map(groupOf))];
|
|
3020
|
+
const full = paths.some((p) => /^layers\.\d+(\.(key|count|motion)(\.|$)|$)/.test(p) || !/^(layers|background|finish)(\.|$)/.test(p));
|
|
3021
|
+
const only = full ? void 0 : (p) => groups.includes(groupOf(p));
|
|
3022
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3023
|
+
for (const t of [now, ...timelineSampleTimesAfter(authored, now, false, only)]) {
|
|
3024
|
+
const held = /* @__PURE__ */ new Map();
|
|
3025
|
+
for (const [p, o] of overrides) if (t < o.until) held.set(p, { ...o, t0: Number.NEGATIVE_INFINITY });
|
|
3026
|
+
const r = applyOverridesAt(resolveTimelineAt(authored, t), held, t);
|
|
3027
|
+
const state = JSON.stringify(groups.map((g) => readSpecPath(r, g))) + "|" + [...held.keys()].join(",");
|
|
3028
|
+
if (seen.has(state)) continue;
|
|
3029
|
+
seen.add(state);
|
|
3030
|
+
if (!validateSpecPaths(r, paths)) return false;
|
|
3031
|
+
}
|
|
3032
|
+
return true;
|
|
3033
|
+
}
|
|
2557
3034
|
var SpecInstance = class {
|
|
2558
3035
|
constructor(spec, ctx, opts = {}) {
|
|
2559
3036
|
this.spec = spec;
|
|
2560
3037
|
this.effSpec = spec;
|
|
3038
|
+
if (spec.timeline) {
|
|
3039
|
+
this.authored = spec;
|
|
3040
|
+
this.effSpec = resolveTimelineAt(spec, 0);
|
|
3041
|
+
}
|
|
2561
3042
|
this.saverCtx = ctx;
|
|
2562
3043
|
this.transparent = opts.transparent === true;
|
|
2563
3044
|
const tier = opts.capabilityTier;
|
|
@@ -2623,6 +3104,31 @@ var SpecInstance = class {
|
|
|
2623
3104
|
lastStructural = "";
|
|
2624
3105
|
/** See `LayerPaintOverride`; null (the default, and every plain mount) draws every layer once. */
|
|
2625
3106
|
paintOverrides = null;
|
|
3107
|
+
/**
|
|
3108
|
+
* The authored spec when it carries a `timeline` (null for every other
|
|
3109
|
+
* scene — which then never enters the timeline path). Each painted frame
|
|
3110
|
+
* resolves it at that frame's own time, so ghosting's warm-up replay and
|
|
3111
|
+
* any seek see the timeline exactly where it was.
|
|
3112
|
+
*/
|
|
3113
|
+
authored = null;
|
|
3114
|
+
/**
|
|
3115
|
+
* Live steers on a timeline scene, by index-form path. A steer on a path no
|
|
3116
|
+
* key animates holds forever (sticky, exactly as on a scene without a
|
|
3117
|
+
* timeline); on an animated path it holds `until` that path's next key,
|
|
3118
|
+
* then glides back to the timeline over `outDur`.
|
|
3119
|
+
*/
|
|
3120
|
+
overrides = /* @__PURE__ */ new Map();
|
|
3121
|
+
/**
|
|
3122
|
+
* The last registered steer per path, as `t|value`. A host re-sends the
|
|
3123
|
+
* whole channel track on every steer (idle-server broadcasts the full track
|
|
3124
|
+
* so viewers converge), and a steer's `t` is the server's stamp — stable
|
|
3125
|
+
* across re-sends. Without this, an unrelated later steer would re-arm a
|
|
3126
|
+
* hold the timeline had already taken back. Keyed by path (not by the full
|
|
3127
|
+
* `path|t|value` history) so a long-running instance re-steered thousands
|
|
3128
|
+
* of times doesn't accumulate an unbounded set — only the most recent id
|
|
3129
|
+
* per path can ever recur.
|
|
3130
|
+
*/
|
|
3131
|
+
seenSteers = /* @__PURE__ */ new Map();
|
|
2626
3132
|
/** Raster short side for a `field` background — halved on the low tiers. */
|
|
2627
3133
|
fieldShortSide;
|
|
2628
3134
|
/** The `field` background's cached raster; null until a field is first painted. */
|
|
@@ -2647,12 +3153,14 @@ var SpecInstance = class {
|
|
|
2647
3153
|
}
|
|
2648
3154
|
sizeCanvas() {
|
|
2649
3155
|
const dpr = Math.min(this.saverCtx.dpr, 2);
|
|
2650
|
-
|
|
2651
|
-
|
|
3156
|
+
const cw = Math.max(1, Math.round(this.w * dpr));
|
|
3157
|
+
const ch = Math.max(1, Math.round(this.h * dpr));
|
|
3158
|
+
if (this.canvas.width !== cw) this.canvas.width = cw;
|
|
3159
|
+
if (this.canvas.height !== ch) this.canvas.height = ch;
|
|
2652
3160
|
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2653
3161
|
if (this.finishPass) {
|
|
2654
|
-
this.finishPass.canvas.width
|
|
2655
|
-
this.finishPass.canvas.height
|
|
3162
|
+
if (this.finishPass.canvas.width !== cw) this.finishPass.canvas.width = cw;
|
|
3163
|
+
if (this.finishPass.canvas.height !== ch) this.finishPass.canvas.height = ch;
|
|
2656
3164
|
this.finishPass.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2657
3165
|
}
|
|
2658
3166
|
this.lastRenderT = Number.NEGATIVE_INFINITY;
|
|
@@ -3105,6 +3613,12 @@ var SpecInstance = class {
|
|
|
3105
3613
|
*/
|
|
3106
3614
|
applyTrack(track) {
|
|
3107
3615
|
const deltas = track?.deltas ?? [];
|
|
3616
|
+
if (this.authored) {
|
|
3617
|
+
const glide = deltas.length ? deltas.reduce((m, d) => Math.max(m, d.dur ?? DEFAULT_STEER_DUR), 0) : DEFAULT_STEER_DUR;
|
|
3618
|
+
if (!this.overrideAuthored(deltas, this.paused ? 0 : glide)) return;
|
|
3619
|
+
if (this.paused) this.renderFrame(this.sceneNow(), this.seed);
|
|
3620
|
+
return;
|
|
3621
|
+
}
|
|
3108
3622
|
const target = applyDeltasToSpec(this.effSpec, deltas);
|
|
3109
3623
|
if (!validateSpec(target).valid) return;
|
|
3110
3624
|
const dur = deltas.length ? deltas.reduce((m, d) => Math.max(m, d.dur ?? DEFAULT_STEER_DUR), 0) : DEFAULT_STEER_DUR;
|
|
@@ -3141,6 +3655,13 @@ var SpecInstance = class {
|
|
|
3141
3655
|
* showing a ghost of the un-steered scene.
|
|
3142
3656
|
*/
|
|
3143
3657
|
applyDeltasNow(deltas) {
|
|
3658
|
+
if (this.authored) {
|
|
3659
|
+
const all = [...deltas];
|
|
3660
|
+
if (!this.overrideAuthored(all, 0)) for (const d of all) this.overrideAuthored([d], 0);
|
|
3661
|
+
this.transition = null;
|
|
3662
|
+
this.lastRenderT = Number.NEGATIVE_INFINITY;
|
|
3663
|
+
return;
|
|
3664
|
+
}
|
|
3144
3665
|
const next = applyRetainedDeltas(this.effSpec, deltas);
|
|
3145
3666
|
if (next === this.effSpec) return;
|
|
3146
3667
|
this.transition = null;
|
|
@@ -3196,6 +3717,7 @@ var SpecInstance = class {
|
|
|
3196
3717
|
* showing through — the ghosting smear. */
|
|
3197
3718
|
paintFrame(t, bgAlpha) {
|
|
3198
3719
|
const { ctx } = this;
|
|
3720
|
+
if (this.authored) this.resolveAuthored(t);
|
|
3199
3721
|
if (!this.transparent) {
|
|
3200
3722
|
ctx.globalAlpha = bgAlpha;
|
|
3201
3723
|
ctx.globalCompositeOperation = "source-over";
|
|
@@ -3204,9 +3726,15 @@ var SpecInstance = class {
|
|
|
3204
3726
|
ctx.globalAlpha = 1;
|
|
3205
3727
|
for (let li = 0; li < this.layers.length; li++) {
|
|
3206
3728
|
const built = this.layers[li];
|
|
3207
|
-
const
|
|
3729
|
+
const opacity = built.layer.opacity;
|
|
3730
|
+
const lifeA = opacity === void 0 ? lifeAlphaAt(built.layer.life, t) : lifeAlphaAt(built.layer.life, t) * clamp013(opacity);
|
|
3208
3731
|
if (lifeA <= 0) continue;
|
|
3209
3732
|
const parentE = this.parentEntityFor(built);
|
|
3733
|
+
const tf = built.layer.transform;
|
|
3734
|
+
if (tf) {
|
|
3735
|
+
ctx.save();
|
|
3736
|
+
this.applyLayerTransform(tf);
|
|
3737
|
+
}
|
|
3210
3738
|
ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
|
|
3211
3739
|
this.drawLinks(built, t, lifeA, parentE);
|
|
3212
3740
|
const override = this.paintOverrides?.get(li) ?? null;
|
|
@@ -3216,13 +3744,80 @@ var SpecInstance = class {
|
|
|
3216
3744
|
this.drawEntity(built, e, t, lifeA, parentE);
|
|
3217
3745
|
continue;
|
|
3218
3746
|
}
|
|
3219
|
-
|
|
3220
|
-
if (
|
|
3747
|
+
const [aOut, aIn] = override.mode === "dip" ? [Math.max(0, 1 - 2 * override.k), Math.max(0, 2 * override.k - 1)] : [1 - override.k, override.k];
|
|
3748
|
+
if (aOut > 0) this.drawEntity(built, e, t, lifeA, parentE, { sprite: override.outgoing, alpha: aOut });
|
|
3749
|
+
if (aIn > 0) this.drawEntity(built, e, t, lifeA, parentE, { alpha: aIn });
|
|
3221
3750
|
}
|
|
3751
|
+
if (tf) ctx.restore();
|
|
3222
3752
|
}
|
|
3223
3753
|
ctx.globalAlpha = 1;
|
|
3224
3754
|
ctx.globalCompositeOperation = "source-over";
|
|
3225
3755
|
}
|
|
3756
|
+
/** A layer `transform`, about the viewport centre, in min(w,h) units (px under `units: 'px'`). */
|
|
3757
|
+
applyLayerTransform(tf) {
|
|
3758
|
+
const { ctx, w, h } = this;
|
|
3759
|
+
const unit = this.effSpec.units === "px" ? 1 : Math.min(w, h);
|
|
3760
|
+
const cx = w / 2;
|
|
3761
|
+
const cy = h / 2;
|
|
3762
|
+
ctx.translate(cx + (tf.x ?? 0) * unit, cy + (tf.y ?? 0) * unit);
|
|
3763
|
+
if (tf.rotate) ctx.rotate(tf.rotate * Math.PI / 180);
|
|
3764
|
+
const s = tf.scale ?? 1;
|
|
3765
|
+
ctx.scale(s * (tf.scaleX ?? 1), s);
|
|
3766
|
+
ctx.translate(-cx, -cy);
|
|
3767
|
+
}
|
|
3768
|
+
/** Scene time for a timeline decision: the last painted frame's, else the loop clock's. */
|
|
3769
|
+
sceneNow() {
|
|
3770
|
+
return this.lastRenderT !== Number.NEGATIVE_INFINITY ? this.lastRenderT : this.lastT;
|
|
3771
|
+
}
|
|
3772
|
+
/** Resolve the authored timeline at `t`, layer the live overrides on top, and adopt the result. */
|
|
3773
|
+
resolveAuthored(t) {
|
|
3774
|
+
const expired = [];
|
|
3775
|
+
const spec = applyOverridesAt(resolveTimelineAt(this.authored, t), this.overrides, t, expired);
|
|
3776
|
+
for (const p of expired) this.overrides.delete(p);
|
|
3777
|
+
this.effSpec = spec;
|
|
3778
|
+
const sig = structuralSignature(spec);
|
|
3779
|
+
if (sig !== this.lastStructural) this.rebuild();
|
|
3780
|
+
else this.layers.forEach((b, i) => {
|
|
3781
|
+
b.layer = spec.layers[i] ?? b.layer;
|
|
3782
|
+
});
|
|
3783
|
+
}
|
|
3784
|
+
/** Register live steers on a timeline scene under the steering rule (see `overrides`). */
|
|
3785
|
+
overrideAuthored(deltas, glide) {
|
|
3786
|
+
const now = this.sceneNow();
|
|
3787
|
+
const shown = this.effSpec;
|
|
3788
|
+
const latest = /* @__PURE__ */ new Map();
|
|
3789
|
+
for (const d of deltas) {
|
|
3790
|
+
const path = canonicalSpecPath(shown, d.path);
|
|
3791
|
+
if (!path) continue;
|
|
3792
|
+
latest.delete(path);
|
|
3793
|
+
latest.set(path, d);
|
|
3794
|
+
}
|
|
3795
|
+
const next = new Map(this.overrides);
|
|
3796
|
+
const armed = [];
|
|
3797
|
+
for (const [path, d] of latest) {
|
|
3798
|
+
const id = steerId(d);
|
|
3799
|
+
if (this.seenSteers.get(path) === id) continue;
|
|
3800
|
+
armed.push([path, id]);
|
|
3801
|
+
next.set(path, armOverride(this.authored, path, glide > 0 ? readSpecPath(shown, path) : d.value, d.value, now, glide));
|
|
3802
|
+
}
|
|
3803
|
+
if (armed.length === 0) return true;
|
|
3804
|
+
if (!overridesStayValid(this.authored, next, now)) return false;
|
|
3805
|
+
for (const [p, id] of armed) this.seenSteers.set(p, id);
|
|
3806
|
+
this.overrides = next;
|
|
3807
|
+
return true;
|
|
3808
|
+
}
|
|
3809
|
+
/** A copy of the live overrides — a sequence carries them into a morph's outgoing endpoint. */
|
|
3810
|
+
timelineOverrides() {
|
|
3811
|
+
return this.authored ? new Map(this.overrides) : null;
|
|
3812
|
+
}
|
|
3813
|
+
/** A copy of which server steers this child already applied, per field (see `seenSteers`). */
|
|
3814
|
+
timelineSeen() {
|
|
3815
|
+
return new Map(this.seenSteers);
|
|
3816
|
+
}
|
|
3817
|
+
/** The spec painted by the last frame (timeline and overrides resolved). */
|
|
3818
|
+
presentedSpec() {
|
|
3819
|
+
return this.effSpec;
|
|
3820
|
+
}
|
|
3226
3821
|
/**
|
|
3227
3822
|
* Deterministic, frame-addressable render (shared by the rAF loop).
|
|
3228
3823
|
*
|
|
@@ -3275,6 +3870,11 @@ var SpecInstance = class {
|
|
|
3275
3870
|
* already knows the signature is unchanged.
|
|
3276
3871
|
*/
|
|
3277
3872
|
hotSwapSpec(spec) {
|
|
3873
|
+
this.adoptAuthored(spec);
|
|
3874
|
+
if (this.authored) {
|
|
3875
|
+
this.resolveAuthored(this.sceneNow());
|
|
3876
|
+
return;
|
|
3877
|
+
}
|
|
3278
3878
|
this.effSpec = spec;
|
|
3279
3879
|
const sig = structuralSignature(this.effSpec);
|
|
3280
3880
|
if (sig !== this.lastStructural) this.rebuild();
|
|
@@ -3282,8 +3882,68 @@ var SpecInstance = class {
|
|
|
3282
3882
|
b.layer = this.effSpec.layers[i] ?? b.layer;
|
|
3283
3883
|
});
|
|
3284
3884
|
}
|
|
3885
|
+
/**
|
|
3886
|
+
* Track whether `spec` carries a timeline. Live overrides belong to the
|
|
3887
|
+
* scene they were made on: a different spec (a new publish, a sequence's
|
|
3888
|
+
* re-steered segment, a morph frame) drops them, as a plain hot-swap
|
|
3889
|
+
* replaces `effSpec` wholesale; the host re-sends the live track.
|
|
3890
|
+
*/
|
|
3891
|
+
adoptAuthored(spec, keepSteers = false) {
|
|
3892
|
+
const next = spec.timeline ? spec : null;
|
|
3893
|
+
if (next !== this.authored && !(keepSteers && next && this.authored)) {
|
|
3894
|
+
this.overrides.clear();
|
|
3895
|
+
this.seenSteers.clear();
|
|
3896
|
+
}
|
|
3897
|
+
this.authored = next;
|
|
3898
|
+
}
|
|
3899
|
+
/**
|
|
3900
|
+
* A sequence's hot swap of a timeline segment: the new scene replaces the
|
|
3901
|
+
* old, but the live steers (and which server steers were already applied)
|
|
3902
|
+
* carry over — a steer the timeline already took back stays taken back —
|
|
3903
|
+
* and only a steer this child has never seen is armed, now. Frame
|
|
3904
|
+
* continuity is kept, so ghosting trails do not restart.
|
|
3905
|
+
*/
|
|
3906
|
+
hotSwapKeepingSteers(spec, retained) {
|
|
3907
|
+
if (!spec.timeline) {
|
|
3908
|
+
this.hotSwapSpec(spec);
|
|
3909
|
+
return;
|
|
3910
|
+
}
|
|
3911
|
+
if (!this.authored) {
|
|
3912
|
+
this.hotSwapSpec(spec);
|
|
3913
|
+
this.applyDeltasNow(retained);
|
|
3914
|
+
return;
|
|
3915
|
+
}
|
|
3916
|
+
this.adoptAuthored(spec, true);
|
|
3917
|
+
for (const d of retained) this.overrideAuthored([d], 0);
|
|
3918
|
+
this.resolveAuthored(this.sceneNow());
|
|
3919
|
+
}
|
|
3920
|
+
/** Arm a steer made while a sequence was mid-morph, on the outgoing end's override set, at that end's scene time. */
|
|
3921
|
+
static armInto(authored, overrides, seen, deltas, now) {
|
|
3922
|
+
const at = resolveTimelineAt(authored, now);
|
|
3923
|
+
const latest = /* @__PURE__ */ new Map();
|
|
3924
|
+
for (const d of deltas) {
|
|
3925
|
+
const path = canonicalSpecPath(at, d.path);
|
|
3926
|
+
if (!path) continue;
|
|
3927
|
+
latest.delete(path);
|
|
3928
|
+
latest.set(path, d);
|
|
3929
|
+
}
|
|
3930
|
+
for (const [path, d] of latest) {
|
|
3931
|
+
const id = steerId(d);
|
|
3932
|
+
if (seen.get(path) === id) continue;
|
|
3933
|
+
const trial = new Map(overrides).set(path, armOverride(authored, path, d.value, d.value, now, 0));
|
|
3934
|
+
if (overridesStayValid(authored, trial, now)) {
|
|
3935
|
+
overrides.set(path, trial.get(path));
|
|
3936
|
+
seen.set(path, id);
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3285
3940
|
/** Paint-only hot-swap: skips structuralSignature (caller guarantees match). */
|
|
3286
3941
|
hotSwapPaint(spec) {
|
|
3942
|
+
this.adoptAuthored(spec);
|
|
3943
|
+
if (this.authored) {
|
|
3944
|
+
this.resolveAuthored(this.sceneNow());
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3287
3947
|
this.effSpec = spec;
|
|
3288
3948
|
this.layers.forEach((b, i) => {
|
|
3289
3949
|
b.layer = this.effSpec.layers[i] ?? b.layer;
|
|
@@ -3359,6 +4019,8 @@ var SequenceInstance = class {
|
|
|
3359
4019
|
* window (if any) has passed.
|
|
3360
4020
|
*/
|
|
3361
4021
|
pendingWrapFade = false;
|
|
4022
|
+
/** The morph in progress: its ends, and the override sets each end resolves with (see `morphEndpoint`). */
|
|
4023
|
+
morphEnds = null;
|
|
3362
4024
|
/**
|
|
3363
4025
|
* Every non-`sequence.segment` delta this instance has been handed, last
|
|
3364
4026
|
* wins per path, merged across `applyTrack` calls. Children are created
|
|
@@ -3483,10 +4145,15 @@ var SequenceInstance = class {
|
|
|
3483
4145
|
const tr = this.seq.segments[from]?.transition;
|
|
3484
4146
|
return tr?.type === "morph" ? tr.dur : 0;
|
|
3485
4147
|
}
|
|
3486
|
-
/** Whether the morph out of `from`
|
|
4148
|
+
/** Whether the morph out of `from` paints its text in two passes — `crossfade` or `dip` (default `step`: strings switch at k > 0). */
|
|
3487
4149
|
morphTextCrossfades(from) {
|
|
3488
4150
|
const tr = this.seq.segments[from]?.transition;
|
|
3489
|
-
return tr?.type === "morph" && tr.text === "crossfade";
|
|
4151
|
+
return tr?.type === "morph" && (tr.text === "crossfade" || tr.text === "dip");
|
|
4152
|
+
}
|
|
4153
|
+
/** The morph out of `from`'s two-pass text mode, for `crossfadeOverrides`. */
|
|
4154
|
+
morphTextMode(from) {
|
|
4155
|
+
const tr = this.seq.segments[from]?.transition;
|
|
4156
|
+
return tr?.type === "morph" && tr.text === "dip" ? "dip" : "crossfade";
|
|
3490
4157
|
}
|
|
3491
4158
|
/**
|
|
3492
4159
|
* The paint overrides for one crossfade frame: every text layer whose
|
|
@@ -3495,7 +4162,7 @@ var SequenceInstance = class {
|
|
|
3495
4162
|
* usual) at `1 − k` under the incoming at `k`. Null when nothing differs,
|
|
3496
4163
|
* so a crossfade between same-worded twins is a plain morph frame.
|
|
3497
4164
|
*/
|
|
3498
|
-
crossfadeOverrides(specA, specB, lerped, k) {
|
|
4165
|
+
crossfadeOverrides(specA, specB, lerped, k, mode = "crossfade") {
|
|
3499
4166
|
let map = null;
|
|
3500
4167
|
for (let i = 0; i < lerped.layers.length; i++) {
|
|
3501
4168
|
if (!textStringsDiffer(specA, specB, i)) continue;
|
|
@@ -3503,7 +4170,7 @@ var SequenceInstance = class {
|
|
|
3503
4170
|
const now = lerped.layers[i].sprite;
|
|
3504
4171
|
const outgoing = from.kind === "text" && now.kind === "text" ? { ...now, strings: from.strings } : from.kind === "textBlock" && now.kind === "textBlock" ? { ...now, text: from.text } : now;
|
|
3505
4172
|
if (outgoing === now) continue;
|
|
3506
|
-
(map ??= /* @__PURE__ */ new Map()).set(i, { outgoing, k });
|
|
4173
|
+
(map ??= /* @__PURE__ */ new Map()).set(i, mode === "dip" ? { outgoing, k, mode } : { outgoing, k });
|
|
3507
4174
|
}
|
|
3508
4175
|
return map;
|
|
3509
4176
|
}
|
|
@@ -3576,6 +4243,46 @@ var SequenceInstance = class {
|
|
|
3576
4243
|
this.renderOffscreen(this.fading, outDur + localT, this.childSeed(this.morphChainRoot(from), seed));
|
|
3577
4244
|
return this.fading;
|
|
3578
4245
|
}
|
|
4246
|
+
/**
|
|
4247
|
+
* One end of a morph at segment time `t`. A scene without a timeline is its
|
|
4248
|
+
* steered scene, as always. With a timeline, the retained track follows the
|
|
4249
|
+
* same rule a child applies (held until the path's next key, sticky
|
|
4250
|
+
* otherwise): the outgoing end uses its child's live overrides when the
|
|
4251
|
+
* morph began from one, and otherwise — like the incoming end, whose child
|
|
4252
|
+
* does not exist yet — arms the retained steers at segment time 0, as a
|
|
4253
|
+
* freshly mounted child would.
|
|
4254
|
+
*/
|
|
4255
|
+
morphEndpoint(index, t, side) {
|
|
4256
|
+
const scene = this.childScene(index);
|
|
4257
|
+
if (!scene.timeline) return this.steeredScene(scene);
|
|
4258
|
+
const ends = this.morphEnds;
|
|
4259
|
+
if (ends && side === "out") ends.outT = t;
|
|
4260
|
+
let ov = side === "out" ? ends?.out ?? null : ends?.in ?? null;
|
|
4261
|
+
if (!ov) {
|
|
4262
|
+
ov = this.armRetained(scene, side === "out" ? ends?.seenOut : void 0);
|
|
4263
|
+
if (ends) {
|
|
4264
|
+
if (side === "out") ends.out = ov;
|
|
4265
|
+
else ends.in = ov;
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
return applyOverridesAt(resolveTimelineAt(scene, t), ov, t);
|
|
4269
|
+
}
|
|
4270
|
+
/** The retained steers that resolve (and validate) on a timeline scene, armed at segment time 0. */
|
|
4271
|
+
armRetained(scene, seen) {
|
|
4272
|
+
const out = /* @__PURE__ */ new Map();
|
|
4273
|
+
if (this.retainedDeltas.size === 0) return out;
|
|
4274
|
+
const at0 = resolveTimelineAt(scene, 0);
|
|
4275
|
+
for (const d of this.retainedDeltas.values()) {
|
|
4276
|
+
const path = canonicalSpecPath(at0, d.path);
|
|
4277
|
+
if (!path) continue;
|
|
4278
|
+
const trial = new Map(out).set(path, armOverride(scene, path, d.value, d.value, 0, 0));
|
|
4279
|
+
if (overridesStayValid(scene, trial, 0)) {
|
|
4280
|
+
out.set(path, trial.get(path));
|
|
4281
|
+
seen?.set(path, steerId(d));
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
return out;
|
|
4285
|
+
}
|
|
3579
4286
|
/** A segment's scene with the retained track applied — `scene` itself when nothing resolves on it. */
|
|
3580
4287
|
steeredScene(scene) {
|
|
3581
4288
|
return this.retainedDeltas.size === 0 ? scene : applyRetainedDeltas(scene, this.retainedDeltas.values());
|
|
@@ -3594,7 +4301,7 @@ var SequenceInstance = class {
|
|
|
3594
4301
|
if (index < 0 || index >= this.seq.segments.length) index = 0;
|
|
3595
4302
|
let child = this.children[index];
|
|
3596
4303
|
if (!child) {
|
|
3597
|
-
child = new SpecInstance(rootScene ?? this.childScene(index), this.childCtx, { transparent: this.bed !== null, skipInitialPaint, capabilityTier: this.capabilityTier, hostPresents: true });
|
|
4304
|
+
child = new SpecInstance(rootScene ?? this.childScene(index), this.childCtx, { transparent: this.bed !== null, skipInitialPaint: skipInitialPaint || this.bed !== null, capabilityTier: this.capabilityTier, hostPresents: true });
|
|
3598
4305
|
this.children[index] = child;
|
|
3599
4306
|
child.setPaused(true);
|
|
3600
4307
|
if (rootScene) child.hotSwapSpec(this.childScene(index));
|
|
@@ -3649,7 +4356,12 @@ var SequenceInstance = class {
|
|
|
3649
4356
|
frameFinish() {
|
|
3650
4357
|
if (this.seq.finish !== void 0) return this.seq.finish;
|
|
3651
4358
|
if (this.activeIndex < 0) return void 0;
|
|
3652
|
-
|
|
4359
|
+
const scene = this.childScene(this.activeIndex);
|
|
4360
|
+
if (scene.timeline) {
|
|
4361
|
+
const live = (this.fadingIn?.index === this.activeIndex ? this.fadingIn.child : null) ?? this.children[this.activeIndex] ?? this.children[this.morphChainRoot(this.activeIndex)];
|
|
4362
|
+
if (live) return live.presentedSpec().finish;
|
|
4363
|
+
}
|
|
4364
|
+
return this.steeredScene(scene).finish;
|
|
3653
4365
|
}
|
|
3654
4366
|
/**
|
|
3655
4367
|
* The seed `frameFinish()`'s grain resolves against — the sequence's own
|
|
@@ -3674,29 +4386,36 @@ var SequenceInstance = class {
|
|
|
3674
4386
|
const resolved = resolveSegment(this.seq, T + this.clockOffset, { releasedBelow: this.releasedBelow });
|
|
3675
4387
|
const { index, localT } = resolved;
|
|
3676
4388
|
this.bed?.renderFrame(T, this.seq.bed?.seed ?? this.seq.seed ?? seed);
|
|
3677
|
-
const prevIdx = index > 0 ? index - 1 : -1;
|
|
3678
|
-
const morphActive = prevIdx >= 0 && this.canMorph(prevIdx) && localT < this.morphDur(prevIdx);
|
|
3679
4389
|
const lastIdx = this.seq.segments.length - 1;
|
|
3680
4390
|
const wrapped = this.seq.loop && index === 0 && lastIdx > 0 && (T + this.clockOffset >= this.timedTotal() || this.pendingWrapFade);
|
|
4391
|
+
const naturalWrap = this.seq.loop && index === 0 && lastIdx > 0 && T + this.clockOffset >= this.timedTotal();
|
|
4392
|
+
const prevIdx = index > 0 ? index - 1 : -1;
|
|
4393
|
+
const morphFrom = prevIdx >= 0 ? prevIdx : naturalWrap && canWrapMorph(this.seq) ? lastIdx : -1;
|
|
4394
|
+
const morphActive = morphFrom >= 0 && (morphFrom === prevIdx ? this.canMorph(prevIdx) : true) && localT < this.morphDur(morphFrom);
|
|
3681
4395
|
const fadeFrom = prevIdx >= 0 ? prevIdx : wrapped ? lastIdx : -1;
|
|
3682
4396
|
const fadeActive = fadeFrom >= 0 && localT < this.fadeDur(fadeFrom);
|
|
3683
4397
|
if (!fadeActive && this.fading) this.releaseFading();
|
|
3684
4398
|
if (this.pendingWrapFade && (index !== 0 || !fadeActive)) this.pendingWrapFade = false;
|
|
3685
4399
|
if (morphActive) {
|
|
3686
4400
|
const chainRoot = this.morphChainRoot(index);
|
|
3687
|
-
this.morphFromIndex
|
|
4401
|
+
if (this.morphFromIndex !== morphFrom || this.morphEnds?.to !== index) {
|
|
4402
|
+
const outChild = this.children[morphFrom];
|
|
4403
|
+
this.morphEnds = { from: morphFrom, to: index, outT: 0, out: outChild?.timelineOverrides() ?? null, seenOut: outChild?.timelineSeen() ?? /* @__PURE__ */ new Map(), in: null };
|
|
4404
|
+
}
|
|
4405
|
+
this.morphFromIndex = morphFrom;
|
|
3688
4406
|
this.activeIndex = index;
|
|
3689
4407
|
for (let i = 0; i < this.children.length; i++) {
|
|
3690
4408
|
if (i !== chainRoot) this.releaseChild(i);
|
|
3691
4409
|
}
|
|
3692
|
-
const child = this.ensureChild(chainRoot, void 0, this.morphTextCrossfades(
|
|
3693
|
-
const dur = this.morphDur(
|
|
4410
|
+
const child = this.ensureChild(chainRoot, void 0, this.morphTextCrossfades(morphFrom));
|
|
4411
|
+
const dur = this.morphDur(morphFrom);
|
|
3694
4412
|
const k = easeSmooth(localT / dur);
|
|
3695
|
-
const
|
|
3696
|
-
const
|
|
4413
|
+
const outDur = this.seq.segments[morphFrom].duration ?? 0;
|
|
4414
|
+
const specA = this.morphEndpoint(morphFrom, outDur + localT, "out");
|
|
4415
|
+
const specB = this.morphEndpoint(index, localT, "in");
|
|
3697
4416
|
const lerped = lerpSpec(specA, specB, k);
|
|
3698
4417
|
child.hotSwapSpec(lerped);
|
|
3699
|
-
child.setLayerPaintOverrides(this.morphTextCrossfades(
|
|
4418
|
+
child.setLayerPaintOverrides(this.morphTextCrossfades(morphFrom) ? this.crossfadeOverrides(specA, specB, lerped, k, this.morphTextMode(morphFrom)) : null);
|
|
3700
4419
|
child.renderFrame(localT, this.childSeed(chainRoot, seed));
|
|
3701
4420
|
} else {
|
|
3702
4421
|
if (this.morphFromIndex >= 0) {
|
|
@@ -3704,6 +4423,7 @@ var SequenceInstance = class {
|
|
|
3704
4423
|
this.releaseChild(oldRoot);
|
|
3705
4424
|
if (oldRoot !== this.morphFromIndex) this.releaseChild(this.morphFromIndex);
|
|
3706
4425
|
this.morphFromIndex = -1;
|
|
4426
|
+
this.morphEnds = null;
|
|
3707
4427
|
}
|
|
3708
4428
|
if (index !== this.activeIndex) {
|
|
3709
4429
|
for (let i = 0; i < this.children.length; i++) {
|
|
@@ -3783,6 +4503,12 @@ var SequenceInstance = class {
|
|
|
3783
4503
|
const childDeltas = deltas.filter((d) => d.path !== "sequence.segment");
|
|
3784
4504
|
if (childDeltas.length > 0) {
|
|
3785
4505
|
if (this.morphFromIndex >= 0) {
|
|
4506
|
+
const ends = this.morphEnds;
|
|
4507
|
+
if (ends) {
|
|
4508
|
+
const outScene = this.childScene(ends.from);
|
|
4509
|
+
if (ends.out && outScene.timeline) SpecInstance.armInto(outScene, ends.out, ends.seenOut, childDeltas, ends.outT);
|
|
4510
|
+
ends.in = null;
|
|
4511
|
+
}
|
|
3786
4512
|
if (!switchedSegment) this.renderFrame(this.renderedT, this.seed);
|
|
3787
4513
|
} else if (this.activeIndex >= 0) {
|
|
3788
4514
|
const child = this.children[this.activeIndex];
|
|
@@ -3812,11 +4538,19 @@ var SequenceInstance = class {
|
|
|
3812
4538
|
if (!sequenceSwapCompatible(this.seq, next)) return false;
|
|
3813
4539
|
this.seq = next;
|
|
3814
4540
|
if (this.bed && next.bed) this.bed.hotSwapSpec(applyRetainedDeltas(this.bedScene(next.bed), this.retainedBedDeltas.values()));
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
4541
|
+
const swap = (child, index) => {
|
|
4542
|
+
if (!child) return;
|
|
4543
|
+
const scene = this.childScene(index);
|
|
4544
|
+
if (!scene.timeline) {
|
|
4545
|
+
child.hotSwapSpec(this.steeredScene(scene));
|
|
4546
|
+
return;
|
|
4547
|
+
}
|
|
4548
|
+
child.hotSwapKeepingSteers(scene, this.retainedDeltas.values());
|
|
4549
|
+
};
|
|
4550
|
+
for (let i = 0; i < this.children.length; i++) swap(this.children[i], i);
|
|
4551
|
+
if (this.fading) swap(this.fading.child, this.fading.index);
|
|
4552
|
+
if (this.fadingIn) swap(this.fadingIn.child, this.fadingIn.index);
|
|
4553
|
+
this.morphEnds = null;
|
|
3820
4554
|
if (this.paused) this.renderFrame(this.renderedT, this.seed);
|
|
3821
4555
|
return true;
|
|
3822
4556
|
}
|
|
@@ -4209,12 +4943,43 @@ function seamsWorthWarning(layer, c, entityCount) {
|
|
|
4209
4943
|
return layer.blend !== "lighter" && layer.blend !== "screen";
|
|
4210
4944
|
}
|
|
4211
4945
|
|
|
4946
|
+
// src/paint.ts
|
|
4947
|
+
function transformPoint(tf, p, w, h, unit) {
|
|
4948
|
+
const s = tf.scale ?? 1;
|
|
4949
|
+
const sx = s * (tf.scaleX ?? 1);
|
|
4950
|
+
let x = (p.x - w / 2) * sx;
|
|
4951
|
+
let y = (p.y - h / 2) * s;
|
|
4952
|
+
if (tf.rotate) {
|
|
4953
|
+
const r = tf.rotate * Math.PI / 180;
|
|
4954
|
+
const c = Math.cos(r);
|
|
4955
|
+
const n = Math.sin(r);
|
|
4956
|
+
[x, y] = [x * c - y * n, x * n + y * c];
|
|
4957
|
+
}
|
|
4958
|
+
return { x: x + w / 2 + (tf.x ?? 0) * unit, y: y + h / 2 + (tf.y ?? 0) * unit };
|
|
4959
|
+
}
|
|
4960
|
+
function transformSize(tf) {
|
|
4961
|
+
if (!tf) return 1;
|
|
4962
|
+
const s = tf.scale ?? 1;
|
|
4963
|
+
return s * Math.sqrt(Math.abs(tf.scaleX ?? 1));
|
|
4964
|
+
}
|
|
4965
|
+
var layerOpacity = (layer) => layer.opacity === void 0 ? 1 : Math.max(0, Math.min(1, layer.opacity));
|
|
4966
|
+
function transformBox(tf, box, w, h, unit) {
|
|
4967
|
+
const pts = [[box.x0, box.y0], [box.x1, box.y0], [box.x0, box.y1], [box.x1, box.y1]].map(([x, y]) => transformPoint(tf, { x, y }, w, h, unit));
|
|
4968
|
+
return {
|
|
4969
|
+
x0: Math.min(...pts.map((p) => p.x)),
|
|
4970
|
+
y0: Math.min(...pts.map((p) => p.y)),
|
|
4971
|
+
x1: Math.max(...pts.map((p) => p.x)),
|
|
4972
|
+
y1: Math.max(...pts.map((p) => p.y))
|
|
4973
|
+
};
|
|
4974
|
+
}
|
|
4975
|
+
|
|
4212
4976
|
// src/advise.ts
|
|
4213
4977
|
var LOW_CONTRAST_FLOOR = 0.05;
|
|
4214
4978
|
var SPARSE_DECLARED_MAX_COVERAGE = 0.02;
|
|
4215
4979
|
var READ_LEGIBILITY_FLOOR = 4.5;
|
|
4216
4980
|
var READ_SAFE_AREA = 0.05;
|
|
4217
4981
|
function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
4982
|
+
spec = resolveTimelineAt(spec, opts.t ?? COHESION_T);
|
|
4218
4983
|
const warnings = [];
|
|
4219
4984
|
const w = viewport.width;
|
|
4220
4985
|
const h = viewport.height;
|
|
@@ -4312,6 +5077,8 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
4312
5077
|
let pixArea;
|
|
4313
5078
|
if (layer.sprite.kind === "circle") {
|
|
4314
5079
|
pixArea = Math.PI * r * r;
|
|
5080
|
+
} else if (layer.sprite.kind === "polygon") {
|
|
5081
|
+
pixArea = polygonArea(polygonPoints(layer.sprite, r));
|
|
4315
5082
|
} else if (layer.sprite.kind === "bar") {
|
|
4316
5083
|
pixArea = e.size * barFraction(layer.sprite, e.barIndex ?? 0) * (e.size2 ?? e.size * 0.2);
|
|
4317
5084
|
} else if (layer.sprite.kind === "textBlock") {
|
|
@@ -4438,8 +5205,10 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
4438
5205
|
const label = layer.key ? `\`${layer.key}\`` : `layers[${li}]`;
|
|
4439
5206
|
for (const e of allEntities[li]) {
|
|
4440
5207
|
const p = positionAt(e, 0, w, h);
|
|
4441
|
-
const
|
|
4442
|
-
const
|
|
5208
|
+
const raw = s.kind === "textBlock" ? textBlockBoxAt(s, p, w, h) : textBoxAt(s, e, p, spec, w, h);
|
|
5209
|
+
const box = layer.transform ? { ...raw, ...transformBox(layer.transform, raw, w, h, scale) } : raw;
|
|
5210
|
+
const baseAlpha = Math.max(0, Math.min(1, e.alpha - e.pulseAmp)) * (s.kind === "textBlock" ? s.opacity ?? 1 : 1);
|
|
5211
|
+
const alpha = layer.opacity === void 0 ? baseAlpha : baseAlpha * layerOpacity(layer);
|
|
4443
5212
|
textBoxes.push({ li, label, alpha, ...box });
|
|
4444
5213
|
}
|
|
4445
5214
|
}
|
|
@@ -4568,8 +5337,8 @@ function brightestAdditivePlate(spec, allEntities, ground, box, w, h) {
|
|
|
4568
5337
|
const growScale = (e.motion === "warp" ? WARP_MAX_SCALE : 1) * (e.emit ? Math.max(e.emit.growFrom, e.emit.growTo) : 1);
|
|
4569
5338
|
const maxDim = Math.max(e.size, e.size2 ?? 0) * (1 + e.growAmp) * growScale;
|
|
4570
5339
|
if (maxDim < box.fs) continue;
|
|
4571
|
-
if (!entityReachesBox(e, maxDim / 2, box, w, h)) continue;
|
|
4572
|
-
const a = Math.min(1, e.alpha + e.pulseAmp);
|
|
5340
|
+
if (!layer.transform && !entityReachesBox(e, maxDim / 2, box, w, h)) continue;
|
|
5341
|
+
const a = layer.opacity === void 0 ? Math.min(1, e.alpha + e.pulseAmp) : Math.min(1, e.alpha + e.pulseAmp) * layerOpacity(layer);
|
|
4573
5342
|
if (a <= 0) continue;
|
|
4574
5343
|
const plate = additivePlate(ground, hexRgb(hex), a, blend);
|
|
4575
5344
|
const lum = relativeLuminance(plate);
|
|
@@ -4636,8 +5405,8 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
|
4636
5405
|
});
|
|
4637
5406
|
}
|
|
4638
5407
|
for (let i = 0; i < seq.segments.length - 1; i++) {
|
|
4639
|
-
const lumaA = backgroundLuma(seq.segments[i].scene);
|
|
4640
|
-
const lumaB = backgroundLuma(seq.segments[i + 1].scene);
|
|
5408
|
+
const lumaA = backgroundLuma(resolveTimelineAt(seq.segments[i].scene, seq.segments[i].duration ?? 0));
|
|
5409
|
+
const lumaB = backgroundLuma(resolveTimelineAt(seq.segments[i + 1].scene, 0));
|
|
4641
5410
|
const delta = Math.abs(lumaA - lumaB);
|
|
4642
5411
|
if (delta > 0.5) {
|
|
4643
5412
|
warnings.push({
|
|
@@ -4656,7 +5425,7 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
|
4656
5425
|
code: "morph-structural-mismatch",
|
|
4657
5426
|
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
4658
5427
|
});
|
|
4659
|
-
} else if (morphNothingMorphable(seq.segments[i].scene, seq.segments[i + 1].scene, { textCrossfade: tr.text === "crossfade" })) {
|
|
5428
|
+
} else if (!seq.segments[i].scene.timeline && !seq.segments[i + 1].scene.timeline && morphNothingMorphable(seq.segments[i].scene, seq.segments[i + 1].scene, { textCrossfade: tr.text === "crossfade" || tr.text === "dip" })) {
|
|
4660
5429
|
warnings.push({
|
|
4661
5430
|
path: `segments[${i}].transition`,
|
|
4662
5431
|
code: "morph-nothing-morphable",
|
|
@@ -4690,7 +5459,7 @@ function describeScene(spec, options = {}) {
|
|
|
4690
5459
|
let pixArea = 0;
|
|
4691
5460
|
for (const e of entities) {
|
|
4692
5461
|
const r = e.size / 2;
|
|
4693
|
-
pixArea += layer.sprite.kind === "circle" ? Math.PI * r * r : e.size * e.size;
|
|
5462
|
+
pixArea += layer.sprite.kind === "circle" ? Math.PI * r * r : layer.sprite.kind === "polygon" ? polygonArea(polygonPoints(layer.sprite, r)) : e.size * e.size;
|
|
4694
5463
|
}
|
|
4695
5464
|
const coverage = pixArea / (w * h);
|
|
4696
5465
|
const meanAlpha = entities.length > 0 ? entities.reduce((s, e) => s + e.alpha, 0) / entities.length : 0;
|
|
@@ -4766,9 +5535,11 @@ import { createRng as createRng4 } from "@idle-screens/core";
|
|
|
4766
5535
|
var GLOW_SPREAD = 2.4;
|
|
4767
5536
|
var LINE_SALIENCE = 3.5;
|
|
4768
5537
|
var GHOST_TAPS = 12;
|
|
5538
|
+
var POLYGON_OUTLINE_CELLS = 3;
|
|
5539
|
+
var POLYGON_SCANLINES = 4;
|
|
4769
5540
|
function buildScene(spec, opts) {
|
|
4770
5541
|
const { width: w, height: h } = opts.viewport ?? { width: 1920, height: 1080 };
|
|
4771
|
-
const seed = opts.seed ?? spec.seed ?? 42;
|
|
5542
|
+
const seed = normalizeSeed(opts.seed ?? spec.seed ?? 42);
|
|
4772
5543
|
const scale = spec.units === "px" ? 1 : Math.min(w, h);
|
|
4773
5544
|
const refVp = spec.referenceViewport ?? LIMITS.referenceViewport;
|
|
4774
5545
|
let countScale = scale > 1 ? Math.min(w, h) / refVp : 1;
|
|
@@ -4780,7 +5551,13 @@ function buildScene(spec, opts) {
|
|
|
4780
5551
|
const layers = spec.layers.map((layer) => ({ layer, entities: buildEntities(layer, rng, w, h, scale, countScale) }));
|
|
4781
5552
|
const byKey = /* @__PURE__ */ new Map();
|
|
4782
5553
|
for (const { layer, entities } of layers) if (layer.key) byKey.set(layer.key, entities);
|
|
4783
|
-
|
|
5554
|
+
let transforms = null;
|
|
5555
|
+
for (const { layer, entities } of layers) {
|
|
5556
|
+
if (!layer.transform) continue;
|
|
5557
|
+
transforms ??= /* @__PURE__ */ new Map();
|
|
5558
|
+
for (const e of entities) transforms.set(e, layer.transform);
|
|
5559
|
+
}
|
|
5560
|
+
return { w, h, scale, seed, layers, byKey, transforms };
|
|
4784
5561
|
}
|
|
4785
5562
|
function posOf(scene, e, t) {
|
|
4786
5563
|
const p = positionAt(e, t, scene.w, scene.h);
|
|
@@ -4792,7 +5569,8 @@ function posOf(scene, e, t) {
|
|
|
4792
5569
|
p.y += pp.y;
|
|
4793
5570
|
}
|
|
4794
5571
|
}
|
|
4795
|
-
|
|
5572
|
+
const tf = scene.transforms?.get(e);
|
|
5573
|
+
return tf ? transformPoint(tf, p, scene.w, scene.h, scene.scale) : p;
|
|
4796
5574
|
}
|
|
4797
5575
|
function textBox(s, e, p) {
|
|
4798
5576
|
const m = s.font ? /(\d+(?:\.\d+)?)px/.exec(s.font) : null;
|
|
@@ -4844,7 +5622,66 @@ function textBlockRevealFraction(s, w, h, t) {
|
|
|
4844
5622
|
}
|
|
4845
5623
|
return rs.progress;
|
|
4846
5624
|
}
|
|
5625
|
+
function polygonCellCoverage(pts, cellW, cellH, c0, c1, r0, r1) {
|
|
5626
|
+
const nc = c1 - c0 + 1;
|
|
5627
|
+
const cov = new Float64Array(nc * (r1 - r0 + 1));
|
|
5628
|
+
const xMin = c0 * cellW;
|
|
5629
|
+
const xMax = (c1 + 1) * cellW;
|
|
5630
|
+
const share = 1 / POLYGON_SCANLINES;
|
|
5631
|
+
const hits = [];
|
|
5632
|
+
const addSpan = (row, xa, xb) => {
|
|
5633
|
+
const a = Math.max(xMin, xa);
|
|
5634
|
+
const b = Math.min(xMax, xb);
|
|
5635
|
+
if (!(b > a)) return;
|
|
5636
|
+
const first = Math.max(c0, Math.floor(a / cellW));
|
|
5637
|
+
const last = Math.min(c1, Math.floor(b / cellW));
|
|
5638
|
+
for (let c = first; c <= last; c++) {
|
|
5639
|
+
const len = Math.min(b, (c + 1) * cellW) - Math.max(a, c * cellW);
|
|
5640
|
+
if (len > 0) cov[row * nc + (c - c0)] += len / cellW * share;
|
|
5641
|
+
}
|
|
5642
|
+
};
|
|
5643
|
+
for (let r = r0; r <= r1; r++) {
|
|
5644
|
+
for (let k = 0; k < POLYGON_SCANLINES; k++) {
|
|
5645
|
+
const y = (r + (k + 0.5) / POLYGON_SCANLINES) * cellH;
|
|
5646
|
+
hits.length = 0;
|
|
5647
|
+
for (let i = 0; i < pts.length; i++) {
|
|
5648
|
+
const p = pts[i];
|
|
5649
|
+
const q = pts[(i + 1) % pts.length];
|
|
5650
|
+
const down = p.y <= y && q.y > y;
|
|
5651
|
+
if (!down && !(q.y <= y && p.y > y)) continue;
|
|
5652
|
+
hits.push({ x: p.x + (y - p.y) / (q.y - p.y) * (q.x - p.x), dir: down ? 1 : -1 });
|
|
5653
|
+
}
|
|
5654
|
+
if (hits.length < 2) continue;
|
|
5655
|
+
hits.sort((m, n) => m.x - n.x);
|
|
5656
|
+
let wind = 0;
|
|
5657
|
+
let start = 0;
|
|
5658
|
+
for (const hit of hits) {
|
|
5659
|
+
const before = wind;
|
|
5660
|
+
wind += hit.dir;
|
|
5661
|
+
if (before === 0 && wind !== 0) start = hit.x;
|
|
5662
|
+
else if (before !== 0 && wind === 0) addSpan(r - r0, start, hit.x);
|
|
5663
|
+
}
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5666
|
+
return cov;
|
|
5667
|
+
}
|
|
5668
|
+
function distanceToOutline(pts, x, y) {
|
|
5669
|
+
let best = Infinity;
|
|
5670
|
+
for (let i = 0; i < pts.length; i++) {
|
|
5671
|
+
const p = pts[i];
|
|
5672
|
+
const q = pts[(i + 1) % pts.length];
|
|
5673
|
+
const ex = q.x - p.x;
|
|
5674
|
+
const ey = q.y - p.y;
|
|
5675
|
+
const len2 = ex * ex + ey * ey;
|
|
5676
|
+
const u = len2 > 0 ? Math.max(0, Math.min(1, ((x - p.x) * ex + (y - p.y) * ey) / len2)) : 0;
|
|
5677
|
+
const dx = x - (p.x + ex * u);
|
|
5678
|
+
const dy = y - (p.y + ey * u);
|
|
5679
|
+
best = Math.min(best, dx * dx + dy * dy);
|
|
5680
|
+
}
|
|
5681
|
+
return Math.sqrt(best);
|
|
5682
|
+
}
|
|
4847
5683
|
function luminanceGrid(spec, opts = {}) {
|
|
5684
|
+
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
4848
5685
|
const scene = buildScene(spec, opts);
|
|
4849
5686
|
const t = opts.t ?? 5e3;
|
|
4850
5687
|
const cols = Math.max(8, Math.min(200, opts.cols ?? 80));
|
|
@@ -4860,7 +5697,7 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
4860
5697
|
} else if (bg.type === "field") {
|
|
4861
5698
|
bgCells = new Array(cols * rows);
|
|
4862
5699
|
const short = Math.max(1, Math.min(w, h));
|
|
4863
|
-
const seed =
|
|
5700
|
+
const seed = scene.seed;
|
|
4864
5701
|
const ft = fieldSampleTime(bg, t);
|
|
4865
5702
|
for (let r = 0; r < rows; r++) {
|
|
4866
5703
|
let rowSum = 0;
|
|
@@ -4943,9 +5780,66 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
4943
5780
|
}
|
|
4944
5781
|
}
|
|
4945
5782
|
};
|
|
5783
|
+
const splatPolygonOutline = (layer, s, e, p, sz, a, lum, tPass) => {
|
|
5784
|
+
const radius = sz / 2;
|
|
5785
|
+
const rot = rotationAt(e, tPass);
|
|
5786
|
+
const cr = Math.cos(rot);
|
|
5787
|
+
const sr = Math.sin(rot);
|
|
5788
|
+
const pts = polygonPoints(s, radius).map((q) => ({ x: p.x + q.x * cr - q.y * sr, y: p.y + q.x * sr + q.y * cr }));
|
|
5789
|
+
const additive = layer.blend === "lighter" || layer.blend === "screen";
|
|
5790
|
+
const glow = !!s.soft && additive;
|
|
5791
|
+
let band = 0;
|
|
5792
|
+
if (glow) {
|
|
5793
|
+
const perimeter = pts.reduce((acc, q, i) => {
|
|
5794
|
+
const n = pts[(i + 1) % pts.length];
|
|
5795
|
+
return acc + Math.hypot(n.x - q.x, n.y - q.y);
|
|
5796
|
+
}, 0);
|
|
5797
|
+
band = perimeter > 0 ? (GLOW_SPREAD - 1) * (2 * polygonArea(pts) / perimeter) : 0;
|
|
5798
|
+
}
|
|
5799
|
+
let minX = Infinity;
|
|
5800
|
+
let maxX = -Infinity;
|
|
5801
|
+
let minY = Infinity;
|
|
5802
|
+
let maxY = -Infinity;
|
|
5803
|
+
for (const q of pts) {
|
|
5804
|
+
minX = Math.min(minX, q.x);
|
|
5805
|
+
maxX = Math.max(maxX, q.x);
|
|
5806
|
+
minY = Math.min(minY, q.y);
|
|
5807
|
+
maxY = Math.max(maxY, q.y);
|
|
5808
|
+
}
|
|
5809
|
+
const c0 = Math.max(0, Math.floor((minX - band) / cellW));
|
|
5810
|
+
const c1 = Math.min(cols - 1, Math.floor((maxX + band) / cellW));
|
|
5811
|
+
const r0 = Math.max(0, Math.floor((minY - band) / cellH));
|
|
5812
|
+
const r1 = Math.min(rows - 1, Math.floor((maxY + band) / cellH));
|
|
5813
|
+
if (c0 > c1 || r0 > r1) return;
|
|
5814
|
+
const cov = polygonCellCoverage(pts, cellW, cellH, c0, c1, r0, r1);
|
|
5815
|
+
const nc = c1 - c0 + 1;
|
|
5816
|
+
for (let r = r0; r <= r1; r++) {
|
|
5817
|
+
for (let c = c0; c <= c1; c++) {
|
|
5818
|
+
const f = cov[(r - r0) * nc + (c - c0)];
|
|
5819
|
+
const x = (c + 0.5) * cellW;
|
|
5820
|
+
const y = (r + 0.5) * cellH;
|
|
5821
|
+
let wgt;
|
|
5822
|
+
if (glow) {
|
|
5823
|
+
const outX = Math.max(0, minX - x, x - maxX);
|
|
5824
|
+
const outY = Math.max(0, minY - y, y - maxY);
|
|
5825
|
+
if (f >= 1) wgt = 1;
|
|
5826
|
+
else if (outX * outX + outY * outY >= band * band) wgt = f;
|
|
5827
|
+
else {
|
|
5828
|
+
const k = band > 0 ? Math.max(0, 1 - distanceToOutline(pts, x, y) / band) : 0;
|
|
5829
|
+
wgt = f + (1 - f) * k * k;
|
|
5830
|
+
}
|
|
5831
|
+
} else if (s.soft) {
|
|
5832
|
+
wgt = f * Math.max(0.1, 1 - Math.hypot(x - p.x, y - p.y) / Math.max(radius, 1e-6));
|
|
5833
|
+
} else {
|
|
5834
|
+
wgt = f;
|
|
5835
|
+
}
|
|
5836
|
+
if (wgt > 0) compose(r * cols + c, lum, a * Math.min(1, wgt), layer.blend);
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5839
|
+
};
|
|
4946
5840
|
const splatPass = (tPass, alphaScale) => {
|
|
4947
5841
|
for (const { layer, entities } of scene.layers) {
|
|
4948
|
-
const lifeA = lifeAlphaAt(layer.life, tPass);
|
|
5842
|
+
const lifeA = layer.opacity === void 0 ? lifeAlphaAt(layer.life, tPass) : lifeAlphaAt(layer.life, tPass) * layerOpacity(layer);
|
|
4949
5843
|
if (lifeA <= 0) continue;
|
|
4950
5844
|
for (const e of entities) {
|
|
4951
5845
|
splatTrail(layer, e, tPass, lifeA, alphaScale);
|
|
@@ -4953,8 +5847,12 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
4953
5847
|
if (a <= 4e-3) continue;
|
|
4954
5848
|
const lum = spriteLuma(layer, e);
|
|
4955
5849
|
const p = posOf(scene, e, tPass);
|
|
4956
|
-
const sz = sizeAt(e, tPass);
|
|
5850
|
+
const sz = layer.transform ? sizeAt(e, tPass) * transformSize(layer.transform) : sizeAt(e, tPass);
|
|
4957
5851
|
const s = layer.sprite;
|
|
5852
|
+
if (s.kind === "polygon" && sz >= POLYGON_OUTLINE_CELLS * Math.min(cellW, cellH)) {
|
|
5853
|
+
splatPolygonOutline(layer, s, e, p, sz, a, lum, tPass);
|
|
5854
|
+
continue;
|
|
5855
|
+
}
|
|
4958
5856
|
if (s.kind === "stroke") {
|
|
4959
5857
|
const pts = strokeSamples(s, sz / 2);
|
|
4960
5858
|
let angle = rotationAt(e, tPass);
|
|
@@ -5200,6 +6098,7 @@ function renderDensityMap(grid) {
|
|
|
5200
6098
|
return lines.join("\n");
|
|
5201
6099
|
}
|
|
5202
6100
|
function textSprites(spec, opts = {}) {
|
|
6101
|
+
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
5203
6102
|
const scene = buildScene(spec, opts);
|
|
5204
6103
|
const t = opts.t ?? 5e3;
|
|
5205
6104
|
const out = [];
|
|
@@ -5237,6 +6136,7 @@ function rankDominance(raw) {
|
|
|
5237
6136
|
}));
|
|
5238
6137
|
}
|
|
5239
6138
|
function rawDominance(spec, opts = {}) {
|
|
6139
|
+
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
5240
6140
|
const scene = buildScene(spec, opts);
|
|
5241
6141
|
const t = opts.t ?? 5e3;
|
|
5242
6142
|
const { w, h, scale } = scene;
|
|
@@ -5261,7 +6161,8 @@ function rawDominance(spec, opts = {}) {
|
|
|
5261
6161
|
return Math.sqrt(dx * dx + dy * dy);
|
|
5262
6162
|
};
|
|
5263
6163
|
const raw = scene.layers.map(({ layer, entities }, layerIndex) => {
|
|
5264
|
-
const lifeA = lifeAlphaAt(layer.life, t);
|
|
6164
|
+
const lifeA = layer.opacity === void 0 ? lifeAlphaAt(layer.life, t) : lifeAlphaAt(layer.life, t) * layerOpacity(layer);
|
|
6165
|
+
const areaScale = layer.transform ? transformSize(layer.transform) ** 2 : 1;
|
|
5265
6166
|
let area = 0;
|
|
5266
6167
|
let lumAcc = 0;
|
|
5267
6168
|
for (const e of entities) {
|
|
@@ -5290,7 +6191,7 @@ function rawDominance(spec, opts = {}) {
|
|
|
5290
6191
|
const box = textBlockBox(s, e, { x: 0, y: 0 }, w, h);
|
|
5291
6192
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55 * textBlockRevealFraction(s, w, h, t) * (s.opacity ?? 1);
|
|
5292
6193
|
} else entArea = sz * sz * 0.55;
|
|
5293
|
-
area += entArea * a;
|
|
6194
|
+
area += layer.transform ? entArea * areaScale * a : entArea * a;
|
|
5294
6195
|
if (layer.trail) {
|
|
5295
6196
|
const fade = layer.trail.fade ?? 1;
|
|
5296
6197
|
const span = sweptDistance(e, layer.trail.length);
|
|
@@ -5320,29 +6221,32 @@ function rawDominance(spec, opts = {}) {
|
|
|
5320
6221
|
return raw;
|
|
5321
6222
|
}
|
|
5322
6223
|
function layerCohesion(spec, opts = {}) {
|
|
6224
|
+
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
5323
6225
|
const scene = buildScene(spec, opts);
|
|
5324
6226
|
return cohesionOf(scene.layers, opts.t ?? 5e3, scene.w, scene.h);
|
|
5325
6227
|
}
|
|
5326
6228
|
function motionStats(spec, opts = {}) {
|
|
5327
|
-
const scene = buildScene(spec, opts);
|
|
5328
6229
|
const t = opts.t ?? 5e3;
|
|
5329
6230
|
const dt = opts.dt ?? 500;
|
|
5330
|
-
const
|
|
5331
|
-
|
|
6231
|
+
const scene0 = buildScene(resolveTimelineAt(spec, t), opts);
|
|
6232
|
+
const scene1 = spec.timeline ? buildScene(resolveTimelineAt(spec, t + dt), opts) : scene0;
|
|
6233
|
+
const { w, h } = scene0;
|
|
6234
|
+
return scene0.layers.map(({ layer, entities }, layerIndex) => {
|
|
6235
|
+
const entities1 = scene1.layers[layerIndex]?.entities ?? entities;
|
|
5332
6236
|
let acc = 0;
|
|
5333
6237
|
let max = 0;
|
|
5334
6238
|
let n = 0;
|
|
5335
|
-
|
|
5336
|
-
const p0 = posOf(
|
|
5337
|
-
const p1 = posOf(
|
|
6239
|
+
entities.forEach((e, i) => {
|
|
6240
|
+
const p0 = posOf(scene0, e, t);
|
|
6241
|
+
const p1 = posOf(scene1, entities1[i] ?? e, t + dt);
|
|
5338
6242
|
const dx = p1.x - p0.x;
|
|
5339
6243
|
const dy = p1.y - p0.y;
|
|
5340
|
-
if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2)
|
|
6244
|
+
if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2) return;
|
|
5341
6245
|
const speed = Math.sqrt(dx * dx + dy * dy) / dt * 1e3;
|
|
5342
6246
|
acc += speed;
|
|
5343
6247
|
max = Math.max(max, speed);
|
|
5344
6248
|
n++;
|
|
5345
|
-
}
|
|
6249
|
+
});
|
|
5346
6250
|
const meanSpeed = n ? acc / n : 0;
|
|
5347
6251
|
return { layerIndex, key: layer.key, meanSpeed, maxSpeed: max, moving: meanSpeed > 0.5 };
|
|
5348
6252
|
});
|
|
@@ -5408,6 +6312,7 @@ function diffScenes(a, b, opts = {}) {
|
|
|
5408
6312
|
};
|
|
5409
6313
|
}
|
|
5410
6314
|
function perceiveScene(spec, opts = {}) {
|
|
6315
|
+
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
5411
6316
|
const grid = luminanceGrid(spec, opts);
|
|
5412
6317
|
return {
|
|
5413
6318
|
t: opts.t ?? 5e3,
|
|
@@ -6268,7 +7173,10 @@ var SIGNAL_BOARD_SPEC = {
|
|
|
6268
7173
|
kind: "bar",
|
|
6269
7174
|
values: [62, 38, 84, 21, 55, 91, 44, 70, 29],
|
|
6270
7175
|
max: 100,
|
|
6271
|
-
|
|
7176
|
+
// Cell pitch is the table's authored `gap` (0.07), so a full-value
|
|
7177
|
+
// bar must stay under that or neighboring cells' bars overlap.
|
|
7178
|
+
// 0.06 leaves a hairline margin.
|
|
7179
|
+
length: 0.06,
|
|
6272
7180
|
thickness: 0.013,
|
|
6273
7181
|
color: "#ffb000",
|
|
6274
7182
|
direction: "right"
|
|
@@ -7900,6 +8808,7 @@ export {
|
|
|
7900
8808
|
COMETS_SPEC,
|
|
7901
8809
|
CONSTELLATION_SPEC,
|
|
7902
8810
|
DASHBOARD_SPEC,
|
|
8811
|
+
DEFAULT_KEY_DUR,
|
|
7903
8812
|
DITHER_ALPHA,
|
|
7904
8813
|
DITHER_TILE,
|
|
7905
8814
|
EXAMPLE_BY_ID,
|
|
@@ -7953,6 +8862,8 @@ export {
|
|
|
7953
8862
|
breakTextBlock,
|
|
7954
8863
|
buildEntities,
|
|
7955
8864
|
canMorph,
|
|
8865
|
+
canWrapMorph,
|
|
8866
|
+
canonicalSpecPath,
|
|
7956
8867
|
cohesionOf,
|
|
7957
8868
|
compileSaver,
|
|
7958
8869
|
compileSequence,
|
|
@@ -7976,9 +8887,11 @@ export {
|
|
|
7976
8887
|
hasHotSwapSequence,
|
|
7977
8888
|
headingAt,
|
|
7978
8889
|
hexToRgb255,
|
|
8890
|
+
ignoredPropertyPaths,
|
|
7979
8891
|
latticeHash,
|
|
7980
8892
|
layerCohesion,
|
|
7981
8893
|
lerpSpec,
|
|
8894
|
+
lerpValue,
|
|
7982
8895
|
lifeAlphaAt,
|
|
7983
8896
|
linkEdges,
|
|
7984
8897
|
linkPairs,
|
|
@@ -7987,16 +8900,19 @@ export {
|
|
|
7987
8900
|
morphChainRoot,
|
|
7988
8901
|
morphNothingMorphable,
|
|
7989
8902
|
motionStats,
|
|
8903
|
+
nextKeyAfter,
|
|
7990
8904
|
normalizeSeed,
|
|
7991
8905
|
perceiveScene,
|
|
7992
8906
|
perceiveSequenceFrame,
|
|
7993
8907
|
pickScreenOp,
|
|
7994
8908
|
positionAt,
|
|
7995
8909
|
presentWithFinish,
|
|
8910
|
+
readSpecPath,
|
|
7996
8911
|
renderBrailleMap,
|
|
7997
8912
|
renderDensityMap,
|
|
7998
8913
|
resolveSegment,
|
|
7999
8914
|
resolveSpecPath,
|
|
8915
|
+
resolveTimelineAt,
|
|
8000
8916
|
revealState,
|
|
8001
8917
|
rgb255Luma,
|
|
8002
8918
|
rotationAt,
|
|
@@ -8018,7 +8934,13 @@ export {
|
|
|
8018
8934
|
textStringsDiffer,
|
|
8019
8935
|
textStringsOf,
|
|
8020
8936
|
textWidthEm,
|
|
8937
|
+
timelineLocalTime,
|
|
8938
|
+
timelineSampleTimes,
|
|
8939
|
+
timelineSampleTimesAfter,
|
|
8940
|
+
timelineTracks,
|
|
8021
8941
|
validateSequence,
|
|
8022
|
-
validateSpec
|
|
8942
|
+
validateSpec,
|
|
8943
|
+
validateSpecPaths,
|
|
8944
|
+
withoutTimeline
|
|
8023
8945
|
};
|
|
8024
8946
|
//# sourceMappingURL=index.js.map
|