@idle-screens/schema 3.5.0 → 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 +114 -13
- package/dist/index.d.ts +316 -3
- package/dist/index.js +1012 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- 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
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") {
|
|
@@ -630,7 +792,7 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
630
792
|
}
|
|
631
793
|
}
|
|
632
794
|
} else {
|
|
633
|
-
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");
|
|
634
796
|
return;
|
|
635
797
|
}
|
|
636
798
|
for (const k of unknownKeys(sprite, knownSet)) {
|
|
@@ -683,6 +845,7 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
683
845
|
if (motion.angle !== void 0 && !isNum(motion.angle)) err(`${path}.angle`, "must be a number (degrees)");
|
|
684
846
|
if (motion.bidirectional !== void 0 && typeof motion.bidirectional !== "boolean") err(`${path}.bidirectional`, "must be a boolean");
|
|
685
847
|
if (motion.bob !== void 0 && !isNum(motion.bob)) err(`${path}.bob`, "must be a number");
|
|
848
|
+
validateEase(motion.ease, `${path}.ease`, err, warn);
|
|
686
849
|
if (isRange(motion.speed) && motion.speed[1] < 1 && !isViewport) {
|
|
687
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`);
|
|
688
851
|
}
|
|
@@ -690,6 +853,7 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
690
853
|
knownSet = KNOWN_RISE;
|
|
691
854
|
speedOk(motion.speed, `${path}.speed`);
|
|
692
855
|
if (motion.sway !== void 0 && !isNum(motion.sway)) err(`${path}.sway`, "must be a number");
|
|
856
|
+
validateEase(motion.ease, `${path}.ease`, err, warn);
|
|
693
857
|
if (isRange(motion.speed) && motion.speed[1] < 1 && !isViewport) {
|
|
694
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`);
|
|
695
859
|
}
|
|
@@ -739,6 +903,7 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
739
903
|
}
|
|
740
904
|
} else if (motion.type === "wander") {
|
|
741
905
|
knownSet = KNOWN_WANDER;
|
|
906
|
+
validateEase(motion.ease, `${path}.ease`, err, warn);
|
|
742
907
|
speedOk(motion.speed, `${path}.speed`);
|
|
743
908
|
if (motion.angle !== void 0 && !isNum(motion.angle)) err(`${path}.angle`, "must be a number (degrees)");
|
|
744
909
|
const meanderCap = isViewport ? LIMITS.maxMeander / refVp : LIMITS.maxMeander;
|
|
@@ -790,6 +955,28 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
790
955
|
warn(`${path}.${k}`, "unknown-property", `unknown motion property '${k}' \u2014 will be ignored`);
|
|
791
956
|
}
|
|
792
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
|
+
}
|
|
793
980
|
function assertValidSpec(spec) {
|
|
794
981
|
const r = validateSpec(spec);
|
|
795
982
|
if (!r.valid) {
|
|
@@ -907,6 +1094,110 @@ ${r.errors.map((e) => ` ${e.path || "<root>"}: ${e.message}`).join("\n")}`);
|
|
|
907
1094
|
return seq;
|
|
908
1095
|
}
|
|
909
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
|
+
|
|
910
1201
|
// src/simulate.ts
|
|
911
1202
|
var WARP_NEAR = 0.08;
|
|
912
1203
|
var WARP_MAX_SCALE = 8;
|
|
@@ -935,6 +1226,21 @@ function drawOsc(rng, amp) {
|
|
|
935
1226
|
}
|
|
936
1227
|
return osc;
|
|
937
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
|
+
}
|
|
938
1244
|
function weightedIndex(u, weights) {
|
|
939
1245
|
const total = weights.reduce((a, b) => a + b, 0);
|
|
940
1246
|
let acc = 0;
|
|
@@ -945,15 +1251,15 @@ function weightedIndex(u, weights) {
|
|
|
945
1251
|
return weights.length - 1;
|
|
946
1252
|
}
|
|
947
1253
|
function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
948
|
-
const effectiveCount = countScale === 1 || layer.layout
|
|
1254
|
+
const effectiveCount = countScale === 1 || layer.layout !== void 0 ? layer.count : Math.max(1, Math.min(
|
|
949
1255
|
Math.round(layer.count * countScale),
|
|
950
1256
|
layer.links ? LIMITS.maxLinkLayerCount : LIMITS.maxPerLayer
|
|
951
1257
|
));
|
|
952
1258
|
const [smin, smax] = layer.size ?? [20, 40];
|
|
953
1259
|
const variants = spriteVariants(layer.sprite);
|
|
954
1260
|
const sprite = layer.sprite;
|
|
955
|
-
const spriteColors = sprite
|
|
956
|
-
const colorWeights = sprite
|
|
1261
|
+
const spriteColors = isShapedSprite(sprite) ? sprite.colors : void 0;
|
|
1262
|
+
const colorWeights = isShapedSprite(sprite) ? sprite.colorWeights : void 0;
|
|
957
1263
|
const colorsLen = spriteColors?.length ?? 0;
|
|
958
1264
|
const cycle = sprite.kind === "emoji" || sprite.kind === "text" ? sprite.cycle : void 0;
|
|
959
1265
|
const mSpec = layer.motion;
|
|
@@ -967,7 +1273,32 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
967
1273
|
if (mSpec.type === "path") {
|
|
968
1274
|
pathPts = mSpec.points.map((p) => ({ x: p.x * w, y: p.y * h }));
|
|
969
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;
|
|
970
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
|
+
}
|
|
971
1302
|
let gridCols = 0;
|
|
972
1303
|
let cellW = 0;
|
|
973
1304
|
let cellH = 0;
|
|
@@ -990,8 +1321,8 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
990
1321
|
}
|
|
991
1322
|
const out = [];
|
|
992
1323
|
for (let i = 0; i < effectiveCount; i++) {
|
|
993
|
-
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;
|
|
994
|
-
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;
|
|
995
1326
|
let vx = 0;
|
|
996
1327
|
let vy = 0;
|
|
997
1328
|
let bob = 0;
|
|
@@ -1055,6 +1386,11 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1055
1386
|
if (layer.position && layer.count === 1) {
|
|
1056
1387
|
x0 = layer.position.x * w;
|
|
1057
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;
|
|
1058
1394
|
} else if (layout?.type === "grid") {
|
|
1059
1395
|
const col = i % gridCols;
|
|
1060
1396
|
const row = Math.floor(i / gridCols);
|
|
@@ -1074,7 +1410,10 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1074
1410
|
x0,
|
|
1075
1411
|
y0,
|
|
1076
1412
|
size,
|
|
1077
|
-
|
|
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,
|
|
1078
1417
|
phase: rng.range(0, Math.PI * 2),
|
|
1079
1418
|
vx,
|
|
1080
1419
|
vy,
|
|
@@ -1098,10 +1437,12 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1098
1437
|
orbitCx: m.type === "orbit" && !orbitParent ? (m.center?.x ?? 0.5) * w : 0,
|
|
1099
1438
|
orbitCy: m.type === "orbit" && !orbitParent ? (m.center?.y ?? 0.5) * h : 0,
|
|
1100
1439
|
// New draws AFTER orbit — guarded so existing specs keep identical streams.
|
|
1101
|
-
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,
|
|
1102
1441
|
cyclePeriod: cycle ? cycle.period : 0,
|
|
1103
1442
|
// Optional feature fields last (guarded draws inside — wander draws 18 here).
|
|
1104
1443
|
...size2 !== void 0 ? { size2 } : {},
|
|
1444
|
+
...sprite.kind === "bar" ? { barIndex: i } : {},
|
|
1445
|
+
...ordered ? { ordered: true } : {},
|
|
1105
1446
|
...orbitParent ? { orbitParent } : {},
|
|
1106
1447
|
...m.type === "wander" ? {
|
|
1107
1448
|
wander: {
|
|
@@ -1122,33 +1463,80 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1122
1463
|
offX: pathOffX,
|
|
1123
1464
|
offY: pathOffY
|
|
1124
1465
|
}
|
|
1125
|
-
} : {}
|
|
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 } } : {}
|
|
1126
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
|
+
}
|
|
1127
1478
|
if (layer.pulse?.wave) {
|
|
1128
1479
|
const wl = Math.max(1e-6, layer.pulse.wave.wavelength * scale);
|
|
1129
1480
|
const ang = (layer.pulse.wave.angle ?? 0) * Math.PI / 180;
|
|
1130
|
-
out[out.length - 1].pulsePhase = -(
|
|
1481
|
+
out[out.length - 1].pulsePhase = clockPhase - (x0 * Math.cos(ang) + y0 * Math.sin(ang)) / wl * Math.PI * 2;
|
|
1131
1482
|
}
|
|
1132
1483
|
}
|
|
1484
|
+
if (layer.emit) {
|
|
1485
|
+
for (let i = 0; i < out.length; i++) out[i].emit = emitParams(layer.emit, i, out.length);
|
|
1486
|
+
}
|
|
1133
1487
|
return out;
|
|
1134
1488
|
}
|
|
1135
1489
|
function warpZ(e, t) {
|
|
1136
1490
|
const wp = e.warp;
|
|
1137
1491
|
return wrap(wp.z0 - wp.vz * (t / 1e3), WARP_NEAR, 1);
|
|
1138
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
|
+
}
|
|
1139
1519
|
function alphaAt(e, t) {
|
|
1140
1520
|
let a = e.alpha;
|
|
1141
|
-
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);
|
|
1142
1522
|
if (e.motion === "warp") {
|
|
1143
1523
|
const z = warpZ(e, t);
|
|
1144
1524
|
a *= Math.max(0, Math.min(1, (1 - z) / 0.2));
|
|
1145
1525
|
}
|
|
1526
|
+
if (e.emit) {
|
|
1527
|
+
const u = emitWindow(e, t);
|
|
1528
|
+
a = u === null ? 0 : a * emitEnvelope(u);
|
|
1529
|
+
}
|
|
1146
1530
|
return a < 0 ? 0 : a > 1 ? 1 : a;
|
|
1147
1531
|
}
|
|
1148
1532
|
function sizeAt(e, t) {
|
|
1149
1533
|
let s = e.size;
|
|
1150
|
-
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));
|
|
1151
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
|
+
}
|
|
1152
1540
|
return s > 0 ? s : 0.1;
|
|
1153
1541
|
}
|
|
1154
1542
|
function rotationAt(e, t) {
|
|
@@ -1179,7 +1567,7 @@ function reflect(v, min, max) {
|
|
|
1179
1567
|
const p = ((v - min) % period + period) % period;
|
|
1180
1568
|
return min + (p < range ? p : period - p);
|
|
1181
1569
|
}
|
|
1182
|
-
function
|
|
1570
|
+
function catmullRom2(p0, p1, p2, p3, u) {
|
|
1183
1571
|
const u2 = u * u;
|
|
1184
1572
|
const u3 = u2 * u;
|
|
1185
1573
|
return 0.5 * (2 * p1 + (-p0 + p2) * u + (2 * p0 - 5 * p1 + 4 * p2 - p3) * u2 + (-p0 + 3 * p1 - 3 * p2 + p3) * u3);
|
|
@@ -1209,8 +1597,8 @@ function pathPosition(e, t) {
|
|
|
1209
1597
|
const p0 = at(i - 1);
|
|
1210
1598
|
const p3 = at(i + 2);
|
|
1211
1599
|
return {
|
|
1212
|
-
x:
|
|
1213
|
-
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
|
|
1214
1602
|
};
|
|
1215
1603
|
}
|
|
1216
1604
|
function positionAt(e, t, w, h) {
|
|
@@ -1237,7 +1625,7 @@ function positionAt(e, t, w, h) {
|
|
|
1237
1625
|
const pos = pathPosition(e, t);
|
|
1238
1626
|
return { x: pos.x, y: pos.y, flip: false };
|
|
1239
1627
|
}
|
|
1240
|
-
const dt = t
|
|
1628
|
+
const dt = motionSeconds(e, t);
|
|
1241
1629
|
const m = e.size;
|
|
1242
1630
|
if (e.motion === "bounce") {
|
|
1243
1631
|
return {
|
|
@@ -1294,7 +1682,8 @@ function headingAt(e, t, w, h) {
|
|
|
1294
1682
|
}
|
|
1295
1683
|
function spriteIndexAt(e, t, variants) {
|
|
1296
1684
|
if (!e.cyclePeriod || variants <= 1) return e.spriteIndex;
|
|
1297
|
-
|
|
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;
|
|
1298
1687
|
}
|
|
1299
1688
|
function wrapDist(pi, pj, doWrap, w, h) {
|
|
1300
1689
|
let dx = pj.x - pi.x;
|
|
@@ -1756,7 +2145,7 @@ var SpecInstance = class {
|
|
|
1756
2145
|
const headAlpha = alphaAt(e, t) * lifeA;
|
|
1757
2146
|
const headSize = sizeAt(e, t);
|
|
1758
2147
|
const sprite = built.layer.sprite;
|
|
1759
|
-
const resolvedColor = sprite
|
|
2148
|
+
const resolvedColor = isShapedSprite(sprite) ? sprite.colors?.[e.colorIndex] ?? sprite.color : sprite.kind === "text" || sprite.kind === "textBlock" ? sprite.color ?? "#e6e8ef" : "#e6e8ef";
|
|
1760
2149
|
const isSoft = sprite.kind === "circle" && sprite.soft;
|
|
1761
2150
|
const wrap2 = built.layer.wrap !== false;
|
|
1762
2151
|
const head = this.entityPos(e, t, parentE);
|
|
@@ -1835,7 +2224,90 @@ var SpecInstance = class {
|
|
|
1835
2224
|
ctx.translate(p.x, p.y);
|
|
1836
2225
|
if (rot) ctx.rotate(rot);
|
|
1837
2226
|
ctx.fillStyle = resolvedColor;
|
|
1838
|
-
|
|
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
|
+
}
|
|
1839
2311
|
ctx.restore();
|
|
1840
2312
|
return;
|
|
1841
2313
|
}
|
|
@@ -2010,7 +2482,7 @@ var SpecInstance = class {
|
|
|
2010
2482
|
let resolvedColor = links.color;
|
|
2011
2483
|
if (!resolvedColor) {
|
|
2012
2484
|
const sprite = built.layer.sprite;
|
|
2013
|
-
if (sprite
|
|
2485
|
+
if (isShapedSprite(sprite)) {
|
|
2014
2486
|
resolvedColor = sprite.colors?.[ei.colorIndex] ?? sprite.color;
|
|
2015
2487
|
} else resolvedColor = "#e6e8ef";
|
|
2016
2488
|
}
|
|
@@ -2373,7 +2845,7 @@ function hexLuma(hex) {
|
|
|
2373
2845
|
}
|
|
2374
2846
|
function spriteLuma(layer, e) {
|
|
2375
2847
|
const s = layer.sprite;
|
|
2376
|
-
if (s
|
|
2848
|
+
if (isShapedSprite(s)) {
|
|
2377
2849
|
return hexLuma(s.colors?.[e.colorIndex] ?? s.color);
|
|
2378
2850
|
}
|
|
2379
2851
|
if (s.kind === "text" || s.kind === "textBlock") return hexLuma(s.color ?? "#e6e8ef");
|
|
@@ -2381,7 +2853,7 @@ function spriteLuma(layer, e) {
|
|
|
2381
2853
|
}
|
|
2382
2854
|
function spriteHex(layer, e) {
|
|
2383
2855
|
const s = layer.sprite;
|
|
2384
|
-
if (s
|
|
2856
|
+
if (isShapedSprite(s)) {
|
|
2385
2857
|
return s.colors?.[e.colorIndex] ?? s.color;
|
|
2386
2858
|
}
|
|
2387
2859
|
if (s.kind === "text" || s.kind === "textBlock") return s.color ?? "#e6e8ef";
|
|
@@ -2419,19 +2891,237 @@ function colourSeparation(layer, sprite, bg) {
|
|
|
2419
2891
|
return Math.sqrt((dr * dr + dg * dg + db * db) / 3);
|
|
2420
2892
|
}
|
|
2421
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
|
+
|
|
2422
3101
|
// src/advise.ts
|
|
2423
3102
|
var LOW_CONTRAST_FLOOR = 0.05;
|
|
2424
|
-
|
|
3103
|
+
var SPARSE_DECLARED_MAX_COVERAGE = 0.02;
|
|
3104
|
+
function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
2425
3105
|
const warnings = [];
|
|
2426
3106
|
const w = viewport.width;
|
|
2427
3107
|
const h = viewport.height;
|
|
2428
3108
|
const scale = spec.units === "px" ? 1 : Math.min(w, h);
|
|
2429
3109
|
const refVp = spec.referenceViewport ?? LIMITS.referenceViewport;
|
|
2430
|
-
|
|
2431
|
-
|
|
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);
|
|
2432
3116
|
const allEntities = spec.layers.map((l) => buildEntities(l, rng, w, h, scale, countScale));
|
|
2433
3117
|
const bgLuma = backgroundLuma(spec);
|
|
2434
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
|
+
);
|
|
2435
3125
|
let totalEntities = 0;
|
|
2436
3126
|
let textLayerCount = 0;
|
|
2437
3127
|
let motionLayerCount = 0;
|
|
@@ -2443,6 +3133,14 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
2443
3133
|
const isStaticText = isText && layer.motion.type === "static";
|
|
2444
3134
|
if (isStaticText) textLayerCount++;
|
|
2445
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
|
+
}
|
|
2446
3144
|
if (layer.trail && layer.motion.type === "static") {
|
|
2447
3145
|
warnings.push({
|
|
2448
3146
|
path: `layers[${li}].trail`,
|
|
@@ -2450,11 +3148,11 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
2450
3148
|
message: "trail has no effect on static entities \u2014 they have no past positions to draw"
|
|
2451
3149
|
});
|
|
2452
3150
|
}
|
|
2453
|
-
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") {
|
|
2454
3152
|
warnings.push({
|
|
2455
3153
|
path: `layers[${li}].sprite`,
|
|
2456
3154
|
code: "streak-on-static",
|
|
2457
|
-
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"}`
|
|
2458
3156
|
});
|
|
2459
3157
|
}
|
|
2460
3158
|
if (layer.motion.type === "wander" && layer.motion.coherence === 1 && entities.length >= 20) {
|
|
@@ -2501,6 +3199,8 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
2501
3199
|
let pixArea;
|
|
2502
3200
|
if (layer.sprite.kind === "circle") {
|
|
2503
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);
|
|
2504
3204
|
} else if (layer.sprite.kind === "textBlock") {
|
|
2505
3205
|
const fsPx = layer.sprite.fontSize * scale;
|
|
2506
3206
|
const lh = (layer.sprite.lineHeight ?? 1.4) * fsPx;
|
|
@@ -2510,7 +3210,13 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
2510
3210
|
} else {
|
|
2511
3211
|
pixArea = e.size * e.size;
|
|
2512
3212
|
}
|
|
2513
|
-
|
|
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);
|
|
2514
3220
|
}
|
|
2515
3221
|
if (layer.links) {
|
|
2516
3222
|
const positions = entities.map((e) => positionAt(e, 0, w, h));
|
|
@@ -2521,20 +3227,35 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
2521
3227
|
for (const edge of edges) totalCoverage += edge.dist * lwPx * la / (w * h);
|
|
2522
3228
|
}
|
|
2523
3229
|
}
|
|
2524
|
-
|
|
3230
|
+
const density = spec.density ?? "normal";
|
|
3231
|
+
const wouldBeSparse = totalCoverage < 5e-4 && spec.layers.length > 0;
|
|
3232
|
+
if (wouldBeSparse && density !== "sparse") {
|
|
2525
3233
|
warnings.push({
|
|
2526
3234
|
path: "layers",
|
|
2527
3235
|
code: "sparse-scene",
|
|
2528
3236
|
message: `alpha-weighted coverage is ${(totalCoverage * 100).toFixed(4)}% \u2014 scene will look empty on most displays`
|
|
2529
3237
|
});
|
|
2530
3238
|
}
|
|
2531
|
-
if (totalEntities > 500) {
|
|
3239
|
+
if (totalEntities > 500 && density !== "dense") {
|
|
2532
3240
|
warnings.push({
|
|
2533
3241
|
path: "layers",
|
|
2534
3242
|
code: "dense-scene",
|
|
2535
3243
|
message: `${totalEntities} entities \u2014 scene may feel crowded and hurt performance on low-end devices`
|
|
2536
3244
|
});
|
|
2537
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
|
+
}
|
|
2538
3259
|
if (spec.layers.length > 0 && textLayerCount > 0 && motionLayerCount === 0) {
|
|
2539
3260
|
const textRatio = textLayerCount / spec.layers.length;
|
|
2540
3261
|
if (textRatio >= 0.8) {
|
|
@@ -2982,6 +3703,36 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
2982
3703
|
const p = posOf(scene, e, tPass);
|
|
2983
3704
|
const sz = sizeAt(e, tPass);
|
|
2984
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
|
+
}
|
|
2985
3736
|
if (s.kind === "streak") {
|
|
2986
3737
|
const heading = headingAt(e, tPass, w, h) ?? 0;
|
|
2987
3738
|
const steps = Math.max(2, Math.ceil(sz / Math.min(cellW, cellH)));
|
|
@@ -3013,9 +3764,22 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
3013
3764
|
centerY = box.cy;
|
|
3014
3765
|
halfX = box.halfX;
|
|
3015
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);
|
|
3016
3779
|
}
|
|
3017
|
-
const circular = s.kind === "circle" || s.kind === "ring";
|
|
3018
|
-
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;
|
|
3019
3783
|
const additive = layer.blend === "lighter" || layer.blend === "screen";
|
|
3020
3784
|
const glow = soft && additive;
|
|
3021
3785
|
const reachX = glow ? halfX * GLOW_SPREAD : halfX;
|
|
@@ -3048,9 +3812,9 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
3048
3812
|
} else {
|
|
3049
3813
|
wgt = soft ? Math.max(0.1, 1 - d / Math.max(halfX, 1e-6)) : 1;
|
|
3050
3814
|
}
|
|
3051
|
-
compose(r * cols + c, lum, a * wgt, layer.blend);
|
|
3815
|
+
compose(r * cols + c, lum, a * wgt * shapeWeight, layer.blend);
|
|
3052
3816
|
} else {
|
|
3053
|
-
compose(r * cols + c, lum, a * inkWeight, layer.blend);
|
|
3817
|
+
compose(r * cols + c, lum, a * inkWeight * shapeWeight, layer.blend);
|
|
3054
3818
|
}
|
|
3055
3819
|
}
|
|
3056
3820
|
}
|
|
@@ -3238,7 +4002,15 @@ function dominanceRanking(spec, opts = {}) {
|
|
|
3238
4002
|
else if (s.kind === "streak") entArea = sz * ((s.width ?? (scale === 1 ? 2 : 2e-3)) * scale) * LINE_SALIENCE;
|
|
3239
4003
|
else if (s.kind === "rect") {
|
|
3240
4004
|
const h2 = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz;
|
|
3241
|
-
|
|
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;
|
|
3242
4014
|
} else if (s.kind === "text") {
|
|
3243
4015
|
const box = textBox(s, e, { x: 0, y: 0 });
|
|
3244
4016
|
entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
|
|
@@ -3283,6 +4055,10 @@ function dominanceRanking(spec, opts = {}) {
|
|
|
3283
4055
|
factors: r.factors
|
|
3284
4056
|
}));
|
|
3285
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
|
+
}
|
|
3286
4062
|
function motionStats(spec, opts = {}) {
|
|
3287
4063
|
const scene = buildScene(spec, opts);
|
|
3288
4064
|
const t = opts.t ?? 5e3;
|
|
@@ -3379,8 +4155,9 @@ function perceiveScene(spec, opts = {}) {
|
|
|
3379
4155
|
colProfile: grid.colProfile,
|
|
3380
4156
|
dominance: dominanceRanking(spec, opts),
|
|
3381
4157
|
motion: motionStats(spec, opts),
|
|
4158
|
+
form: layerCohesion(spec, opts),
|
|
3382
4159
|
text: textSprites(spec, opts),
|
|
3383
|
-
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 })
|
|
3384
4161
|
};
|
|
3385
4162
|
}
|
|
3386
4163
|
|
|
@@ -4096,6 +4873,77 @@ var DASHBOARD_SPEC = {
|
|
|
4096
4873
|
]
|
|
4097
4874
|
};
|
|
4098
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
|
+
|
|
4099
4947
|
// src/examples/haiku.ts
|
|
4100
4948
|
var HAIKU_SPEC = {
|
|
4101
4949
|
schemaVersion: 1,
|
|
@@ -4488,6 +5336,40 @@ var ORRERY_SPEC = {
|
|
|
4488
5336
|
]
|
|
4489
5337
|
};
|
|
4490
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
|
+
|
|
4491
5373
|
// src/examples/procession.ts
|
|
4492
5374
|
var PROCESSION_SPEC = {
|
|
4493
5375
|
schemaVersion: 1,
|
|
@@ -4584,6 +5466,74 @@ var RAIN_SPEC = {
|
|
|
4584
5466
|
]
|
|
4585
5467
|
};
|
|
4586
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
|
+
|
|
4587
5537
|
// src/examples/sakura.ts
|
|
4588
5538
|
var SAKURA_SPEC = {
|
|
4589
5539
|
schemaVersion: 1,
|
|
@@ -4820,7 +5770,10 @@ var SCHEMA_EXAMPLES = [
|
|
|
4820
5770
|
{ id: "matrix-rain", label: "Matrix Rain", spec: MATRIX_RAIN_SPEC },
|
|
4821
5771
|
{ id: "procession", label: "Night Procession", spec: PROCESSION_SPEC },
|
|
4822
5772
|
{ id: "nostalghia-candle", label: "Nostalghia's Candle", spec: NOSTALGHIA_CANDLE_SPEC },
|
|
4823
|
-
{ 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 }
|
|
4824
5777
|
];
|
|
4825
5778
|
var EXAMPLE_SPECS = SCHEMA_EXAMPLES.map((e) => e.spec);
|
|
4826
5779
|
var EXAMPLE_BY_ID = Object.fromEntries(
|
|
@@ -4829,20 +5782,24 @@ var EXAMPLE_BY_ID = Object.fromEntries(
|
|
|
4829
5782
|
export {
|
|
4830
5783
|
AQUARIUM_SPEC,
|
|
4831
5784
|
AURORA_SPEC,
|
|
5785
|
+
COHESION_T,
|
|
4832
5786
|
COMETS_SPEC,
|
|
4833
5787
|
CONSTELLATION_SPEC,
|
|
4834
5788
|
DASHBOARD_SPEC,
|
|
4835
5789
|
EXAMPLE_BY_ID,
|
|
4836
5790
|
EXAMPLE_SPECS,
|
|
5791
|
+
FACETS_SPEC,
|
|
4837
5792
|
HAIKU_SPEC,
|
|
4838
5793
|
LANTERNS_SPEC,
|
|
4839
5794
|
LIMITS,
|
|
4840
5795
|
MATRIX_RAIN_SPEC,
|
|
4841
5796
|
NOSTALGHIA_CANDLE_SPEC,
|
|
4842
5797
|
ORRERY_SPEC,
|
|
5798
|
+
PINGS_SPEC,
|
|
4843
5799
|
POLYGONS_SPEC,
|
|
4844
5800
|
PROCESSION_SPEC,
|
|
4845
5801
|
RAIN_SPEC,
|
|
5802
|
+
RELAY_BOARD_SPEC,
|
|
4846
5803
|
SAKURA_SPEC,
|
|
4847
5804
|
SCHEMA_EXAMPLES,
|
|
4848
5805
|
SCHEMA_VERSION,
|
|
@@ -4857,14 +5814,18 @@ export {
|
|
|
4857
5814
|
assertValidSpec,
|
|
4858
5815
|
breakTextBlock,
|
|
4859
5816
|
buildEntities,
|
|
5817
|
+
cohesionOf,
|
|
4860
5818
|
compileSaver,
|
|
4861
5819
|
compileSequence,
|
|
4862
5820
|
describeScene,
|
|
4863
5821
|
diffScenes,
|
|
4864
5822
|
dominanceRanking,
|
|
4865
5823
|
easeSmooth,
|
|
5824
|
+
emitEnvelope,
|
|
5825
|
+
emitWindow,
|
|
4866
5826
|
graphemeClusters,
|
|
4867
5827
|
headingAt,
|
|
5828
|
+
layerCohesion,
|
|
4868
5829
|
lerpSpec,
|
|
4869
5830
|
lifeAlphaAt,
|
|
4870
5831
|
linkEdges,
|
|
@@ -4880,6 +5841,8 @@ export {
|
|
|
4880
5841
|
resolveSpecPath,
|
|
4881
5842
|
revealState,
|
|
4882
5843
|
rotationAt,
|
|
5844
|
+
seamCauseOf,
|
|
5845
|
+
seamsWorthWarning,
|
|
4883
5846
|
segmentStart,
|
|
4884
5847
|
sizeAt,
|
|
4885
5848
|
spriteIndexAt,
|