@idle-screens/schema 3.9.1 → 3.11.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 +163 -7
- package/dist/index.d.ts +243 -9
- package/dist/index.js +1231 -182
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +95 -2
package/dist/index.js
CHANGED
|
@@ -71,6 +71,21 @@ 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
|
|
88
|
+
maxGroups: 16,
|
|
74
89
|
maxEmitEvery: 6e5,
|
|
75
90
|
// ms — ten minutes; longer silences than that are `life.enter`
|
|
76
91
|
minEmitLife: 500,
|
|
@@ -96,7 +111,7 @@ var LIMITS = {
|
|
|
96
111
|
|
|
97
112
|
// src/steer.ts
|
|
98
113
|
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
99
|
-
var STEERABLE_ROOT_KEYS = /* @__PURE__ */ new Set(["ghosting", "referenceViewport", "finish"]);
|
|
114
|
+
var STEERABLE_ROOT_KEYS = /* @__PURE__ */ new Set(["ghosting", "referenceViewport", "finish", "groups"]);
|
|
100
115
|
function resolveSpecPath(spec, path) {
|
|
101
116
|
if (!spec || typeof spec !== "object" || !path) return null;
|
|
102
117
|
const parts = path.split(".");
|
|
@@ -110,6 +125,8 @@ function resolveSpecPath(spec, path) {
|
|
|
110
125
|
return null;
|
|
111
126
|
}
|
|
112
127
|
}
|
|
128
|
+
if (parts[0] === "layers" && parts.length === 3 && parts[2] === "group") return null;
|
|
129
|
+
if (parts.length >= 2 && parts[parts.length - 1] === "origin" && parts[parts.length - 2] === "transform") return null;
|
|
113
130
|
let node = spec;
|
|
114
131
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
115
132
|
if (node === null || typeof node !== "object") return null;
|
|
@@ -121,6 +138,115 @@ function resolveSpecPath(spec, path) {
|
|
|
121
138
|
const exists = Array.isArray(node) ? Number.isInteger(key) && key >= 0 && key < node.length : last in node;
|
|
122
139
|
return exists ? { parent: node, key } : null;
|
|
123
140
|
}
|
|
141
|
+
function canonicalSpecPath(spec, path) {
|
|
142
|
+
if (!resolveSpecPath(spec, path)) return null;
|
|
143
|
+
const parts = path.split(".");
|
|
144
|
+
const s = spec;
|
|
145
|
+
if (parts[0] !== "layers" && parts[0] !== "background" && Array.isArray(s.layers)) {
|
|
146
|
+
const idx = s.layers.findIndex((l) => l && l.key === parts[0]);
|
|
147
|
+
if (idx !== -1) parts.splice(0, 1, "layers", String(idx));
|
|
148
|
+
}
|
|
149
|
+
return parts.join(".");
|
|
150
|
+
}
|
|
151
|
+
function readSpecPath(spec, path) {
|
|
152
|
+
const loc = resolveSpecPath(spec, path);
|
|
153
|
+
return loc ? loc.parent[loc.key] : void 0;
|
|
154
|
+
}
|
|
155
|
+
var TRANSFORM_IDENTITY = { x: 0, y: 0, scale: 1, scaleX: 1, rotate: 0 };
|
|
156
|
+
function alignTransform(a, b) {
|
|
157
|
+
const isObj2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
158
|
+
if (!isObj2(a) || !isObj2(b)) return [a, b];
|
|
159
|
+
let fa = null;
|
|
160
|
+
let fb = null;
|
|
161
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) {
|
|
162
|
+
const id = TRANSFORM_IDENTITY[key];
|
|
163
|
+
if (id === void 0) continue;
|
|
164
|
+
if (a[key] === void 0) (fa ??= { ...a })[key] = id;
|
|
165
|
+
if (b[key] === void 0) (fb ??= { ...b })[key] = id;
|
|
166
|
+
}
|
|
167
|
+
return [fa ?? a, fb ?? b];
|
|
168
|
+
}
|
|
169
|
+
function alignPaintFields(a, b) {
|
|
170
|
+
const la = a?.layers;
|
|
171
|
+
const lb = b?.layers;
|
|
172
|
+
if (!Array.isArray(la) || !Array.isArray(lb)) return [a, b];
|
|
173
|
+
let outA = null;
|
|
174
|
+
let outB = null;
|
|
175
|
+
const n = Math.min(la.length, lb.length);
|
|
176
|
+
for (let i = 0; i < n; i++) {
|
|
177
|
+
const x = la[i];
|
|
178
|
+
const y = lb[i];
|
|
179
|
+
if (!x || !y || typeof x !== "object" || typeof y !== "object") continue;
|
|
180
|
+
let nx = x;
|
|
181
|
+
let ny = y;
|
|
182
|
+
if (x.group === void 0 !== (y.group === void 0)) {
|
|
183
|
+
if (x.group === void 0) nx = { ...nx, group: y.group };
|
|
184
|
+
else ny = { ...ny, group: x.group };
|
|
185
|
+
}
|
|
186
|
+
if (x.opacity === void 0 !== (y.opacity === void 0)) {
|
|
187
|
+
if (x.opacity === void 0) nx = { ...nx, opacity: 1 };
|
|
188
|
+
else ny = { ...ny, opacity: 1 };
|
|
189
|
+
}
|
|
190
|
+
const tx = x.transform;
|
|
191
|
+
const ty = y.transform;
|
|
192
|
+
if (tx || ty) {
|
|
193
|
+
const fx = { ...tx ?? {} };
|
|
194
|
+
const fy = { ...ty ?? {} };
|
|
195
|
+
let changed = !tx || !ty;
|
|
196
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(fx), ...Object.keys(fy)])) {
|
|
197
|
+
const id = TRANSFORM_IDENTITY[key];
|
|
198
|
+
if (id === void 0) continue;
|
|
199
|
+
if (fx[key] === void 0) {
|
|
200
|
+
fx[key] = id;
|
|
201
|
+
changed = true;
|
|
202
|
+
}
|
|
203
|
+
if (fy[key] === void 0) {
|
|
204
|
+
fy[key] = id;
|
|
205
|
+
changed = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (changed) {
|
|
209
|
+
nx = { ...nx, transform: fx };
|
|
210
|
+
ny = { ...ny, transform: fy };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (nx !== x) (outA ??= la.slice())[i] = nx;
|
|
214
|
+
if (ny !== y) (outB ??= lb.slice())[i] = ny;
|
|
215
|
+
}
|
|
216
|
+
let ra = outA ? { ...a, layers: outA } : a;
|
|
217
|
+
let rb = outB ? { ...b, layers: outB } : b;
|
|
218
|
+
const ga = a.groups;
|
|
219
|
+
const gb = b.groups;
|
|
220
|
+
if (ga || gb) {
|
|
221
|
+
const na = { ...ga ?? {} };
|
|
222
|
+
const nb = { ...gb ?? {} };
|
|
223
|
+
let changed = !ga || !gb;
|
|
224
|
+
for (const name of /* @__PURE__ */ new Set([...Object.keys(na), ...Object.keys(nb)])) {
|
|
225
|
+
const x = na[name] ?? {};
|
|
226
|
+
const y = nb[name] ?? {};
|
|
227
|
+
if (!na[name] || !nb[name]) changed = true;
|
|
228
|
+
let nx = x;
|
|
229
|
+
let ny = y;
|
|
230
|
+
if (x.opacity === void 0 !== (y.opacity === void 0)) {
|
|
231
|
+
if (x.opacity === void 0) nx = { ...nx, opacity: 1 };
|
|
232
|
+
else ny = { ...ny, opacity: 1 };
|
|
233
|
+
changed = true;
|
|
234
|
+
}
|
|
235
|
+
if (!x.transform !== !y.transform) {
|
|
236
|
+
if (!x.transform) nx = { ...nx, transform: {} };
|
|
237
|
+
else ny = { ...ny, transform: {} };
|
|
238
|
+
changed = true;
|
|
239
|
+
}
|
|
240
|
+
na[name] = nx;
|
|
241
|
+
nb[name] = ny;
|
|
242
|
+
}
|
|
243
|
+
if (changed) {
|
|
244
|
+
ra = { ...ra, groups: na };
|
|
245
|
+
rb = { ...rb, groups: nb };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return [ra, rb];
|
|
249
|
+
}
|
|
124
250
|
function applyDeltasToSpec(spec, deltas) {
|
|
125
251
|
const copy = JSON.parse(JSON.stringify(spec));
|
|
126
252
|
for (const d of deltas) {
|
|
@@ -143,7 +269,14 @@ function lerpHex(a, b, k) {
|
|
|
143
269
|
}
|
|
144
270
|
function lerpSpec(from, to, k) {
|
|
145
271
|
const kk = Math.max(0, Math.min(1, k));
|
|
146
|
-
const
|
|
272
|
+
const out = lerpValue(from, to, kk, true);
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
function lerpValue(from, to, k, specRoot = false, rootKey) {
|
|
276
|
+
if (specRoot) [from, to] = alignPaintFields(from, to);
|
|
277
|
+
if (rootKey === "transform") [from, to] = alignTransform(from, to);
|
|
278
|
+
const kk = Math.max(0, Math.min(1, k));
|
|
279
|
+
const walk = (a, b, key, root = false) => {
|
|
147
280
|
if (typeof a === "number" && typeof b === "number") {
|
|
148
281
|
const v = a + (b - a) * kk;
|
|
149
282
|
return key === "count" ? Math.max(1, Math.round(v)) : v;
|
|
@@ -157,13 +290,15 @@ function lerpSpec(from, to, k) {
|
|
|
157
290
|
if (a && b && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
|
|
158
291
|
const out = {};
|
|
159
292
|
for (const kName of Object.keys(b)) {
|
|
160
|
-
|
|
293
|
+
const av = a[kName];
|
|
294
|
+
const bv = b[kName];
|
|
295
|
+
out[kName] = root && kName === "timeline" ? kk > 0 ? bv : av : kName === "transform" ? walk(...alignTransform(av, bv), kName) : walk(av, bv, kName);
|
|
161
296
|
}
|
|
162
297
|
return out;
|
|
163
298
|
}
|
|
164
299
|
return kk > 0 ? b : a;
|
|
165
300
|
};
|
|
166
|
-
return walk(from, to);
|
|
301
|
+
return walk(from, to, rootKey, specRoot);
|
|
167
302
|
}
|
|
168
303
|
function textStringsOf(sprite) {
|
|
169
304
|
if (sprite.kind === "text") return sprite.strings;
|
|
@@ -219,7 +354,8 @@ function morphNothingMorphable(a, b, opts = {}) {
|
|
|
219
354
|
}
|
|
220
355
|
function steerablePaths(spec) {
|
|
221
356
|
if (!spec || typeof spec !== "object") return [];
|
|
222
|
-
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "motionIntensity", "mode", "curve", "layer"]);
|
|
357
|
+
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "motionIntensity", "mode", "curve", "layer", "group", "origin"]);
|
|
358
|
+
const SKIP_ROOT = /* @__PURE__ */ new Set(["timeline"]);
|
|
223
359
|
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops", "bands"]);
|
|
224
360
|
const layers = spec.layers;
|
|
225
361
|
const shadowedRootKeys = new Set(
|
|
@@ -237,8 +373,8 @@ function steerablePaths(spec) {
|
|
|
237
373
|
}
|
|
238
374
|
if (node && typeof node === "object") {
|
|
239
375
|
for (const [k, v] of Object.entries(node)) {
|
|
240
|
-
if (SKIP.has(k)) continue;
|
|
241
|
-
if (!prefix && shadowedRootKeys.has(k)) continue;
|
|
376
|
+
if (SKIP.has(k) && prefix !== "groups") continue;
|
|
377
|
+
if (!prefix && (shadowedRootKeys.has(k) || SKIP_ROOT.has(k))) continue;
|
|
242
378
|
walk(v, prefix ? `${prefix}.${k}` : k, k);
|
|
243
379
|
}
|
|
244
380
|
return;
|
|
@@ -301,13 +437,244 @@ function structuralSignature(spec) {
|
|
|
301
437
|
]);
|
|
302
438
|
}
|
|
303
439
|
|
|
440
|
+
// src/sequence.ts
|
|
441
|
+
function segmentStart(seq, index) {
|
|
442
|
+
let t = 0;
|
|
443
|
+
const n = Math.max(0, Math.min(index, seq.segments.length - 1));
|
|
444
|
+
for (let i = 0; i < n; i++) t += seq.segments[i].duration ?? 0;
|
|
445
|
+
return t;
|
|
446
|
+
}
|
|
447
|
+
function resolveSegment(seq, T, opts = {}) {
|
|
448
|
+
const { segments, loop } = seq;
|
|
449
|
+
if (segments.length === 0) return { index: 0, localT: 0, startT: 0 };
|
|
450
|
+
let totalTimed = 0;
|
|
451
|
+
for (const seg of segments) {
|
|
452
|
+
if (seg.duration != null) totalTimed += seg.duration;
|
|
453
|
+
else break;
|
|
454
|
+
}
|
|
455
|
+
const t0 = Math.max(0, T);
|
|
456
|
+
const walk = (t, releasedBelow) => {
|
|
457
|
+
let cumulative = 0;
|
|
458
|
+
for (let i = 0; i < segments.length; i++) {
|
|
459
|
+
const seg = segments[i];
|
|
460
|
+
if (seg.duration == null) {
|
|
461
|
+
return loop ? null : { index: i, localT: t - cumulative, startT: cumulative };
|
|
462
|
+
}
|
|
463
|
+
if (t < cumulative + seg.duration) {
|
|
464
|
+
return { index: i, localT: t - cumulative, startT: cumulative };
|
|
465
|
+
}
|
|
466
|
+
if (seg.advance === "input" && i >= releasedBelow) {
|
|
467
|
+
return { index: i, localT: t - cumulative, startT: cumulative, held: true };
|
|
468
|
+
}
|
|
469
|
+
cumulative += seg.duration;
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
};
|
|
473
|
+
const first = walk(t0, opts.releasedBelow ?? 0);
|
|
474
|
+
if (first) return first;
|
|
475
|
+
if (loop && totalTimed > 0) {
|
|
476
|
+
const wrapped = walk(t0 % totalTimed, 0);
|
|
477
|
+
if (wrapped) return wrapped;
|
|
478
|
+
}
|
|
479
|
+
const last = segments.length - 1;
|
|
480
|
+
const lastStart = totalTimed - (segments[last].duration ?? 0);
|
|
481
|
+
return { index: last, localT: t0 - lastStart, startT: lastStart };
|
|
482
|
+
}
|
|
483
|
+
function canMorph(seq, from) {
|
|
484
|
+
const seg = seq.segments[from];
|
|
485
|
+
if (!seg || seg.transition?.type !== "morph") return false;
|
|
486
|
+
const next = seq.segments[from + 1];
|
|
487
|
+
if (!next) return false;
|
|
488
|
+
return structuralSignature(seg.scene) === structuralSignature(next.scene);
|
|
489
|
+
}
|
|
490
|
+
function morphChainRoot(seq, index) {
|
|
491
|
+
let i = index;
|
|
492
|
+
while (i > 0 && canMorph(seq, i - 1)) i--;
|
|
493
|
+
return i;
|
|
494
|
+
}
|
|
495
|
+
function canWrapMorph(seq) {
|
|
496
|
+
if (seq.wrapMorph !== true || seq.loop !== true) return false;
|
|
497
|
+
const last = seq.segments.length - 1;
|
|
498
|
+
if (last < 1) return false;
|
|
499
|
+
const seg = seq.segments[last];
|
|
500
|
+
if (seg.transition?.type !== "morph" || seg.duration == null) return false;
|
|
501
|
+
if (morphChainRoot(seq, last) !== 0) return false;
|
|
502
|
+
return structuralSignature(seg.scene) === structuralSignature(seq.segments[0].scene);
|
|
503
|
+
}
|
|
504
|
+
function normalizeSeed(seed) {
|
|
505
|
+
return seed >>> 0 || 1;
|
|
506
|
+
}
|
|
507
|
+
function segmentRenderSeed(seq, index) {
|
|
508
|
+
const root = morphChainRoot(seq, index);
|
|
509
|
+
const scene = seq.segments[root]?.scene;
|
|
510
|
+
if (!scene) return void 0;
|
|
511
|
+
const raw = scene.seed ?? (seq.seed !== void 0 ? seq.seed + root : void 0);
|
|
512
|
+
return raw === void 0 ? void 0 : normalizeSeed(raw);
|
|
513
|
+
}
|
|
514
|
+
function bedRenderSeed(seq) {
|
|
515
|
+
if (!seq.bed) return void 0;
|
|
516
|
+
const raw = seq.bed.seed ?? (seq.seed !== void 0 ? seq.seed + LIMITS.maxSegments : void 0);
|
|
517
|
+
return raw === void 0 ? void 0 : normalizeSeed(raw);
|
|
518
|
+
}
|
|
519
|
+
function sequenceWantsFinish(seq) {
|
|
520
|
+
return seq.finish !== void 0 || seq.segments.some((s) => s.scene.finish !== void 0);
|
|
521
|
+
}
|
|
522
|
+
function sequenceSwapCompatible(prev, next) {
|
|
523
|
+
if (prev.segments.length !== next.segments.length) return false;
|
|
524
|
+
if (prev.finish !== void 0 || next.finish !== void 0) {
|
|
525
|
+
if (prev.seed === void 0 !== (next.seed === void 0)) return false;
|
|
526
|
+
if (prev.seed !== void 0 && next.seed !== void 0 && normalizeSeed(prev.seed) !== normalizeSeed(next.seed)) return false;
|
|
527
|
+
}
|
|
528
|
+
if (sequenceWantsFinish(prev) !== sequenceWantsFinish(next)) return false;
|
|
529
|
+
if (prev.loop !== next.loop) return false;
|
|
530
|
+
if ((prev.sync ?? "mount") !== (next.sync ?? "mount")) return false;
|
|
531
|
+
if (!!prev.bed !== !!next.bed) return false;
|
|
532
|
+
if (prev.bed && next.bed) {
|
|
533
|
+
if (structuralSignature(prev.bed) !== structuralSignature(next.bed)) return false;
|
|
534
|
+
if (bedRenderSeed(prev) !== bedRenderSeed(next)) return false;
|
|
535
|
+
}
|
|
536
|
+
for (let i = 0; i < prev.segments.length; i++) {
|
|
537
|
+
const a = prev.segments[i];
|
|
538
|
+
const b = next.segments[i];
|
|
539
|
+
if (a.duration !== b.duration) return false;
|
|
540
|
+
if ((a.advance ?? "auto") !== (b.advance ?? "auto")) return false;
|
|
541
|
+
if (transitionKey(a.transition) !== transitionKey(b.transition)) return false;
|
|
542
|
+
if (structuralSignature(a.scene) !== structuralSignature(b.scene)) return false;
|
|
543
|
+
if (segmentRenderSeed(prev, i) !== segmentRenderSeed(next, i)) return false;
|
|
544
|
+
}
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
function transitionKey(tr) {
|
|
548
|
+
if (!tr || tr.type === "cut") return "cut";
|
|
549
|
+
if (tr.type === "morph") return `morph:${tr.dur}:${tr.text ?? "step"}`;
|
|
550
|
+
return `fade:${tr.dur}`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/timeline.ts
|
|
554
|
+
var DEFAULT_KEY_DUR = 1e3;
|
|
555
|
+
var tracksCache = /* @__PURE__ */ new WeakMap();
|
|
556
|
+
function withoutTimeline(spec) {
|
|
557
|
+
if (!spec.timeline) return spec;
|
|
558
|
+
const rest = { ...spec };
|
|
559
|
+
delete rest.timeline;
|
|
560
|
+
return rest;
|
|
561
|
+
}
|
|
562
|
+
function timelineTracks(spec) {
|
|
563
|
+
const tl = spec.timeline;
|
|
564
|
+
if (!tl) return /* @__PURE__ */ new Map();
|
|
565
|
+
const hit = tracksCache.get(tl);
|
|
566
|
+
if (hit && hit.spec === spec) return hit.tracks;
|
|
567
|
+
const base = withoutTimeline(spec);
|
|
568
|
+
const tracks = /* @__PURE__ */ new Map();
|
|
569
|
+
for (const k of tl.keys ?? []) {
|
|
570
|
+
const cp = canonicalSpecPath(base, k.path);
|
|
571
|
+
if (!cp) continue;
|
|
572
|
+
const list = tracks.get(cp) ?? [];
|
|
573
|
+
list.push(k);
|
|
574
|
+
tracks.set(cp, list);
|
|
575
|
+
}
|
|
576
|
+
for (const list of tracks.values()) list.sort((a, b) => a.t - b.t);
|
|
577
|
+
tracksCache.set(tl, { spec, tracks });
|
|
578
|
+
return tracks;
|
|
579
|
+
}
|
|
580
|
+
function timelineLocalTime(tl, t) {
|
|
581
|
+
const d = tl.duration ?? 0;
|
|
582
|
+
if (tl.loop && d > 0) return (t % d + d) % d;
|
|
583
|
+
return t;
|
|
584
|
+
}
|
|
585
|
+
function ease(k, x) {
|
|
586
|
+
const c = x <= 0 ? 0 : x >= 1 ? 1 : x;
|
|
587
|
+
if (k.ease === "step") return c >= 1 ? 1 : 0;
|
|
588
|
+
if (k.ease === "linear") return c;
|
|
589
|
+
return easeSmooth(c);
|
|
590
|
+
}
|
|
591
|
+
function evalTrack(keys, start, tt, leaf) {
|
|
592
|
+
let from = start;
|
|
593
|
+
let key = null;
|
|
594
|
+
const at = (tau) => {
|
|
595
|
+
if (!key) return from;
|
|
596
|
+
const dur = key.dur ?? DEFAULT_KEY_DUR;
|
|
597
|
+
if (dur <= 0 || tau >= key.t + dur) return key.value;
|
|
598
|
+
return lerpValue(from, key.value, ease(key, (tau - key.t) / dur), false, leaf);
|
|
599
|
+
};
|
|
600
|
+
for (const k of keys) {
|
|
601
|
+
if (k.t > tt) break;
|
|
602
|
+
from = at(k.t);
|
|
603
|
+
key = k;
|
|
604
|
+
}
|
|
605
|
+
return at(tt);
|
|
606
|
+
}
|
|
607
|
+
function resolveTimelineAt(spec, t) {
|
|
608
|
+
const tl = spec.timeline;
|
|
609
|
+
if (!tl) return spec;
|
|
610
|
+
const base = withoutTimeline(spec);
|
|
611
|
+
const tracks = timelineTracks(spec);
|
|
612
|
+
if (tracks.size === 0) return base;
|
|
613
|
+
const tt = timelineLocalTime(tl, t);
|
|
614
|
+
const deltas = [];
|
|
615
|
+
for (const [path, keys] of tracks) {
|
|
616
|
+
const start = tl.loop ? keys[keys.length - 1].value : readSpecPath(base, path);
|
|
617
|
+
deltas.push({ t: 0, path, value: evalTrack(keys, start, tt, path.slice(path.lastIndexOf(".") + 1)) });
|
|
618
|
+
}
|
|
619
|
+
return applyDeltasToSpec(base, deltas);
|
|
620
|
+
}
|
|
621
|
+
function nextKeyAfter(spec, path, t) {
|
|
622
|
+
const tl = spec.timeline;
|
|
623
|
+
if (!tl) return null;
|
|
624
|
+
const cp = canonicalSpecPath(withoutTimeline(spec), path);
|
|
625
|
+
const keys = cp ? timelineTracks(spec).get(cp) : void 0;
|
|
626
|
+
if (!keys || keys.length === 0) return null;
|
|
627
|
+
const tt = timelineLocalTime(tl, t);
|
|
628
|
+
const next = keys.find((k) => k.t > tt);
|
|
629
|
+
if (next) return { at: t + (next.t - tt), dur: next.dur ?? DEFAULT_KEY_DUR };
|
|
630
|
+
if (tl.loop && (tl.duration ?? 0) > 0) {
|
|
631
|
+
const first = keys[0];
|
|
632
|
+
return { at: t + (tl.duration - tt) + first.t, dur: first.dur ?? DEFAULT_KEY_DUR };
|
|
633
|
+
}
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
function timelineSampleTimes(spec, dense = true, cap = 120, only) {
|
|
637
|
+
const tl = spec.timeline;
|
|
638
|
+
if (!tl) return [];
|
|
639
|
+
const d = tl.duration ?? 0;
|
|
640
|
+
const fold = (t) => tl.loop && d > 0 ? (t % d + d) % d : t;
|
|
641
|
+
const bounds = /* @__PURE__ */ new Set([0]);
|
|
642
|
+
const extras = /* @__PURE__ */ new Set();
|
|
643
|
+
const keys = [];
|
|
644
|
+
for (const [path, list] of timelineTracks(spec)) if (!only || only(path)) keys.push(...list);
|
|
645
|
+
for (const k of keys) {
|
|
646
|
+
if (!Number.isFinite(k.t)) continue;
|
|
647
|
+
const dur = k.dur ?? DEFAULT_KEY_DUR;
|
|
648
|
+
bounds.add(fold(k.t));
|
|
649
|
+
bounds.add(fold(k.t + dur));
|
|
650
|
+
if (dense) for (const f of [0.25, 0.5, 0.75]) extras.add(fold(k.t + dur * f));
|
|
651
|
+
}
|
|
652
|
+
let extra = [...extras].filter((t) => !bounds.has(t)).sort((a, b) => a - b);
|
|
653
|
+
if (extra.length > cap) {
|
|
654
|
+
const step = extra.length / cap;
|
|
655
|
+
extra = Array.from({ length: cap }, (_, i) => extra[Math.floor(i * step)]);
|
|
656
|
+
}
|
|
657
|
+
return [.../* @__PURE__ */ new Set([...bounds, ...extra])].filter(Number.isFinite).sort((a, b) => a - b);
|
|
658
|
+
}
|
|
659
|
+
function timelineSampleTimesAfter(spec, now, dense = false, only) {
|
|
660
|
+
const tl = spec.timeline;
|
|
661
|
+
if (!tl) return [];
|
|
662
|
+
const local = timelineSampleTimes(spec, dense, 120, only);
|
|
663
|
+
const d = tl.duration ?? 0;
|
|
664
|
+
if (tl.loop && d > 0) {
|
|
665
|
+
const phase = (now % d + d) % d;
|
|
666
|
+
return local.map((s) => now + ((s - phase) % d + d) % d).sort((a, b) => a - b);
|
|
667
|
+
}
|
|
668
|
+
return local.filter((s) => s > now);
|
|
669
|
+
}
|
|
670
|
+
|
|
304
671
|
// src/validate.ts
|
|
305
672
|
var HEX2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
306
673
|
var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
307
674
|
var isStr = (v) => typeof v === "string";
|
|
308
675
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
309
676
|
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"]);
|
|
677
|
+
var KNOWN_TOP = /* @__PURE__ */ new Set(["schemaVersion", "id", "label", "seed", "motionIntensity", "density", "units", "referenceViewport", "background", "layers", "ghosting", "finish", "timeline", "groups"]);
|
|
311
678
|
var KNOWN_FINISH = /* @__PURE__ */ new Set(["grain", "dither", "animate"]);
|
|
312
679
|
var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
313
680
|
"count",
|
|
@@ -330,8 +697,17 @@ var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
|
330
697
|
"life",
|
|
331
698
|
"emit",
|
|
332
699
|
"clock",
|
|
333
|
-
"rotate"
|
|
700
|
+
"rotate",
|
|
701
|
+
"opacity",
|
|
702
|
+
"transform",
|
|
703
|
+
"group"
|
|
334
704
|
]);
|
|
705
|
+
var KNOWN_TRANSFORM = /* @__PURE__ */ new Set(["x", "y", "scale", "scaleX", "rotate", "origin"]);
|
|
706
|
+
var KNOWN_GROUP = /* @__PURE__ */ new Set(["transform", "opacity"]);
|
|
707
|
+
var GROUP_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
|
|
708
|
+
var KNOWN_POSITION = /* @__PURE__ */ new Set(["x", "y", "dx", "dy"]);
|
|
709
|
+
var KNOWN_TIMELINE = /* @__PURE__ */ new Set(["loop", "duration", "keys"]);
|
|
710
|
+
var KNOWN_TIMELINE_KEY = /* @__PURE__ */ new Set(["t", "path", "value", "ease", "dur"]);
|
|
335
711
|
var KNOWN_CIRCLE = /* @__PURE__ */ new Set(["kind", "radius", "color", "soft", "colors", "colorWeights"]);
|
|
336
712
|
var KNOWN_RING = /* @__PURE__ */ new Set(["kind", "radius", "color", "width", "colors", "colorWeights"]);
|
|
337
713
|
var KNOWN_STREAK = /* @__PURE__ */ new Set(["kind", "length", "color", "width", "colors", "colorWeights"]);
|
|
@@ -366,6 +742,9 @@ function unknownKeys(obj, known) {
|
|
|
366
742
|
function dimUnit(spec) {
|
|
367
743
|
return isObj(spec) && spec.units === "px" ? "px" : "fractions of min(width, height)";
|
|
368
744
|
}
|
|
745
|
+
function offsetBound(spec) {
|
|
746
|
+
return isObj(spec) && spec.units === "px" ? [LIMITS.maxLayerTransformOffsetPx, "px"] : [LIMITS.maxLayerTransformOffset, "min(w, h) units"];
|
|
747
|
+
}
|
|
369
748
|
function rangeMsg(spec, note) {
|
|
370
749
|
return `must be a [min,max] range of positive ${dimUnit(spec)}${note ? ` (${note})` : ""}`;
|
|
371
750
|
}
|
|
@@ -393,6 +772,42 @@ function ignoredPropertyPaths(spec) {
|
|
|
393
772
|
if (!isObj(spec)) return [];
|
|
394
773
|
return (validateSpecCore(spec).warnings ?? []).filter((w) => w.code === "unknown-property" || w.code === "misplaced-property").map((w) => w.path.replace(/\[(\d+)\]/g, ".$1"));
|
|
395
774
|
}
|
|
775
|
+
function validateSpecPaths(spec, paths) {
|
|
776
|
+
const layers = /* @__PURE__ */ new Set();
|
|
777
|
+
let background = false;
|
|
778
|
+
let finish = false;
|
|
779
|
+
let groups = false;
|
|
780
|
+
for (const p of paths) {
|
|
781
|
+
if (/^layers\.\d+(\.(key|count|motion)(\.|$)|$)/.test(p)) return validateSpec(spec).valid;
|
|
782
|
+
if (p === "groups") return validateSpec(spec).valid;
|
|
783
|
+
const [head, idx] = p.split(".");
|
|
784
|
+
if (head === "layers" && idx !== void 0 && /^\d+$/.test(idx)) layers.add(Number(idx));
|
|
785
|
+
else if (head === "background") background = true;
|
|
786
|
+
else if (head === "finish") finish = true;
|
|
787
|
+
else if (head === "groups") groups = true;
|
|
788
|
+
else return validateSpec(spec).valid;
|
|
789
|
+
}
|
|
790
|
+
let ok = true;
|
|
791
|
+
const err = () => {
|
|
792
|
+
ok = false;
|
|
793
|
+
};
|
|
794
|
+
const warn = () => void 0;
|
|
795
|
+
if (!Array.isArray(spec.layers)) return false;
|
|
796
|
+
for (const i of layers) {
|
|
797
|
+
const layer = spec.layers[i];
|
|
798
|
+
if (layer === void 0) return false;
|
|
799
|
+
validateLayer(layer, `layers[${i}]`, err, warn, spec);
|
|
800
|
+
if (spec.units === "px" && isObj(layer) && isObj(layer.sprite) && layer.sprite.kind === "textBlock") ok = false;
|
|
801
|
+
}
|
|
802
|
+
if (background && spec.background !== void 0) validateBackground(spec.background, err, warn);
|
|
803
|
+
if (finish && spec.finish !== void 0) validateFinish(spec.finish, "finish", err, warn);
|
|
804
|
+
if (groups && spec.groups !== void 0) validateGroups(spec.groups, err, warn, spec);
|
|
805
|
+
if (layers.size > 0) {
|
|
806
|
+
const total = spec.layers.reduce((n, l) => n + (isObj(l) && isNum(l.count) ? l.count : 0), 0);
|
|
807
|
+
if (total > LIMITS.maxTotal) ok = false;
|
|
808
|
+
}
|
|
809
|
+
return ok;
|
|
810
|
+
}
|
|
396
811
|
function validateSpecCore(spec) {
|
|
397
812
|
const errors = [];
|
|
398
813
|
const warnings = [];
|
|
@@ -433,6 +848,7 @@ function validateSpecCore(spec) {
|
|
|
433
848
|
}
|
|
434
849
|
if (spec.background !== void 0) validateBackground(spec.background, err, warn);
|
|
435
850
|
if (spec.finish !== void 0) validateFinish(spec.finish, "finish", err, warn);
|
|
851
|
+
if (spec.groups !== void 0) validateGroups(spec.groups, err, warn, spec);
|
|
436
852
|
if (!Array.isArray(spec.layers) || spec.layers.length === 0) {
|
|
437
853
|
err("layers", "must be a non-empty array");
|
|
438
854
|
} else {
|
|
@@ -444,8 +860,151 @@ function validateSpecCore(spec) {
|
|
|
444
860
|
});
|
|
445
861
|
if (total > LIMITS.maxTotal) err("layers", `total entities ${total} exceeds cap ${LIMITS.maxTotal}`);
|
|
446
862
|
}
|
|
863
|
+
if (spec.timeline !== void 0 && errors.length === 0) validateTimeline(spec, err, warn);
|
|
447
864
|
return { valid: errors.length === 0, errors, warnings };
|
|
448
865
|
}
|
|
866
|
+
function validateTransform(tf, path, err, warn, spec, anchorOk) {
|
|
867
|
+
if (!isObj(tf)) return err(path, "must be an object {x?, y?, scale?, scaleX?, rotate?, origin?}");
|
|
868
|
+
for (const k of unknownKeys(tf, KNOWN_TRANSFORM)) warn(`${path}.${k}`, "unknown-property", `unknown transform property '${k}' \u2014 will be ignored`);
|
|
869
|
+
const [off, unit] = offsetBound(spec);
|
|
870
|
+
const sc = LIMITS.maxLayerTransformScale;
|
|
871
|
+
for (const axis of ["x", "y"]) {
|
|
872
|
+
if (tf[axis] !== void 0 && (!isNum(tf[axis]) || Math.abs(tf[axis]) > off)) err(`${path}.${axis}`, `must be a number within \xB1${off} (${unit})`);
|
|
873
|
+
}
|
|
874
|
+
if (tf.scale !== void 0 && (!isNum(tf.scale) || tf.scale < 0 || tf.scale > sc)) err(`${path}.scale`, `must be a number 0..${sc}`);
|
|
875
|
+
if (tf.scaleX !== void 0 && (!isNum(tf.scaleX) || Math.abs(tf.scaleX) > sc)) err(`${path}.scaleX`, `must be a number within \xB1${sc} (negative mirrors)`);
|
|
876
|
+
if (tf.rotate !== void 0 && (!isNum(tf.rotate) || Math.abs(tf.rotate) > LIMITS.maxLayerTransformRotate)) {
|
|
877
|
+
err(`${path}.rotate`, `must be a number of degrees within \xB1${LIMITS.maxLayerTransformRotate}`);
|
|
878
|
+
}
|
|
879
|
+
if (tf.origin !== void 0) {
|
|
880
|
+
if (tf.origin !== "viewport" && tf.origin !== "anchor") err(`${path}.origin`, "must be 'viewport' | 'anchor'");
|
|
881
|
+
else if (tf.origin === "anchor" && !anchorOk) err(`${path}.origin`, "a group transform always turns about the viewport centre \u2014 'anchor' is for a layer's own transform");
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function validateGroups(groups, err, warn, spec) {
|
|
885
|
+
if (!isObj(groups)) return err("groups", "must be an object of named groups {name: {transform?, opacity?}}");
|
|
886
|
+
const names = Object.keys(groups);
|
|
887
|
+
if (names.length > LIMITS.maxGroups) err("groups", `at most ${LIMITS.maxGroups} groups`);
|
|
888
|
+
for (const name of names) {
|
|
889
|
+
const p = `groups.${name}`;
|
|
890
|
+
if (!GROUP_NAME.test(name)) err(p, "group names are 1\u201332 letters, digits, - or _, starting with a letter");
|
|
891
|
+
const g = groups[name];
|
|
892
|
+
if (!isObj(g)) {
|
|
893
|
+
err(p, "must be an object {transform?, opacity?}");
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
for (const k of unknownKeys(g, KNOWN_GROUP)) warn(`${p}.${k}`, "unknown-property", `unknown group property '${k}' \u2014 will be ignored`);
|
|
897
|
+
if (g.opacity !== void 0 && (!isNum(g.opacity) || g.opacity < 0 || g.opacity > 1)) err(`${p}.opacity`, "must be a number 0..1");
|
|
898
|
+
if (g.transform !== void 0) validateTransform(g.transform, `${p}.transform`, err, warn, spec, false);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function validateTimeline(spec, errOuter, warn) {
|
|
902
|
+
let failed = false;
|
|
903
|
+
const err = (p, m) => {
|
|
904
|
+
failed = true;
|
|
905
|
+
errOuter(p, m);
|
|
906
|
+
};
|
|
907
|
+
const tl = spec.timeline;
|
|
908
|
+
if (!isObj(tl)) return err("timeline", "must be an object {loop?, duration?, keys}");
|
|
909
|
+
for (const k of unknownKeys(tl, KNOWN_TIMELINE)) warn(`timeline.${k}`, "unknown-property", `unknown timeline property '${k}' \u2014 will be ignored`);
|
|
910
|
+
if (tl.loop !== void 0 && typeof tl.loop !== "boolean") err("timeline.loop", "must be a boolean");
|
|
911
|
+
if (tl.duration !== void 0 && (!isNum(tl.duration) || tl.duration <= 0 || tl.duration > LIMITS.maxTimelineDuration)) {
|
|
912
|
+
err("timeline.duration", `must be a number of ms, 0 < duration <= ${LIMITS.maxTimelineDuration}`);
|
|
913
|
+
}
|
|
914
|
+
if (tl.loop === true && tl.duration === void 0) err("timeline.duration", "loop: true needs a duration (the lap length in ms)");
|
|
915
|
+
if (!Array.isArray(tl.keys) || tl.keys.length === 0) return err("timeline.keys", "must be a non-empty array");
|
|
916
|
+
if (tl.keys.length > LIMITS.maxTimelineKeys) return err("timeline.keys", `at most ${LIMITS.maxTimelineKeys} keys`);
|
|
917
|
+
const base = withoutTimeline(spec);
|
|
918
|
+
const baseSig = structuralSignature(base);
|
|
919
|
+
const times = /* @__PURE__ */ new Map();
|
|
920
|
+
let latest = 0;
|
|
921
|
+
tl.keys.forEach((key, i) => {
|
|
922
|
+
const p = `timeline.keys[${i}]`;
|
|
923
|
+
if (!isObj(key)) return err(p, "must be an object {t, path, value, ease?, dur?}");
|
|
924
|
+
for (const k of unknownKeys(key, KNOWN_TIMELINE_KEY)) warn(`${p}.${k}`, "unknown-property", `unknown key property '${k}' \u2014 will be ignored`);
|
|
925
|
+
let ok = true;
|
|
926
|
+
if (!isNum(key.t) || key.t < 0) {
|
|
927
|
+
err(`${p}.t`, "must be a number of ms >= 0");
|
|
928
|
+
ok = false;
|
|
929
|
+
}
|
|
930
|
+
if (key.ease !== void 0 && key.ease !== "step" && key.ease !== "linear" && key.ease !== "smooth") err(`${p}.ease`, "must be 'step' | 'linear' | 'smooth'");
|
|
931
|
+
if (key.dur !== void 0 && (!isNum(key.dur) || key.dur < 0 || key.dur > LIMITS.maxTimelineKeyDur)) {
|
|
932
|
+
err(`${p}.dur`, `must be a number of ms, 0..${LIMITS.maxTimelineKeyDur}`);
|
|
933
|
+
}
|
|
934
|
+
if (!("value" in key)) {
|
|
935
|
+
err(`${p}.value`, "is required");
|
|
936
|
+
ok = false;
|
|
937
|
+
}
|
|
938
|
+
const cp = isStr(key.path) ? canonicalSpecPath(base, key.path) : null;
|
|
939
|
+
if (!isStr(key.path) || key.path.trim() === "") {
|
|
940
|
+
err(`${p}.path`, "must be a non-empty dot-path");
|
|
941
|
+
ok = false;
|
|
942
|
+
} else if (/\.group$/.test(key.path) || /\.transform\.origin$/.test(key.path)) {
|
|
943
|
+
err(`${p}.path`, `'${key.path}' is an enum that can only step \u2014 group membership and a transform's origin are fixed per scene`);
|
|
944
|
+
ok = false;
|
|
945
|
+
} else if (!cp) {
|
|
946
|
+
err(`${p}.path`, `'${key.path}' does not resolve on the spec \u2014 a key can only animate a field the spec already has`);
|
|
947
|
+
ok = false;
|
|
948
|
+
} else if (cp === "timeline" || cp.startsWith("timeline.") || cp === "schemaVersion" || cp === "id" || cp === "label" || cp === "seed") {
|
|
949
|
+
err(`${p}.path`, `'${key.path}' is not animatable`);
|
|
950
|
+
ok = false;
|
|
951
|
+
}
|
|
952
|
+
if (!ok || !cp) return;
|
|
953
|
+
const t = key.t;
|
|
954
|
+
latest = Math.max(latest, t);
|
|
955
|
+
const applied = applyDeltasToSpec(base, [{ t: 0, path: cp, value: key.value }]);
|
|
956
|
+
const r = validateSpecPaths(applied, [cp]) ? { valid: true, errors: [] } : validateSpec(applied);
|
|
957
|
+
if (!r.valid) {
|
|
958
|
+
for (const e of r.errors) err(`${p}.value`, `applied at '${key.path}' \u2192 ${e.path || "<root>"}: ${e.message}`);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
if (isObj(key.value) && cp.endsWith("transform")) {
|
|
962
|
+
const before = readSpecPath(base, cp);
|
|
963
|
+
const o0 = isObj(before) ? before.origin ?? "viewport" : "viewport";
|
|
964
|
+
const o1 = key.value.origin ?? "viewport";
|
|
965
|
+
if (o0 !== o1) warn(`${p}.value`, "timeline-origin-steps", `this key changes the transform's origin (${String(o0)} \u2192 ${String(o1)}): the pivot switches on the key's first frame, so the layer jumps \u2014 keep one origin per layer`);
|
|
966
|
+
}
|
|
967
|
+
if (structuralSignature(applied) !== baseSig) {
|
|
968
|
+
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`);
|
|
969
|
+
}
|
|
970
|
+
const list = times.get(cp) ?? [];
|
|
971
|
+
list.push(t);
|
|
972
|
+
times.set(cp, list);
|
|
973
|
+
const dur = isNum(key.dur) ? key.dur : DEFAULT_KEY_DUR;
|
|
974
|
+
if (tl.loop === true && isNum(tl.duration) && t + dur > tl.duration) {
|
|
975
|
+
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`);
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
if (tl.loop === true && isNum(tl.duration) && latest >= tl.duration) {
|
|
979
|
+
err("timeline.duration", `must be greater than the latest key's t (${latest} ms) under loop`);
|
|
980
|
+
}
|
|
981
|
+
const minGap = LIMITS.minTimelineKeyInterval;
|
|
982
|
+
for (const [path, list] of times) {
|
|
983
|
+
const distinct = [...new Set(list)].sort((a, b) => a - b);
|
|
984
|
+
for (let i = 1; i < distinct.length; i++) {
|
|
985
|
+
if (distinct[i] - distinct[i - 1] < minGap) {
|
|
986
|
+
err("timeline.keys", `keys on '${path}' at ${distinct[i - 1]} and ${distinct[i]} ms are closer than ${minGap} ms (flash safety)`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (tl.loop === true && isNum(tl.duration) && distinct.length > 1) {
|
|
990
|
+
const wrapGap = tl.duration - distinct[distinct.length - 1] + distinct[0];
|
|
991
|
+
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)`);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (failed) return;
|
|
995
|
+
const keyed = [...timelineTracks(spec).keys()];
|
|
996
|
+
const seen = /* @__PURE__ */ new Set();
|
|
997
|
+
for (const t of timelineSampleTimes(spec, true, 120)) {
|
|
998
|
+
const r = resolveTimelineAt(spec, t);
|
|
999
|
+
const state = JSON.stringify(keyed.map((p) => readSpecPath(r, p)));
|
|
1000
|
+
if (seen.has(state)) continue;
|
|
1001
|
+
seen.add(state);
|
|
1002
|
+
if (validateSpecPaths(r, keyed)) continue;
|
|
1003
|
+
const e = validateSpec(r).errors[0];
|
|
1004
|
+
err("timeline", `at t = ${Math.round(t)} ms the keys combine into an invalid scene \u2014 ${e ? `${e.path || "<root>"}: ${e.message}` : "invalid"}`);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
449
1008
|
function color(v, path, err) {
|
|
450
1009
|
if (!isStr(v) || !HEX2.test(v)) err(path, "must be a hex colour like #0a3a52");
|
|
451
1010
|
}
|
|
@@ -569,12 +1128,29 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
569
1128
|
if (layer.key !== void 0 && (!isStr(layer.key) || layer.key.trim() === "")) {
|
|
570
1129
|
err(`${path}.key`, "must be a non-empty string");
|
|
571
1130
|
}
|
|
1131
|
+
if (layer.opacity !== void 0 && (!isNum(layer.opacity) || layer.opacity < 0 || layer.opacity > 1)) {
|
|
1132
|
+
err(`${path}.opacity`, "must be a number 0..1");
|
|
1133
|
+
}
|
|
1134
|
+
if (layer.transform !== void 0) validateTransform(layer.transform, `${path}.transform`, err, warn, spec, true);
|
|
1135
|
+
if (layer.group !== void 0) {
|
|
1136
|
+
const groups = isObj(spec) && isObj(spec.groups) ? spec.groups : void 0;
|
|
1137
|
+
if (!isStr(layer.group)) err(`${path}.group`, "must be the name of a `groups` entry");
|
|
1138
|
+
else if (!groups || !Object.prototype.hasOwnProperty.call(groups, layer.group) || !isObj(groups[layer.group])) err(`${path}.group`, `'${layer.group}' is not in \`groups\` \u2014 declare it there first`);
|
|
1139
|
+
}
|
|
572
1140
|
if (layer.position !== void 0) {
|
|
573
1141
|
if (!isObj(layer.position) || !isNum(layer.position.x) || !isNum(layer.position.y)) {
|
|
574
1142
|
err(`${path}.position`, "must be {x, y} with numbers 0..1");
|
|
575
1143
|
} else {
|
|
576
1144
|
if (layer.position.x < 0 || layer.position.x > 1) err(`${path}.position.x`, "must be 0..1");
|
|
577
1145
|
if (layer.position.y < 0 || layer.position.y > 1) err(`${path}.position.y`, "must be 0..1");
|
|
1146
|
+
const [off, unit] = offsetBound(spec);
|
|
1147
|
+
for (const axis of ["dx", "dy"]) {
|
|
1148
|
+
const v = layer.position[axis];
|
|
1149
|
+
if (v !== void 0 && (!isNum(v) || Math.abs(v) > off)) {
|
|
1150
|
+
err(`${path}.position.${axis}`, `must be a number within \xB1${off} (${unit})`);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
for (const k of unknownKeys(layer.position, KNOWN_POSITION)) warn(`${path}.position.${k}`, "unknown-property", `unknown position property '${k}' \u2014 will be ignored`);
|
|
578
1154
|
const dataLayout = isObj(layer.layout) && (layer.layout.type === "list" || layer.layout.type === "table");
|
|
579
1155
|
if (isNum(layer.count) && layer.count !== 1 && !dataLayout) {
|
|
580
1156
|
err(`${path}.position`, "position requires count: 1 \u2014 or a layout of type 'list' / 'table', where it anchors the whole block");
|
|
@@ -1161,14 +1737,14 @@ function validateShapePoints(points, path, min, err) {
|
|
|
1161
1737
|
}
|
|
1162
1738
|
}
|
|
1163
1739
|
}
|
|
1164
|
-
function validateEase(
|
|
1165
|
-
if (
|
|
1166
|
-
if (!isObj(
|
|
1167
|
-
if (
|
|
1168
|
-
if (!isNum(
|
|
1740
|
+
function validateEase(ease2, path, err, warn) {
|
|
1741
|
+
if (ease2 === void 0) return;
|
|
1742
|
+
if (!isObj(ease2)) return err(path, "must be an object { type: 'settle' | 'buoyant', tau }");
|
|
1743
|
+
if (ease2.type !== "settle" && ease2.type !== "buoyant") err(`${path}.type`, "must be 'settle' | 'buoyant'");
|
|
1744
|
+
if (!isNum(ease2.tau) || ease2.tau < LIMITS.minEaseTau || ease2.tau > LIMITS.maxEaseTau) {
|
|
1169
1745
|
err(`${path}.tau`, `must be ${LIMITS.minEaseTau}..${LIMITS.maxEaseTau} ms`);
|
|
1170
1746
|
}
|
|
1171
|
-
for (const k of unknownKeys(
|
|
1747
|
+
for (const k of unknownKeys(ease2, /* @__PURE__ */ new Set(["type", "tau"]))) {
|
|
1172
1748
|
warn(`${path}.${k}`, "unknown-property", `unknown ease property '${k}' \u2014 will be ignored`);
|
|
1173
1749
|
}
|
|
1174
1750
|
}
|
|
@@ -1200,6 +1776,7 @@ function validateSequence(seq) {
|
|
|
1200
1776
|
if (seq.seed !== void 0 && !isNum(seq.seed)) err("seed", "must be a number");
|
|
1201
1777
|
if (typeof seq.loop !== "boolean") err("loop", "must be a boolean");
|
|
1202
1778
|
if (seq.sync !== void 0 && seq.sync !== "mount" && seq.sync !== "epoch") err("sync", "must be 'mount' | 'epoch'");
|
|
1779
|
+
if (seq.wrapMorph !== void 0 && typeof seq.wrapMorph !== "boolean") err("wrapMorph", "must be a boolean");
|
|
1203
1780
|
if (seq.finish !== void 0) validateFinish(seq.finish, "finish", err, (p, code, message) => void warnings.push({ path: p, code, message }));
|
|
1204
1781
|
if (!Array.isArray(seq.segments) || seq.segments.length === 0) {
|
|
1205
1782
|
err("segments", "must be a non-empty array");
|
|
@@ -1253,8 +1830,8 @@ function validateSequence(seq) {
|
|
|
1253
1830
|
if (isObj(s.transition) && s.transition.text !== void 0) {
|
|
1254
1831
|
if (s.transition.type !== "morph") {
|
|
1255
1832
|
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'");
|
|
1833
|
+
} else if (s.transition.text !== "step" && s.transition.text !== "crossfade" && s.transition.text !== "dip") {
|
|
1834
|
+
err(`${p}.transition.text`, "must be 'step' | 'crossfade' | 'dip'");
|
|
1258
1835
|
}
|
|
1259
1836
|
}
|
|
1260
1837
|
}
|
|
@@ -1311,7 +1888,7 @@ function validateSequence(seq) {
|
|
|
1311
1888
|
code: "morph-structural-mismatch",
|
|
1312
1889
|
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
1313
1890
|
});
|
|
1314
|
-
} else if (morphNothingMorphable(s.scene, next.scene, { textCrossfade: s.transition.text === "crossfade" })) {
|
|
1891
|
+
} 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
1892
|
warnings.push({
|
|
1316
1893
|
path: `segments[${i}].transition`,
|
|
1317
1894
|
code: "morph-nothing-morphable",
|
|
@@ -1320,6 +1897,20 @@ function validateSequence(seq) {
|
|
|
1320
1897
|
}
|
|
1321
1898
|
}
|
|
1322
1899
|
}
|
|
1900
|
+
if (errors.length === 0 && seq.wrapMorph === true) {
|
|
1901
|
+
const sq = seq;
|
|
1902
|
+
const last = sq.segments.length - 1;
|
|
1903
|
+
const lastTr = sq.segments[last]?.transition;
|
|
1904
|
+
let why = null;
|
|
1905
|
+
if (sq.loop !== true) why = "the sequence does not loop";
|
|
1906
|
+
else if (last < 1) why = "there is only one segment";
|
|
1907
|
+
else if (lastTr?.type !== "morph") why = "the last segment declares no morph transition";
|
|
1908
|
+
else if (structuralSignature(sq.segments[last].scene) !== structuralSignature(sq.segments[0].scene)) why = `segment ${last} and segment 0 differ structurally`;
|
|
1909
|
+
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`;
|
|
1910
|
+
if (why || !canWrapMorph(sq)) {
|
|
1911
|
+
warnings.push({ path: "wrapMorph", code: "wrap-morph-inactive", message: `wrapMorph has no effect: ${why ?? "the wrap cannot morph"} \u2014 the wrap stays a cut` });
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1323
1914
|
return { valid: errors.length === 0, errors, warnings };
|
|
1324
1915
|
}
|
|
1325
1916
|
function assertValidSequence(seq) {
|
|
@@ -1511,7 +2102,7 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1511
2102
|
if (mSpec.type === "path") {
|
|
1512
2103
|
pathPts = mSpec.points.map((p) => ({ x: p.x * w, y: p.y * h }));
|
|
1513
2104
|
}
|
|
1514
|
-
const
|
|
2105
|
+
const ease2 = mSpec.type === "drift" || mSpec.type === "rise" || mSpec.type === "wander" ? mSpec.ease : void 0;
|
|
1515
2106
|
const clockPhase = layer.clock ? (layer.clock.phase ?? 0) % 1 * Math.PI * 2 : 0;
|
|
1516
2107
|
const layout = layer.layout;
|
|
1517
2108
|
const ordered = layout?.type === "list" || layout?.type === "table";
|
|
@@ -1530,6 +2121,8 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1530
2121
|
if (layer.position) {
|
|
1531
2122
|
listX0 = layer.position.x * w;
|
|
1532
2123
|
listY0 = layer.position.y * h;
|
|
2124
|
+
if (layer.position.dx) listX0 += layer.position.dx * scale;
|
|
2125
|
+
if (layer.position.dy) listY0 += layer.position.dy * scale;
|
|
1533
2126
|
} else {
|
|
1534
2127
|
const [rx0, rx1] = layer.region?.x ?? [0, 1];
|
|
1535
2128
|
const [ry0, ry1] = layer.region?.y ?? [0, 1];
|
|
@@ -1624,6 +2217,8 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1624
2217
|
if (layer.position && layer.count === 1) {
|
|
1625
2218
|
x0 = layer.position.x * w;
|
|
1626
2219
|
y0 = layer.position.y * h;
|
|
2220
|
+
if (layer.position.dx) x0 += layer.position.dx * scale;
|
|
2221
|
+
if (layer.position.dy) y0 += layer.position.dy * scale;
|
|
1627
2222
|
} else if (layout?.type === "list" || layout?.type === "table") {
|
|
1628
2223
|
rng.next();
|
|
1629
2224
|
rng.next();
|
|
@@ -1705,7 +2300,7 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1705
2300
|
} : {},
|
|
1706
2301
|
// Time-structure fields (2026-09): ease draws nothing; emit's one draw
|
|
1707
2302
|
// per entity happens in a pass AFTER this loop (see below).
|
|
1708
|
-
...
|
|
2303
|
+
...ease2 ? { ease: { type: ease2.type, tau: ease2.tau } } : {},
|
|
1709
2304
|
// Static rotation (2026-09-10): a scalar draws nothing; the range form
|
|
1710
2305
|
// draws one value here, last, only when declared.
|
|
1711
2306
|
...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 } : {}
|
|
@@ -2174,6 +2769,67 @@ import {
|
|
|
2174
2769
|
createRng
|
|
2175
2770
|
} from "@idle-screens/core";
|
|
2176
2771
|
|
|
2772
|
+
// src/paint.ts
|
|
2773
|
+
function layerAnchor(layer, w, h, unit) {
|
|
2774
|
+
const p = layer.position;
|
|
2775
|
+
if (p) return { x: p.x * w + (p.dx ?? 0) * unit, y: p.y * h + (p.dy ?? 0) * unit };
|
|
2776
|
+
const rx = layer.region?.x ?? [0, 1];
|
|
2777
|
+
const ry = layer.region?.y ?? [0, 1];
|
|
2778
|
+
return { x: (rx[0] + rx[1]) / 2 * w, y: (ry[0] + ry[1]) / 2 * h };
|
|
2779
|
+
}
|
|
2780
|
+
function transformOrigin(tf, layer, w, h, unit) {
|
|
2781
|
+
return tf.origin === "anchor" && layer ? layerAnchor(layer, w, h, unit) : { x: w / 2, y: h / 2 };
|
|
2782
|
+
}
|
|
2783
|
+
function transformPoint(tf, p, w, h, unit, origin = { x: w / 2, y: h / 2 }) {
|
|
2784
|
+
const s = tf.scale ?? 1;
|
|
2785
|
+
const sx = s * (tf.scaleX ?? 1);
|
|
2786
|
+
let x = (p.x - origin.x) * sx;
|
|
2787
|
+
let y = (p.y - origin.y) * s;
|
|
2788
|
+
if (tf.rotate) {
|
|
2789
|
+
const r = tf.rotate * Math.PI / 180;
|
|
2790
|
+
const c = Math.cos(r);
|
|
2791
|
+
const n = Math.sin(r);
|
|
2792
|
+
[x, y] = [x * c - y * n, x * n + y * c];
|
|
2793
|
+
}
|
|
2794
|
+
return { x: x + origin.x + (tf.x ?? 0) * unit, y: y + origin.y + (tf.y ?? 0) * unit };
|
|
2795
|
+
}
|
|
2796
|
+
function transformSize(tf) {
|
|
2797
|
+
if (!tf) return 1;
|
|
2798
|
+
const s = tf.scale ?? 1;
|
|
2799
|
+
return s * Math.sqrt(Math.abs(tf.scaleX ?? 1));
|
|
2800
|
+
}
|
|
2801
|
+
function groupOf(spec, layer) {
|
|
2802
|
+
return layer.group === void 0 ? void 0 : spec.groups?.[layer.group];
|
|
2803
|
+
}
|
|
2804
|
+
var clamp01 = (v) => v <= 0 ? 0 : v >= 1 ? 1 : v;
|
|
2805
|
+
function paintOpacity(spec, layer) {
|
|
2806
|
+
const own = layer.opacity === void 0 ? 1 : clamp01(layer.opacity);
|
|
2807
|
+
const g = groupOf(spec, layer);
|
|
2808
|
+
return g?.opacity === void 0 ? own : own * clamp01(g.opacity);
|
|
2809
|
+
}
|
|
2810
|
+
function paintSize(spec, layer) {
|
|
2811
|
+
return transformSize(layer.transform) * transformSize(groupOf(spec, layer)?.transform);
|
|
2812
|
+
}
|
|
2813
|
+
function paintMap(spec, layer, w, h, unit) {
|
|
2814
|
+
const tf = layer.transform;
|
|
2815
|
+
const gtf = groupOf(spec, layer)?.transform;
|
|
2816
|
+
if (!tf && !gtf) return null;
|
|
2817
|
+
const o = tf ? transformOrigin(tf, layer, w, h, unit) : null;
|
|
2818
|
+
return (p) => {
|
|
2819
|
+
const q = tf ? transformPoint(tf, p, w, h, unit, o) : p;
|
|
2820
|
+
return gtf ? transformPoint(gtf, q, w, h, unit) : q;
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
function mapBox(map, box) {
|
|
2824
|
+
const pts = [[box.x0, box.y0], [box.x1, box.y0], [box.x0, box.y1], [box.x1, box.y1]].map(([x, y]) => map({ x, y }));
|
|
2825
|
+
return {
|
|
2826
|
+
x0: Math.min(...pts.map((p) => p.x)),
|
|
2827
|
+
y0: Math.min(...pts.map((p) => p.y)),
|
|
2828
|
+
x1: Math.max(...pts.map((p) => p.x)),
|
|
2829
|
+
y1: Math.max(...pts.map((p) => p.y))
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2177
2833
|
// src/field.ts
|
|
2178
2834
|
var TWO_PI = Math.PI * 2;
|
|
2179
2835
|
var FIELD_BUCKET_MS = 100;
|
|
@@ -2183,7 +2839,7 @@ var WARP_GAIN = 2;
|
|
|
2183
2839
|
var DRIFT_GAIN = 1;
|
|
2184
2840
|
var FIELD_DRIFT_DEFAULT_AMOUNT = 0.3;
|
|
2185
2841
|
var CONTRAST = [1, 1.45, 1.8, 1.95, 2];
|
|
2186
|
-
var
|
|
2842
|
+
var clamp012 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
|
|
2187
2843
|
function latticeHash(ix, iy, seed) {
|
|
2188
2844
|
let h = Math.imul(ix | 0, 668265261) ^ Math.imul(iy | 0, 374761393) ^ Math.imul(seed | 0, 2654435761);
|
|
2189
2845
|
h = Math.imul(h ^ h >>> 15, 2246822507);
|
|
@@ -2246,7 +2902,7 @@ function fieldAt(u, v, t, cfg, seed) {
|
|
|
2246
2902
|
y += warp * WARP_GAIN * qy;
|
|
2247
2903
|
}
|
|
2248
2904
|
const raw = fbm(x, y, s, octaves);
|
|
2249
|
-
return
|
|
2905
|
+
return clamp012(0.5 + (raw - 0.5) * CONTRAST[octaves]);
|
|
2250
2906
|
}
|
|
2251
2907
|
function hexToRgb255(hex) {
|
|
2252
2908
|
const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
|
|
@@ -2262,10 +2918,10 @@ function fieldRgb(value, bands, quantize) {
|
|
|
2262
2918
|
const q = Math.round(quantize ?? n);
|
|
2263
2919
|
let f;
|
|
2264
2920
|
if (q >= 2) {
|
|
2265
|
-
const level = Math.min(q - 1, Math.floor(
|
|
2921
|
+
const level = Math.min(q - 1, Math.floor(clamp012(value) * q));
|
|
2266
2922
|
f = level * (n - 1) / (q - 1);
|
|
2267
2923
|
} else {
|
|
2268
|
-
f =
|
|
2924
|
+
f = clamp012(value) * (n - 1);
|
|
2269
2925
|
}
|
|
2270
2926
|
const i = Math.min(n - 2, Math.floor(f));
|
|
2271
2927
|
const k = f - i;
|
|
@@ -2288,9 +2944,9 @@ var DITHER_TILE = 8;
|
|
|
2288
2944
|
var GRAIN_ALPHA = 0.35;
|
|
2289
2945
|
var DITHER_ALPHA = 0.25;
|
|
2290
2946
|
var FINISH_ANIMATE_HZ = 12;
|
|
2291
|
-
var
|
|
2947
|
+
var clamp013 = (v) => v < 0 ? 0 : v > 1 ? 1 : v;
|
|
2292
2948
|
function finishStrength(f) {
|
|
2293
|
-
return { grain:
|
|
2949
|
+
return { grain: clamp013(f?.grain ?? 0), dither: clamp013(f?.dither ?? 0) };
|
|
2294
2950
|
}
|
|
2295
2951
|
function grainTilePixels(seed) {
|
|
2296
2952
|
const px = new Uint8ClampedArray(GRAIN_TILE * GRAIN_TILE * 4);
|
|
@@ -2422,110 +3078,6 @@ function screen(fp, pat, alpha, off, w, h) {
|
|
|
2422
3078
|
p.restore();
|
|
2423
3079
|
}
|
|
2424
3080
|
|
|
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
3081
|
// src/compile.ts
|
|
2530
3082
|
var DEFAULT_STEER_DUR = 1e3;
|
|
2531
3083
|
function applyRetainedDeltas(spec, deltas) {
|
|
@@ -2573,10 +3125,62 @@ function manifestFor(spec) {
|
|
|
2573
3125
|
workerReady: true
|
|
2574
3126
|
};
|
|
2575
3127
|
}
|
|
3128
|
+
var clamp014 = (v) => v <= 0 ? 0 : v >= 1 ? 1 : v;
|
|
3129
|
+
var EXPIRED = /* @__PURE__ */ Symbol("expired");
|
|
3130
|
+
function overrideHeldAt(o, t) {
|
|
3131
|
+
const k = o.dur > 0 ? easeSmooth(clamp014((t - o.t0) / o.dur)) : 1;
|
|
3132
|
+
return k >= 1 ? o.to : lerpValue(o.from, o.to, k, false, o.leaf);
|
|
3133
|
+
}
|
|
3134
|
+
function overrideValueAt(o, t, timelineValue) {
|
|
3135
|
+
if (t < o.until) return overrideHeldAt(o, t);
|
|
3136
|
+
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);
|
|
3137
|
+
return EXPIRED;
|
|
3138
|
+
}
|
|
3139
|
+
var steerId = (d) => `${d.t}|${JSON.stringify(d.value)}`;
|
|
3140
|
+
function armOverride(authored, path, from, to, now, glide) {
|
|
3141
|
+
const next = nextKeyAfter(authored, path, now);
|
|
3142
|
+
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 };
|
|
3143
|
+
}
|
|
3144
|
+
function applyOverridesAt(resolved, overrides, t, expired) {
|
|
3145
|
+
if (overrides.size === 0) return resolved;
|
|
3146
|
+
const deltas = [];
|
|
3147
|
+
for (const [path, o] of overrides) {
|
|
3148
|
+
const v = overrideValueAt(o, t, () => readSpecPath(resolved, path));
|
|
3149
|
+
if (v === EXPIRED) {
|
|
3150
|
+
expired?.push(path);
|
|
3151
|
+
continue;
|
|
3152
|
+
}
|
|
3153
|
+
deltas.push({ t: 0, path, value: v });
|
|
3154
|
+
}
|
|
3155
|
+
return deltas.length > 0 ? applyDeltasToSpec(resolved, deltas) : resolved;
|
|
3156
|
+
}
|
|
3157
|
+
function overridesStayValid(authored, overrides, now) {
|
|
3158
|
+
const paths = [...overrides.keys()];
|
|
3159
|
+
if (paths.length === 0) return true;
|
|
3160
|
+
const groupOf2 = (p) => p.split(".").slice(0, p.startsWith("layers.") ? 2 : 1).join(".");
|
|
3161
|
+
const groups = [...new Set(paths.map(groupOf2))];
|
|
3162
|
+
const full = paths.some((p) => /^layers\.\d+(\.(key|count|motion)(\.|$)|$)/.test(p) || p === "groups" || !/^(layers|background|finish|groups)(\.|$)/.test(p));
|
|
3163
|
+
const only = full ? void 0 : (p) => groups.includes(groupOf2(p));
|
|
3164
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3165
|
+
for (const t of [now, ...timelineSampleTimesAfter(authored, now, false, only)]) {
|
|
3166
|
+
const held = /* @__PURE__ */ new Map();
|
|
3167
|
+
for (const [p, o] of overrides) if (t < o.until) held.set(p, { ...o, t0: Number.NEGATIVE_INFINITY });
|
|
3168
|
+
const r = applyOverridesAt(resolveTimelineAt(authored, t), held, t);
|
|
3169
|
+
const state = JSON.stringify(groups.map((g) => readSpecPath(r, g))) + "|" + [...held.keys()].join(",");
|
|
3170
|
+
if (seen.has(state)) continue;
|
|
3171
|
+
seen.add(state);
|
|
3172
|
+
if (!validateSpecPaths(r, paths)) return false;
|
|
3173
|
+
}
|
|
3174
|
+
return true;
|
|
3175
|
+
}
|
|
2576
3176
|
var SpecInstance = class {
|
|
2577
3177
|
constructor(spec, ctx, opts = {}) {
|
|
2578
3178
|
this.spec = spec;
|
|
2579
3179
|
this.effSpec = spec;
|
|
3180
|
+
if (spec.timeline) {
|
|
3181
|
+
this.authored = spec;
|
|
3182
|
+
this.effSpec = resolveTimelineAt(spec, 0);
|
|
3183
|
+
}
|
|
2580
3184
|
this.saverCtx = ctx;
|
|
2581
3185
|
this.transparent = opts.transparent === true;
|
|
2582
3186
|
const tier = opts.capabilityTier;
|
|
@@ -2642,6 +3246,31 @@ var SpecInstance = class {
|
|
|
2642
3246
|
lastStructural = "";
|
|
2643
3247
|
/** See `LayerPaintOverride`; null (the default, and every plain mount) draws every layer once. */
|
|
2644
3248
|
paintOverrides = null;
|
|
3249
|
+
/**
|
|
3250
|
+
* The authored spec when it carries a `timeline` (null for every other
|
|
3251
|
+
* scene — which then never enters the timeline path). Each painted frame
|
|
3252
|
+
* resolves it at that frame's own time, so ghosting's warm-up replay and
|
|
3253
|
+
* any seek see the timeline exactly where it was.
|
|
3254
|
+
*/
|
|
3255
|
+
authored = null;
|
|
3256
|
+
/**
|
|
3257
|
+
* Live steers on a timeline scene, by index-form path. A steer on a path no
|
|
3258
|
+
* key animates holds forever (sticky, exactly as on a scene without a
|
|
3259
|
+
* timeline); on an animated path it holds `until` that path's next key,
|
|
3260
|
+
* then glides back to the timeline over `outDur`.
|
|
3261
|
+
*/
|
|
3262
|
+
overrides = /* @__PURE__ */ new Map();
|
|
3263
|
+
/**
|
|
3264
|
+
* The last registered steer per path, as `t|value`. A host re-sends the
|
|
3265
|
+
* whole channel track on every steer (idle-server broadcasts the full track
|
|
3266
|
+
* so viewers converge), and a steer's `t` is the server's stamp — stable
|
|
3267
|
+
* across re-sends. Without this, an unrelated later steer would re-arm a
|
|
3268
|
+
* hold the timeline had already taken back. Keyed by path (not by the full
|
|
3269
|
+
* `path|t|value` history) so a long-running instance re-steered thousands
|
|
3270
|
+
* of times doesn't accumulate an unbounded set — only the most recent id
|
|
3271
|
+
* per path can ever recur.
|
|
3272
|
+
*/
|
|
3273
|
+
seenSteers = /* @__PURE__ */ new Map();
|
|
2645
3274
|
/** Raster short side for a `field` background — halved on the low tiers. */
|
|
2646
3275
|
fieldShortSide;
|
|
2647
3276
|
/** The `field` background's cached raster; null until a field is first painted. */
|
|
@@ -2666,12 +3295,14 @@ var SpecInstance = class {
|
|
|
2666
3295
|
}
|
|
2667
3296
|
sizeCanvas() {
|
|
2668
3297
|
const dpr = Math.min(this.saverCtx.dpr, 2);
|
|
2669
|
-
|
|
2670
|
-
|
|
3298
|
+
const cw = Math.max(1, Math.round(this.w * dpr));
|
|
3299
|
+
const ch = Math.max(1, Math.round(this.h * dpr));
|
|
3300
|
+
if (this.canvas.width !== cw) this.canvas.width = cw;
|
|
3301
|
+
if (this.canvas.height !== ch) this.canvas.height = ch;
|
|
2671
3302
|
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2672
3303
|
if (this.finishPass) {
|
|
2673
|
-
this.finishPass.canvas.width
|
|
2674
|
-
this.finishPass.canvas.height
|
|
3304
|
+
if (this.finishPass.canvas.width !== cw) this.finishPass.canvas.width = cw;
|
|
3305
|
+
if (this.finishPass.canvas.height !== ch) this.finishPass.canvas.height = ch;
|
|
2675
3306
|
this.finishPass.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
2676
3307
|
}
|
|
2677
3308
|
this.lastRenderT = Number.NEGATIVE_INFINITY;
|
|
@@ -3124,6 +3755,12 @@ var SpecInstance = class {
|
|
|
3124
3755
|
*/
|
|
3125
3756
|
applyTrack(track) {
|
|
3126
3757
|
const deltas = track?.deltas ?? [];
|
|
3758
|
+
if (this.authored) {
|
|
3759
|
+
const glide = deltas.length ? deltas.reduce((m, d) => Math.max(m, d.dur ?? DEFAULT_STEER_DUR), 0) : DEFAULT_STEER_DUR;
|
|
3760
|
+
if (!this.overrideAuthored(deltas, this.paused ? 0 : glide)) return;
|
|
3761
|
+
if (this.paused) this.renderFrame(this.sceneNow(), this.seed);
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3127
3764
|
const target = applyDeltasToSpec(this.effSpec, deltas);
|
|
3128
3765
|
if (!validateSpec(target).valid) return;
|
|
3129
3766
|
const dur = deltas.length ? deltas.reduce((m, d) => Math.max(m, d.dur ?? DEFAULT_STEER_DUR), 0) : DEFAULT_STEER_DUR;
|
|
@@ -3160,6 +3797,13 @@ var SpecInstance = class {
|
|
|
3160
3797
|
* showing a ghost of the un-steered scene.
|
|
3161
3798
|
*/
|
|
3162
3799
|
applyDeltasNow(deltas) {
|
|
3800
|
+
if (this.authored) {
|
|
3801
|
+
const all = [...deltas];
|
|
3802
|
+
if (!this.overrideAuthored(all, 0)) for (const d of all) this.overrideAuthored([d], 0);
|
|
3803
|
+
this.transition = null;
|
|
3804
|
+
this.lastRenderT = Number.NEGATIVE_INFINITY;
|
|
3805
|
+
return;
|
|
3806
|
+
}
|
|
3163
3807
|
const next = applyRetainedDeltas(this.effSpec, deltas);
|
|
3164
3808
|
if (next === this.effSpec) return;
|
|
3165
3809
|
this.transition = null;
|
|
@@ -3215,6 +3859,7 @@ var SpecInstance = class {
|
|
|
3215
3859
|
* showing through — the ghosting smear. */
|
|
3216
3860
|
paintFrame(t, bgAlpha) {
|
|
3217
3861
|
const { ctx } = this;
|
|
3862
|
+
if (this.authored) this.resolveAuthored(t);
|
|
3218
3863
|
if (!this.transparent) {
|
|
3219
3864
|
ctx.globalAlpha = bgAlpha;
|
|
3220
3865
|
ctx.globalCompositeOperation = "source-over";
|
|
@@ -3223,9 +3868,19 @@ var SpecInstance = class {
|
|
|
3223
3868
|
ctx.globalAlpha = 1;
|
|
3224
3869
|
for (let li = 0; li < this.layers.length; li++) {
|
|
3225
3870
|
const built = this.layers[li];
|
|
3226
|
-
const
|
|
3871
|
+
const opacity = built.layer.opacity;
|
|
3872
|
+
const group = built.layer.group === void 0 ? void 0 : this.effSpec.groups?.[built.layer.group];
|
|
3873
|
+
let lifeA = opacity === void 0 ? lifeAlphaAt(built.layer.life, t) : lifeAlphaAt(built.layer.life, t) * clamp014(opacity);
|
|
3874
|
+
if (group?.opacity !== void 0) lifeA *= clamp014(group.opacity);
|
|
3227
3875
|
if (lifeA <= 0) continue;
|
|
3228
3876
|
const parentE = this.parentEntityFor(built);
|
|
3877
|
+
const tf = built.layer.transform;
|
|
3878
|
+
const gtf = group?.transform;
|
|
3879
|
+
if (tf || gtf) {
|
|
3880
|
+
ctx.save();
|
|
3881
|
+
if (gtf) this.applyLayerTransform(gtf, null);
|
|
3882
|
+
if (tf) this.applyLayerTransform(tf, built.layer);
|
|
3883
|
+
}
|
|
3229
3884
|
ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
|
|
3230
3885
|
this.drawLinks(built, t, lifeA, parentE);
|
|
3231
3886
|
const override = this.paintOverrides?.get(li) ?? null;
|
|
@@ -3235,13 +3890,83 @@ var SpecInstance = class {
|
|
|
3235
3890
|
this.drawEntity(built, e, t, lifeA, parentE);
|
|
3236
3891
|
continue;
|
|
3237
3892
|
}
|
|
3238
|
-
|
|
3239
|
-
if (
|
|
3893
|
+
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];
|
|
3894
|
+
if (aOut > 0) this.drawEntity(built, e, t, lifeA, parentE, { sprite: override.outgoing, alpha: aOut });
|
|
3895
|
+
if (aIn > 0) this.drawEntity(built, e, t, lifeA, parentE, { alpha: aIn });
|
|
3240
3896
|
}
|
|
3897
|
+
if (tf || gtf) ctx.restore();
|
|
3241
3898
|
}
|
|
3242
3899
|
ctx.globalAlpha = 1;
|
|
3243
3900
|
ctx.globalCompositeOperation = "source-over";
|
|
3244
3901
|
}
|
|
3902
|
+
/**
|
|
3903
|
+
* A `transform`, in min(w,h) units (px under `units: 'px'`), about the
|
|
3904
|
+
* viewport centre — or, with `origin: 'anchor'`, about `layer`'s own anchor
|
|
3905
|
+
* (a group's transform passes no layer: always the viewport centre).
|
|
3906
|
+
*/
|
|
3907
|
+
applyLayerTransform(tf, layer) {
|
|
3908
|
+
const { ctx, w, h } = this;
|
|
3909
|
+
const unit = this.effSpec.units === "px" ? 1 : Math.min(w, h);
|
|
3910
|
+
const { x: cx, y: cy } = transformOrigin(tf, layer, w, h, unit);
|
|
3911
|
+
ctx.translate(cx + (tf.x ?? 0) * unit, cy + (tf.y ?? 0) * unit);
|
|
3912
|
+
if (tf.rotate) ctx.rotate(tf.rotate * Math.PI / 180);
|
|
3913
|
+
const s = tf.scale ?? 1;
|
|
3914
|
+
ctx.scale(s * (tf.scaleX ?? 1), s);
|
|
3915
|
+
ctx.translate(-cx, -cy);
|
|
3916
|
+
}
|
|
3917
|
+
/** Scene time for a timeline decision: the last painted frame's, else the loop clock's. */
|
|
3918
|
+
sceneNow() {
|
|
3919
|
+
return this.lastRenderT !== Number.NEGATIVE_INFINITY ? this.lastRenderT : this.lastT;
|
|
3920
|
+
}
|
|
3921
|
+
/** Resolve the authored timeline at `t`, layer the live overrides on top, and adopt the result. */
|
|
3922
|
+
resolveAuthored(t) {
|
|
3923
|
+
const expired = [];
|
|
3924
|
+
const spec = applyOverridesAt(resolveTimelineAt(this.authored, t), this.overrides, t, expired);
|
|
3925
|
+
for (const p of expired) this.overrides.delete(p);
|
|
3926
|
+
this.effSpec = spec;
|
|
3927
|
+
const sig = structuralSignature(spec);
|
|
3928
|
+
if (sig !== this.lastStructural) this.rebuild();
|
|
3929
|
+
else this.layers.forEach((b, i) => {
|
|
3930
|
+
b.layer = spec.layers[i] ?? b.layer;
|
|
3931
|
+
});
|
|
3932
|
+
}
|
|
3933
|
+
/** Register live steers on a timeline scene under the steering rule (see `overrides`). */
|
|
3934
|
+
overrideAuthored(deltas, glide) {
|
|
3935
|
+
const now = this.sceneNow();
|
|
3936
|
+
const shown = this.effSpec;
|
|
3937
|
+
const latest = /* @__PURE__ */ new Map();
|
|
3938
|
+
for (const d of deltas) {
|
|
3939
|
+
const path = canonicalSpecPath(shown, d.path);
|
|
3940
|
+
if (!path) continue;
|
|
3941
|
+
latest.delete(path);
|
|
3942
|
+
latest.set(path, d);
|
|
3943
|
+
}
|
|
3944
|
+
const next = new Map(this.overrides);
|
|
3945
|
+
const armed = [];
|
|
3946
|
+
for (const [path, d] of latest) {
|
|
3947
|
+
const id = steerId(d);
|
|
3948
|
+
if (this.seenSteers.get(path) === id) continue;
|
|
3949
|
+
armed.push([path, id]);
|
|
3950
|
+
next.set(path, armOverride(this.authored, path, glide > 0 ? readSpecPath(shown, path) : d.value, d.value, now, glide));
|
|
3951
|
+
}
|
|
3952
|
+
if (armed.length === 0) return true;
|
|
3953
|
+
if (!overridesStayValid(this.authored, next, now)) return false;
|
|
3954
|
+
for (const [p, id] of armed) this.seenSteers.set(p, id);
|
|
3955
|
+
this.overrides = next;
|
|
3956
|
+
return true;
|
|
3957
|
+
}
|
|
3958
|
+
/** A copy of the live overrides — a sequence carries them into a morph's outgoing endpoint. */
|
|
3959
|
+
timelineOverrides() {
|
|
3960
|
+
return this.authored ? new Map(this.overrides) : null;
|
|
3961
|
+
}
|
|
3962
|
+
/** A copy of which server steers this child already applied, per field (see `seenSteers`). */
|
|
3963
|
+
timelineSeen() {
|
|
3964
|
+
return new Map(this.seenSteers);
|
|
3965
|
+
}
|
|
3966
|
+
/** The spec painted by the last frame (timeline and overrides resolved). */
|
|
3967
|
+
presentedSpec() {
|
|
3968
|
+
return this.effSpec;
|
|
3969
|
+
}
|
|
3245
3970
|
/**
|
|
3246
3971
|
* Deterministic, frame-addressable render (shared by the rAF loop).
|
|
3247
3972
|
*
|
|
@@ -3294,6 +4019,11 @@ var SpecInstance = class {
|
|
|
3294
4019
|
* already knows the signature is unchanged.
|
|
3295
4020
|
*/
|
|
3296
4021
|
hotSwapSpec(spec) {
|
|
4022
|
+
this.adoptAuthored(spec);
|
|
4023
|
+
if (this.authored) {
|
|
4024
|
+
this.resolveAuthored(this.sceneNow());
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
3297
4027
|
this.effSpec = spec;
|
|
3298
4028
|
const sig = structuralSignature(this.effSpec);
|
|
3299
4029
|
if (sig !== this.lastStructural) this.rebuild();
|
|
@@ -3301,8 +4031,68 @@ var SpecInstance = class {
|
|
|
3301
4031
|
b.layer = this.effSpec.layers[i] ?? b.layer;
|
|
3302
4032
|
});
|
|
3303
4033
|
}
|
|
4034
|
+
/**
|
|
4035
|
+
* Track whether `spec` carries a timeline. Live overrides belong to the
|
|
4036
|
+
* scene they were made on: a different spec (a new publish, a sequence's
|
|
4037
|
+
* re-steered segment, a morph frame) drops them, as a plain hot-swap
|
|
4038
|
+
* replaces `effSpec` wholesale; the host re-sends the live track.
|
|
4039
|
+
*/
|
|
4040
|
+
adoptAuthored(spec, keepSteers = false) {
|
|
4041
|
+
const next = spec.timeline ? spec : null;
|
|
4042
|
+
if (next !== this.authored && !(keepSteers && next && this.authored)) {
|
|
4043
|
+
this.overrides.clear();
|
|
4044
|
+
this.seenSteers.clear();
|
|
4045
|
+
}
|
|
4046
|
+
this.authored = next;
|
|
4047
|
+
}
|
|
4048
|
+
/**
|
|
4049
|
+
* A sequence's hot swap of a timeline segment: the new scene replaces the
|
|
4050
|
+
* old, but the live steers (and which server steers were already applied)
|
|
4051
|
+
* carry over — a steer the timeline already took back stays taken back —
|
|
4052
|
+
* and only a steer this child has never seen is armed, now. Frame
|
|
4053
|
+
* continuity is kept, so ghosting trails do not restart.
|
|
4054
|
+
*/
|
|
4055
|
+
hotSwapKeepingSteers(spec, retained) {
|
|
4056
|
+
if (!spec.timeline) {
|
|
4057
|
+
this.hotSwapSpec(spec);
|
|
4058
|
+
return;
|
|
4059
|
+
}
|
|
4060
|
+
if (!this.authored) {
|
|
4061
|
+
this.hotSwapSpec(spec);
|
|
4062
|
+
this.applyDeltasNow(retained);
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
4065
|
+
this.adoptAuthored(spec, true);
|
|
4066
|
+
for (const d of retained) this.overrideAuthored([d], 0);
|
|
4067
|
+
this.resolveAuthored(this.sceneNow());
|
|
4068
|
+
}
|
|
4069
|
+
/** Arm a steer made while a sequence was mid-morph, on the outgoing end's override set, at that end's scene time. */
|
|
4070
|
+
static armInto(authored, overrides, seen, deltas, now) {
|
|
4071
|
+
const at = resolveTimelineAt(authored, now);
|
|
4072
|
+
const latest = /* @__PURE__ */ new Map();
|
|
4073
|
+
for (const d of deltas) {
|
|
4074
|
+
const path = canonicalSpecPath(at, d.path);
|
|
4075
|
+
if (!path) continue;
|
|
4076
|
+
latest.delete(path);
|
|
4077
|
+
latest.set(path, d);
|
|
4078
|
+
}
|
|
4079
|
+
for (const [path, d] of latest) {
|
|
4080
|
+
const id = steerId(d);
|
|
4081
|
+
if (seen.get(path) === id) continue;
|
|
4082
|
+
const trial = new Map(overrides).set(path, armOverride(authored, path, d.value, d.value, now, 0));
|
|
4083
|
+
if (overridesStayValid(authored, trial, now)) {
|
|
4084
|
+
overrides.set(path, trial.get(path));
|
|
4085
|
+
seen.set(path, id);
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
3304
4089
|
/** Paint-only hot-swap: skips structuralSignature (caller guarantees match). */
|
|
3305
4090
|
hotSwapPaint(spec) {
|
|
4091
|
+
this.adoptAuthored(spec);
|
|
4092
|
+
if (this.authored) {
|
|
4093
|
+
this.resolveAuthored(this.sceneNow());
|
|
4094
|
+
return;
|
|
4095
|
+
}
|
|
3306
4096
|
this.effSpec = spec;
|
|
3307
4097
|
this.layers.forEach((b, i) => {
|
|
3308
4098
|
b.layer = this.effSpec.layers[i] ?? b.layer;
|
|
@@ -3378,6 +4168,8 @@ var SequenceInstance = class {
|
|
|
3378
4168
|
* window (if any) has passed.
|
|
3379
4169
|
*/
|
|
3380
4170
|
pendingWrapFade = false;
|
|
4171
|
+
/** The morph in progress: its ends, and the override sets each end resolves with (see `morphEndpoint`). */
|
|
4172
|
+
morphEnds = null;
|
|
3381
4173
|
/**
|
|
3382
4174
|
* Every non-`sequence.segment` delta this instance has been handed, last
|
|
3383
4175
|
* wins per path, merged across `applyTrack` calls. Children are created
|
|
@@ -3502,10 +4294,15 @@ var SequenceInstance = class {
|
|
|
3502
4294
|
const tr = this.seq.segments[from]?.transition;
|
|
3503
4295
|
return tr?.type === "morph" ? tr.dur : 0;
|
|
3504
4296
|
}
|
|
3505
|
-
/** Whether the morph out of `from`
|
|
4297
|
+
/** Whether the morph out of `from` paints its text in two passes — `crossfade` or `dip` (default `step`: strings switch at k > 0). */
|
|
3506
4298
|
morphTextCrossfades(from) {
|
|
3507
4299
|
const tr = this.seq.segments[from]?.transition;
|
|
3508
|
-
return tr?.type === "morph" && tr.text === "crossfade";
|
|
4300
|
+
return tr?.type === "morph" && (tr.text === "crossfade" || tr.text === "dip");
|
|
4301
|
+
}
|
|
4302
|
+
/** The morph out of `from`'s two-pass text mode, for `crossfadeOverrides`. */
|
|
4303
|
+
morphTextMode(from) {
|
|
4304
|
+
const tr = this.seq.segments[from]?.transition;
|
|
4305
|
+
return tr?.type === "morph" && tr.text === "dip" ? "dip" : "crossfade";
|
|
3509
4306
|
}
|
|
3510
4307
|
/**
|
|
3511
4308
|
* The paint overrides for one crossfade frame: every text layer whose
|
|
@@ -3514,7 +4311,7 @@ var SequenceInstance = class {
|
|
|
3514
4311
|
* usual) at `1 − k` under the incoming at `k`. Null when nothing differs,
|
|
3515
4312
|
* so a crossfade between same-worded twins is a plain morph frame.
|
|
3516
4313
|
*/
|
|
3517
|
-
crossfadeOverrides(specA, specB, lerped, k) {
|
|
4314
|
+
crossfadeOverrides(specA, specB, lerped, k, mode = "crossfade") {
|
|
3518
4315
|
let map = null;
|
|
3519
4316
|
for (let i = 0; i < lerped.layers.length; i++) {
|
|
3520
4317
|
if (!textStringsDiffer(specA, specB, i)) continue;
|
|
@@ -3522,7 +4319,7 @@ var SequenceInstance = class {
|
|
|
3522
4319
|
const now = lerped.layers[i].sprite;
|
|
3523
4320
|
const outgoing = from.kind === "text" && now.kind === "text" ? { ...now, strings: from.strings } : from.kind === "textBlock" && now.kind === "textBlock" ? { ...now, text: from.text } : now;
|
|
3524
4321
|
if (outgoing === now) continue;
|
|
3525
|
-
(map ??= /* @__PURE__ */ new Map()).set(i, { outgoing, k });
|
|
4322
|
+
(map ??= /* @__PURE__ */ new Map()).set(i, mode === "dip" ? { outgoing, k, mode } : { outgoing, k });
|
|
3526
4323
|
}
|
|
3527
4324
|
return map;
|
|
3528
4325
|
}
|
|
@@ -3595,6 +4392,46 @@ var SequenceInstance = class {
|
|
|
3595
4392
|
this.renderOffscreen(this.fading, outDur + localT, this.childSeed(this.morphChainRoot(from), seed));
|
|
3596
4393
|
return this.fading;
|
|
3597
4394
|
}
|
|
4395
|
+
/**
|
|
4396
|
+
* One end of a morph at segment time `t`. A scene without a timeline is its
|
|
4397
|
+
* steered scene, as always. With a timeline, the retained track follows the
|
|
4398
|
+
* same rule a child applies (held until the path's next key, sticky
|
|
4399
|
+
* otherwise): the outgoing end uses its child's live overrides when the
|
|
4400
|
+
* morph began from one, and otherwise — like the incoming end, whose child
|
|
4401
|
+
* does not exist yet — arms the retained steers at segment time 0, as a
|
|
4402
|
+
* freshly mounted child would.
|
|
4403
|
+
*/
|
|
4404
|
+
morphEndpoint(index, t, side) {
|
|
4405
|
+
const scene = this.childScene(index);
|
|
4406
|
+
if (!scene.timeline) return this.steeredScene(scene);
|
|
4407
|
+
const ends = this.morphEnds;
|
|
4408
|
+
if (ends && side === "out") ends.outT = t;
|
|
4409
|
+
let ov = side === "out" ? ends?.out ?? null : ends?.in ?? null;
|
|
4410
|
+
if (!ov) {
|
|
4411
|
+
ov = this.armRetained(scene, side === "out" ? ends?.seenOut : void 0);
|
|
4412
|
+
if (ends) {
|
|
4413
|
+
if (side === "out") ends.out = ov;
|
|
4414
|
+
else ends.in = ov;
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
return applyOverridesAt(resolveTimelineAt(scene, t), ov, t);
|
|
4418
|
+
}
|
|
4419
|
+
/** The retained steers that resolve (and validate) on a timeline scene, armed at segment time 0. */
|
|
4420
|
+
armRetained(scene, seen) {
|
|
4421
|
+
const out = /* @__PURE__ */ new Map();
|
|
4422
|
+
if (this.retainedDeltas.size === 0) return out;
|
|
4423
|
+
const at0 = resolveTimelineAt(scene, 0);
|
|
4424
|
+
for (const d of this.retainedDeltas.values()) {
|
|
4425
|
+
const path = canonicalSpecPath(at0, d.path);
|
|
4426
|
+
if (!path) continue;
|
|
4427
|
+
const trial = new Map(out).set(path, armOverride(scene, path, d.value, d.value, 0, 0));
|
|
4428
|
+
if (overridesStayValid(scene, trial, 0)) {
|
|
4429
|
+
out.set(path, trial.get(path));
|
|
4430
|
+
seen?.set(path, steerId(d));
|
|
4431
|
+
}
|
|
4432
|
+
}
|
|
4433
|
+
return out;
|
|
4434
|
+
}
|
|
3598
4435
|
/** A segment's scene with the retained track applied — `scene` itself when nothing resolves on it. */
|
|
3599
4436
|
steeredScene(scene) {
|
|
3600
4437
|
return this.retainedDeltas.size === 0 ? scene : applyRetainedDeltas(scene, this.retainedDeltas.values());
|
|
@@ -3613,7 +4450,7 @@ var SequenceInstance = class {
|
|
|
3613
4450
|
if (index < 0 || index >= this.seq.segments.length) index = 0;
|
|
3614
4451
|
let child = this.children[index];
|
|
3615
4452
|
if (!child) {
|
|
3616
|
-
child = new SpecInstance(rootScene ?? this.childScene(index), this.childCtx, { transparent: this.bed !== null, skipInitialPaint, capabilityTier: this.capabilityTier, hostPresents: true });
|
|
4453
|
+
child = new SpecInstance(rootScene ?? this.childScene(index), this.childCtx, { transparent: this.bed !== null, skipInitialPaint: skipInitialPaint || this.bed !== null, capabilityTier: this.capabilityTier, hostPresents: true });
|
|
3617
4454
|
this.children[index] = child;
|
|
3618
4455
|
child.setPaused(true);
|
|
3619
4456
|
if (rootScene) child.hotSwapSpec(this.childScene(index));
|
|
@@ -3668,7 +4505,12 @@ var SequenceInstance = class {
|
|
|
3668
4505
|
frameFinish() {
|
|
3669
4506
|
if (this.seq.finish !== void 0) return this.seq.finish;
|
|
3670
4507
|
if (this.activeIndex < 0) return void 0;
|
|
3671
|
-
|
|
4508
|
+
const scene = this.childScene(this.activeIndex);
|
|
4509
|
+
if (scene.timeline) {
|
|
4510
|
+
const live = (this.fadingIn?.index === this.activeIndex ? this.fadingIn.child : null) ?? this.children[this.activeIndex] ?? this.children[this.morphChainRoot(this.activeIndex)];
|
|
4511
|
+
if (live) return live.presentedSpec().finish;
|
|
4512
|
+
}
|
|
4513
|
+
return this.steeredScene(scene).finish;
|
|
3672
4514
|
}
|
|
3673
4515
|
/**
|
|
3674
4516
|
* The seed `frameFinish()`'s grain resolves against — the sequence's own
|
|
@@ -3693,29 +4535,36 @@ var SequenceInstance = class {
|
|
|
3693
4535
|
const resolved = resolveSegment(this.seq, T + this.clockOffset, { releasedBelow: this.releasedBelow });
|
|
3694
4536
|
const { index, localT } = resolved;
|
|
3695
4537
|
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
4538
|
const lastIdx = this.seq.segments.length - 1;
|
|
3699
4539
|
const wrapped = this.seq.loop && index === 0 && lastIdx > 0 && (T + this.clockOffset >= this.timedTotal() || this.pendingWrapFade);
|
|
4540
|
+
const naturalWrap = this.seq.loop && index === 0 && lastIdx > 0 && T + this.clockOffset >= this.timedTotal();
|
|
4541
|
+
const prevIdx = index > 0 ? index - 1 : -1;
|
|
4542
|
+
const morphFrom = prevIdx >= 0 ? prevIdx : naturalWrap && canWrapMorph(this.seq) ? lastIdx : -1;
|
|
4543
|
+
const morphActive = morphFrom >= 0 && (morphFrom === prevIdx ? this.canMorph(prevIdx) : true) && localT < this.morphDur(morphFrom);
|
|
3700
4544
|
const fadeFrom = prevIdx >= 0 ? prevIdx : wrapped ? lastIdx : -1;
|
|
3701
4545
|
const fadeActive = fadeFrom >= 0 && localT < this.fadeDur(fadeFrom);
|
|
3702
4546
|
if (!fadeActive && this.fading) this.releaseFading();
|
|
3703
4547
|
if (this.pendingWrapFade && (index !== 0 || !fadeActive)) this.pendingWrapFade = false;
|
|
3704
4548
|
if (morphActive) {
|
|
3705
4549
|
const chainRoot = this.morphChainRoot(index);
|
|
3706
|
-
this.morphFromIndex
|
|
4550
|
+
if (this.morphFromIndex !== morphFrom || this.morphEnds?.to !== index) {
|
|
4551
|
+
const outChild = this.children[morphFrom];
|
|
4552
|
+
this.morphEnds = { from: morphFrom, to: index, outT: 0, out: outChild?.timelineOverrides() ?? null, seenOut: outChild?.timelineSeen() ?? /* @__PURE__ */ new Map(), in: null };
|
|
4553
|
+
}
|
|
4554
|
+
this.morphFromIndex = morphFrom;
|
|
3707
4555
|
this.activeIndex = index;
|
|
3708
4556
|
for (let i = 0; i < this.children.length; i++) {
|
|
3709
4557
|
if (i !== chainRoot) this.releaseChild(i);
|
|
3710
4558
|
}
|
|
3711
|
-
const child = this.ensureChild(chainRoot, void 0, this.morphTextCrossfades(
|
|
3712
|
-
const dur = this.morphDur(
|
|
4559
|
+
const child = this.ensureChild(chainRoot, void 0, this.morphTextCrossfades(morphFrom));
|
|
4560
|
+
const dur = this.morphDur(morphFrom);
|
|
3713
4561
|
const k = easeSmooth(localT / dur);
|
|
3714
|
-
const
|
|
3715
|
-
const
|
|
4562
|
+
const outDur = this.seq.segments[morphFrom].duration ?? 0;
|
|
4563
|
+
const specA = this.morphEndpoint(morphFrom, outDur + localT, "out");
|
|
4564
|
+
const specB = this.morphEndpoint(index, localT, "in");
|
|
3716
4565
|
const lerped = lerpSpec(specA, specB, k);
|
|
3717
4566
|
child.hotSwapSpec(lerped);
|
|
3718
|
-
child.setLayerPaintOverrides(this.morphTextCrossfades(
|
|
4567
|
+
child.setLayerPaintOverrides(this.morphTextCrossfades(morphFrom) ? this.crossfadeOverrides(specA, specB, lerped, k, this.morphTextMode(morphFrom)) : null);
|
|
3719
4568
|
child.renderFrame(localT, this.childSeed(chainRoot, seed));
|
|
3720
4569
|
} else {
|
|
3721
4570
|
if (this.morphFromIndex >= 0) {
|
|
@@ -3723,6 +4572,7 @@ var SequenceInstance = class {
|
|
|
3723
4572
|
this.releaseChild(oldRoot);
|
|
3724
4573
|
if (oldRoot !== this.morphFromIndex) this.releaseChild(this.morphFromIndex);
|
|
3725
4574
|
this.morphFromIndex = -1;
|
|
4575
|
+
this.morphEnds = null;
|
|
3726
4576
|
}
|
|
3727
4577
|
if (index !== this.activeIndex) {
|
|
3728
4578
|
for (let i = 0; i < this.children.length; i++) {
|
|
@@ -3802,6 +4652,12 @@ var SequenceInstance = class {
|
|
|
3802
4652
|
const childDeltas = deltas.filter((d) => d.path !== "sequence.segment");
|
|
3803
4653
|
if (childDeltas.length > 0) {
|
|
3804
4654
|
if (this.morphFromIndex >= 0) {
|
|
4655
|
+
const ends = this.morphEnds;
|
|
4656
|
+
if (ends) {
|
|
4657
|
+
const outScene = this.childScene(ends.from);
|
|
4658
|
+
if (ends.out && outScene.timeline) SpecInstance.armInto(outScene, ends.out, ends.seenOut, childDeltas, ends.outT);
|
|
4659
|
+
ends.in = null;
|
|
4660
|
+
}
|
|
3805
4661
|
if (!switchedSegment) this.renderFrame(this.renderedT, this.seed);
|
|
3806
4662
|
} else if (this.activeIndex >= 0) {
|
|
3807
4663
|
const child = this.children[this.activeIndex];
|
|
@@ -3831,11 +4687,19 @@ var SequenceInstance = class {
|
|
|
3831
4687
|
if (!sequenceSwapCompatible(this.seq, next)) return false;
|
|
3832
4688
|
this.seq = next;
|
|
3833
4689
|
if (this.bed && next.bed) this.bed.hotSwapSpec(applyRetainedDeltas(this.bedScene(next.bed), this.retainedBedDeltas.values()));
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
4690
|
+
const swap = (child, index) => {
|
|
4691
|
+
if (!child) return;
|
|
4692
|
+
const scene = this.childScene(index);
|
|
4693
|
+
if (!scene.timeline) {
|
|
4694
|
+
child.hotSwapSpec(this.steeredScene(scene));
|
|
4695
|
+
return;
|
|
4696
|
+
}
|
|
4697
|
+
child.hotSwapKeepingSteers(scene, this.retainedDeltas.values());
|
|
4698
|
+
};
|
|
4699
|
+
for (let i = 0; i < this.children.length; i++) swap(this.children[i], i);
|
|
4700
|
+
if (this.fading) swap(this.fading.child, this.fading.index);
|
|
4701
|
+
if (this.fadingIn) swap(this.fadingIn.child, this.fadingIn.index);
|
|
4702
|
+
this.morphEnds = null;
|
|
3839
4703
|
if (this.paused) this.renderFrame(this.renderedT, this.seed);
|
|
3840
4704
|
return true;
|
|
3841
4705
|
}
|
|
@@ -4234,6 +5098,8 @@ var SPARSE_DECLARED_MAX_COVERAGE = 0.02;
|
|
|
4234
5098
|
var READ_LEGIBILITY_FLOOR = 4.5;
|
|
4235
5099
|
var READ_SAFE_AREA = 0.05;
|
|
4236
5100
|
function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
5101
|
+
const hidden = hiddenLayers(spec);
|
|
5102
|
+
spec = resolveTimelineAt(spec, opts.t ?? COHESION_T);
|
|
4237
5103
|
const warnings = [];
|
|
4238
5104
|
const w = viewport.width;
|
|
4239
5105
|
const h = viewport.height;
|
|
@@ -4258,6 +5124,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
4258
5124
|
let textLayerCount = 0;
|
|
4259
5125
|
let motionLayerCount = 0;
|
|
4260
5126
|
for (let li = 0; li < spec.layers.length; li++) {
|
|
5127
|
+
if (hidden?.[li]) continue;
|
|
4261
5128
|
const layer = spec.layers[li];
|
|
4262
5129
|
const entities = allEntities[li];
|
|
4263
5130
|
totalEntities += entities.length;
|
|
@@ -4324,13 +5191,17 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
4324
5191
|
}
|
|
4325
5192
|
let totalCoverage = 0;
|
|
4326
5193
|
for (let li = 0; li < spec.layers.length; li++) {
|
|
5194
|
+
if (hidden?.[li]) continue;
|
|
4327
5195
|
const layer = spec.layers[li];
|
|
4328
5196
|
const entities = allEntities[li];
|
|
5197
|
+
const paintA = layer.opacity === void 0 && layer.group === void 0 ? 1 : paintOpacity(spec, layer);
|
|
4329
5198
|
for (const e of entities) {
|
|
4330
5199
|
const r = e.size / 2;
|
|
4331
5200
|
let pixArea;
|
|
4332
5201
|
if (layer.sprite.kind === "circle") {
|
|
4333
5202
|
pixArea = Math.PI * r * r;
|
|
5203
|
+
} else if (layer.sprite.kind === "polygon") {
|
|
5204
|
+
pixArea = polygonArea(polygonPoints(layer.sprite, r));
|
|
4334
5205
|
} else if (layer.sprite.kind === "bar") {
|
|
4335
5206
|
pixArea = e.size * barFraction(layer.sprite, e.barIndex ?? 0) * (e.size2 ?? e.size * 0.2);
|
|
4336
5207
|
} else if (layer.sprite.kind === "textBlock") {
|
|
@@ -4348,7 +5219,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
4348
5219
|
const g2 = (ga * ga + ga * gb + gb * gb) / 3;
|
|
4349
5220
|
duty = Math.min(layer.emit.life, layer.emit.every) / layer.emit.every * 0.5 * g2;
|
|
4350
5221
|
}
|
|
4351
|
-
totalCoverage += pixArea * e.alpha * duty / (w * h);
|
|
5222
|
+
totalCoverage += paintA === 1 ? pixArea * e.alpha * duty / (w * h) : pixArea * e.alpha * duty * paintA / (w * h);
|
|
4352
5223
|
}
|
|
4353
5224
|
if (layer.links) {
|
|
4354
5225
|
const positions = entities.map((e) => positionAt(e, 0, w, h));
|
|
@@ -4357,7 +5228,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
4357
5228
|
const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
4358
5229
|
const lwPx = (layer.links.width ?? defaultWidth) * scale;
|
|
4359
5230
|
const la = layer.links.alpha ?? 1;
|
|
4360
|
-
for (const edge of edges) totalCoverage += edge.dist * lwPx * la / (w * h);
|
|
5231
|
+
for (const edge of edges) totalCoverage += paintA === 1 ? edge.dist * lwPx * la / (w * h) : edge.dist * lwPx * la * paintA / (w * h);
|
|
4361
5232
|
}
|
|
4362
5233
|
}
|
|
4363
5234
|
const density = spec.density ?? "normal";
|
|
@@ -4400,6 +5271,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
4400
5271
|
}
|
|
4401
5272
|
}
|
|
4402
5273
|
for (let li = 0; li < spec.layers.length; li++) {
|
|
5274
|
+
if (hidden?.[li]) continue;
|
|
4403
5275
|
const layer = spec.layers[li];
|
|
4404
5276
|
if (!layer.links || layer.links.mode === "chain") continue;
|
|
4405
5277
|
const entities = allEntities[li];
|
|
@@ -4416,6 +5288,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
4416
5288
|
}
|
|
4417
5289
|
}
|
|
4418
5290
|
for (let li = 0; li < spec.layers.length; li++) {
|
|
5291
|
+
if (hidden?.[li]) continue;
|
|
4419
5292
|
const layer = spec.layers[li];
|
|
4420
5293
|
if (layer.motion.type === "static") continue;
|
|
4421
5294
|
const entities = allEntities[li];
|
|
@@ -4432,7 +5305,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
4432
5305
|
});
|
|
4433
5306
|
}
|
|
4434
5307
|
}
|
|
4435
|
-
const composed = spec.layers.map((l, li) => ({ l, ents: allEntities[li] })).filter(({ l }) => !["bounce", "warp", "path"].includes(l.motion.type)).filter(({ l }) => !(l.motion.type === "orbit" && l.motion.center && "layer" in l.motion.center));
|
|
5308
|
+
const composed = spec.layers.map((l, li) => ({ l, li, ents: allEntities[li] })).filter(({ li }) => !hidden?.[li]).filter(({ l }) => !["bounce", "warp", "path"].includes(l.motion.type)).filter(({ l }) => !(l.motion.type === "orbit" && l.motion.center && "layer" in l.motion.center));
|
|
4436
5309
|
const composedCount = composed.reduce((s, c) => s + c.ents.length, 0);
|
|
4437
5310
|
if (composedCount >= 8) {
|
|
4438
5311
|
const positions = composed.flatMap(({ ents }) => ents.map((e) => positionAt(e, 0, w, h)));
|
|
@@ -4450,6 +5323,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
4450
5323
|
}
|
|
4451
5324
|
const textBoxes = [];
|
|
4452
5325
|
for (let li = 0; li < spec.layers.length; li++) {
|
|
5326
|
+
if (hidden?.[li]) continue;
|
|
4453
5327
|
const layer = spec.layers[li];
|
|
4454
5328
|
const s = layer.sprite;
|
|
4455
5329
|
if (layer.motion.type !== "static") continue;
|
|
@@ -4457,8 +5331,11 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
4457
5331
|
const label = layer.key ? `\`${layer.key}\`` : `layers[${li}]`;
|
|
4458
5332
|
for (const e of allEntities[li]) {
|
|
4459
5333
|
const p = positionAt(e, 0, w, h);
|
|
4460
|
-
const
|
|
4461
|
-
const
|
|
5334
|
+
const raw = s.kind === "textBlock" ? textBlockBoxAt(s, p, w, h) : textBoxAt(s, e, p, spec, w, h);
|
|
5335
|
+
const map = paintMap(spec, layer, w, h, scale);
|
|
5336
|
+
const box = map ? { ...raw, ...mapBox(map, raw) } : raw;
|
|
5337
|
+
const baseAlpha = Math.max(0, Math.min(1, e.alpha - e.pulseAmp)) * (s.kind === "textBlock" ? s.opacity ?? 1 : 1);
|
|
5338
|
+
const alpha = layer.opacity === void 0 && layer.group === void 0 ? baseAlpha : baseAlpha * paintOpacity(spec, layer);
|
|
4462
5339
|
textBoxes.push({ li, label, alpha, ...box });
|
|
4463
5340
|
}
|
|
4464
5341
|
}
|
|
@@ -4581,14 +5458,15 @@ function brightestAdditivePlate(spec, allEntities, ground, box, w, h) {
|
|
|
4581
5458
|
if (blend !== "lighter" && blend !== "screen") continue;
|
|
4582
5459
|
if (lj === box.li) continue;
|
|
4583
5460
|
const entities = allEntities[lj];
|
|
5461
|
+
const moved = layer.transform !== void 0 || groupOf(spec, layer)?.transform !== void 0;
|
|
4584
5462
|
for (const e of entities) {
|
|
4585
5463
|
const hex = spriteHex(layer, e);
|
|
4586
5464
|
if (hex === null) continue;
|
|
4587
5465
|
const growScale = (e.motion === "warp" ? WARP_MAX_SCALE : 1) * (e.emit ? Math.max(e.emit.growFrom, e.emit.growTo) : 1);
|
|
4588
5466
|
const maxDim = Math.max(e.size, e.size2 ?? 0) * (1 + e.growAmp) * growScale;
|
|
4589
5467
|
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);
|
|
5468
|
+
if (!moved && !entityReachesBox(e, maxDim / 2, box, w, h)) continue;
|
|
5469
|
+
const a = layer.opacity === void 0 && layer.group === void 0 ? Math.min(1, e.alpha + e.pulseAmp) : Math.min(1, e.alpha + e.pulseAmp) * paintOpacity(spec, layer);
|
|
4592
5470
|
if (a <= 0) continue;
|
|
4593
5471
|
const plate = additivePlate(ground, hexRgb(hex), a, blend);
|
|
4594
5472
|
const lum = relativeLuminance(plate);
|
|
@@ -4636,6 +5514,26 @@ function textBlockBoxAt(s, p, w, h) {
|
|
|
4636
5514
|
const { dx, dy } = textBlockAnchorOffset(s, maxWPx, maxLineW, totalH);
|
|
4637
5515
|
return { x0: x0 + dx, y0: p.y + dy, x1: x0 + dx + maxLineW, y1: p.y + dy + totalH, fs: fsPx };
|
|
4638
5516
|
}
|
|
5517
|
+
function hiddenLayers(spec) {
|
|
5518
|
+
if (!spec.groups && !spec.layers.some((l) => l.opacity !== void 0)) return null;
|
|
5519
|
+
const tracks = spec.timeline ? timelineTracks(spec) : null;
|
|
5520
|
+
const pinnedZero = (path, base, ancestors) => {
|
|
5521
|
+
if (base !== 0) return false;
|
|
5522
|
+
if (!tracks) return true;
|
|
5523
|
+
for (const [p, keys] of tracks) {
|
|
5524
|
+
if (p === path && keys.some((k) => k.value !== 0)) return false;
|
|
5525
|
+
if (ancestors.includes(p)) return false;
|
|
5526
|
+
}
|
|
5527
|
+
return true;
|
|
5528
|
+
};
|
|
5529
|
+
const out = spec.layers.map((l, i) => {
|
|
5530
|
+
const lp = `layers.${i}`;
|
|
5531
|
+
if (pinnedZero(`${lp}.opacity`, l.opacity, ["layers", lp])) return true;
|
|
5532
|
+
const g = l.group !== void 0 && spec.groups && Object.prototype.hasOwnProperty.call(spec.groups, l.group) ? spec.groups[l.group] : void 0;
|
|
5533
|
+
return !!g && pinnedZero(`groups.${l.group}.opacity`, g.opacity, ["groups", `groups.${l.group}`]);
|
|
5534
|
+
});
|
|
5535
|
+
return out.some(Boolean) ? out : null;
|
|
5536
|
+
}
|
|
4639
5537
|
function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
4640
5538
|
const warnings = [];
|
|
4641
5539
|
for (let i = 0; i < seq.segments.length; i++) {
|
|
@@ -4655,8 +5553,8 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
|
4655
5553
|
});
|
|
4656
5554
|
}
|
|
4657
5555
|
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);
|
|
5556
|
+
const lumaA = backgroundLuma(resolveTimelineAt(seq.segments[i].scene, seq.segments[i].duration ?? 0));
|
|
5557
|
+
const lumaB = backgroundLuma(resolveTimelineAt(seq.segments[i + 1].scene, 0));
|
|
4660
5558
|
const delta = Math.abs(lumaA - lumaB);
|
|
4661
5559
|
if (delta > 0.5) {
|
|
4662
5560
|
warnings.push({
|
|
@@ -4675,7 +5573,7 @@ function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
|
4675
5573
|
code: "morph-structural-mismatch",
|
|
4676
5574
|
message: `segments ${i}\u2192${i + 1} differ structurally: morph will fall back to cut`
|
|
4677
5575
|
});
|
|
4678
|
-
} else if (morphNothingMorphable(seq.segments[i].scene, seq.segments[i + 1].scene, { textCrossfade: tr.text === "crossfade" })) {
|
|
5576
|
+
} 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
5577
|
warnings.push({
|
|
4680
5578
|
path: `segments[${i}].transition`,
|
|
4681
5579
|
code: "morph-nothing-morphable",
|
|
@@ -4709,7 +5607,7 @@ function describeScene(spec, options = {}) {
|
|
|
4709
5607
|
let pixArea = 0;
|
|
4710
5608
|
for (const e of entities) {
|
|
4711
5609
|
const r = e.size / 2;
|
|
4712
|
-
pixArea += layer.sprite.kind === "circle" ? Math.PI * r * r : e.size * e.size;
|
|
5610
|
+
pixArea += layer.sprite.kind === "circle" ? Math.PI * r * r : layer.sprite.kind === "polygon" ? polygonArea(polygonPoints(layer.sprite, r)) : e.size * e.size;
|
|
4713
5611
|
}
|
|
4714
5612
|
const coverage = pixArea / (w * h);
|
|
4715
5613
|
const meanAlpha = entities.length > 0 ? entities.reduce((s, e) => s + e.alpha, 0) / entities.length : 0;
|
|
@@ -4785,6 +5683,8 @@ import { createRng as createRng4 } from "@idle-screens/core";
|
|
|
4785
5683
|
var GLOW_SPREAD = 2.4;
|
|
4786
5684
|
var LINE_SALIENCE = 3.5;
|
|
4787
5685
|
var GHOST_TAPS = 12;
|
|
5686
|
+
var POLYGON_OUTLINE_CELLS = 3;
|
|
5687
|
+
var POLYGON_SCANLINES = 4;
|
|
4788
5688
|
function buildScene(spec, opts) {
|
|
4789
5689
|
const { width: w, height: h } = opts.viewport ?? { width: 1920, height: 1080 };
|
|
4790
5690
|
const seed = normalizeSeed(opts.seed ?? spec.seed ?? 42);
|
|
@@ -4799,7 +5699,14 @@ function buildScene(spec, opts) {
|
|
|
4799
5699
|
const layers = spec.layers.map((layer) => ({ layer, entities: buildEntities(layer, rng, w, h, scale, countScale) }));
|
|
4800
5700
|
const byKey = /* @__PURE__ */ new Map();
|
|
4801
5701
|
for (const { layer, entities } of layers) if (layer.key) byKey.set(layer.key, entities);
|
|
4802
|
-
|
|
5702
|
+
let transforms = null;
|
|
5703
|
+
for (const { layer, entities } of layers) {
|
|
5704
|
+
const map = paintMap(spec, layer, w, h, scale);
|
|
5705
|
+
if (!map) continue;
|
|
5706
|
+
transforms ??= /* @__PURE__ */ new Map();
|
|
5707
|
+
for (const e of entities) transforms.set(e, map);
|
|
5708
|
+
}
|
|
5709
|
+
return { w, h, scale, seed, layers, byKey, transforms };
|
|
4803
5710
|
}
|
|
4804
5711
|
function posOf(scene, e, t) {
|
|
4805
5712
|
const p = positionAt(e, t, scene.w, scene.h);
|
|
@@ -4811,7 +5718,8 @@ function posOf(scene, e, t) {
|
|
|
4811
5718
|
p.y += pp.y;
|
|
4812
5719
|
}
|
|
4813
5720
|
}
|
|
4814
|
-
|
|
5721
|
+
const map = scene.transforms?.get(e);
|
|
5722
|
+
return map ? map(p) : p;
|
|
4815
5723
|
}
|
|
4816
5724
|
function textBox(s, e, p) {
|
|
4817
5725
|
const m = s.font ? /(\d+(?:\.\d+)?)px/.exec(s.font) : null;
|
|
@@ -4863,7 +5771,66 @@ function textBlockRevealFraction(s, w, h, t) {
|
|
|
4863
5771
|
}
|
|
4864
5772
|
return rs.progress;
|
|
4865
5773
|
}
|
|
5774
|
+
function polygonCellCoverage(pts, cellW, cellH, c0, c1, r0, r1) {
|
|
5775
|
+
const nc = c1 - c0 + 1;
|
|
5776
|
+
const cov = new Float64Array(nc * (r1 - r0 + 1));
|
|
5777
|
+
const xMin = c0 * cellW;
|
|
5778
|
+
const xMax = (c1 + 1) * cellW;
|
|
5779
|
+
const share = 1 / POLYGON_SCANLINES;
|
|
5780
|
+
const hits = [];
|
|
5781
|
+
const addSpan = (row, xa, xb) => {
|
|
5782
|
+
const a = Math.max(xMin, xa);
|
|
5783
|
+
const b = Math.min(xMax, xb);
|
|
5784
|
+
if (!(b > a)) return;
|
|
5785
|
+
const first = Math.max(c0, Math.floor(a / cellW));
|
|
5786
|
+
const last = Math.min(c1, Math.floor(b / cellW));
|
|
5787
|
+
for (let c = first; c <= last; c++) {
|
|
5788
|
+
const len = Math.min(b, (c + 1) * cellW) - Math.max(a, c * cellW);
|
|
5789
|
+
if (len > 0) cov[row * nc + (c - c0)] += len / cellW * share;
|
|
5790
|
+
}
|
|
5791
|
+
};
|
|
5792
|
+
for (let r = r0; r <= r1; r++) {
|
|
5793
|
+
for (let k = 0; k < POLYGON_SCANLINES; k++) {
|
|
5794
|
+
const y = (r + (k + 0.5) / POLYGON_SCANLINES) * cellH;
|
|
5795
|
+
hits.length = 0;
|
|
5796
|
+
for (let i = 0; i < pts.length; i++) {
|
|
5797
|
+
const p = pts[i];
|
|
5798
|
+
const q = pts[(i + 1) % pts.length];
|
|
5799
|
+
const down = p.y <= y && q.y > y;
|
|
5800
|
+
if (!down && !(q.y <= y && p.y > y)) continue;
|
|
5801
|
+
hits.push({ x: p.x + (y - p.y) / (q.y - p.y) * (q.x - p.x), dir: down ? 1 : -1 });
|
|
5802
|
+
}
|
|
5803
|
+
if (hits.length < 2) continue;
|
|
5804
|
+
hits.sort((m, n) => m.x - n.x);
|
|
5805
|
+
let wind = 0;
|
|
5806
|
+
let start = 0;
|
|
5807
|
+
for (const hit of hits) {
|
|
5808
|
+
const before = wind;
|
|
5809
|
+
wind += hit.dir;
|
|
5810
|
+
if (before === 0 && wind !== 0) start = hit.x;
|
|
5811
|
+
else if (before !== 0 && wind === 0) addSpan(r - r0, start, hit.x);
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
5815
|
+
return cov;
|
|
5816
|
+
}
|
|
5817
|
+
function distanceToOutline(pts, x, y) {
|
|
5818
|
+
let best = Infinity;
|
|
5819
|
+
for (let i = 0; i < pts.length; i++) {
|
|
5820
|
+
const p = pts[i];
|
|
5821
|
+
const q = pts[(i + 1) % pts.length];
|
|
5822
|
+
const ex = q.x - p.x;
|
|
5823
|
+
const ey = q.y - p.y;
|
|
5824
|
+
const len2 = ex * ex + ey * ey;
|
|
5825
|
+
const u = len2 > 0 ? Math.max(0, Math.min(1, ((x - p.x) * ex + (y - p.y) * ey) / len2)) : 0;
|
|
5826
|
+
const dx = x - (p.x + ex * u);
|
|
5827
|
+
const dy = y - (p.y + ey * u);
|
|
5828
|
+
best = Math.min(best, dx * dx + dy * dy);
|
|
5829
|
+
}
|
|
5830
|
+
return Math.sqrt(best);
|
|
5831
|
+
}
|
|
4866
5832
|
function luminanceGrid(spec, opts = {}) {
|
|
5833
|
+
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
4867
5834
|
const scene = buildScene(spec, opts);
|
|
4868
5835
|
const t = opts.t ?? 5e3;
|
|
4869
5836
|
const cols = Math.max(8, Math.min(200, opts.cols ?? 80));
|
|
@@ -4962,9 +5929,66 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
4962
5929
|
}
|
|
4963
5930
|
}
|
|
4964
5931
|
};
|
|
5932
|
+
const splatPolygonOutline = (layer, s, e, p, sz, a, lum, tPass) => {
|
|
5933
|
+
const radius = sz / 2;
|
|
5934
|
+
const rot = rotationAt(e, tPass);
|
|
5935
|
+
const cr = Math.cos(rot);
|
|
5936
|
+
const sr = Math.sin(rot);
|
|
5937
|
+
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 }));
|
|
5938
|
+
const additive = layer.blend === "lighter" || layer.blend === "screen";
|
|
5939
|
+
const glow = !!s.soft && additive;
|
|
5940
|
+
let band = 0;
|
|
5941
|
+
if (glow) {
|
|
5942
|
+
const perimeter = pts.reduce((acc, q, i) => {
|
|
5943
|
+
const n = pts[(i + 1) % pts.length];
|
|
5944
|
+
return acc + Math.hypot(n.x - q.x, n.y - q.y);
|
|
5945
|
+
}, 0);
|
|
5946
|
+
band = perimeter > 0 ? (GLOW_SPREAD - 1) * (2 * polygonArea(pts) / perimeter) : 0;
|
|
5947
|
+
}
|
|
5948
|
+
let minX = Infinity;
|
|
5949
|
+
let maxX = -Infinity;
|
|
5950
|
+
let minY = Infinity;
|
|
5951
|
+
let maxY = -Infinity;
|
|
5952
|
+
for (const q of pts) {
|
|
5953
|
+
minX = Math.min(minX, q.x);
|
|
5954
|
+
maxX = Math.max(maxX, q.x);
|
|
5955
|
+
minY = Math.min(minY, q.y);
|
|
5956
|
+
maxY = Math.max(maxY, q.y);
|
|
5957
|
+
}
|
|
5958
|
+
const c0 = Math.max(0, Math.floor((minX - band) / cellW));
|
|
5959
|
+
const c1 = Math.min(cols - 1, Math.floor((maxX + band) / cellW));
|
|
5960
|
+
const r0 = Math.max(0, Math.floor((minY - band) / cellH));
|
|
5961
|
+
const r1 = Math.min(rows - 1, Math.floor((maxY + band) / cellH));
|
|
5962
|
+
if (c0 > c1 || r0 > r1) return;
|
|
5963
|
+
const cov = polygonCellCoverage(pts, cellW, cellH, c0, c1, r0, r1);
|
|
5964
|
+
const nc = c1 - c0 + 1;
|
|
5965
|
+
for (let r = r0; r <= r1; r++) {
|
|
5966
|
+
for (let c = c0; c <= c1; c++) {
|
|
5967
|
+
const f = cov[(r - r0) * nc + (c - c0)];
|
|
5968
|
+
const x = (c + 0.5) * cellW;
|
|
5969
|
+
const y = (r + 0.5) * cellH;
|
|
5970
|
+
let wgt;
|
|
5971
|
+
if (glow) {
|
|
5972
|
+
const outX = Math.max(0, minX - x, x - maxX);
|
|
5973
|
+
const outY = Math.max(0, minY - y, y - maxY);
|
|
5974
|
+
if (f >= 1) wgt = 1;
|
|
5975
|
+
else if (outX * outX + outY * outY >= band * band) wgt = f;
|
|
5976
|
+
else {
|
|
5977
|
+
const k = band > 0 ? Math.max(0, 1 - distanceToOutline(pts, x, y) / band) : 0;
|
|
5978
|
+
wgt = f + (1 - f) * k * k;
|
|
5979
|
+
}
|
|
5980
|
+
} else if (s.soft) {
|
|
5981
|
+
wgt = f * Math.max(0.1, 1 - Math.hypot(x - p.x, y - p.y) / Math.max(radius, 1e-6));
|
|
5982
|
+
} else {
|
|
5983
|
+
wgt = f;
|
|
5984
|
+
}
|
|
5985
|
+
if (wgt > 0) compose(r * cols + c, lum, a * Math.min(1, wgt), layer.blend);
|
|
5986
|
+
}
|
|
5987
|
+
}
|
|
5988
|
+
};
|
|
4965
5989
|
const splatPass = (tPass, alphaScale) => {
|
|
4966
5990
|
for (const { layer, entities } of scene.layers) {
|
|
4967
|
-
const lifeA = lifeAlphaAt(layer.life, tPass);
|
|
5991
|
+
const lifeA = layer.opacity === void 0 && layer.group === void 0 ? lifeAlphaAt(layer.life, tPass) : lifeAlphaAt(layer.life, tPass) * paintOpacity(spec, layer);
|
|
4968
5992
|
if (lifeA <= 0) continue;
|
|
4969
5993
|
for (const e of entities) {
|
|
4970
5994
|
splatTrail(layer, e, tPass, lifeA, alphaScale);
|
|
@@ -4972,8 +5996,12 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
4972
5996
|
if (a <= 4e-3) continue;
|
|
4973
5997
|
const lum = spriteLuma(layer, e);
|
|
4974
5998
|
const p = posOf(scene, e, tPass);
|
|
4975
|
-
const sz = sizeAt(e, tPass);
|
|
5999
|
+
const sz = layer.transform || layer.group !== void 0 ? sizeAt(e, tPass) * paintSize(spec, layer) : sizeAt(e, tPass);
|
|
4976
6000
|
const s = layer.sprite;
|
|
6001
|
+
if (s.kind === "polygon" && sz >= POLYGON_OUTLINE_CELLS * Math.min(cellW, cellH)) {
|
|
6002
|
+
splatPolygonOutline(layer, s, e, p, sz, a, lum, tPass);
|
|
6003
|
+
continue;
|
|
6004
|
+
}
|
|
4977
6005
|
if (s.kind === "stroke") {
|
|
4978
6006
|
const pts = strokeSamples(s, sz / 2);
|
|
4979
6007
|
let angle = rotationAt(e, tPass);
|
|
@@ -5219,6 +6247,7 @@ function renderDensityMap(grid) {
|
|
|
5219
6247
|
return lines.join("\n");
|
|
5220
6248
|
}
|
|
5221
6249
|
function textSprites(spec, opts = {}) {
|
|
6250
|
+
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
5222
6251
|
const scene = buildScene(spec, opts);
|
|
5223
6252
|
const t = opts.t ?? 5e3;
|
|
5224
6253
|
const out = [];
|
|
@@ -5256,6 +6285,7 @@ function rankDominance(raw) {
|
|
|
5256
6285
|
}));
|
|
5257
6286
|
}
|
|
5258
6287
|
function rawDominance(spec, opts = {}) {
|
|
6288
|
+
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
5259
6289
|
const scene = buildScene(spec, opts);
|
|
5260
6290
|
const t = opts.t ?? 5e3;
|
|
5261
6291
|
const { w, h, scale } = scene;
|
|
@@ -5280,7 +6310,9 @@ function rawDominance(spec, opts = {}) {
|
|
|
5280
6310
|
return Math.sqrt(dx * dx + dy * dy);
|
|
5281
6311
|
};
|
|
5282
6312
|
const raw = scene.layers.map(({ layer, entities }, layerIndex) => {
|
|
5283
|
-
const lifeA = lifeAlphaAt(layer.life, t);
|
|
6313
|
+
const lifeA = layer.opacity === void 0 && layer.group === void 0 ? lifeAlphaAt(layer.life, t) : lifeAlphaAt(layer.life, t) * paintOpacity(spec, layer);
|
|
6314
|
+
const painted = layer.transform !== void 0 || layer.group !== void 0;
|
|
6315
|
+
const areaScale = painted ? paintSize(spec, layer) ** 2 : 1;
|
|
5284
6316
|
let area = 0;
|
|
5285
6317
|
let lumAcc = 0;
|
|
5286
6318
|
for (const e of entities) {
|
|
@@ -5309,7 +6341,7 @@ function rawDominance(spec, opts = {}) {
|
|
|
5309
6341
|
const box = textBlockBox(s, e, { x: 0, y: 0 }, w, h);
|
|
5310
6342
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55 * textBlockRevealFraction(s, w, h, t) * (s.opacity ?? 1);
|
|
5311
6343
|
} else entArea = sz * sz * 0.55;
|
|
5312
|
-
area += entArea * a;
|
|
6344
|
+
area += painted ? entArea * areaScale * a : entArea * a;
|
|
5313
6345
|
if (layer.trail) {
|
|
5314
6346
|
const fade = layer.trail.fade ?? 1;
|
|
5315
6347
|
const span = sweptDistance(e, layer.trail.length);
|
|
@@ -5339,29 +6371,32 @@ function rawDominance(spec, opts = {}) {
|
|
|
5339
6371
|
return raw;
|
|
5340
6372
|
}
|
|
5341
6373
|
function layerCohesion(spec, opts = {}) {
|
|
6374
|
+
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
5342
6375
|
const scene = buildScene(spec, opts);
|
|
5343
6376
|
return cohesionOf(scene.layers, opts.t ?? 5e3, scene.w, scene.h);
|
|
5344
6377
|
}
|
|
5345
6378
|
function motionStats(spec, opts = {}) {
|
|
5346
|
-
const scene = buildScene(spec, opts);
|
|
5347
6379
|
const t = opts.t ?? 5e3;
|
|
5348
6380
|
const dt = opts.dt ?? 500;
|
|
5349
|
-
const
|
|
5350
|
-
|
|
6381
|
+
const scene0 = buildScene(resolveTimelineAt(spec, t), opts);
|
|
6382
|
+
const scene1 = spec.timeline ? buildScene(resolveTimelineAt(spec, t + dt), opts) : scene0;
|
|
6383
|
+
const { w, h } = scene0;
|
|
6384
|
+
return scene0.layers.map(({ layer, entities }, layerIndex) => {
|
|
6385
|
+
const entities1 = scene1.layers[layerIndex]?.entities ?? entities;
|
|
5351
6386
|
let acc = 0;
|
|
5352
6387
|
let max = 0;
|
|
5353
6388
|
let n = 0;
|
|
5354
|
-
|
|
5355
|
-
const p0 = posOf(
|
|
5356
|
-
const p1 = posOf(
|
|
6389
|
+
entities.forEach((e, i) => {
|
|
6390
|
+
const p0 = posOf(scene0, e, t);
|
|
6391
|
+
const p1 = posOf(scene1, entities1[i] ?? e, t + dt);
|
|
5357
6392
|
const dx = p1.x - p0.x;
|
|
5358
6393
|
const dy = p1.y - p0.y;
|
|
5359
|
-
if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2)
|
|
6394
|
+
if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2) return;
|
|
5360
6395
|
const speed = Math.sqrt(dx * dx + dy * dy) / dt * 1e3;
|
|
5361
6396
|
acc += speed;
|
|
5362
6397
|
max = Math.max(max, speed);
|
|
5363
6398
|
n++;
|
|
5364
|
-
}
|
|
6399
|
+
});
|
|
5365
6400
|
const meanSpeed = n ? acc / n : 0;
|
|
5366
6401
|
return { layerIndex, key: layer.key, meanSpeed, maxSpeed: max, moving: meanSpeed > 0.5 };
|
|
5367
6402
|
});
|
|
@@ -5427,6 +6462,7 @@ function diffScenes(a, b, opts = {}) {
|
|
|
5427
6462
|
};
|
|
5428
6463
|
}
|
|
5429
6464
|
function perceiveScene(spec, opts = {}) {
|
|
6465
|
+
spec = resolveTimelineAt(spec, opts.t ?? 5e3);
|
|
5430
6466
|
const grid = luminanceGrid(spec, opts);
|
|
5431
6467
|
return {
|
|
5432
6468
|
t: opts.t ?? 5e3,
|
|
@@ -7922,6 +8958,7 @@ export {
|
|
|
7922
8958
|
COMETS_SPEC,
|
|
7923
8959
|
CONSTELLATION_SPEC,
|
|
7924
8960
|
DASHBOARD_SPEC,
|
|
8961
|
+
DEFAULT_KEY_DUR,
|
|
7925
8962
|
DITHER_ALPHA,
|
|
7926
8963
|
DITHER_TILE,
|
|
7927
8964
|
EXAMPLE_BY_ID,
|
|
@@ -7975,6 +9012,8 @@ export {
|
|
|
7975
9012
|
breakTextBlock,
|
|
7976
9013
|
buildEntities,
|
|
7977
9014
|
canMorph,
|
|
9015
|
+
canWrapMorph,
|
|
9016
|
+
canonicalSpecPath,
|
|
7978
9017
|
cohesionOf,
|
|
7979
9018
|
compileSaver,
|
|
7980
9019
|
compileSequence,
|
|
@@ -8002,6 +9041,7 @@ export {
|
|
|
8002
9041
|
latticeHash,
|
|
8003
9042
|
layerCohesion,
|
|
8004
9043
|
lerpSpec,
|
|
9044
|
+
lerpValue,
|
|
8005
9045
|
lifeAlphaAt,
|
|
8006
9046
|
linkEdges,
|
|
8007
9047
|
linkPairs,
|
|
@@ -8010,16 +9050,19 @@ export {
|
|
|
8010
9050
|
morphChainRoot,
|
|
8011
9051
|
morphNothingMorphable,
|
|
8012
9052
|
motionStats,
|
|
9053
|
+
nextKeyAfter,
|
|
8013
9054
|
normalizeSeed,
|
|
8014
9055
|
perceiveScene,
|
|
8015
9056
|
perceiveSequenceFrame,
|
|
8016
9057
|
pickScreenOp,
|
|
8017
9058
|
positionAt,
|
|
8018
9059
|
presentWithFinish,
|
|
9060
|
+
readSpecPath,
|
|
8019
9061
|
renderBrailleMap,
|
|
8020
9062
|
renderDensityMap,
|
|
8021
9063
|
resolveSegment,
|
|
8022
9064
|
resolveSpecPath,
|
|
9065
|
+
resolveTimelineAt,
|
|
8023
9066
|
revealState,
|
|
8024
9067
|
rgb255Luma,
|
|
8025
9068
|
rotationAt,
|
|
@@ -8041,7 +9084,13 @@ export {
|
|
|
8041
9084
|
textStringsDiffer,
|
|
8042
9085
|
textStringsOf,
|
|
8043
9086
|
textWidthEm,
|
|
9087
|
+
timelineLocalTime,
|
|
9088
|
+
timelineSampleTimes,
|
|
9089
|
+
timelineSampleTimesAfter,
|
|
9090
|
+
timelineTracks,
|
|
8044
9091
|
validateSequence,
|
|
8045
|
-
validateSpec
|
|
9092
|
+
validateSpec,
|
|
9093
|
+
validateSpecPaths,
|
|
9094
|
+
withoutTimeline
|
|
8046
9095
|
};
|
|
8047
9096
|
//# sourceMappingURL=index.js.map
|