@idle-screens/schema 3.9.1 → 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 +216 -9
- package/dist/index.js +1068 -169
- 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;
|
|
@@ -301,13 +397,244 @@ function structuralSignature(spec) {
|
|
|
301
397
|
]);
|
|
302
398
|
}
|
|
303
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
|
+
|
|
304
631
|
// src/validate.ts
|
|
305
632
|
var HEX2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
306
633
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
307
634
|
var isStr = (v) => typeof v === "string";
|
|
308
635
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
309
636
|
var isRange = (v) => Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && v[0] <= v[1];
|
|
310
|
-
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"]);
|
|
311
638
|
var KNOWN_FINISH = /* @__PURE__ */ new Set(["grain", "dither", "animate"]);
|
|
312
639
|
var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
313
640
|
"count",
|
|
@@ -330,8 +657,14 @@ var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
|
330
657
|
"life",
|
|
331
658
|
"emit",
|
|
332
659
|
"clock",
|
|
333
|
-
"rotate"
|
|
660
|
+
"rotate",
|
|
661
|
+
"opacity",
|
|
662
|
+
"transform"
|
|
334
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"]);
|
|
335
668
|
var KNOWN_CIRCLE = /* @__PURE__ */ new Set(["kind", "radius", "color", "soft", "colors", "colorWeights"]);
|
|
336
669
|
var KNOWN_RING = /* @__PURE__ */ new Set(["kind", "radius", "color", "width", "colors", "colorWeights"]);
|
|
337
670
|
var KNOWN_STREAK = /* @__PURE__ */ new Set(["kind", "length", "color", "width", "colors", "colorWeights"]);
|
|
@@ -366,6 +699,9 @@ function unknownKeys(obj, known) {
|
|
|
366
699
|
function dimUnit(spec) {
|
|
367
700
|
return isObj(spec) && spec.units === "px" ? "px" : "fractions of min(width, height)";
|
|
368
701
|
}
|
|
702
|
+
function offsetBound(spec) {
|
|
703
|
+
return isObj(spec) && spec.units === "px" ? [LIMITS.maxLayerTransformOffsetPx, "px"] : [LIMITS.maxLayerTransformOffset, "min(w, h) units"];
|
|
704
|
+
}
|
|
369
705
|
function rangeMsg(spec, note) {
|
|
370
706
|
return `must be a [min,max] range of positive ${dimUnit(spec)}${note ? ` (${note})` : ""}`;
|
|
371
707
|
}
|
|
@@ -393,6 +729,38 @@ function ignoredPropertyPaths(spec) {
|
|
|
393
729
|
if (!isObj(spec)) return [];
|
|
394
730
|
return (validateSpecCore(spec).warnings ?? []).filter((w) => w.code === "unknown-property" || w.code === "misplaced-property").map((w) => w.path.replace(/\[(\d+)\]/g, ".$1"));
|
|
395
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
|
+
}
|
|
396
764
|
function validateSpecCore(spec) {
|
|
397
765
|
const errors = [];
|
|
398
766
|
const warnings = [];
|
|
@@ -444,8 +812,107 @@ function validateSpecCore(spec) {
|
|
|
444
812
|
});
|
|
445
813
|
if (total > LIMITS.maxTotal) err("layers", `total entities ${total} exceeds cap ${LIMITS.maxTotal}`);
|
|
446
814
|
}
|
|
815
|
+
if (spec.timeline !== void 0 && errors.length === 0) validateTimeline(spec, err, warn);
|
|
447
816
|
return { valid: errors.length === 0, errors, warnings };
|
|
448
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
|
+
}
|
|
449
916
|
function color(v, path, err) {
|
|
450
917
|
if (!isStr(v) || !HEX2.test(v)) err(path, "must be a hex colour like #0a3a52");
|
|
451
918
|
}
|
|
@@ -569,12 +1036,40 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
569
1036
|
if (layer.key !== void 0 && (!isStr(layer.key) || layer.key.trim() === "")) {
|
|
570
1037
|
err(`${path}.key`, "must be a non-empty string");
|
|
571
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
|
+
}
|
|
572
1059
|
if (layer.position !== void 0) {
|
|
573
1060
|
if (!isObj(layer.position) || !isNum(layer.position.x) || !isNum(layer.position.y)) {
|
|
574
1061
|
err(`${path}.position`, "must be {x, y} with numbers 0..1");
|
|
575
1062
|
} else {
|
|
576
1063
|
if (layer.position.x < 0 || layer.position.x > 1) err(`${path}.position.x`, "must be 0..1");
|
|
577
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`);
|
|
578
1073
|
const dataLayout = isObj(layer.layout) && (layer.layout.type === "list" || layer.layout.type === "table");
|
|
579
1074
|
if (isNum(layer.count) && layer.count !== 1 && !dataLayout) {
|
|
580
1075
|
err(`${path}.position`, "position requires count: 1 \u2014 or a layout of type 'list' / 'table', where it anchors the whole block");
|
|
@@ -1161,14 +1656,14 @@ function validateShapePoints(points, path, min, err) {
|
|
|
1161
1656
|
}
|
|
1162
1657
|
}
|
|
1163
1658
|
}
|
|
1164
|
-
function validateEase(
|
|
1165
|
-
if (
|
|
1166
|
-
if (!isObj(
|
|
1167
|
-
if (
|
|
1168
|
-
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) {
|
|
1169
1664
|
err(`${path}.tau`, `must be ${LIMITS.minEaseTau}..${LIMITS.maxEaseTau} ms`);
|
|
1170
1665
|
}
|
|
1171
|
-
for (const k of unknownKeys(
|
|
1666
|
+
for (const k of unknownKeys(ease2, /* @__PURE__ */ new Set(["type", "tau"]))) {
|
|
1172
1667
|
warn(`${path}.${k}`, "unknown-property", `unknown ease property '${k}' \u2014 will be ignored`);
|
|
1173
1668
|
}
|
|
1174
1669
|
}
|
|
@@ -1200,6 +1695,7 @@ function validateSequence(seq) {
|
|
|
1200
1695
|
if (seq.seed !== void 0 && !isNum(seq.seed)) err("seed", "must be a number");
|
|
1201
1696
|
if (typeof seq.loop !== "boolean") err("loop", "must be a boolean");
|
|
1202
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");
|
|
1203
1699
|
if (seq.finish !== void 0) validateFinish(seq.finish, "finish", err, (p, code, message) => void warnings.push({ path: p, code, message }));
|
|
1204
1700
|
if (!Array.isArray(seq.segments) || seq.segments.length === 0) {
|
|
1205
1701
|
err("segments", "must be a non-empty array");
|
|
@@ -1253,8 +1749,8 @@ function validateSequence(seq) {
|
|
|
1253
1749
|
if (isObj(s.transition) && s.transition.text !== void 0) {
|
|
1254
1750
|
if (s.transition.type !== "morph") {
|
|
1255
1751
|
err(`${p}.transition.text`, "is a morph option (fade already cross-fades whole frames; cut has no window)");
|
|
1256
|
-
} else if (s.transition.text !== "step" && s.transition.text !== "crossfade") {
|
|
1257
|
-
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'");
|
|
1258
1754
|
}
|
|
1259
1755
|
}
|
|
1260
1756
|
}
|
|
@@ -1311,7 +1807,7 @@ function validateSequence(seq) {
|
|
|
1311
1807
|
code: "morph-structural-mismatch",
|
|
1312
1808
|
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
1313
1809
|
});
|
|
1314
|
-
} 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" })) {
|
|
1315
1811
|
warnings.push({
|
|
1316
1812
|
path: `segments[${i}].transition`,
|
|
1317
1813
|
code: "morph-nothing-morphable",
|
|
@@ -1320,6 +1816,20 @@ function validateSequence(seq) {
|
|
|
1320
1816
|
}
|
|
1321
1817
|
}
|
|
1322
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
|
+
}
|
|
1323
1833
|
return { valid: errors.length === 0, errors, warnings };
|
|
1324
1834
|
}
|
|
1325
1835
|
function assertValidSequence(seq) {
|
|
@@ -1511,7 +2021,7 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1511
2021
|
if (mSpec.type === "path") {
|
|
1512
2022
|
pathPts = mSpec.points.map((p) => ({ x: p.x * w, y: p.y * h }));
|
|
1513
2023
|
}
|
|
1514
|
-
const
|
|
2024
|
+
const ease2 = mSpec.type === "drift" || mSpec.type === "rise" || mSpec.type === "wander" ? mSpec.ease : void 0;
|
|
1515
2025
|
const clockPhase = layer.clock ? (layer.clock.phase ?? 0) % 1 * Math.PI * 2 : 0;
|
|
1516
2026
|
const layout = layer.layout;
|
|
1517
2027
|
const ordered = layout?.type === "list" || layout?.type === "table";
|
|
@@ -1530,6 +2040,8 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1530
2040
|
if (layer.position) {
|
|
1531
2041
|
listX0 = layer.position.x * w;
|
|
1532
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;
|
|
1533
2045
|
} else {
|
|
1534
2046
|
const [rx0, rx1] = layer.region?.x ?? [0, 1];
|
|
1535
2047
|
const [ry0, ry1] = layer.region?.y ?? [0, 1];
|
|
@@ -1624,6 +2136,8 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1624
2136
|
if (layer.position && layer.count === 1) {
|
|
1625
2137
|
x0 = layer.position.x * w;
|
|
1626
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;
|
|
1627
2141
|
} else if (layout?.type === "list" || layout?.type === "table") {
|
|
1628
2142
|
rng.next();
|
|
1629
2143
|
rng.next();
|
|
@@ -1705,7 +2219,7 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1705
2219
|
} : {},
|
|
1706
2220
|
// Time-structure fields (2026-09): ease draws nothing; emit's one draw
|
|
1707
2221
|
// per entity happens in a pass AFTER this loop (see below).
|
|
1708
|
-
...
|
|
2222
|
+
...ease2 ? { ease: { type: ease2.type, tau: ease2.tau } } : {},
|
|
1709
2223
|
// Static rotation (2026-09-10): a scalar draws nothing; the range form
|
|
1710
2224
|
// draws one value here, last, only when declared.
|
|
1711
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 } : {}
|
|
@@ -2422,110 +2936,6 @@ function screen(fp, pat, alpha, off, w, h) {
|
|
|
2422
2936
|
p.restore();
|
|
2423
2937
|
}
|
|
2424
2938
|
|
|
2425
|
-
// src/sequence.ts
|
|
2426
|
-
function segmentStart(seq, index) {
|
|
2427
|
-
let t = 0;
|
|
2428
|
-
const n = Math.max(0, Math.min(index, seq.segments.length - 1));
|
|
2429
|
-
for (let i = 0; i < n; i++) t += seq.segments[i].duration ?? 0;
|
|
2430
|
-
return t;
|
|
2431
|
-
}
|
|
2432
|
-
function resolveSegment(seq, T, opts = {}) {
|
|
2433
|
-
const { segments, loop } = seq;
|
|
2434
|
-
if (segments.length === 0) return { index: 0, localT: 0, startT: 0 };
|
|
2435
|
-
let totalTimed = 0;
|
|
2436
|
-
for (const seg of segments) {
|
|
2437
|
-
if (seg.duration != null) totalTimed += seg.duration;
|
|
2438
|
-
else break;
|
|
2439
|
-
}
|
|
2440
|
-
const t0 = Math.max(0, T);
|
|
2441
|
-
const walk = (t, releasedBelow) => {
|
|
2442
|
-
let cumulative = 0;
|
|
2443
|
-
for (let i = 0; i < segments.length; i++) {
|
|
2444
|
-
const seg = segments[i];
|
|
2445
|
-
if (seg.duration == null) {
|
|
2446
|
-
return loop ? null : { index: i, localT: t - cumulative, startT: cumulative };
|
|
2447
|
-
}
|
|
2448
|
-
if (t < cumulative + seg.duration) {
|
|
2449
|
-
return { index: i, localT: t - cumulative, startT: cumulative };
|
|
2450
|
-
}
|
|
2451
|
-
if (seg.advance === "input" && i >= releasedBelow) {
|
|
2452
|
-
return { index: i, localT: t - cumulative, startT: cumulative, held: true };
|
|
2453
|
-
}
|
|
2454
|
-
cumulative += seg.duration;
|
|
2455
|
-
}
|
|
2456
|
-
return null;
|
|
2457
|
-
};
|
|
2458
|
-
const first = walk(t0, opts.releasedBelow ?? 0);
|
|
2459
|
-
if (first) return first;
|
|
2460
|
-
if (loop && totalTimed > 0) {
|
|
2461
|
-
const wrapped = walk(t0 % totalTimed, 0);
|
|
2462
|
-
if (wrapped) return wrapped;
|
|
2463
|
-
}
|
|
2464
|
-
const last = segments.length - 1;
|
|
2465
|
-
const lastStart = totalTimed - (segments[last].duration ?? 0);
|
|
2466
|
-
return { index: last, localT: t0 - lastStart, startT: lastStart };
|
|
2467
|
-
}
|
|
2468
|
-
function canMorph(seq, from) {
|
|
2469
|
-
const seg = seq.segments[from];
|
|
2470
|
-
if (!seg || seg.transition?.type !== "morph") return false;
|
|
2471
|
-
const next = seq.segments[from + 1];
|
|
2472
|
-
if (!next) return false;
|
|
2473
|
-
return structuralSignature(seg.scene) === structuralSignature(next.scene);
|
|
2474
|
-
}
|
|
2475
|
-
function morphChainRoot(seq, index) {
|
|
2476
|
-
let i = index;
|
|
2477
|
-
while (i > 0 && canMorph(seq, i - 1)) i--;
|
|
2478
|
-
return i;
|
|
2479
|
-
}
|
|
2480
|
-
function normalizeSeed(seed) {
|
|
2481
|
-
return seed >>> 0 || 1;
|
|
2482
|
-
}
|
|
2483
|
-
function segmentRenderSeed(seq, index) {
|
|
2484
|
-
const root = morphChainRoot(seq, index);
|
|
2485
|
-
const scene = seq.segments[root]?.scene;
|
|
2486
|
-
if (!scene) return void 0;
|
|
2487
|
-
const raw = scene.seed ?? (seq.seed !== void 0 ? seq.seed + root : void 0);
|
|
2488
|
-
return raw === void 0 ? void 0 : normalizeSeed(raw);
|
|
2489
|
-
}
|
|
2490
|
-
function bedRenderSeed(seq) {
|
|
2491
|
-
if (!seq.bed) return void 0;
|
|
2492
|
-
const raw = seq.bed.seed ?? (seq.seed !== void 0 ? seq.seed + LIMITS.maxSegments : void 0);
|
|
2493
|
-
return raw === void 0 ? void 0 : normalizeSeed(raw);
|
|
2494
|
-
}
|
|
2495
|
-
function sequenceWantsFinish(seq) {
|
|
2496
|
-
return seq.finish !== void 0 || seq.segments.some((s) => s.scene.finish !== void 0);
|
|
2497
|
-
}
|
|
2498
|
-
function sequenceSwapCompatible(prev, next) {
|
|
2499
|
-
if (prev.segments.length !== next.segments.length) return false;
|
|
2500
|
-
if (prev.finish !== void 0 || next.finish !== void 0) {
|
|
2501
|
-
if (prev.seed === void 0 !== (next.seed === void 0)) return false;
|
|
2502
|
-
if (prev.seed !== void 0 && next.seed !== void 0 && normalizeSeed(prev.seed) !== normalizeSeed(next.seed)) return false;
|
|
2503
|
-
}
|
|
2504
|
-
if (sequenceWantsFinish(prev) !== sequenceWantsFinish(next)) return false;
|
|
2505
|
-
if (prev.loop !== next.loop) return false;
|
|
2506
|
-
if ((prev.sync ?? "mount") !== (next.sync ?? "mount")) return false;
|
|
2507
|
-
if (!!prev.bed !== !!next.bed) return false;
|
|
2508
|
-
if (prev.bed && next.bed) {
|
|
2509
|
-
if (structuralSignature(prev.bed) !== structuralSignature(next.bed)) return false;
|
|
2510
|
-
if (bedRenderSeed(prev) !== bedRenderSeed(next)) return false;
|
|
2511
|
-
}
|
|
2512
|
-
for (let i = 0; i < prev.segments.length; i++) {
|
|
2513
|
-
const a = prev.segments[i];
|
|
2514
|
-
const b = next.segments[i];
|
|
2515
|
-
if (a.duration !== b.duration) return false;
|
|
2516
|
-
if ((a.advance ?? "auto") !== (b.advance ?? "auto")) return false;
|
|
2517
|
-
if (transitionKey(a.transition) !== transitionKey(b.transition)) return false;
|
|
2518
|
-
if (structuralSignature(a.scene) !== structuralSignature(b.scene)) return false;
|
|
2519
|
-
if (segmentRenderSeed(prev, i) !== segmentRenderSeed(next, i)) return false;
|
|
2520
|
-
}
|
|
2521
|
-
return true;
|
|
2522
|
-
}
|
|
2523
|
-
function transitionKey(tr) {
|
|
2524
|
-
if (!tr || tr.type === "cut") return "cut";
|
|
2525
|
-
if (tr.type === "morph") return `morph:${tr.dur}:${tr.text ?? "step"}`;
|
|
2526
|
-
return `fade:${tr.dur}`;
|
|
2527
|
-
}
|
|
2528
|
-
|
|
2529
2939
|
// src/compile.ts
|
|
2530
2940
|
var DEFAULT_STEER_DUR = 1e3;
|
|
2531
2941
|
function applyRetainedDeltas(spec, deltas) {
|
|
@@ -2573,10 +2983,62 @@ function manifestFor(spec) {
|
|
|
2573
2983
|
workerReady: true
|
|
2574
2984
|
};
|
|
2575
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
|
+
}
|
|
2576
3034
|
var SpecInstance = class {
|
|
2577
3035
|
constructor(spec, ctx, opts = {}) {
|
|
2578
3036
|
this.spec = spec;
|
|
2579
3037
|
this.effSpec = spec;
|
|
3038
|
+
if (spec.timeline) {
|
|
3039
|
+
this.authored = spec;
|
|
3040
|
+
this.effSpec = resolveTimelineAt(spec, 0);
|
|
3041
|
+
}
|
|
2580
3042
|
this.saverCtx = ctx;
|
|
2581
3043
|
this.transparent = opts.transparent === true;
|
|
2582
3044
|
const tier = opts.capabilityTier;
|
|
@@ -2642,6 +3104,31 @@ var SpecInstance = class {
|
|
|
2642
3104
|
lastStructural = "";
|
|
2643
3105
|
/** See `LayerPaintOverride`; null (the default, and every plain mount) draws every layer once. */
|
|
2644
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();
|
|
2645
3132
|
/** Raster short side for a `field` background — halved on the low tiers. */
|
|
2646
3133
|
fieldShortSide;
|
|
2647
3134
|
/** The `field` background's cached raster; null until a field is first painted. */
|
|
@@ -2666,12 +3153,14 @@ var SpecInstance = class {
|
|
|
2666
3153
|
}
|
|
2667
3154
|
sizeCanvas() {
|
|
2668
3155
|
const dpr = Math.min(this.saverCtx.dpr, 2);
|
|
2669
|
-
|
|
2670
|
-
|
|
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;
|
|
2671
3160
|
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2672
3161
|
if (this.finishPass) {
|
|
2673
|
-
this.finishPass.canvas.width
|
|
2674
|
-
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;
|
|
2675
3164
|
this.finishPass.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2676
3165
|
}
|
|
2677
3166
|
this.lastRenderT = Number.NEGATIVE_INFINITY;
|
|
@@ -3124,6 +3613,12 @@ var SpecInstance = class {
|
|
|
3124
3613
|
*/
|
|
3125
3614
|
applyTrack(track) {
|
|
3126
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
|
+
}
|
|
3127
3622
|
const target = applyDeltasToSpec(this.effSpec, deltas);
|
|
3128
3623
|
if (!validateSpec(target).valid) return;
|
|
3129
3624
|
const dur = deltas.length ? deltas.reduce((m, d) => Math.max(m, d.dur ?? DEFAULT_STEER_DUR), 0) : DEFAULT_STEER_DUR;
|
|
@@ -3160,6 +3655,13 @@ var SpecInstance = class {
|
|
|
3160
3655
|
* showing a ghost of the un-steered scene.
|
|
3161
3656
|
*/
|
|
3162
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
|
+
}
|
|
3163
3665
|
const next = applyRetainedDeltas(this.effSpec, deltas);
|
|
3164
3666
|
if (next === this.effSpec) return;
|
|
3165
3667
|
this.transition = null;
|
|
@@ -3215,6 +3717,7 @@ var SpecInstance = class {
|
|
|
3215
3717
|
* showing through — the ghosting smear. */
|
|
3216
3718
|
paintFrame(t, bgAlpha) {
|
|
3217
3719
|
const { ctx } = this;
|
|
3720
|
+
if (this.authored) this.resolveAuthored(t);
|
|
3218
3721
|
if (!this.transparent) {
|
|
3219
3722
|
ctx.globalAlpha = bgAlpha;
|
|
3220
3723
|
ctx.globalCompositeOperation = "source-over";
|
|
@@ -3223,9 +3726,15 @@ var SpecInstance = class {
|
|
|
3223
3726
|
ctx.globalAlpha = 1;
|
|
3224
3727
|
for (let li = 0; li < this.layers.length; li++) {
|
|
3225
3728
|
const built = this.layers[li];
|
|
3226
|
-
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);
|
|
3227
3731
|
if (lifeA <= 0) continue;
|
|
3228
3732
|
const parentE = this.parentEntityFor(built);
|
|
3733
|
+
const tf = built.layer.transform;
|
|
3734
|
+
if (tf) {
|
|
3735
|
+
ctx.save();
|
|
3736
|
+
this.applyLayerTransform(tf);
|
|
3737
|
+
}
|
|
3229
3738
|
ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
|
|
3230
3739
|
this.drawLinks(built, t, lifeA, parentE);
|
|
3231
3740
|
const override = this.paintOverrides?.get(li) ?? null;
|
|
@@ -3235,13 +3744,80 @@ var SpecInstance = class {
|
|
|
3235
3744
|
this.drawEntity(built, e, t, lifeA, parentE);
|
|
3236
3745
|
continue;
|
|
3237
3746
|
}
|
|
3238
|
-
|
|
3239
|
-
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 });
|
|
3240
3750
|
}
|
|
3751
|
+
if (tf) ctx.restore();
|
|
3241
3752
|
}
|
|
3242
3753
|
ctx.globalAlpha = 1;
|
|
3243
3754
|
ctx.globalCompositeOperation = "source-over";
|
|
3244
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
|
+
}
|
|
3245
3821
|
/**
|
|
3246
3822
|
* Deterministic, frame-addressable render (shared by the rAF loop).
|
|
3247
3823
|
*
|
|
@@ -3294,6 +3870,11 @@ var SpecInstance = class {
|
|
|
3294
3870
|
* already knows the signature is unchanged.
|
|
3295
3871
|
*/
|
|
3296
3872
|
hotSwapSpec(spec) {
|
|
3873
|
+
this.adoptAuthored(spec);
|
|
3874
|
+
if (this.authored) {
|
|
3875
|
+
this.resolveAuthored(this.sceneNow());
|
|
3876
|
+
return;
|
|
3877
|
+
}
|
|
3297
3878
|
this.effSpec = spec;
|
|
3298
3879
|
const sig = structuralSignature(this.effSpec);
|
|
3299
3880
|
if (sig !== this.lastStructural) this.rebuild();
|
|
@@ -3301,8 +3882,68 @@ var SpecInstance = class {
|
|
|
3301
3882
|
b.layer = this.effSpec.layers[i] ?? b.layer;
|
|
3302
3883
|
});
|
|
3303
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
|
+
}
|
|
3304
3940
|
/** Paint-only hot-swap: skips structuralSignature (caller guarantees match). */
|
|
3305
3941
|
hotSwapPaint(spec) {
|
|
3942
|
+
this.adoptAuthored(spec);
|
|
3943
|
+
if (this.authored) {
|
|
3944
|
+
this.resolveAuthored(this.sceneNow());
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3306
3947
|
this.effSpec = spec;
|
|
3307
3948
|
this.layers.forEach((b, i) => {
|
|
3308
3949
|
b.layer = this.effSpec.layers[i] ?? b.layer;
|
|
@@ -3378,6 +4019,8 @@ var SequenceInstance = class {
|
|
|
3378
4019
|
* window (if any) has passed.
|
|
3379
4020
|
*/
|
|
3380
4021
|
pendingWrapFade = false;
|
|
4022
|
+
/** The morph in progress: its ends, and the override sets each end resolves with (see `morphEndpoint`). */
|
|
4023
|
+
morphEnds = null;
|
|
3381
4024
|
/**
|
|
3382
4025
|
* Every non-`sequence.segment` delta this instance has been handed, last
|
|
3383
4026
|
* wins per path, merged across `applyTrack` calls. Children are created
|
|
@@ -3502,10 +4145,15 @@ var SequenceInstance = class {
|
|
|
3502
4145
|
const tr = this.seq.segments[from]?.transition;
|
|
3503
4146
|
return tr?.type === "morph" ? tr.dur : 0;
|
|
3504
4147
|
}
|
|
3505
|
-
/** 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). */
|
|
3506
4149
|
morphTextCrossfades(from) {
|
|
3507
4150
|
const tr = this.seq.segments[from]?.transition;
|
|
3508
|
-
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";
|
|
3509
4157
|
}
|
|
3510
4158
|
/**
|
|
3511
4159
|
* The paint overrides for one crossfade frame: every text layer whose
|
|
@@ -3514,7 +4162,7 @@ var SequenceInstance = class {
|
|
|
3514
4162
|
* usual) at `1 − k` under the incoming at `k`. Null when nothing differs,
|
|
3515
4163
|
* so a crossfade between same-worded twins is a plain morph frame.
|
|
3516
4164
|
*/
|
|
3517
|
-
crossfadeOverrides(specA, specB, lerped, k) {
|
|
4165
|
+
crossfadeOverrides(specA, specB, lerped, k, mode = "crossfade") {
|
|
3518
4166
|
let map = null;
|
|
3519
4167
|
for (let i = 0; i < lerped.layers.length; i++) {
|
|
3520
4168
|
if (!textStringsDiffer(specA, specB, i)) continue;
|
|
@@ -3522,7 +4170,7 @@ var SequenceInstance = class {
|
|
|
3522
4170
|
const now = lerped.layers[i].sprite;
|
|
3523
4171
|
const outgoing = from.kind === "text" && now.kind === "text" ? { ...now, strings: from.strings } : from.kind === "textBlock" && now.kind === "textBlock" ? { ...now, text: from.text } : now;
|
|
3524
4172
|
if (outgoing === now) continue;
|
|
3525
|
-
(map ??= /* @__PURE__ */ new Map()).set(i, { outgoing, k });
|
|
4173
|
+
(map ??= /* @__PURE__ */ new Map()).set(i, mode === "dip" ? { outgoing, k, mode } : { outgoing, k });
|
|
3526
4174
|
}
|
|
3527
4175
|
return map;
|
|
3528
4176
|
}
|
|
@@ -3595,6 +4243,46 @@ var SequenceInstance = class {
|
|
|
3595
4243
|
this.renderOffscreen(this.fading, outDur + localT, this.childSeed(this.morphChainRoot(from), seed));
|
|
3596
4244
|
return this.fading;
|
|
3597
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
|
+
}
|
|
3598
4286
|
/** A segment's scene with the retained track applied — `scene` itself when nothing resolves on it. */
|
|
3599
4287
|
steeredScene(scene) {
|
|
3600
4288
|
return this.retainedDeltas.size === 0 ? scene : applyRetainedDeltas(scene, this.retainedDeltas.values());
|
|
@@ -3613,7 +4301,7 @@ var SequenceInstance = class {
|
|
|
3613
4301
|
if (index < 0 || index >= this.seq.segments.length) index = 0;
|
|
3614
4302
|
let child = this.children[index];
|
|
3615
4303
|
if (!child) {
|
|
3616
|
-
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 });
|
|
3617
4305
|
this.children[index] = child;
|
|
3618
4306
|
child.setPaused(true);
|
|
3619
4307
|
if (rootScene) child.hotSwapSpec(this.childScene(index));
|
|
@@ -3668,7 +4356,12 @@ var SequenceInstance = class {
|
|
|
3668
4356
|
frameFinish() {
|
|
3669
4357
|
if (this.seq.finish !== void 0) return this.seq.finish;
|
|
3670
4358
|
if (this.activeIndex < 0) return void 0;
|
|
3671
|
-
|
|
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;
|
|
3672
4365
|
}
|
|
3673
4366
|
/**
|
|
3674
4367
|
* The seed `frameFinish()`'s grain resolves against — the sequence's own
|
|
@@ -3693,29 +4386,36 @@ var SequenceInstance = class {
|
|
|
3693
4386
|
const resolved = resolveSegment(this.seq, T + this.clockOffset, { releasedBelow: this.releasedBelow });
|
|
3694
4387
|
const { index, localT } = resolved;
|
|
3695
4388
|
this.bed?.renderFrame(T, this.seq.bed?.seed ?? this.seq.seed ?? seed);
|
|
3696
|
-
const prevIdx = index > 0 ? index - 1 : -1;
|
|
3697
|
-
const morphActive = prevIdx >= 0 && this.canMorph(prevIdx) && localT < this.morphDur(prevIdx);
|
|
3698
4389
|
const lastIdx = this.seq.segments.length - 1;
|
|
3699
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);
|
|
3700
4395
|
const fadeFrom = prevIdx >= 0 ? prevIdx : wrapped ? lastIdx : -1;
|
|
3701
4396
|
const fadeActive = fadeFrom >= 0 && localT < this.fadeDur(fadeFrom);
|
|
3702
4397
|
if (!fadeActive && this.fading) this.releaseFading();
|
|
3703
4398
|
if (this.pendingWrapFade && (index !== 0 || !fadeActive)) this.pendingWrapFade = false;
|
|
3704
4399
|
if (morphActive) {
|
|
3705
4400
|
const chainRoot = this.morphChainRoot(index);
|
|
3706
|
-
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;
|
|
3707
4406
|
this.activeIndex = index;
|
|
3708
4407
|
for (let i = 0; i < this.children.length; i++) {
|
|
3709
4408
|
if (i !== chainRoot) this.releaseChild(i);
|
|
3710
4409
|
}
|
|
3711
|
-
const child = this.ensureChild(chainRoot, void 0, this.morphTextCrossfades(
|
|
3712
|
-
const dur = this.morphDur(
|
|
4410
|
+
const child = this.ensureChild(chainRoot, void 0, this.morphTextCrossfades(morphFrom));
|
|
4411
|
+
const dur = this.morphDur(morphFrom);
|
|
3713
4412
|
const k = easeSmooth(localT / dur);
|
|
3714
|
-
const
|
|
3715
|
-
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");
|
|
3716
4416
|
const lerped = lerpSpec(specA, specB, k);
|
|
3717
4417
|
child.hotSwapSpec(lerped);
|
|
3718
|
-
child.setLayerPaintOverrides(this.morphTextCrossfades(
|
|
4418
|
+
child.setLayerPaintOverrides(this.morphTextCrossfades(morphFrom) ? this.crossfadeOverrides(specA, specB, lerped, k, this.morphTextMode(morphFrom)) : null);
|
|
3719
4419
|
child.renderFrame(localT, this.childSeed(chainRoot, seed));
|
|
3720
4420
|
} else {
|
|
3721
4421
|
if (this.morphFromIndex >= 0) {
|
|
@@ -3723,6 +4423,7 @@ var SequenceInstance = class {
|
|
|
3723
4423
|
this.releaseChild(oldRoot);
|
|
3724
4424
|
if (oldRoot !== this.morphFromIndex) this.releaseChild(this.morphFromIndex);
|
|
3725
4425
|
this.morphFromIndex = -1;
|
|
4426
|
+
this.morphEnds = null;
|
|
3726
4427
|
}
|
|
3727
4428
|
if (index !== this.activeIndex) {
|
|
3728
4429
|
for (let i = 0; i < this.children.length; i++) {
|
|
@@ -3802,6 +4503,12 @@ var SequenceInstance = class {
|
|
|
3802
4503
|
const childDeltas = deltas.filter((d) => d.path !== "sequence.segment");
|
|
3803
4504
|
if (childDeltas.length > 0) {
|
|
3804
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
|
+
}
|
|
3805
4512
|
if (!switchedSegment) this.renderFrame(this.renderedT, this.seed);
|
|
3806
4513
|
} else if (this.activeIndex >= 0) {
|
|
3807
4514
|
const child = this.children[this.activeIndex];
|
|
@@ -3831,11 +4538,19 @@ var SequenceInstance = class {
|
|
|
3831
4538
|
if (!sequenceSwapCompatible(this.seq, next)) return false;
|
|
3832
4539
|
this.seq = next;
|
|
3833
4540
|
if (this.bed && next.bed) this.bed.hotSwapSpec(applyRetainedDeltas(this.bedScene(next.bed), this.retainedBedDeltas.values()));
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
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;
|
|
3839
4554
|
if (this.paused) this.renderFrame(this.renderedT, this.seed);
|
|
3840
4555
|
return true;
|
|
3841
4556
|
}
|
|
@@ -4228,12 +4943,43 @@ function seamsWorthWarning(layer, c, entityCount) {
|
|
|
4228
4943
|
return layer.blend !== "lighter" && layer.blend !== "screen";
|
|
4229
4944
|
}
|
|
4230
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
|
+
|
|
4231
4976
|
// src/advise.ts
|
|
4232
4977
|
var LOW_CONTRAST_FLOOR = 0.05;
|
|
4233
4978
|
var SPARSE_DECLARED_MAX_COVERAGE = 0.02;
|
|
4234
4979
|
var READ_LEGIBILITY_FLOOR = 4.5;
|
|
4235
4980
|
var READ_SAFE_AREA = 0.05;
|
|
4236
4981
|
function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
4982
|
+
spec = resolveTimelineAt(spec, opts.t ?? COHESION_T);
|
|
4237
4983
|
const warnings = [];
|
|
4238
4984
|
const w = viewport.width;
|
|
4239
4985
|
const h = viewport.height;
|
|
@@ -4331,6 +5077,8 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
4331
5077
|
let pixArea;
|
|
4332
5078
|
if (layer.sprite.kind === "circle") {
|
|
4333
5079
|
pixArea = Math.PI * r * r;
|
|
5080
|
+
} else if (layer.sprite.kind === "polygon") {
|
|
5081
|
+
pixArea = polygonArea(polygonPoints(layer.sprite, r));
|
|
4334
5082
|
} else if (layer.sprite.kind === "bar") {
|
|
4335
5083
|
pixArea = e.size * barFraction(layer.sprite, e.barIndex ?? 0) * (e.size2 ?? e.size * 0.2);
|
|
4336
5084
|
} else if (layer.sprite.kind === "textBlock") {
|
|
@@ -4457,8 +5205,10 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
4457
5205
|
const label = layer.key ? `\`${layer.key}\`` : `layers[${li}]`;
|
|
4458
5206
|
for (const e of allEntities[li]) {
|
|
4459
5207
|
const p = positionAt(e, 0, w, h);
|
|
4460
|
-
const
|
|
4461
|
-
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);
|
|
4462
5212
|
textBoxes.push({ li, label, alpha, ...box });
|
|
4463
5213
|
}
|
|
4464
5214
|
}
|
|
@@ -4587,8 +5337,8 @@ function brightestAdditivePlate(spec, allEntities, ground, box, w, h) {
|
|
|
4587
5337
|
const growScale = (e.motion === "warp" ? WARP_MAX_SCALE : 1) * (e.emit ? Math.max(e.emit.growFrom, e.emit.growTo) : 1);
|
|
4588
5338
|
const maxDim = Math.max(e.size, e.size2 ?? 0) * (1 + e.growAmp) * growScale;
|
|
4589
5339
|
if (maxDim < box.fs) continue;
|
|
4590
|
-
if (!entityReachesBox(e, maxDim / 2, box, w, h)) continue;
|
|
4591
|
-
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);
|
|
4592
5342
|
if (a <= 0) continue;
|
|
4593
5343
|
const plate = additivePlate(ground, hexRgb(hex), a, blend);
|
|
4594
5344
|
const lum = relativeLuminance(plate);
|
|
@@ -4655,8 +5405,8 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
|
4655
5405
|
});
|
|
4656
5406
|
}
|
|
4657
5407
|
for (let i = 0; i < seq.segments.length - 1; i++) {
|
|
4658
|
-
const lumaA = backgroundLuma(seq.segments[i].scene);
|
|
4659
|
-
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));
|
|
4660
5410
|
const delta = Math.abs(lumaA - lumaB);
|
|
4661
5411
|
if (delta > 0.5) {
|
|
4662
5412
|
warnings.push({
|
|
@@ -4675,7 +5425,7 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
|
4675
5425
|
code: "morph-structural-mismatch",
|
|
4676
5426
|
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
4677
5427
|
});
|
|
4678
|
-
} 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" })) {
|
|
4679
5429
|
warnings.push({
|
|
4680
5430
|
path: `segments[${i}].transition`,
|
|
4681
5431
|
code: "morph-nothing-morphable",
|
|
@@ -4709,7 +5459,7 @@ function describeScene(spec, options = {}) {
|
|
|
4709
5459
|
let pixArea = 0;
|
|
4710
5460
|
for (const e of entities) {
|
|
4711
5461
|
const r = e.size / 2;
|
|
4712
|
-
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;
|
|
4713
5463
|
}
|
|
4714
5464
|
const coverage = pixArea / (w * h);
|
|
4715
5465
|
const meanAlpha = entities.length > 0 ? entities.reduce((s, e) => s + e.alpha, 0) / entities.length : 0;
|
|
@@ -4785,6 +5535,8 @@ import { createRng as createRng4 } from "@idle-screens/core";
|
|
|
4785
5535
|
var GLOW_SPREAD = 2.4;
|
|
4786
5536
|
var LINE_SALIENCE = 3.5;
|
|
4787
5537
|
var GHOST_TAPS = 12;
|
|
5538
|
+
var POLYGON_OUTLINE_CELLS = 3;
|
|
5539
|
+
var POLYGON_SCANLINES = 4;
|
|
4788
5540
|
function buildScene(spec, opts) {
|
|
4789
5541
|
const { width: w, height: h } = opts.viewport ?? { width: 1920, height: 1080 };
|
|
4790
5542
|
const seed = normalizeSeed(opts.seed ?? spec.seed ?? 42);
|
|
@@ -4799,7 +5551,13 @@ function buildScene(spec, opts) {
|
|
|
4799
5551
|
const layers = spec.layers.map((layer) => ({ layer, entities: buildEntities(layer, rng, w, h, scale, countScale) }));
|
|
4800
5552
|
const byKey = /* @__PURE__ */ new Map();
|
|
4801
5553
|
for (const { layer, entities } of layers) if (layer.key) byKey.set(layer.key, entities);
|
|
4802
|
-
|
|
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 };
|
|
4803
5561
|
}
|
|
4804
5562
|
function posOf(scene, e, t) {
|
|
4805
5563
|
const p = positionAt(e, t, scene.w, scene.h);
|
|
@@ -4811,7 +5569,8 @@ function posOf(scene, e, t) {
|
|
|
4811
5569
|
p.y += pp.y;
|
|
4812
5570
|
}
|
|
4813
5571
|
}
|
|
4814
|
-
|
|
5572
|
+
const tf = scene.transforms?.get(e);
|
|
5573
|
+
return tf ? transformPoint(tf, p, scene.w, scene.h, scene.scale) : p;
|
|
4815
5574
|
}
|
|
4816
5575
|
function textBox(s, e, p) {
|
|
4817
5576
|
const m = s.font ? /(\d+(?:\.\d+)?)px/.exec(s.font) : null;
|
|
@@ -4863,7 +5622,66 @@ function textBlockRevealFraction(s, w, h, t) {
|
|
|
4863
5622
|
}
|
|
4864
5623
|
return rs.progress;
|
|
4865
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
|
+
}
|
|
4866
5683
|
function luminanceGrid(spec, opts = {}) {
|
|
5684
|
+
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
4867
5685
|
const scene = buildScene(spec, opts);
|
|
4868
5686
|
const t = opts.t ?? 5e3;
|
|
4869
5687
|
const cols = Math.max(8, Math.min(200, opts.cols ?? 80));
|
|
@@ -4962,9 +5780,66 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
4962
5780
|
}
|
|
4963
5781
|
}
|
|
4964
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
|
+
};
|
|
4965
5840
|
const splatPass = (tPass, alphaScale) => {
|
|
4966
5841
|
for (const { layer, entities } of scene.layers) {
|
|
4967
|
-
const lifeA = lifeAlphaAt(layer.life, tPass);
|
|
5842
|
+
const lifeA = layer.opacity === void 0 ? lifeAlphaAt(layer.life, tPass) : lifeAlphaAt(layer.life, tPass) * layerOpacity(layer);
|
|
4968
5843
|
if (lifeA <= 0) continue;
|
|
4969
5844
|
for (const e of entities) {
|
|
4970
5845
|
splatTrail(layer, e, tPass, lifeA, alphaScale);
|
|
@@ -4972,8 +5847,12 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
4972
5847
|
if (a <= 4e-3) continue;
|
|
4973
5848
|
const lum = spriteLuma(layer, e);
|
|
4974
5849
|
const p = posOf(scene, e, tPass);
|
|
4975
|
-
const sz = sizeAt(e, tPass);
|
|
5850
|
+
const sz = layer.transform ? sizeAt(e, tPass) * transformSize(layer.transform) : sizeAt(e, tPass);
|
|
4976
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
|
+
}
|
|
4977
5856
|
if (s.kind === "stroke") {
|
|
4978
5857
|
const pts = strokeSamples(s, sz / 2);
|
|
4979
5858
|
let angle = rotationAt(e, tPass);
|
|
@@ -5219,6 +6098,7 @@ function renderDensityMap(grid) {
|
|
|
5219
6098
|
return lines.join("\n");
|
|
5220
6099
|
}
|
|
5221
6100
|
function textSprites(spec, opts = {}) {
|
|
6101
|
+
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
5222
6102
|
const scene = buildScene(spec, opts);
|
|
5223
6103
|
const t = opts.t ?? 5e3;
|
|
5224
6104
|
const out = [];
|
|
@@ -5256,6 +6136,7 @@ function rankDominance(raw) {
|
|
|
5256
6136
|
}));
|
|
5257
6137
|
}
|
|
5258
6138
|
function rawDominance(spec, opts = {}) {
|
|
6139
|
+
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
5259
6140
|
const scene = buildScene(spec, opts);
|
|
5260
6141
|
const t = opts.t ?? 5e3;
|
|
5261
6142
|
const { w, h, scale } = scene;
|
|
@@ -5280,7 +6161,8 @@ function rawDominance(spec, opts = {}) {
|
|
|
5280
6161
|
return Math.sqrt(dx * dx + dy * dy);
|
|
5281
6162
|
};
|
|
5282
6163
|
const raw = scene.layers.map(({ layer, entities }, layerIndex) => {
|
|
5283
|
-
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;
|
|
5284
6166
|
let area = 0;
|
|
5285
6167
|
let lumAcc = 0;
|
|
5286
6168
|
for (const e of entities) {
|
|
@@ -5309,7 +6191,7 @@ function rawDominance(spec, opts = {}) {
|
|
|
5309
6191
|
const box = textBlockBox(s, e, { x: 0, y: 0 }, w, h);
|
|
5310
6192
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55 * textBlockRevealFraction(s, w, h, t) * (s.opacity ?? 1);
|
|
5311
6193
|
} else entArea = sz * sz * 0.55;
|
|
5312
|
-
area += entArea * a;
|
|
6194
|
+
area += layer.transform ? entArea * areaScale * a : entArea * a;
|
|
5313
6195
|
if (layer.trail) {
|
|
5314
6196
|
const fade = layer.trail.fade ?? 1;
|
|
5315
6197
|
const span = sweptDistance(e, layer.trail.length);
|
|
@@ -5339,29 +6221,32 @@ function rawDominance(spec, opts = {}) {
|
|
|
5339
6221
|
return raw;
|
|
5340
6222
|
}
|
|
5341
6223
|
function layerCohesion(spec, opts = {}) {
|
|
6224
|
+
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
5342
6225
|
const scene = buildScene(spec, opts);
|
|
5343
6226
|
return cohesionOf(scene.layers, opts.t ?? 5e3, scene.w, scene.h);
|
|
5344
6227
|
}
|
|
5345
6228
|
function motionStats(spec, opts = {}) {
|
|
5346
|
-
const scene = buildScene(spec, opts);
|
|
5347
6229
|
const t = opts.t ?? 5e3;
|
|
5348
6230
|
const dt = opts.dt ?? 500;
|
|
5349
|
-
const
|
|
5350
|
-
|
|
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;
|
|
5351
6236
|
let acc = 0;
|
|
5352
6237
|
let max = 0;
|
|
5353
6238
|
let n = 0;
|
|
5354
|
-
|
|
5355
|
-
const p0 = posOf(
|
|
5356
|
-
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);
|
|
5357
6242
|
const dx = p1.x - p0.x;
|
|
5358
6243
|
const dy = p1.y - p0.y;
|
|
5359
|
-
if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2)
|
|
6244
|
+
if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2) return;
|
|
5360
6245
|
const speed = Math.sqrt(dx * dx + dy * dy) / dt * 1e3;
|
|
5361
6246
|
acc += speed;
|
|
5362
6247
|
max = Math.max(max, speed);
|
|
5363
6248
|
n++;
|
|
5364
|
-
}
|
|
6249
|
+
});
|
|
5365
6250
|
const meanSpeed = n ? acc / n : 0;
|
|
5366
6251
|
return { layerIndex, key: layer.key, meanSpeed, maxSpeed: max, moving: meanSpeed > 0.5 };
|
|
5367
6252
|
});
|
|
@@ -5427,6 +6312,7 @@ function diffScenes(a, b, opts = {}) {
|
|
|
5427
6312
|
};
|
|
5428
6313
|
}
|
|
5429
6314
|
function perceiveScene(spec, opts = {}) {
|
|
6315
|
+
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
5430
6316
|
const grid = luminanceGrid(spec, opts);
|
|
5431
6317
|
return {
|
|
5432
6318
|
t: opts.t ?? 5e3,
|
|
@@ -7922,6 +8808,7 @@ export {
|
|
|
7922
8808
|
COMETS_SPEC,
|
|
7923
8809
|
CONSTELLATION_SPEC,
|
|
7924
8810
|
DASHBOARD_SPEC,
|
|
8811
|
+
DEFAULT_KEY_DUR,
|
|
7925
8812
|
DITHER_ALPHA,
|
|
7926
8813
|
DITHER_TILE,
|
|
7927
8814
|
EXAMPLE_BY_ID,
|
|
@@ -7975,6 +8862,8 @@ export {
|
|
|
7975
8862
|
breakTextBlock,
|
|
7976
8863
|
buildEntities,
|
|
7977
8864
|
canMorph,
|
|
8865
|
+
canWrapMorph,
|
|
8866
|
+
canonicalSpecPath,
|
|
7978
8867
|
cohesionOf,
|
|
7979
8868
|
compileSaver,
|
|
7980
8869
|
compileSequence,
|
|
@@ -8002,6 +8891,7 @@ export {
|
|
|
8002
8891
|
latticeHash,
|
|
8003
8892
|
layerCohesion,
|
|
8004
8893
|
lerpSpec,
|
|
8894
|
+
lerpValue,
|
|
8005
8895
|
lifeAlphaAt,
|
|
8006
8896
|
linkEdges,
|
|
8007
8897
|
linkPairs,
|
|
@@ -8010,16 +8900,19 @@ export {
|
|
|
8010
8900
|
morphChainRoot,
|
|
8011
8901
|
morphNothingMorphable,
|
|
8012
8902
|
motionStats,
|
|
8903
|
+
nextKeyAfter,
|
|
8013
8904
|
normalizeSeed,
|
|
8014
8905
|
perceiveScene,
|
|
8015
8906
|
perceiveSequenceFrame,
|
|
8016
8907
|
pickScreenOp,
|
|
8017
8908
|
positionAt,
|
|
8018
8909
|
presentWithFinish,
|
|
8910
|
+
readSpecPath,
|
|
8019
8911
|
renderBrailleMap,
|
|
8020
8912
|
renderDensityMap,
|
|
8021
8913
|
resolveSegment,
|
|
8022
8914
|
resolveSpecPath,
|
|
8915
|
+
resolveTimelineAt,
|
|
8023
8916
|
revealState,
|
|
8024
8917
|
rgb255Luma,
|
|
8025
8918
|
rotationAt,
|
|
@@ -8041,7 +8934,13 @@ export {
|
|
|
8041
8934
|
textStringsDiffer,
|
|
8042
8935
|
textStringsOf,
|
|
8043
8936
|
textWidthEm,
|
|
8937
|
+
timelineLocalTime,
|
|
8938
|
+
timelineSampleTimes,
|
|
8939
|
+
timelineSampleTimesAfter,
|
|
8940
|
+
timelineTracks,
|
|
8044
8941
|
validateSequence,
|
|
8045
|
-
validateSpec
|
|
8942
|
+
validateSpec,
|
|
8943
|
+
validateSpecPaths,
|
|
8944
|
+
withoutTimeline
|
|
8046
8945
|
};
|
|
8047
8946
|
//# sourceMappingURL=index.js.map
|