@idle-screens/schema 3.4.5 → 3.6.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 +153 -20
- package/dist/index.d.ts +368 -10
- package/dist/index.js +1204 -84
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/saver-spec.schema.json +230 -35
package/dist/index.js
CHANGED
|
@@ -55,7 +55,30 @@ var LIMITS = {
|
|
|
55
55
|
minSegmentDuration: 1e3,
|
|
56
56
|
// ms — flash-safety floor: segment cuts are luminance transitions
|
|
57
57
|
maxTransitionDur: 5e3,
|
|
58
|
-
minTransitionDur: 200
|
|
58
|
+
minTransitionDur: 200,
|
|
59
|
+
minEmitEvery: 1e3,
|
|
60
|
+
// ms — at most one event per second per entity (flash safety)
|
|
61
|
+
maxEmitEvery: 6e5,
|
|
62
|
+
// ms — ten minutes; longer silences than that are `life.enter`
|
|
63
|
+
minEmitLife: 500,
|
|
64
|
+
// ms — an event is a smooth envelope, never a cut
|
|
65
|
+
maxEmitGrow: 8,
|
|
66
|
+
// × base size across an event window
|
|
67
|
+
minClockedPeriod: 1e3,
|
|
68
|
+
// ms — a `clock`ed layer breathes in unison, so 1 Hz not 2
|
|
69
|
+
minClockRate: 0.1,
|
|
70
|
+
maxClockRate: 4,
|
|
71
|
+
defaultListGap: 0.06,
|
|
72
|
+
// viewport units between list/table cells (40 px in px specs)
|
|
73
|
+
defaultListGapPx: 40,
|
|
74
|
+
minPolygonSides: 3,
|
|
75
|
+
maxPolygonSides: 12,
|
|
76
|
+
maxShapePoints: 24,
|
|
77
|
+
// polygon.points / stroke.points
|
|
78
|
+
minEaseTau: 100,
|
|
79
|
+
// ms
|
|
80
|
+
maxEaseTau: 12e4
|
|
81
|
+
// ms
|
|
59
82
|
};
|
|
60
83
|
|
|
61
84
|
// src/steer.ts
|
|
@@ -173,12 +196,14 @@ function structuralSignature(spec) {
|
|
|
173
196
|
l.spin,
|
|
174
197
|
l.grow,
|
|
175
198
|
l.layout,
|
|
199
|
+
l.emit,
|
|
200
|
+
l.clock,
|
|
176
201
|
l.sprite.kind,
|
|
177
202
|
// Dimensional draws baked into entities: radius (circle/ring), length
|
|
178
203
|
// (streak), width+aspect (rect), palette pick (all shaped sprites).
|
|
179
204
|
s.radius,
|
|
180
205
|
s.length,
|
|
181
|
-
l.sprite.kind === "rect" ? [s.width, s.aspect] : void 0,
|
|
206
|
+
l.sprite.kind === "rect" ? [s.width, s.aspect] : l.sprite.kind === "bar" ? [s.length, s.thickness] : void 0,
|
|
182
207
|
Array.isArray(s.colors) ? s.colors.length : void 0,
|
|
183
208
|
s.colorWeights,
|
|
184
209
|
l.sprite.kind === "emoji" ? [l.sprite.glyphs.length, l.sprite.cycle?.period] : l.sprite.kind === "text" ? [l.sprite.strings.length, l.sprite.cycle?.period] : l.sprite.kind === "textBlock" ? [l.sprite.fontSize] : void 0
|
|
@@ -193,7 +218,7 @@ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
193
218
|
var isStr = (v) => typeof v === "string";
|
|
194
219
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
195
220
|
var isRange = (v) => Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && v[0] <= v[1];
|
|
196
|
-
var KNOWN_TOP = /* @__PURE__ */ new Set(["schemaVersion", "id", "label", "seed", "motionIntensity", "units", "referenceViewport", "background", "layers", "ghosting"]);
|
|
221
|
+
var KNOWN_TOP = /* @__PURE__ */ new Set(["schemaVersion", "id", "label", "seed", "motionIntensity", "density", "units", "referenceViewport", "background", "layers", "ghosting"]);
|
|
197
222
|
var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
198
223
|
"count",
|
|
199
224
|
"sprite",
|
|
@@ -212,28 +237,33 @@ var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
|
212
237
|
"trail",
|
|
213
238
|
"links",
|
|
214
239
|
"layout",
|
|
215
|
-
"life"
|
|
240
|
+
"life",
|
|
241
|
+
"emit",
|
|
242
|
+
"clock"
|
|
216
243
|
]);
|
|
217
244
|
var KNOWN_CIRCLE = /* @__PURE__ */ new Set(["kind", "radius", "color", "soft", "colors", "colorWeights"]);
|
|
218
245
|
var KNOWN_RING = /* @__PURE__ */ new Set(["kind", "radius", "color", "width", "colors", "colorWeights"]);
|
|
219
246
|
var KNOWN_STREAK = /* @__PURE__ */ new Set(["kind", "length", "color", "width", "colors", "colorWeights"]);
|
|
220
|
-
var KNOWN_RECT = /* @__PURE__ */ new Set(["kind", "width", "aspect", "color", "colors", "colorWeights"]);
|
|
247
|
+
var KNOWN_RECT = /* @__PURE__ */ new Set(["kind", "width", "aspect", "color", "feather", "colors", "colorWeights"]);
|
|
248
|
+
var KNOWN_BAR = /* @__PURE__ */ new Set(["kind", "values", "length", "thickness", "color", "max", "direction", "colors", "colorWeights"]);
|
|
249
|
+
var KNOWN_POLYGON = /* @__PURE__ */ new Set(["kind", "radius", "color", "sides", "points", "soft", "colors", "colorWeights"]);
|
|
250
|
+
var KNOWN_STROKE = /* @__PURE__ */ new Set(["kind", "length", "points", "color", "width", "curve", "taper", "orient", "colors", "colorWeights"]);
|
|
221
251
|
var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
|
|
222
252
|
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle"]);
|
|
223
253
|
var KNOWN_TEXT_BLOCK = /* @__PURE__ */ new Set(["kind", "text", "maxWidth", "fontSize", "lineHeight", "align", "color", "reveal"]);
|
|
224
|
-
var KNOWN_REVEAL = /* @__PURE__ */ new Set(["progress", "mode", "speed", "caret"]);
|
|
254
|
+
var KNOWN_REVEAL = /* @__PURE__ */ new Set(["progress", "mode", "speed", "caret", "fade"]);
|
|
225
255
|
var KNOWN_REVEAL_CARET = /* @__PURE__ */ new Set(["blink", "color"]);
|
|
226
|
-
var KNOWN_DRIFT = /* @__PURE__ */ new Set(["type", "speed", "angle", "bidirectional", "bob"]);
|
|
227
|
-
var KNOWN_RISE = /* @__PURE__ */ new Set(["type", "speed", "sway"]);
|
|
256
|
+
var KNOWN_DRIFT = /* @__PURE__ */ new Set(["type", "speed", "angle", "bidirectional", "bob", "ease"]);
|
|
257
|
+
var KNOWN_RISE = /* @__PURE__ */ new Set(["type", "speed", "sway", "ease"]);
|
|
228
258
|
var KNOWN_BOUNCE = /* @__PURE__ */ new Set(["type", "speed"]);
|
|
229
259
|
var KNOWN_STATIC = /* @__PURE__ */ new Set(["type"]);
|
|
230
260
|
var KNOWN_ORBIT = /* @__PURE__ */ new Set(["type", "speed", "radius", "center"]);
|
|
231
|
-
var KNOWN_WANDER = /* @__PURE__ */ new Set(["type", "speed", "angle", "meander", "coherence"]);
|
|
261
|
+
var KNOWN_WANDER = /* @__PURE__ */ new Set(["type", "speed", "angle", "meander", "coherence", "ease"]);
|
|
232
262
|
var KNOWN_WARP = /* @__PURE__ */ new Set(["type", "speed", "center"]);
|
|
233
263
|
var KNOWN_PATH = /* @__PURE__ */ new Set(["type", "points", "duration", "curve", "closed", "scatter"]);
|
|
234
264
|
var KNOWN_BG_SOLID = /* @__PURE__ */ new Set(["type", "color"]);
|
|
235
265
|
var KNOWN_BG_GRADIENT = /* @__PURE__ */ new Set(["type", "stops", "band", "drift"]);
|
|
236
|
-
var LAYER_PROPS_ON_SPRITE = /* @__PURE__ */ new Set(["blend", "trail", "alpha", "pulse", "spin", "grow", "region", "links", "flip", "wrap", "key"]);
|
|
266
|
+
var LAYER_PROPS_ON_SPRITE = /* @__PURE__ */ new Set(["blend", "trail", "alpha", "pulse", "spin", "grow", "region", "links", "flip", "wrap", "key", "emit", "clock", "life", "layout"]);
|
|
237
267
|
function unknownKeys(obj, known) {
|
|
238
268
|
return Object.keys(obj).filter((k) => !known.has(k));
|
|
239
269
|
}
|
|
@@ -263,6 +293,9 @@ function validateSpec(spec) {
|
|
|
263
293
|
if (spec.motionIntensity !== void 0 && !["calm", "moderate", "energetic"].includes(spec.motionIntensity)) {
|
|
264
294
|
err("motionIntensity", "must be calm | moderate | energetic");
|
|
265
295
|
}
|
|
296
|
+
if (spec.density !== void 0 && !["sparse", "normal", "dense"].includes(spec.density)) {
|
|
297
|
+
err("density", "must be sparse | normal | dense");
|
|
298
|
+
}
|
|
266
299
|
if (spec.units !== void 0 && spec.units !== "px" && spec.units !== "viewport") {
|
|
267
300
|
err("units", "must be 'px' | 'viewport'");
|
|
268
301
|
}
|
|
@@ -379,7 +412,10 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
379
412
|
} else {
|
|
380
413
|
if (layer.position.x < 0 || layer.position.x > 1) err(`${path}.position.x`, "must be 0..1");
|
|
381
414
|
if (layer.position.y < 0 || layer.position.y > 1) err(`${path}.position.y`, "must be 0..1");
|
|
382
|
-
|
|
415
|
+
const dataLayout = isObj(layer.layout) && (layer.layout.type === "list" || layer.layout.type === "table");
|
|
416
|
+
if (isNum(layer.count) && layer.count !== 1 && !dataLayout) {
|
|
417
|
+
err(`${path}.position`, "position requires count: 1 \u2014 or a layout of type 'list' / 'table', where it anchors the whole block");
|
|
418
|
+
}
|
|
383
419
|
}
|
|
384
420
|
}
|
|
385
421
|
if (layer.region !== void 0) {
|
|
@@ -468,8 +504,37 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
468
504
|
}
|
|
469
505
|
}
|
|
470
506
|
if (layer.layout !== void 0) {
|
|
471
|
-
if (
|
|
472
|
-
|
|
507
|
+
if (isObj(layer.layout) && (layer.layout.type === "list" || layer.layout.type === "table")) {
|
|
508
|
+
const lay = layer.layout;
|
|
509
|
+
if (lay.type === "table" && (!isNum(lay.columns) || !Number.isInteger(lay.columns) || lay.columns < 1 || lay.columns > LIMITS.maxGridColumns)) {
|
|
510
|
+
err(`${path}.layout.columns`, `must be an integer 1..${LIMITS.maxGridColumns}`);
|
|
511
|
+
}
|
|
512
|
+
const gapOk = (v) => isNum(v) && v > 0;
|
|
513
|
+
if (lay.gap !== void 0) {
|
|
514
|
+
if (isNum(lay.gap)) {
|
|
515
|
+
if (!gapOk(lay.gap)) err(`${path}.layout.gap`, "must be > 0 (viewport units of min(w,h), or px)");
|
|
516
|
+
} else if (lay.type === "table" && isObj(lay.gap)) {
|
|
517
|
+
if (lay.gap.x !== void 0 && !gapOk(lay.gap.x)) err(`${path}.layout.gap.x`, "must be > 0");
|
|
518
|
+
if (lay.gap.y !== void 0 && !gapOk(lay.gap.y)) err(`${path}.layout.gap.y`, "must be > 0");
|
|
519
|
+
for (const k of unknownKeys(lay.gap, /* @__PURE__ */ new Set(["x", "y"]))) {
|
|
520
|
+
warn(`${path}.layout.gap.${k}`, "unknown-property", `unknown gap property '${k}' \u2014 will be ignored`);
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
err(`${path}.layout.gap`, lay.type === "table" ? "must be a number > 0 or { x?, y? }" : "must be a number > 0");
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
for (const k of unknownKeys(lay, new Set(lay.type === "list" ? ["type", "gap"] : ["type", "columns", "gap"]))) {
|
|
527
|
+
warn(`${path}.layout.${k}`, "unknown-property", `unknown layout property '${k}' \u2014 will be ignored`);
|
|
528
|
+
}
|
|
529
|
+
const sp = layer.sprite;
|
|
530
|
+
if (isObj(sp) && isNum(layer.count)) {
|
|
531
|
+
const variants = sp.kind === "text" && Array.isArray(sp.strings) ? sp.strings.length : sp.kind === "emoji" && Array.isArray(sp.glyphs) ? sp.glyphs.length : sp.kind === "bar" && Array.isArray(sp.values) ? sp.values.length : null;
|
|
532
|
+
if (variants !== null && variants !== layer.count) {
|
|
533
|
+
warn(`${path}.count`, "list-length-mismatch", `count is ${layer.count} but the sprite carries ${variants} ${sp.kind === "bar" ? "values" : "variants"} \u2014 a ${lay.type} layout reads them in order, so they cycle or go unused`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
} else if (!isObj(layer.layout) || layer.layout.type !== "grid") {
|
|
537
|
+
err(`${path}.layout`, "must be an object with type: 'grid' | 'list' | 'table'");
|
|
473
538
|
} else {
|
|
474
539
|
if (layer.layout.columns !== void 0 && (!isNum(layer.layout.columns) || !Number.isInteger(layer.layout.columns) || layer.layout.columns < 1 || layer.layout.columns > LIMITS.maxGridColumns)) {
|
|
475
540
|
err(`${path}.layout.columns`, `must be an integer 1..${LIMITS.maxGridColumns}`);
|
|
@@ -519,6 +584,64 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
519
584
|
}
|
|
520
585
|
}
|
|
521
586
|
}
|
|
587
|
+
if (layer.emit !== void 0) {
|
|
588
|
+
if (!isObj(layer.emit)) err(`${path}.emit`, "must be an object { every, life, jitter?, grow? }");
|
|
589
|
+
else {
|
|
590
|
+
const em = layer.emit;
|
|
591
|
+
if (!isNum(em.every) || em.every < LIMITS.minEmitEvery || em.every > LIMITS.maxEmitEvery) {
|
|
592
|
+
err(`${path}.emit.every`, `must be ${LIMITS.minEmitEvery}..${LIMITS.maxEmitEvery} ms (at most one event per second per entity)`);
|
|
593
|
+
}
|
|
594
|
+
if (!isNum(em.life) || em.life < LIMITS.minEmitLife) {
|
|
595
|
+
err(`${path}.emit.life`, `must be >= ${LIMITS.minEmitLife} ms (an event is a smooth envelope, never a cut)`);
|
|
596
|
+
} else if (isNum(em.every) && em.life > em.every) {
|
|
597
|
+
err(`${path}.emit.life`, "must be <= emit.every (the visible window cannot outlast its period)");
|
|
598
|
+
}
|
|
599
|
+
if (em.jitter !== void 0 && (!isNum(em.jitter) || em.jitter < 0 || em.jitter > 1)) {
|
|
600
|
+
err(`${path}.emit.jitter`, "must be 0..1 (0 = evenly staggered, 1 = scattered by a fixed sequence)");
|
|
601
|
+
}
|
|
602
|
+
if (em.grow !== void 0) {
|
|
603
|
+
if (!Array.isArray(em.grow) || em.grow.length !== 2 || !isNum(em.grow[0]) || !isNum(em.grow[1])) {
|
|
604
|
+
err(`${path}.emit.grow`, "must be [from, to] size multipliers");
|
|
605
|
+
} else if (em.grow[0] < 0 || em.grow[1] < 0 || em.grow[0] > LIMITS.maxEmitGrow || em.grow[1] > LIMITS.maxEmitGrow) {
|
|
606
|
+
err(`${path}.emit.grow`, `each multiplier must be 0..${LIMITS.maxEmitGrow}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (em.jitter === 0 && isNum(em.every) && isNum(em.life) && isNum(layer.count) && layer.count > 1 && em.life > em.every / layer.count) {
|
|
610
|
+
warn(`${path}.emit.life`, "emit-overlap", `jitter 0 staggers ${layer.count} entities ${(em.every / layer.count).toFixed(0)} ms apart but each stays lit ${em.life} ms \u2014 windows overlap, so more than one event is visible at a time. Shorten life or lengthen every`);
|
|
611
|
+
}
|
|
612
|
+
for (const k of unknownKeys(em, /* @__PURE__ */ new Set(["every", "life", "jitter", "grow"]))) {
|
|
613
|
+
warn(`${path}.emit.${k}`, "unknown-property", `unknown emit property '${k}' \u2014 will be ignored`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (layer.clock !== void 0) {
|
|
618
|
+
if (!isObj(layer.clock)) err(`${path}.clock`, "must be an object { phase?, rate? }");
|
|
619
|
+
else {
|
|
620
|
+
const ck = layer.clock;
|
|
621
|
+
if (ck.phase !== void 0 && (!isNum(ck.phase) || ck.phase < 0 || ck.phase > 1)) {
|
|
622
|
+
err(`${path}.clock.phase`, "must be 0..1 (turns)");
|
|
623
|
+
}
|
|
624
|
+
const rate = ck.rate === void 0 ? 1 : ck.rate;
|
|
625
|
+
if (!isNum(rate) || rate < LIMITS.minClockRate || rate > LIMITS.maxClockRate) {
|
|
626
|
+
err(`${path}.clock.rate`, `must be ${LIMITS.minClockRate}..${LIMITS.maxClockRate}`);
|
|
627
|
+
} else {
|
|
628
|
+
const floor = LIMITS.minClockedPeriod * rate;
|
|
629
|
+
if (isObj(layer.pulse) && isNum(layer.pulse.period) && layer.pulse.period < floor) {
|
|
630
|
+
err(`${path}.pulse.period`, `must be >= ${floor} ms when the layer has a clock (period / rate >= ${LIMITS.minClockedPeriod} \u2014 the whole layer breathes in unison)`);
|
|
631
|
+
}
|
|
632
|
+
if (isObj(layer.grow) && isNum(layer.grow.period) && layer.grow.period < floor) {
|
|
633
|
+
err(`${path}.grow.period`, `must be >= ${floor} ms when the layer has a clock (period / rate >= ${LIMITS.minClockedPeriod})`);
|
|
634
|
+
}
|
|
635
|
+
const sp = layer.sprite;
|
|
636
|
+
if (isObj(sp) && isObj(sp.cycle) && isNum(sp.cycle.period) && sp.cycle.period < floor) {
|
|
637
|
+
err(`${path}.sprite.cycle.period`, `must be >= ${floor} ms when the layer has a clock (period / rate >= ${LIMITS.minClockedPeriod})`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
for (const k of unknownKeys(ck, /* @__PURE__ */ new Set(["phase", "rate"]))) {
|
|
641
|
+
warn(`${path}.clock.${k}`, "unknown-property", `unknown clock property '${k}' \u2014 will be ignored`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
522
645
|
validateSprite(layer.sprite, `${path}.sprite`, err, warn);
|
|
523
646
|
validateMotion(layer.motion, `${path}.motion`, err, warn, spec);
|
|
524
647
|
}
|
|
@@ -571,6 +694,45 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
571
694
|
if (sprite.aspect !== void 0 && (!isRange(sprite.aspect) || sprite.aspect[0] <= 0)) {
|
|
572
695
|
err(`${path}.aspect`, "must be a [min,max] range of positive height/width ratios");
|
|
573
696
|
}
|
|
697
|
+
if (sprite.feather !== void 0 && (!isNum(sprite.feather) || sprite.feather < 0 || sprite.feather > 1)) {
|
|
698
|
+
err(`${path}.feather`, "must be 0..1 (fraction of the half-size that fades out)");
|
|
699
|
+
}
|
|
700
|
+
color(sprite.color, `${path}.color`, err);
|
|
701
|
+
validatePalette(sprite, path, err);
|
|
702
|
+
} else if (sprite.kind === "bar") {
|
|
703
|
+
knownSet = KNOWN_BAR;
|
|
704
|
+
if (!Array.isArray(sprite.values) || sprite.values.length === 0 || sprite.values.length > LIMITS.maxPerLayer || !sprite.values.every((v) => isNum(v) && v >= 0)) {
|
|
705
|
+
err(`${path}.values`, `must be 1..${LIMITS.maxPerLayer} numbers >= 0`);
|
|
706
|
+
}
|
|
707
|
+
if (!isNum(sprite.length) || sprite.length <= 0) err(`${path}.length`, "must be > 0 (full-scale bar length)");
|
|
708
|
+
if (!isNum(sprite.thickness) || sprite.thickness <= 0) err(`${path}.thickness`, "must be > 0");
|
|
709
|
+
if (sprite.max !== void 0 && (!isNum(sprite.max) || sprite.max <= 0)) err(`${path}.max`, "must be > 0");
|
|
710
|
+
if (sprite.direction !== void 0 && !["right", "left", "up", "down"].includes(sprite.direction)) {
|
|
711
|
+
err(`${path}.direction`, "must be 'right' | 'left' | 'up' | 'down'");
|
|
712
|
+
}
|
|
713
|
+
color(sprite.color, `${path}.color`, err);
|
|
714
|
+
validatePalette(sprite, path, err);
|
|
715
|
+
} else if (sprite.kind === "polygon") {
|
|
716
|
+
knownSet = KNOWN_POLYGON;
|
|
717
|
+
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px (circumradius)");
|
|
718
|
+
if (sprite.sides !== void 0 && sprite.points !== void 0) {
|
|
719
|
+
err(`${path}.sides`, "use sides (a regular polygon) or points (a custom one), not both");
|
|
720
|
+
}
|
|
721
|
+
if (sprite.sides !== void 0 && (!isNum(sprite.sides) || !Number.isInteger(sprite.sides) || sprite.sides < LIMITS.minPolygonSides || sprite.sides > LIMITS.maxPolygonSides)) {
|
|
722
|
+
err(`${path}.sides`, `must be an integer ${LIMITS.minPolygonSides}..${LIMITS.maxPolygonSides}`);
|
|
723
|
+
}
|
|
724
|
+
if (sprite.points !== void 0) validateShapePoints(sprite.points, `${path}.points`, 3, err);
|
|
725
|
+
if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
|
|
726
|
+
color(sprite.color, `${path}.color`, err);
|
|
727
|
+
validatePalette(sprite, path, err);
|
|
728
|
+
} else if (sprite.kind === "stroke") {
|
|
729
|
+
knownSet = KNOWN_STROKE;
|
|
730
|
+
if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`, "must be a [min,max] range of positive px (the mark's bounding size)");
|
|
731
|
+
validateShapePoints(sprite.points, `${path}.points`, 2, err);
|
|
732
|
+
if (sprite.width !== void 0 && (!isNum(sprite.width) || sprite.width <= 0)) err(`${path}.width`, "must be > 0");
|
|
733
|
+
if (sprite.curve !== void 0 && sprite.curve !== "smooth" && sprite.curve !== "linear") err(`${path}.curve`, "must be 'smooth' | 'linear'");
|
|
734
|
+
if (sprite.taper !== void 0 && typeof sprite.taper !== "boolean") err(`${path}.taper`, "must be a boolean");
|
|
735
|
+
if (sprite.orient !== void 0 && typeof sprite.orient !== "boolean") err(`${path}.orient`, "must be a boolean");
|
|
574
736
|
color(sprite.color, `${path}.color`, err);
|
|
575
737
|
validatePalette(sprite, path, err);
|
|
576
738
|
} else if (sprite.kind === "textBlock") {
|
|
@@ -604,8 +766,11 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
604
766
|
if (rv.progress !== void 0 && (!isNum(rv.progress) || rv.progress < 0 || rv.progress > 1)) {
|
|
605
767
|
err(`${path}.reveal.progress`, "must be a number between 0 and 1");
|
|
606
768
|
}
|
|
607
|
-
if (rv.mode !== void 0 && !["typewriter", "word", "line"].includes(rv.mode)) {
|
|
608
|
-
err(`${path}.reveal.mode`, "must be 'typewriter' | 'word' | 'line'");
|
|
769
|
+
if (rv.mode !== void 0 && !["typewriter", "word", "line", "glyphFade"].includes(rv.mode)) {
|
|
770
|
+
err(`${path}.reveal.mode`, "must be 'typewriter' | 'word' | 'line' | 'glyphFade'");
|
|
771
|
+
}
|
|
772
|
+
if (rv.fade !== void 0 && (!isNum(rv.fade) || rv.fade <= 0 || rv.fade > 1)) {
|
|
773
|
+
err(`${path}.reveal.fade`, "must be between 0 (exclusive) and 1 (fraction of progress)");
|
|
609
774
|
}
|
|
610
775
|
if (rv.speed !== void 0 && (!isNum(rv.speed) || rv.speed < 0 || rv.speed > LIMITS.maxRevealSpeed)) {
|
|
611
776
|
err(`${path}.reveal.speed`, `must be between 0 and ${LIMITS.maxRevealSpeed} (graphemes/sec)`);
|
|
@@ -627,7 +792,7 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
627
792
|
}
|
|
628
793
|
}
|
|
629
794
|
} else {
|
|
630
|
-
err(`${path}.kind`, "must be emoji | text | circle | ring | streak | rect | textBlock");
|
|
795
|
+
err(`${path}.kind`, "must be emoji | text | circle | ring | streak | rect | bar | polygon | stroke | textBlock");
|
|
631
796
|
return;
|
|
632
797
|
}
|
|
633
798
|
for (const k of unknownKeys(sprite, knownSet)) {
|
|
@@ -680,6 +845,7 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
680
845
|
if (motion.angle !== void 0 && !isNum(motion.angle)) err(`${path}.angle`, "must be a number (degrees)");
|
|
681
846
|
if (motion.bidirectional !== void 0 && typeof motion.bidirectional !== "boolean") err(`${path}.bidirectional`, "must be a boolean");
|
|
682
847
|
if (motion.bob !== void 0 && !isNum(motion.bob)) err(`${path}.bob`, "must be a number");
|
|
848
|
+
validateEase(motion.ease, `${path}.ease`, err, warn);
|
|
683
849
|
if (isRange(motion.speed) && motion.speed[1] < 1 && !isViewport) {
|
|
684
850
|
warn(`${path}.speed`, "near-zero-speed", `max speed is ${motion.speed[1]} px/sec \u2014 entities will appear frozen. Typical range: 10\u2013200 px/sec`);
|
|
685
851
|
}
|
|
@@ -687,6 +853,7 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
687
853
|
knownSet = KNOWN_RISE;
|
|
688
854
|
speedOk(motion.speed, `${path}.speed`);
|
|
689
855
|
if (motion.sway !== void 0 && !isNum(motion.sway)) err(`${path}.sway`, "must be a number");
|
|
856
|
+
validateEase(motion.ease, `${path}.ease`, err, warn);
|
|
690
857
|
if (isRange(motion.speed) && motion.speed[1] < 1 && !isViewport) {
|
|
691
858
|
warn(`${path}.speed`, "near-zero-speed", `max speed is ${motion.speed[1]} px/sec \u2014 entities will appear frozen. Typical range: 5\u201380 px/sec`);
|
|
692
859
|
}
|
|
@@ -736,6 +903,7 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
736
903
|
}
|
|
737
904
|
} else if (motion.type === "wander") {
|
|
738
905
|
knownSet = KNOWN_WANDER;
|
|
906
|
+
validateEase(motion.ease, `${path}.ease`, err, warn);
|
|
739
907
|
speedOk(motion.speed, `${path}.speed`);
|
|
740
908
|
if (motion.angle !== void 0 && !isNum(motion.angle)) err(`${path}.angle`, "must be a number (degrees)");
|
|
741
909
|
const meanderCap = isViewport ? LIMITS.maxMeander / refVp : LIMITS.maxMeander;
|
|
@@ -787,6 +955,28 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
787
955
|
warn(`${path}.${k}`, "unknown-property", `unknown motion property '${k}' \u2014 will be ignored`);
|
|
788
956
|
}
|
|
789
957
|
}
|
|
958
|
+
function validateShapePoints(points, path, min, err) {
|
|
959
|
+
if (!Array.isArray(points) || points.length < min || points.length > LIMITS.maxShapePoints) {
|
|
960
|
+
return err(path, `must be ${min}..${LIMITS.maxShapePoints} [x, y] pairs in unit coordinates (-1..1)`);
|
|
961
|
+
}
|
|
962
|
+
for (let i = 0; i < points.length; i++) {
|
|
963
|
+
const pt = points[i];
|
|
964
|
+
if (!Array.isArray(pt) || pt.length !== 2 || !isNum(pt[0]) || !isNum(pt[1]) || Math.abs(pt[0]) > 1 || Math.abs(pt[1]) > 1) {
|
|
965
|
+
err(`${path}[${i}]`, "must be an [x, y] pair with each coordinate in -1..1");
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
function validateEase(ease, path, err, warn) {
|
|
970
|
+
if (ease === void 0) return;
|
|
971
|
+
if (!isObj(ease)) return err(path, "must be an object { type: 'settle' | 'buoyant', tau }");
|
|
972
|
+
if (ease.type !== "settle" && ease.type !== "buoyant") err(`${path}.type`, "must be 'settle' | 'buoyant'");
|
|
973
|
+
if (!isNum(ease.tau) || ease.tau < LIMITS.minEaseTau || ease.tau > LIMITS.maxEaseTau) {
|
|
974
|
+
err(`${path}.tau`, `must be ${LIMITS.minEaseTau}..${LIMITS.maxEaseTau} ms`);
|
|
975
|
+
}
|
|
976
|
+
for (const k of unknownKeys(ease, /* @__PURE__ */ new Set(["type", "tau"]))) {
|
|
977
|
+
warn(`${path}.${k}`, "unknown-property", `unknown ease property '${k}' \u2014 will be ignored`);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
790
980
|
function assertValidSpec(spec) {
|
|
791
981
|
const r = validateSpec(spec);
|
|
792
982
|
if (!r.valid) {
|
|
@@ -904,6 +1094,110 @@ ${r.errors.map((e) => ` ${e.path || "<root>"}: ${e.message}`).join("\n")}`);
|
|
|
904
1094
|
return seq;
|
|
905
1095
|
}
|
|
906
1096
|
|
|
1097
|
+
// src/shapes.ts
|
|
1098
|
+
function isShapedSprite(s) {
|
|
1099
|
+
return s.kind === "circle" || s.kind === "ring" || s.kind === "streak" || s.kind === "rect" || s.kind === "polygon" || s.kind === "stroke" || s.kind === "bar";
|
|
1100
|
+
}
|
|
1101
|
+
function barFraction(s, index) {
|
|
1102
|
+
const n = s.values.length;
|
|
1103
|
+
if (n === 0) return 0;
|
|
1104
|
+
const v = s.values[(index % n + n) % n] ?? 0;
|
|
1105
|
+
const max = s.max ?? s.values.reduce((m, x) => Math.max(m, x), 0);
|
|
1106
|
+
if (!(max > 0)) return 0;
|
|
1107
|
+
return Math.max(0, Math.min(1, v / max));
|
|
1108
|
+
}
|
|
1109
|
+
function barBox(direction, len, thick) {
|
|
1110
|
+
switch (direction) {
|
|
1111
|
+
case "left":
|
|
1112
|
+
return { cx: -len / 2, cy: 0, halfX: len / 2, halfY: thick / 2 };
|
|
1113
|
+
case "up":
|
|
1114
|
+
return { cx: 0, cy: -len / 2, halfX: thick / 2, halfY: len / 2 };
|
|
1115
|
+
case "down":
|
|
1116
|
+
return { cx: 0, cy: len / 2, halfX: thick / 2, halfY: len / 2 };
|
|
1117
|
+
default:
|
|
1118
|
+
return { cx: len / 2, cy: 0, halfX: len / 2, halfY: thick / 2 };
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
var DEFAULT_POLYGON_SIDES = 6;
|
|
1122
|
+
var STROKE_SAMPLES = 24;
|
|
1123
|
+
function polygonPoints(s, radius) {
|
|
1124
|
+
if (s.points && s.points.length >= 3) {
|
|
1125
|
+
return s.points.map(([x, y]) => ({ x: x * radius, y: y * radius }));
|
|
1126
|
+
}
|
|
1127
|
+
const n = Math.max(3, Math.round(s.sides ?? DEFAULT_POLYGON_SIDES));
|
|
1128
|
+
const out = [];
|
|
1129
|
+
for (let k = 0; k < n; k++) {
|
|
1130
|
+
const a = -Math.PI / 2 + 2 * Math.PI * k / n;
|
|
1131
|
+
out.push({ x: Math.cos(a) * radius, y: Math.sin(a) * radius });
|
|
1132
|
+
}
|
|
1133
|
+
return out;
|
|
1134
|
+
}
|
|
1135
|
+
function polygonArea(pts) {
|
|
1136
|
+
let a = 0;
|
|
1137
|
+
for (let i = 0; i < pts.length; i++) {
|
|
1138
|
+
const p = pts[i];
|
|
1139
|
+
const q = pts[(i + 1) % pts.length];
|
|
1140
|
+
a += p.x * q.y - q.x * p.y;
|
|
1141
|
+
}
|
|
1142
|
+
return Math.abs(a) / 2;
|
|
1143
|
+
}
|
|
1144
|
+
function polygonFill(s, radius) {
|
|
1145
|
+
if (radius <= 0) return 0;
|
|
1146
|
+
const disc = Math.PI * radius * radius;
|
|
1147
|
+
return Math.min(1, polygonArea(polygonPoints(s, radius)) / disc);
|
|
1148
|
+
}
|
|
1149
|
+
function catmullRom(p0, p1, p2, p3, u) {
|
|
1150
|
+
const u2 = u * u;
|
|
1151
|
+
const u3 = u2 * u;
|
|
1152
|
+
return 0.5 * (2 * p1 + (-p0 + p2) * u + (2 * p0 - 5 * p1 + 4 * p2 - p3) * u2 + (-p0 + 3 * p1 - 3 * p2 + p3) * u3);
|
|
1153
|
+
}
|
|
1154
|
+
function strokeSamples(s, halfSize, n) {
|
|
1155
|
+
const pts = s.points.map(([x, y]) => ({ x: x * halfSize, y: y * halfSize }));
|
|
1156
|
+
const m = pts.length;
|
|
1157
|
+
if (m < 2) return pts;
|
|
1158
|
+
const smooth = s.curve !== "linear" && m >= 3;
|
|
1159
|
+
const segs = m - 1;
|
|
1160
|
+
n = n ?? Math.max(STROKE_SAMPLES, segs * 6 + 1);
|
|
1161
|
+
const at = (k) => pts[Math.max(0, Math.min(m - 1, k))];
|
|
1162
|
+
const out = [];
|
|
1163
|
+
for (let i = 0; i < n; i++) {
|
|
1164
|
+
const u = i / (n - 1) * segs;
|
|
1165
|
+
const seg = Math.min(segs - 1, Math.floor(u));
|
|
1166
|
+
const local = u - seg;
|
|
1167
|
+
const p1 = at(seg);
|
|
1168
|
+
const p2 = at(seg + 1);
|
|
1169
|
+
if (!smooth) {
|
|
1170
|
+
out.push({ x: p1.x + (p2.x - p1.x) * local, y: p1.y + (p2.y - p1.y) * local });
|
|
1171
|
+
} else {
|
|
1172
|
+
const p0 = at(seg - 1);
|
|
1173
|
+
const p3 = at(seg + 2);
|
|
1174
|
+
out.push({ x: catmullRom(p0.x, p1.x, p2.x, p3.x, local), y: catmullRom(p0.y, p1.y, p2.y, p3.y, local) });
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return out;
|
|
1178
|
+
}
|
|
1179
|
+
function pathLength(pts) {
|
|
1180
|
+
let l = 0;
|
|
1181
|
+
for (let i = 1; i < pts.length; i++) {
|
|
1182
|
+
const dx = pts[i].x - pts[i - 1].x;
|
|
1183
|
+
const dy = pts[i].y - pts[i - 1].y;
|
|
1184
|
+
l += Math.sqrt(dx * dx + dy * dy);
|
|
1185
|
+
}
|
|
1186
|
+
return l;
|
|
1187
|
+
}
|
|
1188
|
+
function strokeTaper(u) {
|
|
1189
|
+
return Math.max(0.15, Math.sin(Math.PI * Math.max(0, Math.min(1, u))));
|
|
1190
|
+
}
|
|
1191
|
+
function strokeWidthPx(s, scale) {
|
|
1192
|
+
return Math.max(0.5, (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale);
|
|
1193
|
+
}
|
|
1194
|
+
function featherAlphas(steps, additive) {
|
|
1195
|
+
const out = [];
|
|
1196
|
+
for (let k = 1; k <= steps; k++) out.push(additive ? 1 / steps : 1 / (steps - k + 1));
|
|
1197
|
+
return out;
|
|
1198
|
+
}
|
|
1199
|
+
var FEATHER_STEPS = 6;
|
|
1200
|
+
|
|
907
1201
|
// src/simulate.ts
|
|
908
1202
|
var WARP_NEAR = 0.08;
|
|
909
1203
|
var WARP_MAX_SCALE = 8;
|
|
@@ -932,6 +1226,21 @@ function drawOsc(rng, amp) {
|
|
|
932
1226
|
}
|
|
933
1227
|
return osc;
|
|
934
1228
|
}
|
|
1229
|
+
var GOLDEN = 0.6180339887498949;
|
|
1230
|
+
function emitParams(emit, i, n) {
|
|
1231
|
+
const every = emit.every;
|
|
1232
|
+
const jitter = Math.max(0, Math.min(1, emit.jitter ?? 1));
|
|
1233
|
+
const scattered = (i + 0.5) * GOLDEN % 1 * every;
|
|
1234
|
+
const even = i / Math.max(1, n) * every;
|
|
1235
|
+
const phase = ((even * (1 - jitter) + scattered * jitter) % every + every) % every;
|
|
1236
|
+
return {
|
|
1237
|
+
every,
|
|
1238
|
+
life: Math.min(emit.life, every),
|
|
1239
|
+
phase,
|
|
1240
|
+
growFrom: emit.grow?.[0] ?? 1,
|
|
1241
|
+
growTo: emit.grow?.[1] ?? 1
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
935
1244
|
function weightedIndex(u, weights) {
|
|
936
1245
|
const total = weights.reduce((a, b) => a + b, 0);
|
|
937
1246
|
let acc = 0;
|
|
@@ -942,15 +1251,15 @@ function weightedIndex(u, weights) {
|
|
|
942
1251
|
return weights.length - 1;
|
|
943
1252
|
}
|
|
944
1253
|
function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
945
|
-
const effectiveCount = countScale === 1 || layer.layout
|
|
1254
|
+
const effectiveCount = countScale === 1 || layer.layout !== void 0 ? layer.count : Math.max(1, Math.min(
|
|
946
1255
|
Math.round(layer.count * countScale),
|
|
947
1256
|
layer.links ? LIMITS.maxLinkLayerCount : LIMITS.maxPerLayer
|
|
948
1257
|
));
|
|
949
1258
|
const [smin, smax] = layer.size ?? [20, 40];
|
|
950
1259
|
const variants = spriteVariants(layer.sprite);
|
|
951
1260
|
const sprite = layer.sprite;
|
|
952
|
-
const spriteColors = sprite
|
|
953
|
-
const colorWeights = sprite
|
|
1261
|
+
const spriteColors = isShapedSprite(sprite) ? sprite.colors : void 0;
|
|
1262
|
+
const colorWeights = isShapedSprite(sprite) ? sprite.colorWeights : void 0;
|
|
954
1263
|
const colorsLen = spriteColors?.length ?? 0;
|
|
955
1264
|
const cycle = sprite.kind === "emoji" || sprite.kind === "text" ? sprite.cycle : void 0;
|
|
956
1265
|
const mSpec = layer.motion;
|
|
@@ -964,7 +1273,32 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
964
1273
|
if (mSpec.type === "path") {
|
|
965
1274
|
pathPts = mSpec.points.map((p) => ({ x: p.x * w, y: p.y * h }));
|
|
966
1275
|
}
|
|
1276
|
+
const ease = mSpec.type === "drift" || mSpec.type === "rise" || mSpec.type === "wander" ? mSpec.ease : void 0;
|
|
1277
|
+
const clockPhase = layer.clock ? (layer.clock.phase ?? 0) % 1 * Math.PI * 2 : 0;
|
|
967
1278
|
const layout = layer.layout;
|
|
1279
|
+
const ordered = layout?.type === "list" || layout?.type === "table";
|
|
1280
|
+
let gapX = 0;
|
|
1281
|
+
let gapY = 0;
|
|
1282
|
+
let listX0 = 0;
|
|
1283
|
+
let listY0 = 0;
|
|
1284
|
+
let listCols = 1;
|
|
1285
|
+
if (layout?.type === "list" || layout?.type === "table") {
|
|
1286
|
+
const dflt = scale === 1 ? LIMITS.defaultListGapPx : LIMITS.defaultListGap;
|
|
1287
|
+
const g = layout.gap ?? dflt;
|
|
1288
|
+
gapX = (typeof g === "number" ? g : g.x ?? dflt) * scale;
|
|
1289
|
+
gapY = (typeof g === "number" ? g : g.y ?? dflt) * scale;
|
|
1290
|
+
listCols = layout.type === "list" ? 1 : Math.max(1, Math.round(layout.columns));
|
|
1291
|
+
const rows = Math.max(1, Math.ceil(effectiveCount / listCols));
|
|
1292
|
+
if (layer.position) {
|
|
1293
|
+
listX0 = layer.position.x * w;
|
|
1294
|
+
listY0 = layer.position.y * h;
|
|
1295
|
+
} else {
|
|
1296
|
+
const [rx0, rx1] = layer.region?.x ?? [0, 1];
|
|
1297
|
+
const [ry0, ry1] = layer.region?.y ?? [0, 1];
|
|
1298
|
+
listX0 = (rx0 + rx1) / 2 * w - (listCols - 1) * gapX / 2;
|
|
1299
|
+
listY0 = (ry0 + ry1) / 2 * h - (rows - 1) * gapY / 2;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
968
1302
|
let gridCols = 0;
|
|
969
1303
|
let cellW = 0;
|
|
970
1304
|
let cellH = 0;
|
|
@@ -987,8 +1321,8 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
987
1321
|
}
|
|
988
1322
|
const out = [];
|
|
989
1323
|
for (let i = 0; i < effectiveCount; i++) {
|
|
990
|
-
const size = sprite.kind === "textBlock" ? sprite.fontSize * scale : sprite.kind === "circle" || sprite.kind === "ring" ? rng.range(sprite.radius[0], sprite.radius[1]) * 2 * scale : sprite.kind === "streak" ? rng.range(sprite.length[0], sprite.length[1]) * scale : sprite.kind === "rect" ? rng.range(sprite.width[0], sprite.width[1]) * scale : rng.range(smin, smax) * scale;
|
|
991
|
-
const size2 = sprite.kind === "rect" ? size * (sprite.aspect ? rng.range(sprite.aspect[0], sprite.aspect[1]) : 1) : void 0;
|
|
1324
|
+
const size = sprite.kind === "textBlock" ? sprite.fontSize * scale : sprite.kind === "circle" || sprite.kind === "ring" || sprite.kind === "polygon" ? rng.range(sprite.radius[0], sprite.radius[1]) * 2 * scale : sprite.kind === "streak" || sprite.kind === "stroke" ? rng.range(sprite.length[0], sprite.length[1]) * scale : sprite.kind === "rect" ? rng.range(sprite.width[0], sprite.width[1]) * scale : sprite.kind === "bar" ? sprite.length * scale : rng.range(smin, smax) * scale;
|
|
1325
|
+
const size2 = sprite.kind === "rect" ? size * (sprite.aspect ? rng.range(sprite.aspect[0], sprite.aspect[1]) : 1) : sprite.kind === "bar" ? sprite.thickness * scale : void 0;
|
|
992
1326
|
let vx = 0;
|
|
993
1327
|
let vy = 0;
|
|
994
1328
|
let bob = 0;
|
|
@@ -1052,6 +1386,11 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1052
1386
|
if (layer.position && layer.count === 1) {
|
|
1053
1387
|
x0 = layer.position.x * w;
|
|
1054
1388
|
y0 = layer.position.y * h;
|
|
1389
|
+
} else if (layout?.type === "list" || layout?.type === "table") {
|
|
1390
|
+
rng.next();
|
|
1391
|
+
rng.next();
|
|
1392
|
+
x0 = listX0 + i % listCols * gapX;
|
|
1393
|
+
y0 = listY0 + Math.floor(i / listCols) * gapY;
|
|
1055
1394
|
} else if (layout?.type === "grid") {
|
|
1056
1395
|
const col = i % gridCols;
|
|
1057
1396
|
const row = Math.floor(i / gridCols);
|
|
@@ -1071,7 +1410,10 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1071
1410
|
x0,
|
|
1072
1411
|
y0,
|
|
1073
1412
|
size,
|
|
1074
|
-
|
|
1413
|
+
// Data layouts read variants in order — strings[i] beside bar i. The
|
|
1414
|
+
// seeded draw is still consumed so a layout is placement only: toggling
|
|
1415
|
+
// list/table never disturbs the layer's alpha, pulse or spin stream.
|
|
1416
|
+
spriteIndex: variants > 1 ? ordered ? (rng.int(0, variants - 1), i % variants) : rng.int(0, variants - 1) : 0,
|
|
1075
1417
|
phase: rng.range(0, Math.PI * 2),
|
|
1076
1418
|
vx,
|
|
1077
1419
|
vy,
|
|
@@ -1095,10 +1437,12 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1095
1437
|
orbitCx: m.type === "orbit" && !orbitParent ? (m.center?.x ?? 0.5) * w : 0,
|
|
1096
1438
|
orbitCy: m.type === "orbit" && !orbitParent ? (m.center?.y ?? 0.5) * h : 0,
|
|
1097
1439
|
// New draws AFTER orbit — guarded so existing specs keep identical streams.
|
|
1098
|
-
colorIndex: colorsLen > 0 ? colorWeights && colorWeights.length === colorsLen ? weightedIndex(rng.next(), colorWeights) : rng.int(0, colorsLen - 1) : -1,
|
|
1440
|
+
colorIndex: colorsLen > 0 ? colorWeights && colorWeights.length === colorsLen ? weightedIndex(rng.next(), colorWeights) : ordered ? (rng.int(0, colorsLen - 1), i % colorsLen) : rng.int(0, colorsLen - 1) : -1,
|
|
1099
1441
|
cyclePeriod: cycle ? cycle.period : 0,
|
|
1100
1442
|
// Optional feature fields last (guarded draws inside — wander draws 18 here).
|
|
1101
1443
|
...size2 !== void 0 ? { size2 } : {},
|
|
1444
|
+
...sprite.kind === "bar" ? { barIndex: i } : {},
|
|
1445
|
+
...ordered ? { ordered: true } : {},
|
|
1102
1446
|
...orbitParent ? { orbitParent } : {},
|
|
1103
1447
|
...m.type === "wander" ? {
|
|
1104
1448
|
wander: {
|
|
@@ -1119,33 +1463,80 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1119
1463
|
offX: pathOffX,
|
|
1120
1464
|
offY: pathOffY
|
|
1121
1465
|
}
|
|
1122
|
-
} : {}
|
|
1466
|
+
} : {},
|
|
1467
|
+
// Time-structure fields (2026-09): ease draws nothing; emit's one draw
|
|
1468
|
+
// per entity happens in a pass AFTER this loop (see below).
|
|
1469
|
+
...ease ? { ease: { type: ease.type, tau: ease.tau } } : {}
|
|
1123
1470
|
});
|
|
1471
|
+
if (layer.clock) {
|
|
1472
|
+
const ent = out[out.length - 1];
|
|
1473
|
+
ent.pulsePhase = clockPhase;
|
|
1474
|
+
ent.growPhase = clockPhase;
|
|
1475
|
+
ent.cyclePhase = clockPhase;
|
|
1476
|
+
ent.clockRate = layer.clock.rate ?? 1;
|
|
1477
|
+
}
|
|
1124
1478
|
if (layer.pulse?.wave) {
|
|
1125
1479
|
const wl = Math.max(1e-6, layer.pulse.wave.wavelength * scale);
|
|
1126
1480
|
const ang = (layer.pulse.wave.angle ?? 0) * Math.PI / 180;
|
|
1127
|
-
out[out.length - 1].pulsePhase = -(
|
|
1481
|
+
out[out.length - 1].pulsePhase = clockPhase - (x0 * Math.cos(ang) + y0 * Math.sin(ang)) / wl * Math.PI * 2;
|
|
1128
1482
|
}
|
|
1129
1483
|
}
|
|
1484
|
+
if (layer.emit) {
|
|
1485
|
+
for (let i = 0; i < out.length; i++) out[i].emit = emitParams(layer.emit, i, out.length);
|
|
1486
|
+
}
|
|
1130
1487
|
return out;
|
|
1131
1488
|
}
|
|
1132
1489
|
function warpZ(e, t) {
|
|
1133
1490
|
const wp = e.warp;
|
|
1134
1491
|
return wrap(wp.z0 - wp.vz * (t / 1e3), WARP_NEAR, 1);
|
|
1135
1492
|
}
|
|
1493
|
+
function smoothstep(x) {
|
|
1494
|
+
const c = x < 0 ? 0 : x > 1 ? 1 : x;
|
|
1495
|
+
return c * c * (3 - 2 * c);
|
|
1496
|
+
}
|
|
1497
|
+
var EMIT_ATTACK = 0.25;
|
|
1498
|
+
function emitWindow(e, t) {
|
|
1499
|
+
const em = e.emit;
|
|
1500
|
+
if (!em) return null;
|
|
1501
|
+
const local = ((t - em.phase) % em.every + em.every) % em.every;
|
|
1502
|
+
return local < em.life ? local / em.life : null;
|
|
1503
|
+
}
|
|
1504
|
+
function emitEnvelope(u) {
|
|
1505
|
+
if (u < 0 || u >= 1) return 0;
|
|
1506
|
+
return u < EMIT_ATTACK ? smoothstep(u / EMIT_ATTACK) : 1 - smoothstep((u - EMIT_ATTACK) / (1 - EMIT_ATTACK));
|
|
1507
|
+
}
|
|
1508
|
+
function clockTime(e, t) {
|
|
1509
|
+
return e.clockRate ? t * e.clockRate : t;
|
|
1510
|
+
}
|
|
1511
|
+
function motionSeconds(e, t) {
|
|
1512
|
+
if (!e.ease) return t / 1e3;
|
|
1513
|
+
let tl = t;
|
|
1514
|
+
if (e.emit) tl = ((t - e.emit.phase) % e.emit.every + e.emit.every) % e.emit.every;
|
|
1515
|
+
const tau = e.ease.tau;
|
|
1516
|
+
const k = 1 - Math.exp(-tl / tau);
|
|
1517
|
+
return (e.ease.type === "settle" ? tau * k : tl - tau * k) / 1e3;
|
|
1518
|
+
}
|
|
1136
1519
|
function alphaAt(e, t) {
|
|
1137
1520
|
let a = e.alpha;
|
|
1138
|
-
if (e.pulseAmp) a = e.alpha + e.pulseAmp * Math.sin(t * 2 * Math.PI / e.pulsePeriod + e.pulsePhase);
|
|
1521
|
+
if (e.pulseAmp) a = e.alpha + e.pulseAmp * Math.sin(clockTime(e, t) * 2 * Math.PI / e.pulsePeriod + e.pulsePhase);
|
|
1139
1522
|
if (e.motion === "warp") {
|
|
1140
1523
|
const z = warpZ(e, t);
|
|
1141
1524
|
a *= Math.max(0, Math.min(1, (1 - z) / 0.2));
|
|
1142
1525
|
}
|
|
1526
|
+
if (e.emit) {
|
|
1527
|
+
const u = emitWindow(e, t);
|
|
1528
|
+
a = u === null ? 0 : a * emitEnvelope(u);
|
|
1529
|
+
}
|
|
1143
1530
|
return a < 0 ? 0 : a > 1 ? 1 : a;
|
|
1144
1531
|
}
|
|
1145
1532
|
function sizeAt(e, t) {
|
|
1146
1533
|
let s = e.size;
|
|
1147
|
-
if (e.growAmp) s = e.size * (1 + e.growAmp * Math.sin(t * 2 * Math.PI / e.growPeriod + e.growPhase));
|
|
1534
|
+
if (e.growAmp) s = e.size * (1 + e.growAmp * Math.sin(clockTime(e, t) * 2 * Math.PI / e.growPeriod + e.growPhase));
|
|
1148
1535
|
if (e.motion === "warp") s *= Math.min(1 / warpZ(e, t), WARP_MAX_SCALE);
|
|
1536
|
+
if (e.emit && (e.emit.growFrom !== 1 || e.emit.growTo !== 1)) {
|
|
1537
|
+
const u = emitWindow(e, t);
|
|
1538
|
+
if (u !== null) s *= e.emit.growFrom + (e.emit.growTo - e.emit.growFrom) * u;
|
|
1539
|
+
}
|
|
1149
1540
|
return s > 0 ? s : 0.1;
|
|
1150
1541
|
}
|
|
1151
1542
|
function rotationAt(e, t) {
|
|
@@ -1176,7 +1567,7 @@ function reflect(v, min, max) {
|
|
|
1176
1567
|
const p = ((v - min) % period + period) % period;
|
|
1177
1568
|
return min + (p < range ? p : period - p);
|
|
1178
1569
|
}
|
|
1179
|
-
function
|
|
1570
|
+
function catmullRom2(p0, p1, p2, p3, u) {
|
|
1180
1571
|
const u2 = u * u;
|
|
1181
1572
|
const u3 = u2 * u;
|
|
1182
1573
|
return 0.5 * (2 * p1 + (-p0 + p2) * u + (2 * p0 - 5 * p1 + 4 * p2 - p3) * u2 + (-p0 + 3 * p1 - 3 * p2 + p3) * u3);
|
|
@@ -1206,8 +1597,8 @@ function pathPosition(e, t) {
|
|
|
1206
1597
|
const p0 = at(i - 1);
|
|
1207
1598
|
const p3 = at(i + 2);
|
|
1208
1599
|
return {
|
|
1209
|
-
x:
|
|
1210
|
-
y:
|
|
1600
|
+
x: catmullRom2(p0.x, p1.x, p2.x, p3.x, local) + p.offX,
|
|
1601
|
+
y: catmullRom2(p0.y, p1.y, p2.y, p3.y, local) + p.offY
|
|
1211
1602
|
};
|
|
1212
1603
|
}
|
|
1213
1604
|
function positionAt(e, t, w, h) {
|
|
@@ -1234,7 +1625,7 @@ function positionAt(e, t, w, h) {
|
|
|
1234
1625
|
const pos = pathPosition(e, t);
|
|
1235
1626
|
return { x: pos.x, y: pos.y, flip: false };
|
|
1236
1627
|
}
|
|
1237
|
-
const dt = t
|
|
1628
|
+
const dt = motionSeconds(e, t);
|
|
1238
1629
|
const m = e.size;
|
|
1239
1630
|
if (e.motion === "bounce") {
|
|
1240
1631
|
return {
|
|
@@ -1291,7 +1682,8 @@ function headingAt(e, t, w, h) {
|
|
|
1291
1682
|
}
|
|
1292
1683
|
function spriteIndexAt(e, t, variants) {
|
|
1293
1684
|
if (!e.cyclePeriod || variants <= 1) return e.spriteIndex;
|
|
1294
|
-
|
|
1685
|
+
if (e.ordered) return (e.spriteIndex + Math.floor(clockTime(e, t) / e.cyclePeriod)) % variants;
|
|
1686
|
+
return Math.floor(clockTime(e, t) / e.cyclePeriod + (e.cyclePhase ?? e.phase) / (2 * Math.PI)) % variants;
|
|
1295
1687
|
}
|
|
1296
1688
|
function wrapDist(pi, pj, doWrap, w, h) {
|
|
1297
1689
|
let dx = pj.x - pi.x;
|
|
@@ -1462,6 +1854,17 @@ function revealState(lines, reveal, tMs) {
|
|
|
1462
1854
|
const mode = reveal.mode ?? "typewriter";
|
|
1463
1855
|
let fullLines = 0;
|
|
1464
1856
|
let partialText = "";
|
|
1857
|
+
let glyphAlphas;
|
|
1858
|
+
if (mode === "glyphFade") {
|
|
1859
|
+
const f = Math.min(1, Math.max(0.01, reveal.fade ?? 0.15));
|
|
1860
|
+
let g = 0;
|
|
1861
|
+
glyphAlphas = lineClusters.map(
|
|
1862
|
+
(clusters) => clusters.map(() => {
|
|
1863
|
+
const start = totalGraphemes > 0 ? g++ / totalGraphemes * (1 - f) : 0;
|
|
1864
|
+
return Math.max(0, Math.min(1, (progress - start) / f));
|
|
1865
|
+
})
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1465
1868
|
if (mode === "line") {
|
|
1466
1869
|
fullLines = Math.round(progress * lines.length);
|
|
1467
1870
|
} else if (mode === "word") {
|
|
@@ -1501,7 +1904,7 @@ function revealState(lines, reveal, tMs) {
|
|
|
1501
1904
|
caretLine = 0;
|
|
1502
1905
|
caretPrefix = "";
|
|
1503
1906
|
}
|
|
1504
|
-
return { progress, fullLines, partialText, caretLine, caretPrefix };
|
|
1907
|
+
return { progress, fullLines, partialText, caretLine, caretPrefix, glyphAlphas };
|
|
1505
1908
|
}
|
|
1506
1909
|
|
|
1507
1910
|
// src/compile.ts
|
|
@@ -1510,7 +1913,13 @@ import {
|
|
|
1510
1913
|
} from "@idle-screens/core";
|
|
1511
1914
|
|
|
1512
1915
|
// src/sequence.ts
|
|
1513
|
-
function
|
|
1916
|
+
function segmentStart(seq, index) {
|
|
1917
|
+
let t = 0;
|
|
1918
|
+
const n = Math.max(0, Math.min(index, seq.segments.length - 1));
|
|
1919
|
+
for (let i = 0; i < n; i++) t += seq.segments[i].duration ?? 0;
|
|
1920
|
+
return t;
|
|
1921
|
+
}
|
|
1922
|
+
function resolveSegment(seq, T, opts = {}) {
|
|
1514
1923
|
const { segments, loop } = seq;
|
|
1515
1924
|
if (segments.length === 0) return { index: 0, localT: 0, startT: 0 };
|
|
1516
1925
|
let totalTimed = 0;
|
|
@@ -1518,24 +1927,33 @@ function resolveSegment(seq, T) {
|
|
|
1518
1927
|
if (seg.duration != null) totalTimed += seg.duration;
|
|
1519
1928
|
else break;
|
|
1520
1929
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1930
|
+
const t0 = Math.max(0, T);
|
|
1931
|
+
const walk = (t, releasedBelow) => {
|
|
1932
|
+
let cumulative = 0;
|
|
1933
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1934
|
+
const seg = segments[i];
|
|
1935
|
+
if (seg.duration == null) {
|
|
1936
|
+
return loop ? null : { index: i, localT: t - cumulative, startT: cumulative };
|
|
1937
|
+
}
|
|
1938
|
+
if (t < cumulative + seg.duration) {
|
|
1939
|
+
return { index: i, localT: t - cumulative, startT: cumulative };
|
|
1940
|
+
}
|
|
1941
|
+
if (seg.advance === "input" && i >= releasedBelow) {
|
|
1942
|
+
return { index: i, localT: t - cumulative, startT: cumulative, held: true };
|
|
1943
|
+
}
|
|
1944
|
+
cumulative += seg.duration;
|
|
1533
1945
|
}
|
|
1534
|
-
|
|
1946
|
+
return null;
|
|
1947
|
+
};
|
|
1948
|
+
const first = walk(t0, opts.releasedBelow ?? 0);
|
|
1949
|
+
if (first) return first;
|
|
1950
|
+
if (loop && totalTimed > 0) {
|
|
1951
|
+
const wrapped = walk(t0 % totalTimed, 0);
|
|
1952
|
+
if (wrapped) return wrapped;
|
|
1535
1953
|
}
|
|
1536
1954
|
const last = segments.length - 1;
|
|
1537
|
-
const lastStart =
|
|
1538
|
-
return { index: last, localT:
|
|
1955
|
+
const lastStart = totalTimed - (segments[last].duration ?? 0);
|
|
1956
|
+
return { index: last, localT: t0 - lastStart, startT: lastStart };
|
|
1539
1957
|
}
|
|
1540
1958
|
|
|
1541
1959
|
// src/compile.ts
|
|
@@ -1727,7 +2145,7 @@ var SpecInstance = class {
|
|
|
1727
2145
|
const headAlpha = alphaAt(e, t) * lifeA;
|
|
1728
2146
|
const headSize = sizeAt(e, t);
|
|
1729
2147
|
const sprite = built.layer.sprite;
|
|
1730
|
-
const resolvedColor = sprite
|
|
2148
|
+
const resolvedColor = isShapedSprite(sprite) ? sprite.colors?.[e.colorIndex] ?? sprite.color : sprite.kind === "text" || sprite.kind === "textBlock" ? sprite.color ?? "#e6e8ef" : "#e6e8ef";
|
|
1731
2149
|
const isSoft = sprite.kind === "circle" && sprite.soft;
|
|
1732
2150
|
const wrap2 = built.layer.wrap !== false;
|
|
1733
2151
|
const head = this.entityPos(e, t, parentE);
|
|
@@ -1806,7 +2224,90 @@ var SpecInstance = class {
|
|
|
1806
2224
|
ctx.translate(p.x, p.y);
|
|
1807
2225
|
if (rot) ctx.rotate(rot);
|
|
1808
2226
|
ctx.fillStyle = resolvedColor;
|
|
1809
|
-
|
|
2227
|
+
const feather = sprite.feather ?? 0;
|
|
2228
|
+
if (feather > 0) {
|
|
2229
|
+
const base = ctx.globalAlpha;
|
|
2230
|
+
const additive = built.layer.blend === "lighter";
|
|
2231
|
+
const alphas = featherAlphas(FEATHER_STEPS, additive);
|
|
2232
|
+
for (let j = 1; j <= FEATHER_STEPS; j++) {
|
|
2233
|
+
const f = 1 - feather * (j - 1) / FEATHER_STEPS;
|
|
2234
|
+
const fw = sz * f;
|
|
2235
|
+
const fh = rh * f;
|
|
2236
|
+
ctx.globalAlpha = base * alphas[j - 1];
|
|
2237
|
+
ctx.fillRect(-fw / 2, -fh / 2, fw, fh);
|
|
2238
|
+
}
|
|
2239
|
+
ctx.globalAlpha = base;
|
|
2240
|
+
} else {
|
|
2241
|
+
ctx.fillRect(-sz / 2, -rh / 2, sz, rh);
|
|
2242
|
+
}
|
|
2243
|
+
ctx.restore();
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
if (sprite.kind === "bar") {
|
|
2247
|
+
const len = sz * barFraction(sprite, e.barIndex ?? 0);
|
|
2248
|
+
if (len <= 0) return;
|
|
2249
|
+
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
2250
|
+
const th = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz * 0.2;
|
|
2251
|
+
const box = barBox(sprite.direction ?? "right", len, th);
|
|
2252
|
+
ctx.save();
|
|
2253
|
+
ctx.translate(p.x, p.y);
|
|
2254
|
+
if (rot) ctx.rotate(rot);
|
|
2255
|
+
ctx.fillStyle = resolvedColor;
|
|
2256
|
+
ctx.fillRect(box.cx - box.halfX, box.cy - box.halfY, box.halfX * 2, box.halfY * 2);
|
|
2257
|
+
ctx.restore();
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
if (sprite.kind === "polygon") {
|
|
2261
|
+
const r = sz / 2;
|
|
2262
|
+
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
2263
|
+
const pts = polygonPoints(sprite, r);
|
|
2264
|
+
ctx.save();
|
|
2265
|
+
ctx.translate(p.x, p.y);
|
|
2266
|
+
if (rot) ctx.rotate(rot);
|
|
2267
|
+
if (sprite.soft) {
|
|
2268
|
+
const g = ctx.createRadialGradient(0, 0, 0, 0, 0, r);
|
|
2269
|
+
g.addColorStop(0, resolvedColor);
|
|
2270
|
+
g.addColorStop(0.35, hexToRgba(resolvedColor, 0.75));
|
|
2271
|
+
g.addColorStop(1, hexToRgba(resolvedColor, 0));
|
|
2272
|
+
ctx.fillStyle = g;
|
|
2273
|
+
} else {
|
|
2274
|
+
ctx.fillStyle = resolvedColor;
|
|
2275
|
+
}
|
|
2276
|
+
ctx.beginPath();
|
|
2277
|
+
ctx.moveTo(pts[0].x, pts[0].y);
|
|
2278
|
+
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
|
|
2279
|
+
ctx.closePath();
|
|
2280
|
+
ctx.fill();
|
|
2281
|
+
ctx.restore();
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
if (sprite.kind === "stroke") {
|
|
2285
|
+
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
2286
|
+
const pts = strokeSamples(sprite, sz / 2);
|
|
2287
|
+
const lw = strokeWidthPx(sprite, unitScale);
|
|
2288
|
+
ctx.save();
|
|
2289
|
+
ctx.translate(p.x, p.y);
|
|
2290
|
+
let angle = rot;
|
|
2291
|
+
if (sprite.orient) angle += headingAt(e, t, this.w, this.h) ?? 0;
|
|
2292
|
+
if (angle) ctx.rotate(angle);
|
|
2293
|
+
ctx.strokeStyle = resolvedColor;
|
|
2294
|
+
ctx.lineCap = "round";
|
|
2295
|
+
ctx.lineJoin = "round";
|
|
2296
|
+
if (sprite.taper) {
|
|
2297
|
+
for (let i = 1; i < pts.length; i++) {
|
|
2298
|
+
ctx.lineWidth = Math.max(0.5, lw * strokeTaper((i - 0.5) / (pts.length - 1)));
|
|
2299
|
+
ctx.beginPath();
|
|
2300
|
+
ctx.moveTo(pts[i - 1].x, pts[i - 1].y);
|
|
2301
|
+
ctx.lineTo(pts[i].x, pts[i].y);
|
|
2302
|
+
ctx.stroke();
|
|
2303
|
+
}
|
|
2304
|
+
} else {
|
|
2305
|
+
ctx.lineWidth = lw;
|
|
2306
|
+
ctx.beginPath();
|
|
2307
|
+
ctx.moveTo(pts[0].x, pts[0].y);
|
|
2308
|
+
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
|
|
2309
|
+
ctx.stroke();
|
|
2310
|
+
}
|
|
1810
2311
|
ctx.restore();
|
|
1811
2312
|
return;
|
|
1812
2313
|
}
|
|
@@ -1853,11 +2354,39 @@ var SpecInstance = class {
|
|
|
1853
2354
|
ctx.textBaseline = "top";
|
|
1854
2355
|
ctx.textAlign = align;
|
|
1855
2356
|
const xOff = align === "center" ? maxWPx / 2 : align === "right" ? maxWPx : 0;
|
|
1856
|
-
|
|
1857
|
-
ctx.
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
2357
|
+
if (rs?.glyphAlphas) {
|
|
2358
|
+
ctx.textAlign = "left";
|
|
2359
|
+
const baseAlpha = ctx.globalAlpha;
|
|
2360
|
+
for (let li = 0; li < lines.length; li++) {
|
|
2361
|
+
const alphas = rs.glyphAlphas[li] ?? [];
|
|
2362
|
+
if (alphas.length > 0 && alphas[0] <= 0) break;
|
|
2363
|
+
const clusters = graphemeClusters(lines[li].text);
|
|
2364
|
+
const lw = ctx.measureText(lines[li].text).width;
|
|
2365
|
+
const x0 = align === "center" ? (maxWPx - lw) / 2 : align === "right" ? maxWPx - lw : 0;
|
|
2366
|
+
let opaque = 0;
|
|
2367
|
+
while (opaque < clusters.length && (alphas[opaque] ?? 0) >= 1) opaque++;
|
|
2368
|
+
let prefix = "";
|
|
2369
|
+
if (opaque > 0) {
|
|
2370
|
+
prefix = clusters.slice(0, opaque).join("");
|
|
2371
|
+
ctx.globalAlpha = baseAlpha;
|
|
2372
|
+
ctx.fillText(prefix, x0, li * lh);
|
|
2373
|
+
}
|
|
2374
|
+
for (let gi = opaque; gi < clusters.length; gi++) {
|
|
2375
|
+
const a = alphas[gi] ?? 0;
|
|
2376
|
+
if (a <= 0) break;
|
|
2377
|
+
ctx.globalAlpha = baseAlpha * a;
|
|
2378
|
+
ctx.fillText(clusters[gi], x0 + ctx.measureText(prefix).width, li * lh);
|
|
2379
|
+
prefix += clusters[gi];
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
ctx.globalAlpha = baseAlpha;
|
|
2383
|
+
} else {
|
|
2384
|
+
for (let li = 0; li < fullLines; li++) {
|
|
2385
|
+
ctx.fillText(lines[li].text, xOff, li * lh);
|
|
2386
|
+
}
|
|
2387
|
+
if (rs && rs.partialText.length > 0) {
|
|
2388
|
+
ctx.fillText(rs.partialText, xOff, fullLines * lh);
|
|
2389
|
+
}
|
|
1861
2390
|
}
|
|
1862
2391
|
if (rs && reveal.caret) {
|
|
1863
2392
|
const cfg = reveal.caret === true ? {} : reveal.caret;
|
|
@@ -1953,7 +2482,7 @@ var SpecInstance = class {
|
|
|
1953
2482
|
let resolvedColor = links.color;
|
|
1954
2483
|
if (!resolvedColor) {
|
|
1955
2484
|
const sprite = built.layer.sprite;
|
|
1956
|
-
if (sprite
|
|
2485
|
+
if (isShapedSprite(sprite)) {
|
|
1957
2486
|
resolvedColor = sprite.colors?.[ei.colorIndex] ?? sprite.color;
|
|
1958
2487
|
} else resolvedColor = "#e6e8ef";
|
|
1959
2488
|
}
|
|
@@ -2091,6 +2620,18 @@ var SequenceInstance = class {
|
|
|
2091
2620
|
startT = 0;
|
|
2092
2621
|
baseT = 0;
|
|
2093
2622
|
lastT = 0;
|
|
2623
|
+
/** The T most recently passed to renderFrame — the clock a steer displaces. */
|
|
2624
|
+
renderedT = 0;
|
|
2625
|
+
/**
|
|
2626
|
+
* The clicker's two pieces of state. `clockOffset` is added to every T so
|
|
2627
|
+
* a `sequence.segment` steer lands the timeline at the target segment's
|
|
2628
|
+
* start and STAYS there as the wall clock keeps ticking — the steer moves
|
|
2629
|
+
* the clock instead of fighting it. `releasedBelow` records that the steer
|
|
2630
|
+
* counts as the presenter clicking past every `advance: 'input'` hold
|
|
2631
|
+
* before the target (see `resolveSegment`).
|
|
2632
|
+
*/
|
|
2633
|
+
clockOffset = 0;
|
|
2634
|
+
releasedBelow = 0;
|
|
2094
2635
|
constructor(seq, ctx) {
|
|
2095
2636
|
this.seq = seq;
|
|
2096
2637
|
let surface = ctx.surface ?? null;
|
|
@@ -2173,7 +2714,8 @@ var SequenceInstance = class {
|
|
|
2173
2714
|
this.renderFrame(this.lastT, this.seed);
|
|
2174
2715
|
}
|
|
2175
2716
|
renderFrame(T, seed) {
|
|
2176
|
-
|
|
2717
|
+
this.renderedT = T;
|
|
2718
|
+
const resolved = resolveSegment(this.seq, T + this.clockOffset, { releasedBelow: this.releasedBelow });
|
|
2177
2719
|
const { index, localT } = resolved;
|
|
2178
2720
|
const prevIdx = index > 0 ? index - 1 : -1;
|
|
2179
2721
|
const morphActive = prevIdx >= 0 && this.canMorph(prevIdx) && localT < this.morphDur(prevIdx);
|
|
@@ -2242,14 +2784,9 @@ var SequenceInstance = class {
|
|
|
2242
2784
|
const segDelta = deltas.find((d) => d.path === "sequence.segment");
|
|
2243
2785
|
if (segDelta !== void 0 && typeof segDelta.value === "number") {
|
|
2244
2786
|
const idx = Math.max(0, Math.min(this.seq.segments.length - 1, Math.round(segDelta.value)));
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
}
|
|
2249
|
-
this.activeIndex = idx;
|
|
2250
|
-
const child = this.ensureChild(idx);
|
|
2251
|
-
child.renderFrame(0, this.seq.segments[idx].scene.seed ?? this.seq.seed ?? 0);
|
|
2252
|
-
}
|
|
2787
|
+
this.clockOffset = segmentStart(this.seq, idx) - this.renderedT;
|
|
2788
|
+
this.releasedBelow = idx;
|
|
2789
|
+
this.renderFrame(this.renderedT, this.seed);
|
|
2253
2790
|
}
|
|
2254
2791
|
const childDeltas = deltas.filter((d) => d.path !== "sequence.segment");
|
|
2255
2792
|
if (childDeltas.length > 0 && this.activeIndex >= 0) {
|
|
@@ -2308,7 +2845,7 @@ function hexLuma(hex) {
|
|
|
2308
2845
|
}
|
|
2309
2846
|
function spriteLuma(layer, e) {
|
|
2310
2847
|
const s = layer.sprite;
|
|
2311
|
-
if (s
|
|
2848
|
+
if (isShapedSprite(s)) {
|
|
2312
2849
|
return hexLuma(s.colors?.[e.colorIndex] ?? s.color);
|
|
2313
2850
|
}
|
|
2314
2851
|
if (s.kind === "text" || s.kind === "textBlock") return hexLuma(s.color ?? "#e6e8ef");
|
|
@@ -2316,7 +2853,7 @@ function spriteLuma(layer, e) {
|
|
|
2316
2853
|
}
|
|
2317
2854
|
function spriteHex(layer, e) {
|
|
2318
2855
|
const s = layer.sprite;
|
|
2319
|
-
if (s
|
|
2856
|
+
if (isShapedSprite(s)) {
|
|
2320
2857
|
return s.colors?.[e.colorIndex] ?? s.color;
|
|
2321
2858
|
}
|
|
2322
2859
|
if (s.kind === "text" || s.kind === "textBlock") return s.color ?? "#e6e8ef";
|
|
@@ -2354,19 +2891,237 @@ function colourSeparation(layer, sprite, bg) {
|
|
|
2354
2891
|
return Math.sqrt((dr * dr + dg * dg + db * db) / 3);
|
|
2355
2892
|
}
|
|
2356
2893
|
|
|
2894
|
+
// src/cohesion.ts
|
|
2895
|
+
var MERGE_FLOOR = 0.35;
|
|
2896
|
+
var WASH_ALPHA_MAX = 0.45;
|
|
2897
|
+
var COHESION_T = 5e3;
|
|
2898
|
+
var OUTLINE_SAMPLES = 24;
|
|
2899
|
+
var MAX_TRACED = 240;
|
|
2900
|
+
var EDGE_INSET = 1e-4;
|
|
2901
|
+
function circleShape(cx, cy, r) {
|
|
2902
|
+
return {
|
|
2903
|
+
cx,
|
|
2904
|
+
cy,
|
|
2905
|
+
br: r,
|
|
2906
|
+
area: Math.PI * r * r,
|
|
2907
|
+
contains: (x, y) => (x - cx) * (x - cx) + (y - cy) * (y - cy) < r * r,
|
|
2908
|
+
outline: (n) => Array.from({ length: n }, (_, i) => {
|
|
2909
|
+
const a = i / n * Math.PI * 2;
|
|
2910
|
+
const inner = r * (1 - EDGE_INSET);
|
|
2911
|
+
return { x: cx + inner * Math.cos(a), y: cy + inner * Math.sin(a) };
|
|
2912
|
+
})
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
function polyShape(cx, cy, raw) {
|
|
2916
|
+
const abs = raw.filter((p, i) => {
|
|
2917
|
+
const q = raw[(i + 1) % raw.length];
|
|
2918
|
+
return Math.hypot(q.x - p.x, q.y - p.y) > 0;
|
|
2919
|
+
});
|
|
2920
|
+
if (abs.length < 3) return null;
|
|
2921
|
+
let br = 0;
|
|
2922
|
+
for (const p of abs) br = Math.max(br, Math.hypot(p.x - cx, p.y - cy));
|
|
2923
|
+
const inside = (x, y) => {
|
|
2924
|
+
let hit = false;
|
|
2925
|
+
for (let i = 0, j = abs.length - 1; i < abs.length; j = i++) {
|
|
2926
|
+
const a = abs[i];
|
|
2927
|
+
const b = abs[j];
|
|
2928
|
+
if (a.y > y !== b.y > y && x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x) hit = !hit;
|
|
2929
|
+
}
|
|
2930
|
+
return hit;
|
|
2931
|
+
};
|
|
2932
|
+
const seg = abs.map((p, i) => {
|
|
2933
|
+
const q = abs[(i + 1) % abs.length];
|
|
2934
|
+
return Math.hypot(q.x - p.x, q.y - p.y);
|
|
2935
|
+
});
|
|
2936
|
+
const perimeter = seg.reduce((a, b) => a + b, 0);
|
|
2937
|
+
const shortest = Math.min(...seg);
|
|
2938
|
+
const step = Math.min(br * EDGE_INSET, shortest * 0.25);
|
|
2939
|
+
const area = polygonArea(abs);
|
|
2940
|
+
if (!(area > 0)) return null;
|
|
2941
|
+
return {
|
|
2942
|
+
cx,
|
|
2943
|
+
cy,
|
|
2944
|
+
br,
|
|
2945
|
+
area,
|
|
2946
|
+
contains: inside,
|
|
2947
|
+
/**
|
|
2948
|
+
* `n` points spread along the perimeter by ARC LENGTH, not one share per
|
|
2949
|
+
* edge — a long skinny rectangle has two edges carrying almost all of its
|
|
2950
|
+
* perimeter, and an equal-per-edge split would report a fully buried long
|
|
2951
|
+
* side as only a quarter covered. Each point is then stepped along its
|
|
2952
|
+
* edge's inward normal, whichever of the two candidates lands inside.
|
|
2953
|
+
*/
|
|
2954
|
+
outline: (n) => {
|
|
2955
|
+
if (!(perimeter > 0)) return [];
|
|
2956
|
+
const out = [];
|
|
2957
|
+
for (let i = 0; i < n; i++) {
|
|
2958
|
+
let d = i / n * perimeter;
|
|
2959
|
+
let k = 0;
|
|
2960
|
+
while (k < seg.length - 1 && d > seg[k]) {
|
|
2961
|
+
d -= seg[k];
|
|
2962
|
+
k++;
|
|
2963
|
+
}
|
|
2964
|
+
const a = abs[k];
|
|
2965
|
+
const b = abs[(k + 1) % abs.length];
|
|
2966
|
+
const len = seg[k];
|
|
2967
|
+
const f = d / len;
|
|
2968
|
+
const px = a.x + (b.x - a.x) * f;
|
|
2969
|
+
const py = a.y + (b.y - a.y) * f;
|
|
2970
|
+
const ux = (b.x - a.x) / len;
|
|
2971
|
+
const uy = (b.y - a.y) / len;
|
|
2972
|
+
for (let t = step; t > 0; t /= 8) {
|
|
2973
|
+
const qx1 = px - uy * t;
|
|
2974
|
+
const qy1 = py + ux * t;
|
|
2975
|
+
if (inside(qx1, qy1)) {
|
|
2976
|
+
out.push({ x: qx1, y: qy1 });
|
|
2977
|
+
break;
|
|
2978
|
+
}
|
|
2979
|
+
const qx2 = px + uy * t;
|
|
2980
|
+
const qy2 = py - ux * t;
|
|
2981
|
+
if (inside(qx2, qy2)) {
|
|
2982
|
+
out.push({ x: qx2, y: qy2 });
|
|
2983
|
+
break;
|
|
2984
|
+
}
|
|
2985
|
+
if (t < step * 1e-3) break;
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
return out;
|
|
2989
|
+
}
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
function rectShape(cx, cy, halfW, halfH, rot) {
|
|
2993
|
+
const cos = Math.cos(rot);
|
|
2994
|
+
const sin = Math.sin(rot);
|
|
2995
|
+
const corner = (lx, ly) => ({ x: cx + lx * cos - ly * sin, y: cy + lx * sin + ly * cos });
|
|
2996
|
+
return polyShape(cx, cy, [
|
|
2997
|
+
corner(-halfW, -halfH),
|
|
2998
|
+
corner(halfW, -halfH),
|
|
2999
|
+
corner(halfW, halfH),
|
|
3000
|
+
corner(-halfW, halfH)
|
|
3001
|
+
]);
|
|
3002
|
+
}
|
|
3003
|
+
function polygonShape(cx, cy, pts, rot) {
|
|
3004
|
+
const cos = Math.cos(rot);
|
|
3005
|
+
const sin = Math.sin(rot);
|
|
3006
|
+
return polyShape(cx, cy, pts.map((p) => ({ x: cx + p.x * cos - p.y * sin, y: cy + p.x * sin + p.y * cos })));
|
|
3007
|
+
}
|
|
3008
|
+
function shapeOf(layer, e, t, w, h) {
|
|
3009
|
+
const s = layer.sprite;
|
|
3010
|
+
if (s.kind !== "circle" && s.kind !== "rect" && s.kind !== "polygon") return null;
|
|
3011
|
+
const p = positionAt(e, t, w, h);
|
|
3012
|
+
const sz = sizeAt(e, t);
|
|
3013
|
+
if (!(sz > 0)) return null;
|
|
3014
|
+
if (s.kind === "circle") return circleShape(p.x, p.y, sz / 2);
|
|
3015
|
+
const rot = rotationAt(e, t);
|
|
3016
|
+
if (s.kind === "rect") {
|
|
3017
|
+
const h2 = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz;
|
|
3018
|
+
return rectShape(p.x, p.y, sz / 2, h2 / 2, rot);
|
|
3019
|
+
}
|
|
3020
|
+
return polygonShape(p.x, p.y, polygonPoints(s, sz / 2), rot);
|
|
3021
|
+
}
|
|
3022
|
+
function seamCauseOf(layer) {
|
|
3023
|
+
const s = layer.sprite;
|
|
3024
|
+
if ((s.kind === "circle" || s.kind === "polygon") && s.soft) {
|
|
3025
|
+
return "sprite.soft \u2014 radial falloff never forms a hard union";
|
|
3026
|
+
}
|
|
3027
|
+
if (s.kind === "rect" && s.feather) {
|
|
3028
|
+
return "rect.feather \u2014 a feathered edge never forms a hard union";
|
|
3029
|
+
}
|
|
3030
|
+
if (layer.blend) {
|
|
3031
|
+
return `blend: ${layer.blend} \u2014 overlaps composite instead of covering`;
|
|
3032
|
+
}
|
|
3033
|
+
const [lo, hi] = layer.alpha ?? [1, 1];
|
|
3034
|
+
if (lo < 1 || hi < 1) {
|
|
3035
|
+
return `alpha ${lo === hi ? lo : `${lo}..${hi}`} \u2014 every overlap paints a darker patch`;
|
|
3036
|
+
}
|
|
3037
|
+
if (layer.pulse && layer.pulse.amp > 0) {
|
|
3038
|
+
return "pulse \u2014 the layer is translucent for part of every cycle";
|
|
3039
|
+
}
|
|
3040
|
+
const palette = "colors" in s && Array.isArray(s.colors) ? s.colors : null;
|
|
3041
|
+
const weights = "colorWeights" in s && Array.isArray(s.colorWeights) ? s.colorWeights : null;
|
|
3042
|
+
if (palette) {
|
|
3043
|
+
const aligned = weights !== null && weights.length === palette.length;
|
|
3044
|
+
const reachable = new Set(palette.filter((_, i) => !aligned || (weights[i] ?? 0) > 0));
|
|
3045
|
+
if (reachable.size > 1) {
|
|
3046
|
+
return `sprite.colors has ${reachable.size} reachable colours \u2014 neighbours of unlike colour show their join`;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
return null;
|
|
3050
|
+
}
|
|
3051
|
+
function cohesionOf(layers, t, w, h) {
|
|
3052
|
+
return layers.map(({ layer, entities }, layerIndex) => {
|
|
3053
|
+
const seamCause = seamCauseOf(layer);
|
|
3054
|
+
const base = {
|
|
3055
|
+
layerIndex,
|
|
3056
|
+
key: layer.key,
|
|
3057
|
+
seamless: seamCause === null,
|
|
3058
|
+
seamCause
|
|
3059
|
+
};
|
|
3060
|
+
if (lifeAlphaAt(layer.life, t) <= 0) return { ...base, overlap: null, reads: null };
|
|
3061
|
+
const shapes = entities.map((e) => shapeOf(layer, e, t, w, h));
|
|
3062
|
+
const solid = shapes.filter((x) => x !== null && x.area > 0);
|
|
3063
|
+
if (solid.length < 2) return { ...base, overlap: null, reads: null };
|
|
3064
|
+
const stride = Math.max(1, Math.ceil(solid.length / MAX_TRACED));
|
|
3065
|
+
let covered = 0;
|
|
3066
|
+
let traced = 0;
|
|
3067
|
+
for (let i = 0; i < solid.length; i += stride) {
|
|
3068
|
+
const self = solid[i];
|
|
3069
|
+
const pts = self.outline(OUTLINE_SAMPLES);
|
|
3070
|
+
if (pts.length === 0) continue;
|
|
3071
|
+
let hit = 0;
|
|
3072
|
+
for (const p of pts) {
|
|
3073
|
+
for (let j = 0; j < solid.length; j++) {
|
|
3074
|
+
if (j === i) continue;
|
|
3075
|
+
const other = solid[j];
|
|
3076
|
+
const dx = p.x - other.cx;
|
|
3077
|
+
const dy = p.y - other.cy;
|
|
3078
|
+
if (dx * dx + dy * dy > other.br * other.br) continue;
|
|
3079
|
+
if (other.contains(p.x, p.y)) {
|
|
3080
|
+
hit++;
|
|
3081
|
+
break;
|
|
3082
|
+
}
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
covered += hit / pts.length;
|
|
3086
|
+
traced++;
|
|
3087
|
+
}
|
|
3088
|
+
if (traced === 0) return { ...base, overlap: null, reads: null };
|
|
3089
|
+
const overlap = Number((covered / traced).toFixed(4));
|
|
3090
|
+
const reads = overlap < MERGE_FLOOR ? "marks" : seamCause === null ? "mass" : "seamed";
|
|
3091
|
+
return { ...base, overlap, reads };
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
function seamsWorthWarning(layer, c, entityCount) {
|
|
3095
|
+
if (c.reads !== "seamed" || entityCount < 6) return false;
|
|
3096
|
+
const [lo, hi] = layer.alpha ?? [1, 1];
|
|
3097
|
+
if ((lo + hi) / 2 < WASH_ALPHA_MAX) return false;
|
|
3098
|
+
return layer.blend !== "lighter" && layer.blend !== "screen";
|
|
3099
|
+
}
|
|
3100
|
+
|
|
2357
3101
|
// src/advise.ts
|
|
2358
3102
|
var LOW_CONTRAST_FLOOR = 0.05;
|
|
2359
|
-
|
|
3103
|
+
var SPARSE_DECLARED_MAX_COVERAGE = 0.02;
|
|
3104
|
+
function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
2360
3105
|
const warnings = [];
|
|
2361
3106
|
const w = viewport.width;
|
|
2362
3107
|
const h = viewport.height;
|
|
2363
3108
|
const scale = spec.units === "px" ? 1 : Math.min(w, h);
|
|
2364
3109
|
const refVp = spec.referenceViewport ?? LIMITS.referenceViewport;
|
|
2365
|
-
|
|
2366
|
-
|
|
3110
|
+
let countScale = scale > 1 ? Math.min(w, h) / refVp : 1;
|
|
3111
|
+
if (countScale > 1) {
|
|
3112
|
+
const rawTotal = spec.layers.reduce((sum, l) => sum + Math.round(l.count * countScale), 0);
|
|
3113
|
+
if (rawTotal > LIMITS.maxTotal) countScale *= LIMITS.maxTotal / rawTotal;
|
|
3114
|
+
}
|
|
3115
|
+
const rng = createRng2(opts.seed ?? spec.seed ?? 42);
|
|
2367
3116
|
const allEntities = spec.layers.map((l) => buildEntities(l, rng, w, h, scale, countScale));
|
|
2368
3117
|
const bgLuma = backgroundLuma(spec);
|
|
2369
3118
|
const bgRgb = backgroundRgb(spec);
|
|
3119
|
+
const cohesion = cohesionOf(
|
|
3120
|
+
spec.layers.map((layer, i) => ({ layer, entities: allEntities[i] })),
|
|
3121
|
+
opts.t ?? COHESION_T,
|
|
3122
|
+
w,
|
|
3123
|
+
h
|
|
3124
|
+
);
|
|
2370
3125
|
let totalEntities = 0;
|
|
2371
3126
|
let textLayerCount = 0;
|
|
2372
3127
|
let motionLayerCount = 0;
|
|
@@ -2378,6 +3133,14 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
2378
3133
|
const isStaticText = isText && layer.motion.type === "static";
|
|
2379
3134
|
if (isStaticText) textLayerCount++;
|
|
2380
3135
|
if (layer.motion.type !== "static") motionLayerCount++;
|
|
3136
|
+
const coh = cohesion[li];
|
|
3137
|
+
if (seamsWorthWarning(layer, coh, entities.length)) {
|
|
3138
|
+
warnings.push({
|
|
3139
|
+
path: `layers[${li}]`,
|
|
3140
|
+
code: "overlap-seams",
|
|
3141
|
+
message: `entities bury ${Math.round((coh.overlap ?? 0) * 100)}% of each other's outlines, so this layer is drawn as one shape \u2014 but ${coh.seamCause}, so every overlap paints a visible edge and it will read as a pile of sprites instead. A layer merges into a single silhouette only when it is one flat colour at alpha 1, hard-edged, with no blend and no pulse.`
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
2381
3144
|
if (layer.trail && layer.motion.type === "static") {
|
|
2382
3145
|
warnings.push({
|
|
2383
3146
|
path: `layers[${li}].trail`,
|
|
@@ -2385,11 +3148,11 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
2385
3148
|
message: "trail has no effect on static entities \u2014 they have no past positions to draw"
|
|
2386
3149
|
});
|
|
2387
3150
|
}
|
|
2388
|
-
if (layer.sprite.kind === "streak" && layer.motion.type === "static") {
|
|
3151
|
+
if ((layer.sprite.kind === "streak" || layer.sprite.kind === "stroke" && layer.sprite.orient) && layer.motion.type === "static") {
|
|
2389
3152
|
warnings.push({
|
|
2390
3153
|
path: `layers[${li}].sprite`,
|
|
2391
3154
|
code: "streak-on-static",
|
|
2392
|
-
message: "streak sprites orient along the motion heading \u2014 static entities have none and
|
|
3155
|
+
message: `${layer.sprite.kind === "streak" ? "streak sprites orient" : "an oriented stroke turns"} along the motion heading \u2014 static entities have none, so the heading contributes nothing${layer.spin ? " (spin still rotates the mark)" : " and it renders at angle 0"}`
|
|
2393
3156
|
});
|
|
2394
3157
|
}
|
|
2395
3158
|
if (layer.motion.type === "wander" && layer.motion.coherence === 1 && entities.length >= 20) {
|
|
@@ -2436,6 +3199,8 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
2436
3199
|
let pixArea;
|
|
2437
3200
|
if (layer.sprite.kind === "circle") {
|
|
2438
3201
|
pixArea = Math.PI * r * r;
|
|
3202
|
+
} else if (layer.sprite.kind === "bar") {
|
|
3203
|
+
pixArea = e.size * barFraction(layer.sprite, e.barIndex ?? 0) * (e.size2 ?? e.size * 0.2);
|
|
2439
3204
|
} else if (layer.sprite.kind === "textBlock") {
|
|
2440
3205
|
const fsPx = layer.sprite.fontSize * scale;
|
|
2441
3206
|
const lh = (layer.sprite.lineHeight ?? 1.4) * fsPx;
|
|
@@ -2445,7 +3210,13 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
2445
3210
|
} else {
|
|
2446
3211
|
pixArea = e.size * e.size;
|
|
2447
3212
|
}
|
|
2448
|
-
|
|
3213
|
+
let duty = 1;
|
|
3214
|
+
if (layer.emit) {
|
|
3215
|
+
const [ga, gb] = layer.emit.grow ?? [1, 1];
|
|
3216
|
+
const g2 = (ga * ga + ga * gb + gb * gb) / 3;
|
|
3217
|
+
duty = Math.min(layer.emit.life, layer.emit.every) / layer.emit.every * 0.5 * g2;
|
|
3218
|
+
}
|
|
3219
|
+
totalCoverage += pixArea * e.alpha * duty / (w * h);
|
|
2449
3220
|
}
|
|
2450
3221
|
if (layer.links) {
|
|
2451
3222
|
const positions = entities.map((e) => positionAt(e, 0, w, h));
|
|
@@ -2456,20 +3227,35 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
2456
3227
|
for (const edge of edges) totalCoverage += edge.dist * lwPx * la / (w * h);
|
|
2457
3228
|
}
|
|
2458
3229
|
}
|
|
2459
|
-
|
|
3230
|
+
const density = spec.density ?? "normal";
|
|
3231
|
+
const wouldBeSparse = totalCoverage < 5e-4 && spec.layers.length > 0;
|
|
3232
|
+
if (wouldBeSparse && density !== "sparse") {
|
|
2460
3233
|
warnings.push({
|
|
2461
3234
|
path: "layers",
|
|
2462
3235
|
code: "sparse-scene",
|
|
2463
3236
|
message: `alpha-weighted coverage is ${(totalCoverage * 100).toFixed(4)}% \u2014 scene will look empty on most displays`
|
|
2464
3237
|
});
|
|
2465
3238
|
}
|
|
2466
|
-
if (totalEntities > 500) {
|
|
3239
|
+
if (totalEntities > 500 && density !== "dense") {
|
|
2467
3240
|
warnings.push({
|
|
2468
3241
|
path: "layers",
|
|
2469
3242
|
code: "dense-scene",
|
|
2470
3243
|
message: `${totalEntities} entities \u2014 scene may feel crowded and hurt performance on low-end devices`
|
|
2471
3244
|
});
|
|
2472
3245
|
}
|
|
3246
|
+
if (density === "sparse" && totalCoverage > SPARSE_DECLARED_MAX_COVERAGE) {
|
|
3247
|
+
warnings.push({
|
|
3248
|
+
path: "density",
|
|
3249
|
+
code: "density-mismatch",
|
|
3250
|
+
message: `declared sparse but alpha-weighted coverage is ${(totalCoverage * 100).toFixed(2)}% \u2014 either the declaration or the scene is wrong`
|
|
3251
|
+
});
|
|
3252
|
+
} else if (density === "dense" && wouldBeSparse) {
|
|
3253
|
+
warnings.push({
|
|
3254
|
+
path: "density",
|
|
3255
|
+
code: "density-mismatch",
|
|
3256
|
+
message: `declared dense but alpha-weighted coverage is ${(totalCoverage * 100).toFixed(4)}% \u2014 the scene will look empty`
|
|
3257
|
+
});
|
|
3258
|
+
}
|
|
2473
3259
|
if (spec.layers.length > 0 && textLayerCount > 0 && motionLayerCount === 0) {
|
|
2474
3260
|
const textRatio = textLayerCount / spec.layers.length;
|
|
2475
3261
|
if (textRatio >= 0.8) {
|
|
@@ -2529,8 +3315,88 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
2529
3315
|
});
|
|
2530
3316
|
}
|
|
2531
3317
|
}
|
|
3318
|
+
const textBoxes = [];
|
|
3319
|
+
for (let li = 0; li < spec.layers.length; li++) {
|
|
3320
|
+
const layer = spec.layers[li];
|
|
3321
|
+
const s = layer.sprite;
|
|
3322
|
+
if (layer.motion.type !== "static") continue;
|
|
3323
|
+
if (s.kind !== "text" && s.kind !== "textBlock") continue;
|
|
3324
|
+
const label = layer.key ? `\`${layer.key}\`` : `layers[${li}]`;
|
|
3325
|
+
for (const e of allEntities[li]) {
|
|
3326
|
+
const p = positionAt(e, 0, w, h);
|
|
3327
|
+
const box = s.kind === "textBlock" ? textBlockBoxAt(s, p, w, h) : textBoxAt(s, e, p, spec, w, h);
|
|
3328
|
+
textBoxes.push({ li, label, ...box });
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
const tolX = w * 0.01;
|
|
3332
|
+
const tolY = h * 0.01;
|
|
3333
|
+
const offFlagged = /* @__PURE__ */ new Set();
|
|
3334
|
+
for (const b of textBoxes) {
|
|
3335
|
+
if (offFlagged.has(b.li)) continue;
|
|
3336
|
+
const edges = [];
|
|
3337
|
+
if (b.x0 < -tolX) edges.push(`left edge by ~${Math.round(-b.x0)}px`);
|
|
3338
|
+
if (b.x1 > w + tolX) edges.push(`right edge by ~${Math.round(b.x1 - w)}px`);
|
|
3339
|
+
if (b.y0 < -tolY) edges.push(`top edge by ~${Math.round(-b.y0)}px`);
|
|
3340
|
+
if (b.y1 > h + tolY) edges.push(`bottom edge by ~${Math.round(b.y1 - h)}px`);
|
|
3341
|
+
if (edges.length === 0) continue;
|
|
3342
|
+
offFlagged.add(b.li);
|
|
3343
|
+
warnings.push({
|
|
3344
|
+
path: `layers[${b.li}]`,
|
|
3345
|
+
code: "text-off-screen",
|
|
3346
|
+
message: `${b.label} text runs off the ${edges.join(" and the ")} at ${w}\xD7${h} \u2014 move position, or shrink fontSize / maxWidth`
|
|
3347
|
+
});
|
|
3348
|
+
}
|
|
3349
|
+
const pairFlagged = /* @__PURE__ */ new Set();
|
|
3350
|
+
for (let i = 0; i < textBoxes.length; i++) {
|
|
3351
|
+
for (let j = i + 1; j < textBoxes.length; j++) {
|
|
3352
|
+
const a = textBoxes[i];
|
|
3353
|
+
const b = textBoxes[j];
|
|
3354
|
+
if (a.li === b.li) continue;
|
|
3355
|
+
const pair = `${Math.min(a.li, b.li)}:${Math.max(a.li, b.li)}`;
|
|
3356
|
+
if (pairFlagged.has(pair)) continue;
|
|
3357
|
+
const ix = Math.min(a.x1, b.x1) - Math.max(a.x0, b.x0);
|
|
3358
|
+
const iy = Math.min(a.y1, b.y1) - Math.max(a.y0, b.y0);
|
|
3359
|
+
if (ix <= 0 || iy <= 0) continue;
|
|
3360
|
+
const smaller = Math.min((a.x1 - a.x0) * (a.y1 - a.y0), (b.x1 - b.x0) * (b.y1 - b.y0));
|
|
3361
|
+
if (smaller <= 0) continue;
|
|
3362
|
+
const share = ix * iy / smaller;
|
|
3363
|
+
if (share <= 0.1) continue;
|
|
3364
|
+
pairFlagged.add(pair);
|
|
3365
|
+
const [first, second] = a.li < b.li ? [a, b] : [b, a];
|
|
3366
|
+
warnings.push({
|
|
3367
|
+
path: `layers[${first.li}]`,
|
|
3368
|
+
code: "text-overlap",
|
|
3369
|
+
message: `${first.label} and ${second.label} text boxes overlap (~${Math.round(share * 100)}% of the smaller one) \u2014 they will paint over each other; move one or shrink it`
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
2532
3373
|
return warnings;
|
|
2533
3374
|
}
|
|
3375
|
+
function textBoxAt(s, e, p, spec, w, h) {
|
|
3376
|
+
const m = s.font ? /(\d{1,5}(?:\.\d{1,4})?|\.\d{1,4})px/.exec(s.font) : null;
|
|
3377
|
+
const fontScale = spec.units === "px" ? 1 : Math.min(w, h) / (spec.referenceViewport ?? LIMITS.referenceViewport);
|
|
3378
|
+
const fh = m ? Number(m[1]) * fontScale : e.size;
|
|
3379
|
+
const str = s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
|
|
3380
|
+
let fw = textWidthEm(str) * fh;
|
|
3381
|
+
if (s.maxWidth) fw = Math.min(fw, s.maxWidth * (spec.units === "px" ? 1 : Math.min(w, h)));
|
|
3382
|
+
const align = s.align ?? "center";
|
|
3383
|
+
const baseline = s.baseline ?? "middle";
|
|
3384
|
+
const x0 = align === "left" ? p.x : align === "right" ? p.x - fw : p.x - fw / 2;
|
|
3385
|
+
const y0 = baseline === "top" ? p.y : baseline === "bottom" ? p.y - fh : p.y - fh / 2;
|
|
3386
|
+
return { x0, y0, x1: x0 + fw, y1: y0 + fh };
|
|
3387
|
+
}
|
|
3388
|
+
function textBlockBoxAt(s, p, w, h) {
|
|
3389
|
+
const unit = Math.min(w, h);
|
|
3390
|
+
const fsPx = s.fontSize * unit;
|
|
3391
|
+
const lh = (s.lineHeight ?? 1.4) * fsPx;
|
|
3392
|
+
const maxWPx = s.maxWidth * unit;
|
|
3393
|
+
const lines = breakTextBlock(s.text, maxWPx / fsPx);
|
|
3394
|
+
const maxLineW = lines.reduce((mx, l) => Math.max(mx, l.widthEm), 0) * fsPx;
|
|
3395
|
+
const totalH = lines.length * lh;
|
|
3396
|
+
const align = s.align ?? "left";
|
|
3397
|
+
const x0 = align === "center" ? p.x + (maxWPx - maxLineW) / 2 : align === "right" ? p.x + maxWPx - maxLineW : p.x;
|
|
3398
|
+
return { x0, y0: p.y, x1: x0 + maxLineW, y1: p.y + totalH };
|
|
3399
|
+
}
|
|
2534
3400
|
function adviseSequence(seq, viewport = { width: 1920, height: 1080 }) {
|
|
2535
3401
|
const warnings = [];
|
|
2536
3402
|
for (let i = 0; i < seq.segments.length; i++) {
|
|
@@ -2729,7 +3595,17 @@ function textBlockRevealFraction(s, w, h, t) {
|
|
|
2729
3595
|
const unitScale = Math.min(w, h);
|
|
2730
3596
|
const maxWEm = s.maxWidth * unitScale / (s.fontSize * unitScale);
|
|
2731
3597
|
const lines = breakTextBlock(s.text, maxWEm);
|
|
2732
|
-
|
|
3598
|
+
const rs = revealState(lines, s.reveal, t);
|
|
3599
|
+
if (rs.glyphAlphas) {
|
|
3600
|
+
let sum = 0;
|
|
3601
|
+
let n = 0;
|
|
3602
|
+
for (const line of rs.glyphAlphas) for (const a of line) {
|
|
3603
|
+
sum += a;
|
|
3604
|
+
n++;
|
|
3605
|
+
}
|
|
3606
|
+
return n > 0 ? sum / n : 1;
|
|
3607
|
+
}
|
|
3608
|
+
return rs.progress;
|
|
2733
3609
|
}
|
|
2734
3610
|
function luminanceGrid(spec, opts = {}) {
|
|
2735
3611
|
const scene = buildScene(spec, opts);
|
|
@@ -2827,6 +3703,36 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
2827
3703
|
const p = posOf(scene, e, tPass);
|
|
2828
3704
|
const sz = sizeAt(e, tPass);
|
|
2829
3705
|
const s = layer.sprite;
|
|
3706
|
+
if (s.kind === "stroke") {
|
|
3707
|
+
const pts = strokeSamples(s, sz / 2);
|
|
3708
|
+
let angle = rotationAt(e, tPass);
|
|
3709
|
+
if (s.orient) angle += headingAt(e, tPass, w, h) ?? 0;
|
|
3710
|
+
const ca = Math.cos(angle);
|
|
3711
|
+
const sa = Math.sin(angle);
|
|
3712
|
+
const lw = strokeWidthPx(s, scale);
|
|
3713
|
+
const stampAt = (q, u) => {
|
|
3714
|
+
const x = p.x + q.x * ca - q.y * sa;
|
|
3715
|
+
const y = p.y + q.x * sa + q.y * ca;
|
|
3716
|
+
const c = Math.floor(x / cellW);
|
|
3717
|
+
const r = Math.floor(y / cellH);
|
|
3718
|
+
if (c < 0 || c >= cols || r < 0 || r >= rows) return;
|
|
3719
|
+
const taper = s.taper ? strokeTaper(u) : 1;
|
|
3720
|
+
compose(r * cols + c, lum, a * Math.min(1, lw * taper / cellH), layer.blend);
|
|
3721
|
+
};
|
|
3722
|
+
const step = Math.min(cellW, cellH) / 2;
|
|
3723
|
+
stampAt(pts[0], 0);
|
|
3724
|
+
for (let i = 1; i < pts.length; i++) {
|
|
3725
|
+
const q0 = pts[i - 1];
|
|
3726
|
+
const q1 = pts[i];
|
|
3727
|
+
const segLen = Math.hypot(q1.x - q0.x, q1.y - q0.y);
|
|
3728
|
+
const sub = Math.max(1, Math.ceil(segLen / step));
|
|
3729
|
+
for (let k = 1; k <= sub; k++) {
|
|
3730
|
+
const f = k / sub;
|
|
3731
|
+
stampAt({ x: q0.x + (q1.x - q0.x) * f, y: q0.y + (q1.y - q0.y) * f }, (i - 1 + f) / (pts.length - 1));
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
continue;
|
|
3735
|
+
}
|
|
2830
3736
|
if (s.kind === "streak") {
|
|
2831
3737
|
const heading = headingAt(e, tPass, w, h) ?? 0;
|
|
2832
3738
|
const steps = Math.max(2, Math.ceil(sz / Math.min(cellW, cellH)));
|
|
@@ -2858,9 +3764,22 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
2858
3764
|
centerY = box.cy;
|
|
2859
3765
|
halfX = box.halfX;
|
|
2860
3766
|
halfY = box.halfY;
|
|
3767
|
+
} else if (s.kind === "bar") {
|
|
3768
|
+
const len = sz * barFraction(s, e.barIndex ?? 0);
|
|
3769
|
+
if (len <= 0) continue;
|
|
3770
|
+
const th = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz * 0.2;
|
|
3771
|
+
const box = barBox(s.direction ?? "right", len, th);
|
|
3772
|
+
const rot = rotationAt(e, tPass);
|
|
3773
|
+
const cr = Math.cos(rot);
|
|
3774
|
+
const sr = Math.sin(rot);
|
|
3775
|
+
centerX = p.x + box.cx * cr - box.cy * sr;
|
|
3776
|
+
centerY = p.y + box.cx * sr + box.cy * cr;
|
|
3777
|
+
halfX = Math.abs(box.halfX * cr) + Math.abs(box.halfY * sr);
|
|
3778
|
+
halfY = Math.abs(box.halfX * sr) + Math.abs(box.halfY * cr);
|
|
2861
3779
|
}
|
|
2862
|
-
const circular = s.kind === "circle" || s.kind === "ring";
|
|
2863
|
-
const soft = s.kind === "circle" && !!s.soft;
|
|
3780
|
+
const circular = s.kind === "circle" || s.kind === "ring" || s.kind === "polygon";
|
|
3781
|
+
const soft = (s.kind === "circle" || s.kind === "polygon") && !!s.soft;
|
|
3782
|
+
const shapeWeight = s.kind === "polygon" ? polygonFill(s, sz / 2) : s.kind === "rect" && s.feather ? (1 - s.feather / 2) * (1 - s.feather / 2) : 1;
|
|
2864
3783
|
const additive = layer.blend === "lighter" || layer.blend === "screen";
|
|
2865
3784
|
const glow = soft && additive;
|
|
2866
3785
|
const reachX = glow ? halfX * GLOW_SPREAD : halfX;
|
|
@@ -2893,9 +3812,9 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
2893
3812
|
} else {
|
|
2894
3813
|
wgt = soft ? Math.max(0.1, 1 - d / Math.max(halfX, 1e-6)) : 1;
|
|
2895
3814
|
}
|
|
2896
|
-
compose(r * cols + c, lum, a * wgt, layer.blend);
|
|
3815
|
+
compose(r * cols + c, lum, a * wgt * shapeWeight, layer.blend);
|
|
2897
3816
|
} else {
|
|
2898
|
-
compose(r * cols + c, lum, a * inkWeight, layer.blend);
|
|
3817
|
+
compose(r * cols + c, lum, a * inkWeight * shapeWeight, layer.blend);
|
|
2899
3818
|
}
|
|
2900
3819
|
}
|
|
2901
3820
|
}
|
|
@@ -3083,7 +4002,15 @@ function dominanceRanking(spec, opts = {}) {
|
|
|
3083
4002
|
else if (s.kind === "streak") entArea = sz * ((s.width ?? (scale === 1 ? 2 : 2e-3)) * scale) * LINE_SALIENCE;
|
|
3084
4003
|
else if (s.kind === "rect") {
|
|
3085
4004
|
const h2 = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz;
|
|
3086
|
-
|
|
4005
|
+
const f = s.feather ? (1 - s.feather / 2) * (1 - s.feather / 2) : 1;
|
|
4006
|
+
entArea = sz * h2 * f;
|
|
4007
|
+
} else if (s.kind === "polygon") entArea = polygonArea(polygonPoints(s, sz / 2));
|
|
4008
|
+
else if (s.kind === "bar") {
|
|
4009
|
+
const th = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz * 0.2;
|
|
4010
|
+
entArea = sz * barFraction(s, e.barIndex ?? 0) * th;
|
|
4011
|
+
} else if (s.kind === "stroke") {
|
|
4012
|
+
const meanTaper = s.taper ? 0.64 : 1;
|
|
4013
|
+
entArea = pathLength(strokeSamples(s, sz / 2)) * strokeWidthPx(s, scale) * meanTaper * LINE_SALIENCE;
|
|
3087
4014
|
} else if (s.kind === "text") {
|
|
3088
4015
|
const box = textBox(s, e, { x: 0, y: 0 });
|
|
3089
4016
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
|
|
@@ -3128,6 +4055,10 @@ function dominanceRanking(spec, opts = {}) {
|
|
|
3128
4055
|
factors: r.factors
|
|
3129
4056
|
}));
|
|
3130
4057
|
}
|
|
4058
|
+
function layerCohesion(spec, opts = {}) {
|
|
4059
|
+
const scene = buildScene(spec, opts);
|
|
4060
|
+
return cohesionOf(scene.layers, opts.t ?? 5e3, scene.w, scene.h);
|
|
4061
|
+
}
|
|
3131
4062
|
function motionStats(spec, opts = {}) {
|
|
3132
4063
|
const scene = buildScene(spec, opts);
|
|
3133
4064
|
const t = opts.t ?? 5e3;
|
|
@@ -3224,8 +4155,9 @@ function perceiveScene(spec, opts = {}) {
|
|
|
3224
4155
|
colProfile: grid.colProfile,
|
|
3225
4156
|
dominance: dominanceRanking(spec, opts),
|
|
3226
4157
|
motion: motionStats(spec, opts),
|
|
4158
|
+
form: layerCohesion(spec, opts),
|
|
3227
4159
|
text: textSprites(spec, opts),
|
|
3228
|
-
advisories: adviseSpec(spec, opts.viewport ?? { width: 1920, height: 1080 })
|
|
4160
|
+
advisories: adviseSpec(spec, opts.viewport ?? { width: 1920, height: 1080 }, { t: opts.t, seed: opts.seed })
|
|
3229
4161
|
};
|
|
3230
4162
|
}
|
|
3231
4163
|
|
|
@@ -3941,6 +4873,77 @@ var DASHBOARD_SPEC = {
|
|
|
3941
4873
|
]
|
|
3942
4874
|
};
|
|
3943
4875
|
|
|
4876
|
+
// src/examples/facets.ts
|
|
4877
|
+
var FACETS_SPEC = {
|
|
4878
|
+
schemaVersion: 1,
|
|
4879
|
+
id: "facets",
|
|
4880
|
+
label: "Facets",
|
|
4881
|
+
seed: 1911,
|
|
4882
|
+
motionIntensity: "calm",
|
|
4883
|
+
background: {
|
|
4884
|
+
type: "gradient",
|
|
4885
|
+
stops: [
|
|
4886
|
+
{ at: 0, color: "#101626" },
|
|
4887
|
+
{ at: 1, color: "#1c1a2e" }
|
|
4888
|
+
]
|
|
4889
|
+
},
|
|
4890
|
+
layers: [
|
|
4891
|
+
{
|
|
4892
|
+
key: "planes",
|
|
4893
|
+
count: 3,
|
|
4894
|
+
sprite: {
|
|
4895
|
+
kind: "rect",
|
|
4896
|
+
width: [0.28, 0.42],
|
|
4897
|
+
aspect: [0.6, 1.4],
|
|
4898
|
+
color: "#b3402f",
|
|
4899
|
+
colors: ["#b3402f", "#c9862b", "#2d4f8a"],
|
|
4900
|
+
feather: 0.6
|
|
4901
|
+
},
|
|
4902
|
+
alpha: [0.35, 0.5],
|
|
4903
|
+
region: { x: [0.2, 0.8], y: [0.2, 0.8] },
|
|
4904
|
+
motion: { type: "wander", speed: [1e-3, 3e-3], meander: 0.02 },
|
|
4905
|
+
spin: [-2, 2]
|
|
4906
|
+
},
|
|
4907
|
+
{
|
|
4908
|
+
key: "facets",
|
|
4909
|
+
count: 14,
|
|
4910
|
+
sprite: {
|
|
4911
|
+
kind: "polygon",
|
|
4912
|
+
radius: [0.02, 0.06],
|
|
4913
|
+
points: [[-1, 0.9], [0.2, -1], [1, 0.4]],
|
|
4914
|
+
color: "#f2e8c9",
|
|
4915
|
+
colors: ["#f2e8c9", "#e0b53a", "#6fa8dc", "#d94f3d"]
|
|
4916
|
+
},
|
|
4917
|
+
alpha: [0.6, 0.9],
|
|
4918
|
+
motion: { type: "wander", speed: [3e-3, 8e-3], meander: 0.03 },
|
|
4919
|
+
spin: [-8, 8]
|
|
4920
|
+
},
|
|
4921
|
+
{
|
|
4922
|
+
key: "arcs",
|
|
4923
|
+
count: 12,
|
|
4924
|
+
sprite: {
|
|
4925
|
+
kind: "stroke",
|
|
4926
|
+
length: [0.08, 0.18],
|
|
4927
|
+
points: [[-1, 0.3], [-0.5, -0.6], [0.3, -0.7], [1, 0.1]],
|
|
4928
|
+
width: 4e-3,
|
|
4929
|
+
taper: true,
|
|
4930
|
+
color: "#f2e8c9"
|
|
4931
|
+
},
|
|
4932
|
+
alpha: [0.5, 0.8],
|
|
4933
|
+
motion: { type: "drift", speed: [4e-3, 0.01], angle: 20, bidirectional: true },
|
|
4934
|
+
spin: [-4, 4]
|
|
4935
|
+
},
|
|
4936
|
+
{
|
|
4937
|
+
key: "hexes",
|
|
4938
|
+
count: 8,
|
|
4939
|
+
sprite: { kind: "polygon", radius: [8e-3, 0.016], sides: 6, color: "#e0b53a", soft: true },
|
|
4940
|
+
alpha: [0.5, 0.8],
|
|
4941
|
+
blend: "lighter",
|
|
4942
|
+
motion: { type: "wander", speed: [2e-3, 5e-3] }
|
|
4943
|
+
}
|
|
4944
|
+
]
|
|
4945
|
+
};
|
|
4946
|
+
|
|
3944
4947
|
// src/examples/haiku.ts
|
|
3945
4948
|
var HAIKU_SPEC = {
|
|
3946
4949
|
schemaVersion: 1,
|
|
@@ -4333,6 +5336,40 @@ var ORRERY_SPEC = {
|
|
|
4333
5336
|
]
|
|
4334
5337
|
};
|
|
4335
5338
|
|
|
5339
|
+
// src/examples/pings.ts
|
|
5340
|
+
var PINGS_SPEC = {
|
|
5341
|
+
schemaVersion: 1,
|
|
5342
|
+
id: "pings",
|
|
5343
|
+
label: "Pings",
|
|
5344
|
+
seed: 7,
|
|
5345
|
+
motionIntensity: "calm",
|
|
5346
|
+
ghosting: 0.9,
|
|
5347
|
+
background: { type: "solid", color: "#03060a" },
|
|
5348
|
+
layers: [
|
|
5349
|
+
{
|
|
5350
|
+
key: "ping",
|
|
5351
|
+
count: 3,
|
|
5352
|
+
sprite: { kind: "ring", radius: [0.05, 0.05], color: "#4fb3a8", width: 15e-4 },
|
|
5353
|
+
alpha: [0.35, 0.35],
|
|
5354
|
+
blend: "lighter",
|
|
5355
|
+
region: { x: [0.2, 0.8], y: [0.25, 0.75] },
|
|
5356
|
+
motion: { type: "static" },
|
|
5357
|
+
// One ring at a time: three entities evenly staggered across 12 s.
|
|
5358
|
+
emit: { every: 12e3, life: 4e3, jitter: 0, grow: [0.15, 2.4] }
|
|
5359
|
+
},
|
|
5360
|
+
{
|
|
5361
|
+
key: "motes",
|
|
5362
|
+
count: 14,
|
|
5363
|
+
sprite: { kind: "circle", radius: [12e-4, 2e-3], color: "#4fb3a8", soft: true },
|
|
5364
|
+
alpha: [0.1, 0.22],
|
|
5365
|
+
blend: "lighter",
|
|
5366
|
+
// Each mote is flung upward and comes to rest within ~2.5 s of its event.
|
|
5367
|
+
motion: { type: "rise", speed: [0.01, 0.02], sway: 4e-3, ease: { type: "settle", tau: 2500 } },
|
|
5368
|
+
emit: { every: 16e3, life: 7e3, grow: [0.6, 1.4] }
|
|
5369
|
+
}
|
|
5370
|
+
]
|
|
5371
|
+
};
|
|
5372
|
+
|
|
4336
5373
|
// src/examples/procession.ts
|
|
4337
5374
|
var PROCESSION_SPEC = {
|
|
4338
5375
|
schemaVersion: 1,
|
|
@@ -4429,6 +5466,74 @@ var RAIN_SPEC = {
|
|
|
4429
5466
|
]
|
|
4430
5467
|
};
|
|
4431
5468
|
|
|
5469
|
+
// src/examples/relay-board.ts
|
|
5470
|
+
var RELAY_BOARD_SPEC = {
|
|
5471
|
+
schemaVersion: 1,
|
|
5472
|
+
id: "relay-board",
|
|
5473
|
+
label: "Relay Board",
|
|
5474
|
+
seed: 9,
|
|
5475
|
+
motionIntensity: "calm",
|
|
5476
|
+
background: {
|
|
5477
|
+
type: "gradient",
|
|
5478
|
+
stops: [
|
|
5479
|
+
{ at: 0, color: "#05070d" },
|
|
5480
|
+
{ at: 1, color: "#0a0d18" }
|
|
5481
|
+
]
|
|
5482
|
+
},
|
|
5483
|
+
layers: [
|
|
5484
|
+
{
|
|
5485
|
+
key: "dust",
|
|
5486
|
+
count: 40,
|
|
5487
|
+
sprite: { kind: "circle", radius: [8e-4, 16e-4], color: "#22344a", soft: true },
|
|
5488
|
+
alpha: [0.2, 0.45],
|
|
5489
|
+
blend: "lighter",
|
|
5490
|
+
motion: { type: "wander", speed: [1e-3, 3e-3] }
|
|
5491
|
+
},
|
|
5492
|
+
{
|
|
5493
|
+
key: "title",
|
|
5494
|
+
count: 1,
|
|
5495
|
+
sprite: { kind: "textBlock", text: "RELAY LINKS \u2014 UPLINK MARGIN", maxWidth: 0.6, fontSize: 0.028, color: "#9fb4c9" },
|
|
5496
|
+
motion: { type: "static" },
|
|
5497
|
+
position: { x: 0.12, y: 0.16 }
|
|
5498
|
+
},
|
|
5499
|
+
{
|
|
5500
|
+
key: "labels",
|
|
5501
|
+
count: 6,
|
|
5502
|
+
sprite: { kind: "text", strings: ["VIREO-9", "TIDE", "ORRERY", "COMET", "LANTERN", "SAKURA"], font: "bold monospace", align: "left", color: "#e6e8ef" },
|
|
5503
|
+
size: [0.024, 0.024],
|
|
5504
|
+
motion: { type: "static" },
|
|
5505
|
+
position: { x: 0.12, y: 0.3 },
|
|
5506
|
+
layout: { type: "list", gap: 0.075 }
|
|
5507
|
+
},
|
|
5508
|
+
{
|
|
5509
|
+
key: "bars",
|
|
5510
|
+
count: 6,
|
|
5511
|
+
sprite: {
|
|
5512
|
+
kind: "bar",
|
|
5513
|
+
values: [82, 64, 91, 37, 58, 73],
|
|
5514
|
+
max: 100,
|
|
5515
|
+
length: 0.5,
|
|
5516
|
+
thickness: 0.02,
|
|
5517
|
+
color: "#17e8c8",
|
|
5518
|
+
colors: ["#17e8c8", "#35d0ff", "#ffd93b", "#ff6ad5", "#ad8cff", "#17e8c8"]
|
|
5519
|
+
},
|
|
5520
|
+
alpha: [0.85, 0.85],
|
|
5521
|
+
motion: { type: "static" },
|
|
5522
|
+
position: { x: 0.3, y: 0.3 },
|
|
5523
|
+
layout: { type: "list", gap: 0.075 }
|
|
5524
|
+
},
|
|
5525
|
+
{
|
|
5526
|
+
key: "readouts",
|
|
5527
|
+
count: 6,
|
|
5528
|
+
sprite: { kind: "text", strings: ["82%", "64%", "91%", "37%", "58%", "73%"], font: "monospace", align: "left", color: "#9fb4c9" },
|
|
5529
|
+
size: [0.02, 0.02],
|
|
5530
|
+
motion: { type: "static" },
|
|
5531
|
+
position: { x: 0.86, y: 0.3 },
|
|
5532
|
+
layout: { type: "list", gap: 0.075 }
|
|
5533
|
+
}
|
|
5534
|
+
]
|
|
5535
|
+
};
|
|
5536
|
+
|
|
4432
5537
|
// src/examples/sakura.ts
|
|
4433
5538
|
var SAKURA_SPEC = {
|
|
4434
5539
|
schemaVersion: 1,
|
|
@@ -4665,7 +5770,10 @@ var SCHEMA_EXAMPLES = [
|
|
|
4665
5770
|
{ id: "matrix-rain", label: "Matrix Rain", spec: MATRIX_RAIN_SPEC },
|
|
4666
5771
|
{ id: "procession", label: "Night Procession", spec: PROCESSION_SPEC },
|
|
4667
5772
|
{ id: "nostalghia-candle", label: "Nostalghia's Candle", spec: NOSTALGHIA_CANDLE_SPEC },
|
|
4668
|
-
{ id: "haiku", label: "Haiku", spec: HAIKU_SPEC }
|
|
5773
|
+
{ id: "haiku", label: "Haiku", spec: HAIKU_SPEC },
|
|
5774
|
+
{ id: "pings", label: "Pings", spec: PINGS_SPEC },
|
|
5775
|
+
{ id: "facets", label: "Facets", spec: FACETS_SPEC },
|
|
5776
|
+
{ id: "relay-board", label: "Relay Board", spec: RELAY_BOARD_SPEC }
|
|
4669
5777
|
];
|
|
4670
5778
|
var EXAMPLE_SPECS = SCHEMA_EXAMPLES.map((e) => e.spec);
|
|
4671
5779
|
var EXAMPLE_BY_ID = Object.fromEntries(
|
|
@@ -4674,20 +5782,24 @@ var EXAMPLE_BY_ID = Object.fromEntries(
|
|
|
4674
5782
|
export {
|
|
4675
5783
|
AQUARIUM_SPEC,
|
|
4676
5784
|
AURORA_SPEC,
|
|
5785
|
+
COHESION_T,
|
|
4677
5786
|
COMETS_SPEC,
|
|
4678
5787
|
CONSTELLATION_SPEC,
|
|
4679
5788
|
DASHBOARD_SPEC,
|
|
4680
5789
|
EXAMPLE_BY_ID,
|
|
4681
5790
|
EXAMPLE_SPECS,
|
|
5791
|
+
FACETS_SPEC,
|
|
4682
5792
|
HAIKU_SPEC,
|
|
4683
5793
|
LANTERNS_SPEC,
|
|
4684
5794
|
LIMITS,
|
|
4685
5795
|
MATRIX_RAIN_SPEC,
|
|
4686
5796
|
NOSTALGHIA_CANDLE_SPEC,
|
|
4687
5797
|
ORRERY_SPEC,
|
|
5798
|
+
PINGS_SPEC,
|
|
4688
5799
|
POLYGONS_SPEC,
|
|
4689
5800
|
PROCESSION_SPEC,
|
|
4690
5801
|
RAIN_SPEC,
|
|
5802
|
+
RELAY_BOARD_SPEC,
|
|
4691
5803
|
SAKURA_SPEC,
|
|
4692
5804
|
SCHEMA_EXAMPLES,
|
|
4693
5805
|
SCHEMA_VERSION,
|
|
@@ -4702,14 +5814,18 @@ export {
|
|
|
4702
5814
|
assertValidSpec,
|
|
4703
5815
|
breakTextBlock,
|
|
4704
5816
|
buildEntities,
|
|
5817
|
+
cohesionOf,
|
|
4705
5818
|
compileSaver,
|
|
4706
5819
|
compileSequence,
|
|
4707
5820
|
describeScene,
|
|
4708
5821
|
diffScenes,
|
|
4709
5822
|
dominanceRanking,
|
|
4710
5823
|
easeSmooth,
|
|
5824
|
+
emitEnvelope,
|
|
5825
|
+
emitWindow,
|
|
4711
5826
|
graphemeClusters,
|
|
4712
5827
|
headingAt,
|
|
5828
|
+
layerCohesion,
|
|
4713
5829
|
lerpSpec,
|
|
4714
5830
|
lifeAlphaAt,
|
|
4715
5831
|
linkEdges,
|
|
@@ -4725,12 +5841,16 @@ export {
|
|
|
4725
5841
|
resolveSpecPath,
|
|
4726
5842
|
revealState,
|
|
4727
5843
|
rotationAt,
|
|
5844
|
+
seamCauseOf,
|
|
5845
|
+
seamsWorthWarning,
|
|
5846
|
+
segmentStart,
|
|
4728
5847
|
sizeAt,
|
|
4729
5848
|
spriteIndexAt,
|
|
4730
5849
|
spriteVariants,
|
|
4731
5850
|
steerablePaths,
|
|
4732
5851
|
structuralSignature,
|
|
4733
5852
|
textSprites,
|
|
5853
|
+
textWidthEm,
|
|
4734
5854
|
validateSequence,
|
|
4735
5855
|
validateSpec
|
|
4736
5856
|
};
|