@idle-screens/schema 2.2.0 → 2.3.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 +157 -36
- package/dist/index.d.ts +458 -25
- package/dist/index.js +1777 -347
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +185 -7
package/dist/index.js
CHANGED
|
@@ -28,8 +28,21 @@ var LIMITS = {
|
|
|
28
28
|
// dots per trail
|
|
29
29
|
minDriftPeriod: 1e4,
|
|
30
30
|
// ms — background drift floor (10 s)
|
|
31
|
-
maxDriftAmount: 0.3
|
|
31
|
+
maxDriftAmount: 0.3,
|
|
32
32
|
// fraction of gradient stop shift
|
|
33
|
+
maxGhosting: 0.95,
|
|
34
|
+
// frame persistence cap — bounds the seek warm-up replay
|
|
35
|
+
maxGhostReplayFrames: 120,
|
|
36
|
+
// fixed-step frames replayed on a non-contiguous seek
|
|
37
|
+
maxMeander: 500,
|
|
38
|
+
// px — wander harmonic amplitude cap (viewport cap: /referenceViewport)
|
|
39
|
+
maxWarpSpeed: 1.5,
|
|
40
|
+
// depth-units/sec — full near-to-far span in ~0.7 s at max
|
|
41
|
+
minPathPoints: 2,
|
|
42
|
+
maxPathPoints: 24,
|
|
43
|
+
minPathDuration: 2e3,
|
|
44
|
+
// ms — a path lap can't be faster than this
|
|
45
|
+
maxGridColumns: 100
|
|
33
46
|
};
|
|
34
47
|
|
|
35
48
|
// src/validate.ts
|
|
@@ -38,7 +51,7 @@ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
|
38
51
|
var isStr = (v) => typeof v === "string";
|
|
39
52
|
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
40
53
|
var isRange = (v) => Array.isArray(v) && v.length === 2 && isNum(v[0]) && isNum(v[1]) && v[0] <= v[1];
|
|
41
|
-
var KNOWN_TOP = /* @__PURE__ */ new Set(["schemaVersion", "id", "label", "seed", "motionIntensity", "units", "background", "layers"]);
|
|
54
|
+
var KNOWN_TOP = /* @__PURE__ */ new Set(["schemaVersion", "id", "label", "seed", "motionIntensity", "units", "referenceViewport", "background", "layers", "ghosting"]);
|
|
42
55
|
var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
43
56
|
"count",
|
|
44
57
|
"sprite",
|
|
@@ -55,9 +68,14 @@ var KNOWN_LAYER = /* @__PURE__ */ new Set([
|
|
|
55
68
|
"key",
|
|
56
69
|
"position",
|
|
57
70
|
"trail",
|
|
58
|
-
"links"
|
|
71
|
+
"links",
|
|
72
|
+
"layout",
|
|
73
|
+
"life"
|
|
59
74
|
]);
|
|
60
|
-
var KNOWN_CIRCLE = /* @__PURE__ */ new Set(["kind", "radius", "color", "soft", "colors"]);
|
|
75
|
+
var KNOWN_CIRCLE = /* @__PURE__ */ new Set(["kind", "radius", "color", "soft", "colors", "colorWeights"]);
|
|
76
|
+
var KNOWN_RING = /* @__PURE__ */ new Set(["kind", "radius", "color", "width", "colors", "colorWeights"]);
|
|
77
|
+
var KNOWN_STREAK = /* @__PURE__ */ new Set(["kind", "length", "color", "width", "colors", "colorWeights"]);
|
|
78
|
+
var KNOWN_RECT = /* @__PURE__ */ new Set(["kind", "width", "aspect", "color", "colors", "colorWeights"]);
|
|
61
79
|
var KNOWN_EMOJI = /* @__PURE__ */ new Set(["kind", "glyphs", "cycle"]);
|
|
62
80
|
var KNOWN_TEXT = /* @__PURE__ */ new Set(["kind", "strings", "color", "font", "align", "baseline", "maxWidth", "cycle"]);
|
|
63
81
|
var KNOWN_DRIFT = /* @__PURE__ */ new Set(["type", "speed", "angle", "bidirectional", "bob"]);
|
|
@@ -65,6 +83,9 @@ var KNOWN_RISE = /* @__PURE__ */ new Set(["type", "speed", "sway"]);
|
|
|
65
83
|
var KNOWN_BOUNCE = /* @__PURE__ */ new Set(["type", "speed"]);
|
|
66
84
|
var KNOWN_STATIC = /* @__PURE__ */ new Set(["type"]);
|
|
67
85
|
var KNOWN_ORBIT = /* @__PURE__ */ new Set(["type", "speed", "radius", "center"]);
|
|
86
|
+
var KNOWN_WANDER = /* @__PURE__ */ new Set(["type", "speed", "angle", "meander", "coherence"]);
|
|
87
|
+
var KNOWN_WARP = /* @__PURE__ */ new Set(["type", "speed", "center"]);
|
|
88
|
+
var KNOWN_PATH = /* @__PURE__ */ new Set(["type", "points", "duration", "curve", "closed", "scatter"]);
|
|
68
89
|
var KNOWN_BG_SOLID = /* @__PURE__ */ new Set(["type", "color"]);
|
|
69
90
|
var KNOWN_BG_GRADIENT = /* @__PURE__ */ new Set(["type", "stops", "band", "drift"]);
|
|
70
91
|
var LAYER_PROPS_ON_SPRITE = /* @__PURE__ */ new Set(["blend", "trail", "alpha", "pulse", "spin", "grow", "region", "links", "flip", "wrap", "key"]);
|
|
@@ -87,6 +108,14 @@ function validateSpec(spec) {
|
|
|
87
108
|
if (spec.units !== void 0 && spec.units !== "px" && spec.units !== "viewport") {
|
|
88
109
|
err("units", "must be 'px' | 'viewport'");
|
|
89
110
|
}
|
|
111
|
+
if (spec.referenceViewport !== void 0) {
|
|
112
|
+
if (!isNum(spec.referenceViewport) || spec.referenceViewport < 100 || spec.referenceViewport > 8640) {
|
|
113
|
+
err("referenceViewport", "must be a number between 100 and 8640");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (spec.ghosting !== void 0 && (!isNum(spec.ghosting) || spec.ghosting < 0 || spec.ghosting > LIMITS.maxGhosting)) {
|
|
117
|
+
err("ghosting", `must be a number 0..${LIMITS.maxGhosting}`);
|
|
118
|
+
}
|
|
90
119
|
for (const k of unknownKeys(spec, KNOWN_TOP)) {
|
|
91
120
|
warn(k, "unknown-property", `unknown top-level property '${k}' \u2014 will be ignored`);
|
|
92
121
|
}
|
|
@@ -154,7 +183,7 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
154
183
|
if (k === "id") {
|
|
155
184
|
warn(`${path}.id`, "misplaced-property", `'id' is not a layer property \u2014 did you mean 'key'? Use 'key' to name a layer`);
|
|
156
185
|
} else if (k === "depth") {
|
|
157
|
-
warn(`${path}.depth`, "unknown-property", `'depth' is not
|
|
186
|
+
warn(`${path}.depth`, "unknown-property", `'depth' is not a layer property \u2014 use motion type 'warp' for true perspective depth, or fake parallax with size/speed/alpha across layers`);
|
|
158
187
|
} else {
|
|
159
188
|
warn(`${path}.${k}`, "unknown-property", `unknown layer property '${k}' \u2014 will be ignored`);
|
|
160
189
|
}
|
|
@@ -172,7 +201,9 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
172
201
|
if (layer.alpha !== void 0 && (!isRange(layer.alpha) || layer.alpha[0] < 0 || layer.alpha[1] > 1)) {
|
|
173
202
|
err(`${path}.alpha`, "must be a [min,max] range within 0..1");
|
|
174
203
|
}
|
|
175
|
-
if (layer.blend !== void 0 &&
|
|
204
|
+
if (layer.blend !== void 0 && !["lighter", "screen", "multiply"].includes(layer.blend)) {
|
|
205
|
+
err(`${path}.blend`, "must be 'lighter' | 'screen' | 'multiply' when set");
|
|
206
|
+
}
|
|
176
207
|
if (layer.key !== void 0 && (!isStr(layer.key) || layer.key.trim() === "")) {
|
|
177
208
|
err(`${path}.key`, "must be a non-empty string");
|
|
178
209
|
}
|
|
@@ -205,6 +236,17 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
205
236
|
if (!isNum(layer.pulse.period) || layer.pulse.period < LIMITS.minPulsePeriod) {
|
|
206
237
|
err(`${path}.pulse.period`, `must be >= ${LIMITS.minPulsePeriod} ms (flash-safety floor)`);
|
|
207
238
|
}
|
|
239
|
+
if (layer.pulse.wave !== void 0) {
|
|
240
|
+
if (!isObj(layer.pulse.wave)) err(`${path}.pulse.wave`, "must be an object");
|
|
241
|
+
else {
|
|
242
|
+
if (!isNum(layer.pulse.wave.wavelength) || layer.pulse.wave.wavelength <= 0) {
|
|
243
|
+
err(`${path}.pulse.wave.wavelength`, "must be > 0 (px or viewport-units)");
|
|
244
|
+
}
|
|
245
|
+
if (layer.pulse.wave.angle !== void 0 && !isNum(layer.pulse.wave.angle)) {
|
|
246
|
+
err(`${path}.pulse.wave.angle`, "must be a number (degrees)");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
208
250
|
}
|
|
209
251
|
}
|
|
210
252
|
if (layer.spin !== void 0) {
|
|
@@ -238,11 +280,61 @@ function validateLayer(layer, path, err, warn, spec) {
|
|
|
238
280
|
if (layer.links.width !== void 0 && (!isNum(layer.links.width) || layer.links.width <= 0)) {
|
|
239
281
|
err(`${path}.links.width`, "must be > 0");
|
|
240
282
|
}
|
|
283
|
+
if (layer.links.mode !== void 0 && !["nearest", "chain", "random"].includes(layer.links.mode)) {
|
|
284
|
+
err(`${path}.links.mode`, "must be 'nearest' | 'chain' | 'random'");
|
|
285
|
+
}
|
|
286
|
+
if (layer.links.falloff !== void 0 && typeof layer.links.falloff !== "boolean") {
|
|
287
|
+
err(`${path}.links.falloff`, "must be a boolean");
|
|
288
|
+
}
|
|
289
|
+
if (layer.links.closed !== void 0 && typeof layer.links.closed !== "boolean") {
|
|
290
|
+
err(`${path}.links.closed`, "must be a boolean");
|
|
291
|
+
}
|
|
241
292
|
if (isNum(layer.count) && layer.count > LIMITS.maxLinkLayerCount) {
|
|
242
293
|
err(`${path}.links`, `layer count must be <= ${LIMITS.maxLinkLayerCount} when links is set`);
|
|
243
294
|
}
|
|
244
295
|
}
|
|
245
296
|
}
|
|
297
|
+
if (layer.layout !== void 0) {
|
|
298
|
+
if (!isObj(layer.layout) || layer.layout.type !== "grid") {
|
|
299
|
+
err(`${path}.layout`, "must be an object with type: 'grid'");
|
|
300
|
+
} else {
|
|
301
|
+
if (layer.layout.columns !== void 0 && (!isNum(layer.layout.columns) || !Number.isInteger(layer.layout.columns) || layer.layout.columns < 1 || layer.layout.columns > LIMITS.maxGridColumns)) {
|
|
302
|
+
err(`${path}.layout.columns`, `must be an integer 1..${LIMITS.maxGridColumns}`);
|
|
303
|
+
}
|
|
304
|
+
const jit = layer.layout.jitter;
|
|
305
|
+
if (jit !== void 0) {
|
|
306
|
+
const jitterOk = (v) => isNum(v) && v >= 0 && v <= 1;
|
|
307
|
+
if (isNum(jit)) {
|
|
308
|
+
if (!jitterOk(jit)) err(`${path}.layout.jitter`, "must be 0..1");
|
|
309
|
+
} else if (isObj(jit)) {
|
|
310
|
+
if (jit.x !== void 0 && !jitterOk(jit.x)) err(`${path}.layout.jitter.x`, "must be 0..1");
|
|
311
|
+
if (jit.y !== void 0 && !jitterOk(jit.y)) err(`${path}.layout.jitter.y`, "must be 0..1");
|
|
312
|
+
} else {
|
|
313
|
+
err(`${path}.layout.jitter`, "must be a number 0..1 or { x?, y? }");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (layer.position !== void 0) {
|
|
317
|
+
warn(`${path}.layout`, "layout-with-position", "position overrides layout for the single entity \u2014 layout has no effect");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (layer.life !== void 0) {
|
|
322
|
+
if (!isObj(layer.life)) err(`${path}.life`, "must be an object");
|
|
323
|
+
else {
|
|
324
|
+
if (layer.life.enter !== void 0 && (!isNum(layer.life.enter) || layer.life.enter < 0)) {
|
|
325
|
+
err(`${path}.life.enter`, "must be >= 0 (ms)");
|
|
326
|
+
}
|
|
327
|
+
if (layer.life.exit !== void 0 && (!isNum(layer.life.exit) || layer.life.exit < 0)) {
|
|
328
|
+
err(`${path}.life.exit`, "must be >= 0 (ms)");
|
|
329
|
+
}
|
|
330
|
+
if (isNum(layer.life.enter) && isNum(layer.life.exit) && layer.life.exit <= layer.life.enter) {
|
|
331
|
+
err(`${path}.life.exit`, "must be greater than life.enter");
|
|
332
|
+
}
|
|
333
|
+
if (layer.life.fade !== void 0 && (!isNum(layer.life.fade) || layer.life.fade < 0)) {
|
|
334
|
+
err(`${path}.life.fade`, "must be >= 0 (ms)");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
246
338
|
if (layer.trail !== void 0) {
|
|
247
339
|
if (!isObj(layer.trail)) err(`${path}.trail`, "must be an object");
|
|
248
340
|
else {
|
|
@@ -287,15 +379,29 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
287
379
|
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px");
|
|
288
380
|
color(sprite.color, `${path}.color`, err);
|
|
289
381
|
if (sprite.soft !== void 0 && typeof sprite.soft !== "boolean") err(`${path}.soft`, "must be a boolean");
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
382
|
+
validatePalette(sprite, path, err);
|
|
383
|
+
} else if (sprite.kind === "ring") {
|
|
384
|
+
knownSet = KNOWN_RING;
|
|
385
|
+
if (!isRange(sprite.radius) || sprite.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px");
|
|
386
|
+
color(sprite.color, `${path}.color`, err);
|
|
387
|
+
if (sprite.width !== void 0 && (!isNum(sprite.width) || sprite.width <= 0)) err(`${path}.width`, "must be > 0");
|
|
388
|
+
validatePalette(sprite, path, err);
|
|
389
|
+
} else if (sprite.kind === "streak") {
|
|
390
|
+
knownSet = KNOWN_STREAK;
|
|
391
|
+
if (!isRange(sprite.length) || sprite.length[0] <= 0) err(`${path}.length`, "must be a [min,max] range of positive px");
|
|
392
|
+
color(sprite.color, `${path}.color`, err);
|
|
393
|
+
if (sprite.width !== void 0 && (!isNum(sprite.width) || sprite.width <= 0)) err(`${path}.width`, "must be > 0");
|
|
394
|
+
validatePalette(sprite, path, err);
|
|
395
|
+
} else if (sprite.kind === "rect") {
|
|
396
|
+
knownSet = KNOWN_RECT;
|
|
397
|
+
if (!isRange(sprite.width) || sprite.width[0] <= 0) err(`${path}.width`, "must be a [min,max] range of positive px");
|
|
398
|
+
if (sprite.aspect !== void 0 && (!isRange(sprite.aspect) || sprite.aspect[0] <= 0)) {
|
|
399
|
+
err(`${path}.aspect`, "must be a [min,max] range of positive height/width ratios");
|
|
296
400
|
}
|
|
401
|
+
color(sprite.color, `${path}.color`, err);
|
|
402
|
+
validatePalette(sprite, path, err);
|
|
297
403
|
} else {
|
|
298
|
-
err(`${path}.kind`, "must be emoji | text | circle");
|
|
404
|
+
err(`${path}.kind`, "must be emoji | text | circle | ring | streak | rect");
|
|
299
405
|
return;
|
|
300
406
|
}
|
|
301
407
|
for (const k of unknownKeys(sprite, knownSet)) {
|
|
@@ -306,6 +412,24 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
306
412
|
}
|
|
307
413
|
}
|
|
308
414
|
}
|
|
415
|
+
function validatePalette(sprite, path, err) {
|
|
416
|
+
if (sprite.colors !== void 0) {
|
|
417
|
+
if (!Array.isArray(sprite.colors) || sprite.colors.length === 0) {
|
|
418
|
+
err(`${path}.colors`, "must be a non-empty array of hex colours");
|
|
419
|
+
} else {
|
|
420
|
+
sprite.colors.forEach((c, ci) => color(c, `${path}.colors[${ci}]`, err));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (sprite.colorWeights !== void 0) {
|
|
424
|
+
if (!Array.isArray(sprite.colors)) {
|
|
425
|
+
err(`${path}.colorWeights`, "requires colors to be set");
|
|
426
|
+
} else if (!Array.isArray(sprite.colorWeights) || sprite.colorWeights.length !== sprite.colors.length) {
|
|
427
|
+
err(`${path}.colorWeights`, "must be an array the same length as colors");
|
|
428
|
+
} else if (!sprite.colorWeights.every((v) => isNum(v) && v >= 0) || !sprite.colorWeights.some((v) => isNum(v) && v > 0)) {
|
|
429
|
+
err(`${path}.colorWeights`, "weights must be >= 0 with at least one > 0");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
309
433
|
function validateCycle(sprite, path, err) {
|
|
310
434
|
if (sprite.cycle === void 0) return;
|
|
311
435
|
if (!isObj(sprite.cycle)) return err(`${path}.cycle`, "must be an object");
|
|
@@ -315,10 +439,12 @@ function validateCycle(sprite, path, err) {
|
|
|
315
439
|
}
|
|
316
440
|
function validateMotion(motion, path, err, warn, spec) {
|
|
317
441
|
if (!isObj(motion)) return err(path, "must be an object");
|
|
318
|
-
const isViewport = isObj(spec)
|
|
319
|
-
const
|
|
442
|
+
const isViewport = !isObj(spec) || spec.units !== "px";
|
|
443
|
+
const refVp = isObj(spec) && isNum(spec.referenceViewport) ? spec.referenceViewport : LIMITS.referenceViewport;
|
|
444
|
+
const speedCap = isViewport ? LIMITS.maxSpeed / refVp : LIMITS.maxSpeed;
|
|
320
445
|
const speedOk = (v, p) => {
|
|
321
446
|
if (!isRange(v)) err(p, "must be a [min,max] range");
|
|
447
|
+
else if (v[0] < 0) err(p, "speed lower bound must be >= 0");
|
|
322
448
|
else if (v[1] > speedCap) err(p, `speed exceeds cap ${isViewport ? speedCap.toFixed(2) + " viewport-units/sec" : LIMITS.maxSpeed + " px/sec"}`);
|
|
323
449
|
};
|
|
324
450
|
let knownSet;
|
|
@@ -356,6 +482,47 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
356
482
|
}
|
|
357
483
|
}
|
|
358
484
|
if (!isRange(motion.radius) || motion.radius[0] <= 0) err(`${path}.radius`, "must be a [min,max] range of positive px");
|
|
485
|
+
if (motion.center !== void 0) {
|
|
486
|
+
if (!isObj(motion.center)) {
|
|
487
|
+
err(`${path}.center`, "must be {x, y} (0..1) or { layer: key }");
|
|
488
|
+
} else if ("layer" in motion.center) {
|
|
489
|
+
if (!isStr(motion.center.layer) || motion.center.layer.trim() === "") {
|
|
490
|
+
err(`${path}.center.layer`, "must be a non-empty layer key");
|
|
491
|
+
} else if (isObj(spec) && Array.isArray(spec.layers)) {
|
|
492
|
+
const layers = spec.layers;
|
|
493
|
+
const parent = layers.find((l) => isObj(l) && l.key === motion.center.layer);
|
|
494
|
+
if (!parent) {
|
|
495
|
+
err(`${path}.center.layer`, `no layer with key '${motion.center.layer}'`);
|
|
496
|
+
} else {
|
|
497
|
+
if (parent.count !== 1) err(`${path}.center.layer`, "parent layer must have count: 1");
|
|
498
|
+
const pm = parent.motion;
|
|
499
|
+
if (isObj(pm) && pm.type === "orbit" && isObj(pm.center) && "layer" in pm.center) {
|
|
500
|
+
err(`${path}.center.layer`, "parent may not itself orbit a layer (one level deep only)");
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
} else if (!isNum(motion.center.x) || !isNum(motion.center.y)) {
|
|
505
|
+
err(`${path}.center`, "must be {x, y} with numbers 0..1");
|
|
506
|
+
} else {
|
|
507
|
+
if (motion.center.x < 0 || motion.center.x > 1) err(`${path}.center.x`, "must be 0..1");
|
|
508
|
+
if (motion.center.y < 0 || motion.center.y > 1) err(`${path}.center.y`, "must be 0..1");
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} else if (motion.type === "wander") {
|
|
512
|
+
knownSet = KNOWN_WANDER;
|
|
513
|
+
speedOk(motion.speed, `${path}.speed`);
|
|
514
|
+
if (motion.angle !== void 0 && !isNum(motion.angle)) err(`${path}.angle`, "must be a number (degrees)");
|
|
515
|
+
const meanderCap = isViewport ? LIMITS.maxMeander / refVp : LIMITS.maxMeander;
|
|
516
|
+
if (motion.meander !== void 0 && (!isNum(motion.meander) || motion.meander <= 0 || motion.meander > meanderCap)) {
|
|
517
|
+
err(`${path}.meander`, `must be > 0 and <= ${isViewport ? meanderCap.toFixed(3) + " viewport-units" : LIMITS.maxMeander + " px"}`);
|
|
518
|
+
}
|
|
519
|
+
if (motion.coherence !== void 0 && (!isNum(motion.coherence) || motion.coherence < 0 || motion.coherence > 1)) {
|
|
520
|
+
err(`${path}.coherence`, "must be 0..1");
|
|
521
|
+
}
|
|
522
|
+
} else if (motion.type === "warp") {
|
|
523
|
+
knownSet = KNOWN_WARP;
|
|
524
|
+
if (!isRange(motion.speed) || motion.speed[0] < 0) err(`${path}.speed`, "must be a [min,max] range >= 0 (depth-units/sec)");
|
|
525
|
+
else if (motion.speed[1] > LIMITS.maxWarpSpeed) err(`${path}.speed`, `warp speed exceeds cap ${LIMITS.maxWarpSpeed} depth-units/sec`);
|
|
359
526
|
if (motion.center !== void 0) {
|
|
360
527
|
if (!isObj(motion.center) || !isNum(motion.center.x) || !isNum(motion.center.y)) {
|
|
361
528
|
err(`${path}.center`, "must be {x, y} with numbers 0..1");
|
|
@@ -364,8 +531,30 @@ function validateMotion(motion, path, err, warn, spec) {
|
|
|
364
531
|
if (motion.center.y < 0 || motion.center.y > 1) err(`${path}.center.y`, "must be 0..1");
|
|
365
532
|
}
|
|
366
533
|
}
|
|
534
|
+
} else if (motion.type === "path") {
|
|
535
|
+
knownSet = KNOWN_PATH;
|
|
536
|
+
if (!Array.isArray(motion.points) || motion.points.length < LIMITS.minPathPoints || motion.points.length > LIMITS.maxPathPoints) {
|
|
537
|
+
err(`${path}.points`, `must be an array of ${LIMITS.minPathPoints}..${LIMITS.maxPathPoints} points`);
|
|
538
|
+
} else {
|
|
539
|
+
motion.points.forEach((p, pi) => {
|
|
540
|
+
if (!isObj(p) || !isNum(p.x) || !isNum(p.y) || p.x < 0 || p.x > 1 || p.y < 0 || p.y > 1) {
|
|
541
|
+
err(`${path}.points[${pi}]`, "must be {x, y} with numbers 0..1");
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
if (!isNum(motion.duration) || motion.duration < LIMITS.minPathDuration) {
|
|
546
|
+
err(`${path}.duration`, `must be >= ${LIMITS.minPathDuration} ms`);
|
|
547
|
+
}
|
|
548
|
+
if (motion.curve !== void 0 && motion.curve !== "linear" && motion.curve !== "smooth") {
|
|
549
|
+
err(`${path}.curve`, "must be 'linear' | 'smooth'");
|
|
550
|
+
}
|
|
551
|
+
if (motion.closed !== void 0 && typeof motion.closed !== "boolean") err(`${path}.closed`, "must be a boolean");
|
|
552
|
+
const scatterCap = isViewport ? LIMITS.maxMeander / refVp : LIMITS.maxMeander;
|
|
553
|
+
if (motion.scatter !== void 0 && (!isNum(motion.scatter) || motion.scatter <= 0 || motion.scatter > scatterCap)) {
|
|
554
|
+
err(`${path}.scatter`, `must be > 0 and <= ${isViewport ? scatterCap.toFixed(3) + " viewport-units" : LIMITS.maxMeander + " px"}`);
|
|
555
|
+
}
|
|
367
556
|
} else {
|
|
368
|
-
err(`${path}.type`, "must be drift | rise | bounce | static | orbit");
|
|
557
|
+
err(`${path}.type`, "must be drift | rise | bounce | static | orbit | wander | warp | path");
|
|
369
558
|
return;
|
|
370
559
|
}
|
|
371
560
|
for (const k of unknownKeys(motion, knownSet)) {
|
|
@@ -382,11 +571,42 @@ ${r.errors.map((e) => ` ${e.path || "<root>"}: ${e.message}`).join("\n")}`);
|
|
|
382
571
|
}
|
|
383
572
|
|
|
384
573
|
// src/simulate.ts
|
|
574
|
+
var WARP_NEAR = 0.08;
|
|
575
|
+
var WARP_MAX_SCALE = 8;
|
|
385
576
|
function spriteVariants(sprite) {
|
|
386
577
|
if (sprite.kind === "emoji") return sprite.glyphs.length;
|
|
387
578
|
if (sprite.kind === "text") return sprite.strings.length;
|
|
388
579
|
return 1;
|
|
389
580
|
}
|
|
581
|
+
var OCT_AMP = [1, 0.5, 0.25];
|
|
582
|
+
var OCT_PERIOD = [
|
|
583
|
+
[6e3, 14e3],
|
|
584
|
+
[3e3, 7e3],
|
|
585
|
+
[1500, 4e3]
|
|
586
|
+
];
|
|
587
|
+
function drawOsc(rng, amp) {
|
|
588
|
+
const osc = { ax: [], fx: [], phx: [], ay: [], fy: [], phy: [] };
|
|
589
|
+
for (let i = 0; i < 3; i++) {
|
|
590
|
+
osc.ax.push(amp * OCT_AMP[i] * rng.range(0.6, 1.4));
|
|
591
|
+
osc.fx.push(2 * Math.PI / rng.range(OCT_PERIOD[i][0], OCT_PERIOD[i][1]));
|
|
592
|
+
osc.phx.push(rng.range(0, Math.PI * 2));
|
|
593
|
+
}
|
|
594
|
+
for (let i = 0; i < 3; i++) {
|
|
595
|
+
osc.ay.push(amp * OCT_AMP[i] * rng.range(0.6, 1.4));
|
|
596
|
+
osc.fy.push(2 * Math.PI / rng.range(OCT_PERIOD[i][0], OCT_PERIOD[i][1]));
|
|
597
|
+
osc.phy.push(rng.range(0, Math.PI * 2));
|
|
598
|
+
}
|
|
599
|
+
return osc;
|
|
600
|
+
}
|
|
601
|
+
function weightedIndex(u, weights) {
|
|
602
|
+
const total = weights.reduce((a, b) => a + b, 0);
|
|
603
|
+
let acc = 0;
|
|
604
|
+
for (let i = 0; i < weights.length; i++) {
|
|
605
|
+
acc += weights[i] / total;
|
|
606
|
+
if (u < acc) return i;
|
|
607
|
+
}
|
|
608
|
+
return weights.length - 1;
|
|
609
|
+
}
|
|
390
610
|
function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
391
611
|
const effectiveCount = countScale === 1 ? layer.count : Math.max(1, Math.min(
|
|
392
612
|
Math.round(layer.count * countScale),
|
|
@@ -394,15 +614,55 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
394
614
|
));
|
|
395
615
|
const [smin, smax] = layer.size ?? [20, 40];
|
|
396
616
|
const variants = spriteVariants(layer.sprite);
|
|
397
|
-
const
|
|
398
|
-
const
|
|
617
|
+
const sprite = layer.sprite;
|
|
618
|
+
const spriteColors = sprite.kind === "circle" || sprite.kind === "ring" || sprite.kind === "streak" || sprite.kind === "rect" ? sprite.colors : void 0;
|
|
619
|
+
const colorWeights = sprite.kind === "circle" || sprite.kind === "ring" || sprite.kind === "streak" || sprite.kind === "rect" ? sprite.colorWeights : void 0;
|
|
620
|
+
const colorsLen = spriteColors?.length ?? 0;
|
|
621
|
+
const cycle = sprite.kind === "emoji" || sprite.kind === "text" ? sprite.cycle : void 0;
|
|
622
|
+
const mSpec = layer.motion;
|
|
623
|
+
let sharedOsc = null;
|
|
624
|
+
let meanderPx = 0;
|
|
625
|
+
if (mSpec.type === "wander") {
|
|
626
|
+
meanderPx = (mSpec.meander ?? (scale === 1 ? 60 : 0.05)) * scale;
|
|
627
|
+
sharedOsc = drawOsc(rng, meanderPx);
|
|
628
|
+
}
|
|
629
|
+
let pathPts = null;
|
|
630
|
+
if (mSpec.type === "path") {
|
|
631
|
+
pathPts = mSpec.points.map((p) => ({ x: p.x * w, y: p.y * h }));
|
|
632
|
+
}
|
|
633
|
+
const layout = layer.layout;
|
|
634
|
+
let gridCols = 0;
|
|
635
|
+
let cellW = 0;
|
|
636
|
+
let cellH = 0;
|
|
637
|
+
let gridX0 = 0;
|
|
638
|
+
let gridY0 = 0;
|
|
639
|
+
if (layout?.type === "grid") {
|
|
640
|
+
const [rx0, rx1] = layer.region?.x ?? [0, 1];
|
|
641
|
+
const [ry0, ry1] = layer.region?.y ?? [0, 1];
|
|
642
|
+
const gw = Math.max(1, (rx1 - rx0) * w);
|
|
643
|
+
const gh = Math.max(1, (ry1 - ry0) * h);
|
|
644
|
+
gridCols = Math.min(
|
|
645
|
+
layout.columns ?? Math.max(1, Math.round(Math.sqrt(effectiveCount * (gw / gh)))),
|
|
646
|
+
LIMITS.maxGridColumns
|
|
647
|
+
);
|
|
648
|
+
const gridRows = Math.max(1, Math.ceil(effectiveCount / gridCols));
|
|
649
|
+
cellW = gw / gridCols;
|
|
650
|
+
cellH = gh / gridRows;
|
|
651
|
+
gridX0 = rx0 * w;
|
|
652
|
+
gridY0 = ry0 * h;
|
|
653
|
+
}
|
|
399
654
|
const out = [];
|
|
400
655
|
for (let i = 0; i < effectiveCount; i++) {
|
|
401
|
-
const size =
|
|
656
|
+
const size = 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;
|
|
657
|
+
const size2 = sprite.kind === "rect" ? size * (sprite.aspect ? rng.range(sprite.aspect[0], sprite.aspect[1]) : 1) : void 0;
|
|
402
658
|
let vx = 0;
|
|
403
659
|
let vy = 0;
|
|
404
660
|
let bob = 0;
|
|
405
661
|
let motion = "drift";
|
|
662
|
+
let warpParams;
|
|
663
|
+
let pathPhase = 0;
|
|
664
|
+
let pathOffX = 0;
|
|
665
|
+
let pathOffY = 0;
|
|
406
666
|
const m = layer.motion;
|
|
407
667
|
if (m.type === "static") {
|
|
408
668
|
motion = "static";
|
|
@@ -426,18 +686,53 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
426
686
|
} else if (m.type === "orbit") {
|
|
427
687
|
motion = "orbit";
|
|
428
688
|
vx = rng.range(m.speed[0], m.speed[1]);
|
|
689
|
+
} else if (m.type === "wander") {
|
|
690
|
+
motion = "wander";
|
|
691
|
+
const s = rng.range(m.speed[0], m.speed[1]) * scale;
|
|
692
|
+
const heading = m.angle !== void 0 ? m.angle * Math.PI / 180 : rng.range(0, Math.PI * 2);
|
|
693
|
+
vx = s * Math.cos(heading);
|
|
694
|
+
vy = s * Math.sin(heading);
|
|
695
|
+
} else if (m.type === "warp") {
|
|
696
|
+
motion = "warp";
|
|
697
|
+
const vz = rng.range(m.speed[0], m.speed[1]);
|
|
698
|
+
const theta = rng.range(0, Math.PI * 2);
|
|
699
|
+
const r = rng.range(0.15, 1);
|
|
700
|
+
warpParams = {
|
|
701
|
+
ux: Math.cos(theta) * r,
|
|
702
|
+
uy: Math.sin(theta) * r,
|
|
703
|
+
z0: rng.range(WARP_NEAR, 1),
|
|
704
|
+
vz,
|
|
705
|
+
cx: (m.center?.x ?? 0.5) * w,
|
|
706
|
+
cy: (m.center?.y ?? 0.5) * h
|
|
707
|
+
};
|
|
708
|
+
} else if (m.type === "path") {
|
|
709
|
+
motion = "path";
|
|
710
|
+
pathPhase = rng.next();
|
|
711
|
+
if (m.scatter) {
|
|
712
|
+
pathOffX = rng.range(-1, 1) * m.scatter * scale;
|
|
713
|
+
pathOffY = rng.range(-1, 1) * m.scatter * scale;
|
|
714
|
+
}
|
|
429
715
|
}
|
|
430
716
|
let x0;
|
|
431
717
|
let y0;
|
|
432
718
|
if (layer.position && layer.count === 1) {
|
|
433
719
|
x0 = layer.position.x * w;
|
|
434
720
|
y0 = layer.position.y * h;
|
|
721
|
+
} else if (layout?.type === "grid") {
|
|
722
|
+
const col = i % gridCols;
|
|
723
|
+
const row = Math.floor(i / gridCols);
|
|
724
|
+
const j = layout.jitter ?? 0;
|
|
725
|
+
const jx = typeof j === "number" ? j : j.x ?? 0;
|
|
726
|
+
const jy = typeof j === "number" ? j : j.y ?? 0;
|
|
727
|
+
x0 = gridX0 + (col + 0.5) * cellW + rng.range(-0.5, 0.5) * jx * cellW;
|
|
728
|
+
y0 = gridY0 + (row + 0.5) * cellH + rng.range(-0.5, 0.5) * jy * cellH;
|
|
435
729
|
} else {
|
|
436
730
|
const [rx0, rx1] = layer.region?.x ?? [0, 1];
|
|
437
731
|
const [ry0, ry1] = layer.region?.y ?? [0, 1];
|
|
438
732
|
x0 = rng.range(rx0 * w, rx1 * w);
|
|
439
733
|
y0 = rng.range(ry0 * h, ry1 * h);
|
|
440
734
|
}
|
|
735
|
+
const orbitParent = m.type === "orbit" && m.center && "layer" in m.center ? m.center.layer : void 0;
|
|
441
736
|
out.push({
|
|
442
737
|
x0,
|
|
443
738
|
y0,
|
|
@@ -461,29 +756,79 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
461
756
|
growPeriod: layer.grow?.period ?? 1e3,
|
|
462
757
|
growPhase: layer.grow ? rng.range(0, Math.PI * 2) : 0,
|
|
463
758
|
orbitR: m.type === "orbit" ? rng.range(m.radius[0], m.radius[1]) * scale : 0,
|
|
464
|
-
orbitCx: m.type === "orbit" ? (m.center?.x ?? 0.5) * w : 0,
|
|
465
|
-
orbitCy: m.type === "orbit" ? (m.center?.y ?? 0.5) * h : 0,
|
|
759
|
+
orbitCx: m.type === "orbit" && !orbitParent ? (m.center?.x ?? 0.5) * w : 0,
|
|
760
|
+
orbitCy: m.type === "orbit" && !orbitParent ? (m.center?.y ?? 0.5) * h : 0,
|
|
466
761
|
// New draws AFTER orbit — guarded so existing specs keep identical streams.
|
|
467
|
-
colorIndex: colorsLen > 0 ? rng.int(0, colorsLen - 1) : -1,
|
|
468
|
-
cyclePeriod: cycle ? cycle.period : 0
|
|
762
|
+
colorIndex: colorsLen > 0 ? colorWeights && colorWeights.length === colorsLen ? weightedIndex(rng.next(), colorWeights) : rng.int(0, colorsLen - 1) : -1,
|
|
763
|
+
cyclePeriod: cycle ? cycle.period : 0,
|
|
764
|
+
// Optional feature fields last (guarded draws inside — wander draws 18 here).
|
|
765
|
+
...size2 !== void 0 ? { size2 } : {},
|
|
766
|
+
...orbitParent ? { orbitParent } : {},
|
|
767
|
+
...m.type === "wander" ? {
|
|
768
|
+
wander: {
|
|
769
|
+
own: drawOsc(rng, meanderPx),
|
|
770
|
+
shared: sharedOsc,
|
|
771
|
+
coherence: Math.max(0, Math.min(1, m.coherence ?? 0)),
|
|
772
|
+
margin: meanderPx * (OCT_AMP[0] + OCT_AMP[1] + OCT_AMP[2]) * 1.4
|
|
773
|
+
}
|
|
774
|
+
} : {},
|
|
775
|
+
...warpParams ? { warp: warpParams } : {},
|
|
776
|
+
...m.type === "path" && pathPts ? {
|
|
777
|
+
path: {
|
|
778
|
+
pts: pathPts,
|
|
779
|
+
duration: m.duration,
|
|
780
|
+
closed: m.closed !== false,
|
|
781
|
+
smooth: m.curve !== "linear",
|
|
782
|
+
phase: pathPhase,
|
|
783
|
+
offX: pathOffX,
|
|
784
|
+
offY: pathOffY
|
|
785
|
+
}
|
|
786
|
+
} : {}
|
|
469
787
|
});
|
|
788
|
+
if (layer.pulse?.wave) {
|
|
789
|
+
const wl = Math.max(1e-6, layer.pulse.wave.wavelength * scale);
|
|
790
|
+
const ang = (layer.pulse.wave.angle ?? 0) * Math.PI / 180;
|
|
791
|
+
out[out.length - 1].pulsePhase = -((x0 * Math.cos(ang) + y0 * Math.sin(ang)) / wl) * Math.PI * 2;
|
|
792
|
+
}
|
|
470
793
|
}
|
|
471
794
|
return out;
|
|
472
795
|
}
|
|
796
|
+
function warpZ(e, t) {
|
|
797
|
+
const wp = e.warp;
|
|
798
|
+
return wrap(wp.z0 - wp.vz * (t / 1e3), WARP_NEAR, 1);
|
|
799
|
+
}
|
|
473
800
|
function alphaAt(e, t) {
|
|
474
|
-
|
|
475
|
-
|
|
801
|
+
let a = e.alpha;
|
|
802
|
+
if (e.pulseAmp) a = e.alpha + e.pulseAmp * Math.sin(t * 2 * Math.PI / e.pulsePeriod + e.pulsePhase);
|
|
803
|
+
if (e.motion === "warp") {
|
|
804
|
+
const z = warpZ(e, t);
|
|
805
|
+
a *= Math.max(0, Math.min(1, (1 - z) / 0.2));
|
|
806
|
+
}
|
|
476
807
|
return a < 0 ? 0 : a > 1 ? 1 : a;
|
|
477
808
|
}
|
|
478
809
|
function sizeAt(e, t) {
|
|
479
|
-
|
|
480
|
-
|
|
810
|
+
let s = e.size;
|
|
811
|
+
if (e.growAmp) s = e.size * (1 + e.growAmp * Math.sin(t * 2 * Math.PI / e.growPeriod + e.growPhase));
|
|
812
|
+
if (e.motion === "warp") s *= Math.min(1 / warpZ(e, t), WARP_MAX_SCALE);
|
|
481
813
|
return s > 0 ? s : 0.1;
|
|
482
814
|
}
|
|
483
815
|
function rotationAt(e, t) {
|
|
484
816
|
if (!e.spinSpeed) return 0;
|
|
485
817
|
return e.spinPhase + e.spinSpeed * (t / 1e3);
|
|
486
818
|
}
|
|
819
|
+
function lifeAlphaAt(life, t) {
|
|
820
|
+
if (!life) return 1;
|
|
821
|
+
const fade = life.fade ?? 1e3;
|
|
822
|
+
let a = 1;
|
|
823
|
+
if (life.enter !== void 0) {
|
|
824
|
+
if (t < life.enter) return 0;
|
|
825
|
+
a = fade > 0 ? Math.min(1, (t - life.enter) / fade) : 1;
|
|
826
|
+
}
|
|
827
|
+
if (life.exit !== void 0 && t >= life.exit) {
|
|
828
|
+
a *= fade > 0 ? Math.max(0, 1 - (t - life.exit) / fade) : 0;
|
|
829
|
+
}
|
|
830
|
+
return a;
|
|
831
|
+
}
|
|
487
832
|
function wrap(v, min, max) {
|
|
488
833
|
const range = max - min;
|
|
489
834
|
return ((v - min) % range + range) % range + min;
|
|
@@ -495,6 +840,40 @@ function reflect(v, min, max) {
|
|
|
495
840
|
const p = ((v - min) % period + period) % period;
|
|
496
841
|
return min + (p < range ? p : period - p);
|
|
497
842
|
}
|
|
843
|
+
function catmullRom(p0, p1, p2, p3, u) {
|
|
844
|
+
const u2 = u * u;
|
|
845
|
+
const u3 = u2 * u;
|
|
846
|
+
return 0.5 * (2 * p1 + (-p0 + p2) * u + (2 * p0 - 5 * p1 + 4 * p2 - p3) * u2 + (-p0 + 3 * p1 - 3 * p2 + p3) * u3);
|
|
847
|
+
}
|
|
848
|
+
function pathPosition(e, t) {
|
|
849
|
+
const p = e.path;
|
|
850
|
+
const n = p.pts.length;
|
|
851
|
+
let s = (t / p.duration + p.phase) % 1;
|
|
852
|
+
if (s < 0) s += 1;
|
|
853
|
+
if (!p.closed) {
|
|
854
|
+
const pp = s * 2;
|
|
855
|
+
s = pp < 1 ? pp : 2 - pp;
|
|
856
|
+
}
|
|
857
|
+
const segs = p.closed ? n : n - 1;
|
|
858
|
+
const u = Math.min(s * segs, segs - 1e-9);
|
|
859
|
+
const i = Math.floor(u);
|
|
860
|
+
const local = u - i;
|
|
861
|
+
const at = (k) => p.closed ? p.pts[(k % n + n) % n] : p.pts[Math.max(0, Math.min(n - 1, k))];
|
|
862
|
+
const p1 = at(i);
|
|
863
|
+
const p2 = at(i + 1);
|
|
864
|
+
if (!p.smooth) {
|
|
865
|
+
return {
|
|
866
|
+
x: p1.x + (p2.x - p1.x) * local + p.offX,
|
|
867
|
+
y: p1.y + (p2.y - p1.y) * local + p.offY
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
const p0 = at(i - 1);
|
|
871
|
+
const p3 = at(i + 2);
|
|
872
|
+
return {
|
|
873
|
+
x: catmullRom(p0.x, p1.x, p2.x, p3.x, local) + p.offX,
|
|
874
|
+
y: catmullRom(p0.y, p1.y, p2.y, p3.y, local) + p.offY
|
|
875
|
+
};
|
|
876
|
+
}
|
|
498
877
|
function positionAt(e, t, w, h) {
|
|
499
878
|
if (e.motion === "static") return { x: e.x0, y: e.y0, flip: false };
|
|
500
879
|
if (e.motion === "orbit") {
|
|
@@ -505,6 +884,20 @@ function positionAt(e, t, w, h) {
|
|
|
505
884
|
flip: false
|
|
506
885
|
};
|
|
507
886
|
}
|
|
887
|
+
if (e.motion === "warp") {
|
|
888
|
+
const wp = e.warp;
|
|
889
|
+
const persp = 1 / warpZ(e, t);
|
|
890
|
+
const halfMin = Math.min(w, h) / 2;
|
|
891
|
+
return {
|
|
892
|
+
x: wp.cx + wp.ux * halfMin * persp,
|
|
893
|
+
y: wp.cy + wp.uy * halfMin * persp,
|
|
894
|
+
flip: false
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
if (e.motion === "path") {
|
|
898
|
+
const pos = pathPosition(e, t);
|
|
899
|
+
return { x: pos.x, y: pos.y, flip: false };
|
|
900
|
+
}
|
|
508
901
|
const dt = t / 1e3;
|
|
509
902
|
const m = e.size;
|
|
510
903
|
if (e.motion === "bounce") {
|
|
@@ -521,15 +914,94 @@ function positionAt(e, t, w, h) {
|
|
|
521
914
|
flip: false
|
|
522
915
|
};
|
|
523
916
|
}
|
|
917
|
+
if (e.motion === "wander") {
|
|
918
|
+
const wp = e.wander;
|
|
919
|
+
const c = wp.coherence;
|
|
920
|
+
let hx = 0;
|
|
921
|
+
let hy = 0;
|
|
922
|
+
for (let i = 0; i < 3; i++) {
|
|
923
|
+
if (c < 1) {
|
|
924
|
+
hx += (1 - c) * wp.own.ax[i] * Math.sin(wp.own.fx[i] * t + wp.own.phx[i]);
|
|
925
|
+
hy += (1 - c) * wp.own.ay[i] * Math.sin(wp.own.fy[i] * t + wp.own.phy[i]);
|
|
926
|
+
}
|
|
927
|
+
if (c > 0) {
|
|
928
|
+
hx += c * wp.shared.ax[i] * Math.sin(wp.shared.fx[i] * t + wp.shared.phx[i]);
|
|
929
|
+
hy += c * wp.shared.ay[i] * Math.sin(wp.shared.fy[i] * t + wp.shared.phy[i]);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const margin = m + wp.margin;
|
|
933
|
+
return {
|
|
934
|
+
x: wrap(e.x0 + e.vx * dt + hx, -margin, w + margin),
|
|
935
|
+
y: wrap(e.y0 + e.vy * dt + hy, -margin, h + margin),
|
|
936
|
+
flip: e.headingLeft
|
|
937
|
+
};
|
|
938
|
+
}
|
|
524
939
|
const x = wrap(e.x0 + e.vx * dt, -m, w + m);
|
|
525
940
|
let y = e.vy !== 0 ? wrap(e.y0 + e.vy * dt, -m, h + m) : e.y0;
|
|
526
941
|
if (e.bob) y += e.bob * Math.sin(t / 500 + e.phase);
|
|
527
942
|
return { x, y, flip: e.headingLeft };
|
|
528
943
|
}
|
|
944
|
+
function headingAt(e, t, w, h) {
|
|
945
|
+
const dtMs = 32;
|
|
946
|
+
const t1 = t < dtMs ? t + dtMs : t;
|
|
947
|
+
const t0 = t1 - dtMs;
|
|
948
|
+
const a = positionAt(e, t0, w, h);
|
|
949
|
+
const b = positionAt(e, t1, w, h);
|
|
950
|
+
const dx = b.x - a.x;
|
|
951
|
+
const dy = b.y - a.y;
|
|
952
|
+
if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2) return null;
|
|
953
|
+
if (dx === 0 && dy === 0) return null;
|
|
954
|
+
return Math.atan2(dy, dx);
|
|
955
|
+
}
|
|
529
956
|
function spriteIndexAt(e, t, variants) {
|
|
530
957
|
if (!e.cyclePeriod || variants <= 1) return e.spriteIndex;
|
|
531
958
|
return Math.floor(t / e.cyclePeriod + e.phase / (2 * Math.PI)) % variants;
|
|
532
959
|
}
|
|
960
|
+
function wrapDist(pi, pj, doWrap, w, h) {
|
|
961
|
+
let dx = pj.x - pi.x;
|
|
962
|
+
let dy = pj.y - pi.y;
|
|
963
|
+
if (doWrap) {
|
|
964
|
+
if (Math.abs(dx) > w / 2) dx = dx > 0 ? dx - w : dx + w;
|
|
965
|
+
if (Math.abs(dy) > h / 2) dy = dy > 0 ? dy - h : dy + h;
|
|
966
|
+
}
|
|
967
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
968
|
+
}
|
|
969
|
+
function linkEdges(links, positions, maxDistPx, doWrap, w, h) {
|
|
970
|
+
const n = positions.length;
|
|
971
|
+
const mode = links.mode ?? "nearest";
|
|
972
|
+
if (mode === "chain") {
|
|
973
|
+
const out = [];
|
|
974
|
+
for (let i = 0; i < n - 1; i++) {
|
|
975
|
+
out.push({ i, j: i + 1, dist: wrapDist(positions[i], positions[i + 1], doWrap, w, h) });
|
|
976
|
+
}
|
|
977
|
+
if (links.closed && n > 2) out.push({ i: n - 1, j: 0, dist: wrapDist(positions[n - 1], positions[0], doWrap, w, h) });
|
|
978
|
+
return out;
|
|
979
|
+
}
|
|
980
|
+
if (mode === "random") {
|
|
981
|
+
const stride = Math.max(1, Math.round(n * 0.381966));
|
|
982
|
+
const edges = /* @__PURE__ */ new Set();
|
|
983
|
+
const out = [];
|
|
984
|
+
for (let i = 0; i < n; i++) {
|
|
985
|
+
for (let mIdx = 1; mIdx <= links.k; mIdx++) {
|
|
986
|
+
const j = (i + mIdx * stride) % n;
|
|
987
|
+
if (j === i) continue;
|
|
988
|
+
const dist = wrapDist(positions[i], positions[j], doWrap, w, h);
|
|
989
|
+
if (dist > maxDistPx) continue;
|
|
990
|
+
const key = `${Math.min(i, j)}:${Math.max(i, j)}`;
|
|
991
|
+
if (!edges.has(key)) {
|
|
992
|
+
edges.add(key);
|
|
993
|
+
out.push({ i: Math.min(i, j), j: Math.max(i, j), dist });
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
return out;
|
|
998
|
+
}
|
|
999
|
+
return linkPairs(positions, links.k, maxDistPx, doWrap, w, h).map(([i, j]) => ({
|
|
1000
|
+
i,
|
|
1001
|
+
j,
|
|
1002
|
+
dist: wrapDist(positions[i], positions[j], doWrap, w, h)
|
|
1003
|
+
}));
|
|
1004
|
+
}
|
|
533
1005
|
function linkPairs(positions, k, maxDist, doWrap, w, h) {
|
|
534
1006
|
const n = positions.length;
|
|
535
1007
|
const edges = /* @__PURE__ */ new Set();
|
|
@@ -638,7 +1110,7 @@ function lerpSpec(from, to, k) {
|
|
|
638
1110
|
}
|
|
639
1111
|
function steerablePaths(spec) {
|
|
640
1112
|
if (!spec || typeof spec !== "object") return [];
|
|
641
|
-
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units"]);
|
|
1113
|
+
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "mode", "curve", "layer"]);
|
|
642
1114
|
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops"]);
|
|
643
1115
|
const out = [];
|
|
644
1116
|
const walk = (node, prefix, key) => {
|
|
@@ -669,23 +1141,32 @@ function easeSmooth(k) {
|
|
|
669
1141
|
function structuralSignature(spec) {
|
|
670
1142
|
return JSON.stringify([
|
|
671
1143
|
spec.units,
|
|
672
|
-
spec.layers.map((l) =>
|
|
673
|
-
l.
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1144
|
+
spec.layers.map((l) => {
|
|
1145
|
+
const s = l.sprite;
|
|
1146
|
+
return [
|
|
1147
|
+
l.count,
|
|
1148
|
+
l.size,
|
|
1149
|
+
l.region,
|
|
1150
|
+
l.position,
|
|
1151
|
+
l.motion,
|
|
1152
|
+
l.wrap,
|
|
1153
|
+
l.flip,
|
|
1154
|
+
l.alpha,
|
|
1155
|
+
l.pulse,
|
|
1156
|
+
l.spin,
|
|
1157
|
+
l.grow,
|
|
1158
|
+
l.layout,
|
|
1159
|
+
l.sprite.kind,
|
|
1160
|
+
// Dimensional draws baked into entities: radius (circle/ring), length
|
|
1161
|
+
// (streak), width+aspect (rect), palette pick (all shaped sprites).
|
|
1162
|
+
s.radius,
|
|
1163
|
+
s.length,
|
|
1164
|
+
l.sprite.kind === "rect" ? [s.width, s.aspect] : void 0,
|
|
1165
|
+
Array.isArray(s.colors) ? s.colors.length : void 0,
|
|
1166
|
+
s.colorWeights,
|
|
1167
|
+
l.sprite.kind === "emoji" ? [l.sprite.glyphs.length, l.sprite.cycle?.period] : l.sprite.kind === "text" ? [l.sprite.strings.length, l.sprite.cycle?.period] : void 0
|
|
1168
|
+
];
|
|
1169
|
+
})
|
|
689
1170
|
]);
|
|
690
1171
|
}
|
|
691
1172
|
|
|
@@ -696,6 +1177,12 @@ function hexToRgba(hex, alpha) {
|
|
|
696
1177
|
const n = parseInt(h.slice(1), 16);
|
|
697
1178
|
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${alpha})`;
|
|
698
1179
|
}
|
|
1180
|
+
var FONT_PREFIX_RE = /^((?:(?:italic|oblique|bold|bolder|lighter|normal|\d{3})\s+)+)/;
|
|
1181
|
+
function composeFontShorthand(sz, font) {
|
|
1182
|
+
const m = FONT_PREFIX_RE.exec(font);
|
|
1183
|
+
if (m) return `${m[1].trim()} ${sz}px ${font.slice(m[0].length)}`;
|
|
1184
|
+
return `${sz}px ${font}`;
|
|
1185
|
+
}
|
|
699
1186
|
function manifestFor(spec) {
|
|
700
1187
|
const total = spec.layers.reduce((n, l) => n + l.count, 0);
|
|
701
1188
|
const costTier = total < 30 ? "idle" : total < 150 ? "low" : total < 400 ? "medium" : "high";
|
|
@@ -755,6 +1242,8 @@ var SpecInstance = class {
|
|
|
755
1242
|
baseT = 0;
|
|
756
1243
|
// elapsed logical time carried across pause/resume
|
|
757
1244
|
lastT = 0;
|
|
1245
|
+
/** Last time painted — detects non-contiguous seeks for the ghosting warm-up replay. */
|
|
1246
|
+
lastRenderT = Number.NEGATIVE_INFINITY;
|
|
758
1247
|
/** The spec currently being rendered (base spec + any applied steering). */
|
|
759
1248
|
effSpec;
|
|
760
1249
|
/** Active glide between two resolved specs (live setParam/applyTrack). */
|
|
@@ -765,12 +1254,14 @@ var SpecInstance = class {
|
|
|
765
1254
|
this.canvas.width = Math.max(1, Math.round(this.w * dpr));
|
|
766
1255
|
this.canvas.height = Math.max(1, Math.round(this.h * dpr));
|
|
767
1256
|
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
1257
|
+
this.lastRenderT = Number.NEGATIVE_INFINITY;
|
|
768
1258
|
}
|
|
769
1259
|
/** (Re)seed and place all entities — deterministic for the seed + viewport. */
|
|
770
1260
|
rebuild() {
|
|
771
1261
|
const rng = createRng(this.seed);
|
|
772
|
-
const scale = this.effSpec.units === "
|
|
773
|
-
|
|
1262
|
+
const scale = this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h);
|
|
1263
|
+
const refVp = this.effSpec.referenceViewport ?? LIMITS.referenceViewport;
|
|
1264
|
+
let countScale = scale > 1 ? Math.min(this.w, this.h) / refVp : 1;
|
|
774
1265
|
if (countScale > 1) {
|
|
775
1266
|
const rawTotal = this.effSpec.layers.reduce((s, l) => s + Math.round(l.count * countScale), 0);
|
|
776
1267
|
if (rawTotal > LIMITS.maxTotal) countScale *= LIMITS.maxTotal / rawTotal;
|
|
@@ -820,30 +1311,49 @@ var SpecInstance = class {
|
|
|
820
1311
|
ctx.fillRect(0, 0, w, h);
|
|
821
1312
|
if (bg.band) {
|
|
822
1313
|
ctx.fillStyle = bg.band.color;
|
|
823
|
-
const bh = bg.band.height * (this.effSpec.units === "
|
|
1314
|
+
const bh = bg.band.height * (this.effSpec.units === "px" ? 1 : Math.min(w, h));
|
|
824
1315
|
ctx.fillRect(0, h - bh, w, bh);
|
|
825
1316
|
}
|
|
826
1317
|
}
|
|
827
|
-
|
|
1318
|
+
/** Position with parent-orbit resolution: a layer-parented orbit entity's
|
|
1319
|
+
* positionAt is an offset around (0,0); add the parent's own analytic position. */
|
|
1320
|
+
entityPos(e, t, parentE) {
|
|
1321
|
+
const p = positionAt(e, t, this.w, this.h);
|
|
1322
|
+
if (parentE && e.orbitParent) {
|
|
1323
|
+
const pp = positionAt(parentE, t, this.w, this.h);
|
|
1324
|
+
p.x += pp.x;
|
|
1325
|
+
p.y += pp.y;
|
|
1326
|
+
}
|
|
1327
|
+
return p;
|
|
1328
|
+
}
|
|
1329
|
+
/** Resolve the single parent entity for a layer-parented orbit layer (or null). */
|
|
1330
|
+
parentEntityFor(built) {
|
|
1331
|
+
const m = built.layer.motion;
|
|
1332
|
+
if (m.type !== "orbit" || !m.center || !("layer" in m.center)) return null;
|
|
1333
|
+
const key = m.center.layer;
|
|
1334
|
+
const parent = this.layers.find((b) => b.layer.key === key);
|
|
1335
|
+
return parent?.entities[0] ?? null;
|
|
1336
|
+
}
|
|
1337
|
+
drawTrail(built, e, t, lifeA, parentE) {
|
|
828
1338
|
const trail = built.layer.trail;
|
|
829
1339
|
if (!trail) return;
|
|
830
1340
|
const { ctx, w, h } = this;
|
|
831
1341
|
const fade = trail.fade ?? 1;
|
|
832
1342
|
const n = Math.min(Math.ceil(trail.length / 50), LIMITS.maxTrailSamples);
|
|
833
|
-
const headAlpha = alphaAt(e, t);
|
|
1343
|
+
const headAlpha = alphaAt(e, t) * lifeA;
|
|
834
1344
|
const headSize = sizeAt(e, t);
|
|
835
1345
|
const sprite = built.layer.sprite;
|
|
836
|
-
const resolvedColor = sprite.kind === "circle" ? sprite.colors?.[e.colorIndex] ?? sprite.color : sprite.kind === "text" ? sprite.color ?? "#e6e8ef" : "#e6e8ef";
|
|
1346
|
+
const resolvedColor = sprite.kind === "circle" || sprite.kind === "ring" || sprite.kind === "streak" || sprite.kind === "rect" ? sprite.colors?.[e.colorIndex] ?? sprite.color : sprite.kind === "text" ? sprite.color ?? "#e6e8ef" : "#e6e8ef";
|
|
837
1347
|
const isSoft = sprite.kind === "circle" && sprite.soft;
|
|
838
1348
|
const wrap2 = built.layer.wrap !== false;
|
|
839
|
-
const head =
|
|
1349
|
+
const head = this.entityPos(e, t, parentE);
|
|
840
1350
|
let prevX = head.x;
|
|
841
1351
|
let prevY = head.y;
|
|
842
1352
|
for (let s = 1; s <= n; s++) {
|
|
843
1353
|
const k = s / n;
|
|
844
1354
|
const pastT = t - k * trail.length;
|
|
845
1355
|
if (pastT < 0) break;
|
|
846
|
-
const pos =
|
|
1356
|
+
const pos = this.entityPos(e, pastT, parentE);
|
|
847
1357
|
if (wrap2) {
|
|
848
1358
|
const dx = pos.x - prevX;
|
|
849
1359
|
const dy = pos.y - prevY;
|
|
@@ -870,13 +1380,52 @@ var SpecInstance = class {
|
|
|
870
1380
|
ctx.fill();
|
|
871
1381
|
}
|
|
872
1382
|
}
|
|
873
|
-
drawEntity(built, e, t) {
|
|
1383
|
+
drawEntity(built, e, t, lifeA, parentE) {
|
|
874
1384
|
const { ctx } = this;
|
|
875
|
-
const p =
|
|
1385
|
+
const p = this.entityPos(e, t, parentE);
|
|
876
1386
|
const sprite = built.layer.sprite;
|
|
877
1387
|
const sz = sizeAt(e, t);
|
|
878
1388
|
const rot = rotationAt(e, t);
|
|
879
|
-
|
|
1389
|
+
const unitScale = this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h);
|
|
1390
|
+
ctx.globalAlpha = alphaAt(e, t) * lifeA;
|
|
1391
|
+
if (sprite.kind === "ring") {
|
|
1392
|
+
const r = sz / 2;
|
|
1393
|
+
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
1394
|
+
ctx.strokeStyle = resolvedColor;
|
|
1395
|
+
ctx.lineWidth = Math.max(0.5, (sprite.width ?? (unitScale === 1 ? 2 : 2e-3)) * unitScale);
|
|
1396
|
+
ctx.beginPath();
|
|
1397
|
+
ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
|
|
1398
|
+
ctx.stroke();
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
if (sprite.kind === "streak") {
|
|
1402
|
+
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
1403
|
+
const heading = headingAt(e, t, this.w, this.h) ?? 0;
|
|
1404
|
+
const tailX = p.x - Math.cos(heading) * sz;
|
|
1405
|
+
const tailY = p.y - Math.sin(heading) * sz;
|
|
1406
|
+
const g = ctx.createLinearGradient(tailX, tailY, p.x, p.y);
|
|
1407
|
+
g.addColorStop(0, hexToRgba(resolvedColor, 0));
|
|
1408
|
+
g.addColorStop(1, resolvedColor);
|
|
1409
|
+
ctx.strokeStyle = g;
|
|
1410
|
+
ctx.lineWidth = Math.max(0.5, (sprite.width ?? (unitScale === 1 ? 2 : 2e-3)) * unitScale);
|
|
1411
|
+
ctx.lineCap = "round";
|
|
1412
|
+
ctx.beginPath();
|
|
1413
|
+
ctx.moveTo(tailX, tailY);
|
|
1414
|
+
ctx.lineTo(p.x, p.y);
|
|
1415
|
+
ctx.stroke();
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
if (sprite.kind === "rect") {
|
|
1419
|
+
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
1420
|
+
const rh = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz;
|
|
1421
|
+
ctx.save();
|
|
1422
|
+
ctx.translate(p.x, p.y);
|
|
1423
|
+
if (rot) ctx.rotate(rot);
|
|
1424
|
+
ctx.fillStyle = resolvedColor;
|
|
1425
|
+
ctx.fillRect(-sz / 2, -rh / 2, sz, rh);
|
|
1426
|
+
ctx.restore();
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
880
1429
|
if (sprite.kind === "circle") {
|
|
881
1430
|
const r = sz / 2;
|
|
882
1431
|
const resolvedColor = sprite.colors?.[e.colorIndex] ?? sprite.color;
|
|
@@ -914,11 +1463,11 @@ var SpecInstance = class {
|
|
|
914
1463
|
} else {
|
|
915
1464
|
ctx.textAlign = sprite.align ?? "center";
|
|
916
1465
|
ctx.textBaseline = sprite.baseline ?? "middle";
|
|
917
|
-
ctx.font = sprite.font
|
|
1466
|
+
ctx.font = sprite.font ? /\dpx/.test(sprite.font) ? sprite.font : composeFontShorthand(sz, sprite.font) : `${sz}px system-ui, sans-serif`;
|
|
918
1467
|
ctx.fillStyle = sprite.color ?? "#e6e8ef";
|
|
919
1468
|
const idx = spriteIndexAt(e, t, sprite.strings.length);
|
|
920
1469
|
const text = sprite.strings[idx] ?? sprite.strings[0];
|
|
921
|
-
const mw = sprite.maxWidth ? sprite.maxWidth * (this.effSpec.units === "
|
|
1470
|
+
const mw = sprite.maxWidth ? sprite.maxWidth * (this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h)) : void 0;
|
|
922
1471
|
if (mw) ctx.fillText(text, 0, 0, mw);
|
|
923
1472
|
else ctx.fillText(text, 0, 0);
|
|
924
1473
|
}
|
|
@@ -962,26 +1511,32 @@ var SpecInstance = class {
|
|
|
962
1511
|
this.renderFrame(this.lastT, this.seed);
|
|
963
1512
|
}
|
|
964
1513
|
}
|
|
965
|
-
drawLinks(built, t) {
|
|
1514
|
+
drawLinks(built, t, lifeA, parentE) {
|
|
966
1515
|
const { links } = built.layer;
|
|
967
1516
|
if (!links) return;
|
|
968
1517
|
const { ctx } = this;
|
|
969
|
-
const
|
|
970
|
-
const
|
|
971
|
-
const
|
|
972
|
-
const
|
|
1518
|
+
const motionWraps = ["drift", "rise", "wander"].includes(built.layer.motion.type);
|
|
1519
|
+
const wrap2 = built.layer.wrap !== false && motionWraps;
|
|
1520
|
+
const positions = built.entities.map((e) => this.entityPos(e, t, parentE));
|
|
1521
|
+
const maxDistPx = links.maxDist * (this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h));
|
|
1522
|
+
const edges = linkEdges(links, positions, maxDistPx, wrap2, this.w, this.h);
|
|
1523
|
+
const lw = (links.width ?? 1) * (this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h));
|
|
973
1524
|
ctx.lineWidth = lw;
|
|
974
|
-
|
|
1525
|
+
ctx.lineCap = "butt";
|
|
1526
|
+
for (const { i, j, dist } of edges) {
|
|
975
1527
|
const pi = positions[i];
|
|
976
1528
|
const pj = positions[j];
|
|
977
1529
|
const ei = built.entities[i];
|
|
978
1530
|
let resolvedColor = links.color;
|
|
979
1531
|
if (!resolvedColor) {
|
|
980
1532
|
const sprite = built.layer.sprite;
|
|
981
|
-
if (sprite.kind === "circle"
|
|
982
|
-
|
|
1533
|
+
if (sprite.kind === "circle" || sprite.kind === "ring" || sprite.kind === "streak" || sprite.kind === "rect") {
|
|
1534
|
+
resolvedColor = sprite.colors?.[ei.colorIndex] ?? sprite.color;
|
|
1535
|
+
} else resolvedColor = "#e6e8ef";
|
|
983
1536
|
}
|
|
984
|
-
|
|
1537
|
+
let a = links.alpha ?? alphaAt(ei, t);
|
|
1538
|
+
if (links.falloff) a *= Math.max(0, 1 - dist / maxDistPx);
|
|
1539
|
+
ctx.globalAlpha = a * lifeA;
|
|
985
1540
|
ctx.strokeStyle = resolvedColor;
|
|
986
1541
|
let dx = pj.x - pi.x;
|
|
987
1542
|
let dy = pj.y - pi.y;
|
|
@@ -995,24 +1550,57 @@ var SpecInstance = class {
|
|
|
995
1550
|
ctx.stroke();
|
|
996
1551
|
}
|
|
997
1552
|
}
|
|
998
|
-
/**
|
|
999
|
-
|
|
1000
|
-
|
|
1553
|
+
/** Paint one composite pass at time t. `bgAlpha` < 1 leaves the previous frame
|
|
1554
|
+
* showing through — the ghosting smear. */
|
|
1555
|
+
paintFrame(t, bgAlpha) {
|
|
1001
1556
|
const { ctx } = this;
|
|
1002
|
-
ctx.globalAlpha =
|
|
1557
|
+
ctx.globalAlpha = bgAlpha;
|
|
1003
1558
|
ctx.globalCompositeOperation = "source-over";
|
|
1004
1559
|
this.drawBackground(t);
|
|
1560
|
+
ctx.globalAlpha = 1;
|
|
1005
1561
|
for (const built of this.layers) {
|
|
1562
|
+
const lifeA = lifeAlphaAt(built.layer.life, t);
|
|
1563
|
+
if (lifeA <= 0) continue;
|
|
1564
|
+
const parentE = this.parentEntityFor(built);
|
|
1006
1565
|
ctx.globalCompositeOperation = built.layer.blend ?? "source-over";
|
|
1007
|
-
this.drawLinks(built, t);
|
|
1566
|
+
this.drawLinks(built, t, lifeA, parentE);
|
|
1008
1567
|
for (const e of built.entities) {
|
|
1009
|
-
this.drawTrail(built, e, t);
|
|
1010
|
-
this.drawEntity(built, e, t);
|
|
1568
|
+
this.drawTrail(built, e, t, lifeA, parentE);
|
|
1569
|
+
this.drawEntity(built, e, t, lifeA, parentE);
|
|
1011
1570
|
}
|
|
1012
1571
|
}
|
|
1013
1572
|
ctx.globalAlpha = 1;
|
|
1014
1573
|
ctx.globalCompositeOperation = "source-over";
|
|
1015
1574
|
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Deterministic, frame-addressable render (shared by the rAF loop).
|
|
1577
|
+
*
|
|
1578
|
+
* With `ghosting` set, contiguous frames composite over the previous frame with a
|
|
1579
|
+
* refresh-rate-normalized fade. A non-contiguous seek (pause render, e2e frame
|
|
1580
|
+
* addressing) replays a bounded fixed-step warm-up from a full clear, so the
|
|
1581
|
+
* accumulated smear at time t is identical on every run — the determinism proof
|
|
1582
|
+
* survives statefulness because the "state" is reconstructed from pure history.
|
|
1583
|
+
*/
|
|
1584
|
+
renderFrame(t, _seed) {
|
|
1585
|
+
this.stepTransition(t);
|
|
1586
|
+
const g = this.effSpec.ghosting ?? 0;
|
|
1587
|
+
if (g > 0) {
|
|
1588
|
+
const dt = 1e3 / 60;
|
|
1589
|
+
const contiguous = this.lastRenderT !== Number.NEGATIVE_INFINITY && t > this.lastRenderT && t - this.lastRenderT <= 250;
|
|
1590
|
+
if (contiguous) {
|
|
1591
|
+
this.paintFrame(t, 1 - Math.pow(g, (t - this.lastRenderT) / dt));
|
|
1592
|
+
} else {
|
|
1593
|
+
const k = Math.min(Math.ceil(Math.log(1 / 255) / Math.log(g)), LIMITS.maxGhostReplayFrames);
|
|
1594
|
+
const t0 = Math.max(0, t - k * dt);
|
|
1595
|
+
this.paintFrame(t0, 1);
|
|
1596
|
+
for (let ft = t0 + dt; ft < t - dt / 2; ft += dt) this.paintFrame(ft, 1 - g);
|
|
1597
|
+
if (t > t0) this.paintFrame(t, 1 - g);
|
|
1598
|
+
}
|
|
1599
|
+
} else {
|
|
1600
|
+
this.paintFrame(t, 1);
|
|
1601
|
+
}
|
|
1602
|
+
this.lastRenderT = t;
|
|
1603
|
+
}
|
|
1016
1604
|
setPaused(paused) {
|
|
1017
1605
|
this.paused = paused;
|
|
1018
1606
|
if (paused) {
|
|
@@ -1049,8 +1637,9 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
1049
1637
|
const warnings = [];
|
|
1050
1638
|
const w = viewport.width;
|
|
1051
1639
|
const h = viewport.height;
|
|
1052
|
-
const scale = spec.units === "
|
|
1053
|
-
const
|
|
1640
|
+
const scale = spec.units === "px" ? 1 : Math.min(w, h);
|
|
1641
|
+
const refVp = spec.referenceViewport ?? LIMITS.referenceViewport;
|
|
1642
|
+
const countScale = scale > 1 ? Math.min(w, h) / refVp : 1;
|
|
1054
1643
|
const rng = createRng2(spec.seed ?? 42);
|
|
1055
1644
|
const allEntities = spec.layers.map((l) => buildEntities(l, rng, w, h, scale, countScale));
|
|
1056
1645
|
let totalEntities = 0;
|
|
@@ -1071,6 +1660,20 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
1071
1660
|
message: "trail has no effect on static entities \u2014 they have no past positions to draw"
|
|
1072
1661
|
});
|
|
1073
1662
|
}
|
|
1663
|
+
if (layer.sprite.kind === "streak" && layer.motion.type === "static") {
|
|
1664
|
+
warnings.push({
|
|
1665
|
+
path: `layers[${li}].sprite`,
|
|
1666
|
+
code: "streak-on-static",
|
|
1667
|
+
message: "streak sprites orient along the motion heading \u2014 static entities have none and will render at angle 0"
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
if (layer.motion.type === "wander" && layer.motion.coherence === 1 && entities.length >= 20) {
|
|
1671
|
+
warnings.push({
|
|
1672
|
+
path: `layers[${li}].motion.coherence`,
|
|
1673
|
+
code: "full-coherence",
|
|
1674
|
+
message: "coherence 1 makes every entity share identical harmonics \u2014 the layer moves as a rigid sheet. 0.5\u20130.8 reads as schooling"
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1074
1677
|
if (layer.sprite.kind === "circle") {
|
|
1075
1678
|
const maxR = entities.reduce((m, e) => Math.max(m, e.size / 2), 0);
|
|
1076
1679
|
const maxAlpha = entities.reduce((m, e) => Math.max(m, e.alpha), 0);
|
|
@@ -1092,6 +1695,14 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
1092
1695
|
const pixArea = layer.sprite.kind === "circle" ? Math.PI * r * r : e.size * e.size;
|
|
1093
1696
|
totalCoverage += pixArea * e.alpha / (w * h);
|
|
1094
1697
|
}
|
|
1698
|
+
if (layer.links) {
|
|
1699
|
+
const positions = entities.map((e) => positionAt(e, 0, w, h));
|
|
1700
|
+
const maxDistPx = layer.links.maxDist * scale;
|
|
1701
|
+
const edges = linkEdges(layer.links, positions, maxDistPx, layer.wrap !== false, w, h);
|
|
1702
|
+
const lwPx = (layer.links.width ?? 1) * scale;
|
|
1703
|
+
const la = layer.links.alpha ?? 1;
|
|
1704
|
+
for (const edge of edges) totalCoverage += edge.dist * lwPx * la / (w * h);
|
|
1705
|
+
}
|
|
1095
1706
|
}
|
|
1096
1707
|
if (totalCoverage < 5e-4 && spec.layers.length > 0) {
|
|
1097
1708
|
warnings.push({
|
|
@@ -1119,7 +1730,7 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
1119
1730
|
}
|
|
1120
1731
|
for (let li = 0; li < spec.layers.length; li++) {
|
|
1121
1732
|
const layer = spec.layers[li];
|
|
1122
|
-
if (!layer.links) continue;
|
|
1733
|
+
if (!layer.links || layer.links.mode === "chain") continue;
|
|
1123
1734
|
const entities = allEntities[li];
|
|
1124
1735
|
const positions = entities.map((e) => positionAt(e, 0, w, h));
|
|
1125
1736
|
const maxDist = layer.links.maxDist * scale;
|
|
@@ -1150,8 +1761,10 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }) {
|
|
|
1150
1761
|
});
|
|
1151
1762
|
}
|
|
1152
1763
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1764
|
+
const composed = spec.layers.map((l, li) => ({ l, ents: allEntities[li] })).filter(({ l }) => !["bounce", "warp", "path"].includes(l.motion.type)).filter(({ l }) => !(l.motion.type === "orbit" && l.motion.center && "layer" in l.motion.center));
|
|
1765
|
+
const composedCount = composed.reduce((s, c) => s + c.ents.length, 0);
|
|
1766
|
+
if (composedCount >= 8) {
|
|
1767
|
+
const positions = composed.flatMap(({ ents }) => ents.map((e) => positionAt(e, 0, w, h)));
|
|
1155
1768
|
const cx = positions.reduce((s, p) => s + p.x, 0) / positions.length;
|
|
1156
1769
|
const cy = positions.reduce((s, p) => s + p.y, 0) / positions.length;
|
|
1157
1770
|
const offX = Math.abs(cx / w - 0.5);
|
|
@@ -1174,7 +1787,8 @@ function describeScene(spec, options = {}) {
|
|
|
1174
1787
|
const seed = options.seed ?? spec.seed ?? 42;
|
|
1175
1788
|
const times = options.times ?? [0, 5e3, 15e3];
|
|
1176
1789
|
const scale = spec.units === "viewport" ? Math.min(w, h) : 1;
|
|
1177
|
-
const
|
|
1790
|
+
const refVp = spec.referenceViewport ?? LIMITS.referenceViewport;
|
|
1791
|
+
const countScale = scale > 1 ? Math.min(w, h) / refVp : 1;
|
|
1178
1792
|
const rng = createRng3(seed);
|
|
1179
1793
|
const builtLayers = spec.layers.map((layer) => ({
|
|
1180
1794
|
layer,
|
|
@@ -1259,6 +1873,418 @@ function describeScene(spec, options = {}) {
|
|
|
1259
1873
|
};
|
|
1260
1874
|
}
|
|
1261
1875
|
|
|
1876
|
+
// src/perceive.ts
|
|
1877
|
+
import { createRng as createRng4 } from "@idle-screens/core";
|
|
1878
|
+
function buildScene(spec, opts) {
|
|
1879
|
+
const { width: w, height: h } = opts.viewport ?? { width: 1920, height: 1080 };
|
|
1880
|
+
const seed = opts.seed ?? spec.seed ?? 42;
|
|
1881
|
+
const scale = spec.units === "px" ? 1 : Math.min(w, h);
|
|
1882
|
+
const refVp = spec.referenceViewport ?? LIMITS.referenceViewport;
|
|
1883
|
+
let countScale = scale > 1 ? Math.min(w, h) / refVp : 1;
|
|
1884
|
+
if (countScale > 1) {
|
|
1885
|
+
const rawTotal = spec.layers.reduce((s, l) => s + Math.round(l.count * countScale), 0);
|
|
1886
|
+
if (rawTotal > LIMITS.maxTotal) countScale *= LIMITS.maxTotal / rawTotal;
|
|
1887
|
+
}
|
|
1888
|
+
const rng = createRng4(seed);
|
|
1889
|
+
const layers = spec.layers.map((layer) => ({ layer, entities: buildEntities(layer, rng, w, h, scale, countScale) }));
|
|
1890
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1891
|
+
for (const { layer, entities } of layers) if (layer.key) byKey.set(layer.key, entities);
|
|
1892
|
+
return { w, h, scale, layers, byKey };
|
|
1893
|
+
}
|
|
1894
|
+
function posOf(scene, e, t) {
|
|
1895
|
+
const p = positionAt(e, t, scene.w, scene.h);
|
|
1896
|
+
if (e.orbitParent) {
|
|
1897
|
+
const parent = scene.byKey.get(e.orbitParent)?.[0];
|
|
1898
|
+
if (parent) {
|
|
1899
|
+
const pp = positionAt(parent, t, scene.w, scene.h);
|
|
1900
|
+
p.x += pp.x;
|
|
1901
|
+
p.y += pp.y;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return p;
|
|
1905
|
+
}
|
|
1906
|
+
function hexLuma(hex) {
|
|
1907
|
+
const h = hex.length === 4 ? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}` : hex;
|
|
1908
|
+
const n = parseInt(h.slice(1), 16);
|
|
1909
|
+
if (Number.isNaN(n)) return 0.7;
|
|
1910
|
+
const r = (n >> 16 & 255) / 255;
|
|
1911
|
+
const g = (n >> 8 & 255) / 255;
|
|
1912
|
+
const b = (n & 255) / 255;
|
|
1913
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
1914
|
+
}
|
|
1915
|
+
function spriteLuma(layer, e) {
|
|
1916
|
+
const s = layer.sprite;
|
|
1917
|
+
if (s.kind === "circle" || s.kind === "ring" || s.kind === "streak" || s.kind === "rect") {
|
|
1918
|
+
return hexLuma(s.colors?.[e.colorIndex] ?? s.color);
|
|
1919
|
+
}
|
|
1920
|
+
if (s.kind === "text") return hexLuma(s.color ?? "#e6e8ef");
|
|
1921
|
+
return 0.75;
|
|
1922
|
+
}
|
|
1923
|
+
function textBox(s, e, p) {
|
|
1924
|
+
const m = s.font ? /(\d+(?:\.\d+)?)px/.exec(s.font) : null;
|
|
1925
|
+
const fh = m ? Number(m[1]) : e.size;
|
|
1926
|
+
const str = s.strings[e.spriteIndex] ?? s.strings[0] ?? "";
|
|
1927
|
+
const fw = Math.max(fh, 0.62 * fh * Math.min(str.length, 48));
|
|
1928
|
+
const align = s.align ?? "center";
|
|
1929
|
+
const baseline = s.baseline ?? "middle";
|
|
1930
|
+
return {
|
|
1931
|
+
cx: align === "left" ? p.x + fw / 2 : align === "right" ? p.x - fw / 2 : p.x,
|
|
1932
|
+
cy: baseline === "top" ? p.y + fh * 0.6 : baseline === "bottom" ? p.y - fh * 0.6 : p.y,
|
|
1933
|
+
halfX: fw / 2,
|
|
1934
|
+
halfY: fh * 0.6
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
function luminanceGrid(spec, opts = {}) {
|
|
1938
|
+
const scene = buildScene(spec, opts);
|
|
1939
|
+
const t = opts.t ?? 5e3;
|
|
1940
|
+
const cols = Math.max(8, Math.min(200, opts.cols ?? 80));
|
|
1941
|
+
const rows = Math.max(8, Math.min(200, opts.rows ?? 48));
|
|
1942
|
+
const { w, h, scale } = scene;
|
|
1943
|
+
const cellW = w / cols;
|
|
1944
|
+
const cellH = h / rows;
|
|
1945
|
+
const bg = spec.background;
|
|
1946
|
+
const bgRow = new Array(rows).fill(0);
|
|
1947
|
+
if (!bg || bg.type === "solid") {
|
|
1948
|
+
bgRow.fill(hexLuma(bg?.color ?? "#05050a"));
|
|
1949
|
+
} else {
|
|
1950
|
+
const stops = [...bg.stops].sort((a, b) => a.at - b.at);
|
|
1951
|
+
for (let r = 0; r < rows; r++) {
|
|
1952
|
+
const y = (r + 0.5) / rows;
|
|
1953
|
+
let lum;
|
|
1954
|
+
if (y <= stops[0].at) lum = hexLuma(stops[0].color);
|
|
1955
|
+
else if (y >= stops[stops.length - 1].at) lum = hexLuma(stops[stops.length - 1].color);
|
|
1956
|
+
else {
|
|
1957
|
+
let i = 0;
|
|
1958
|
+
while (i < stops.length - 1 && stops[i + 1].at < y) i++;
|
|
1959
|
+
const a = stops[i];
|
|
1960
|
+
const b = stops[i + 1];
|
|
1961
|
+
const k = b.at === a.at ? 0 : (y - a.at) / (b.at - a.at);
|
|
1962
|
+
lum = hexLuma(a.color) + (hexLuma(b.color) - hexLuma(a.color)) * k;
|
|
1963
|
+
}
|
|
1964
|
+
bgRow[r] = lum;
|
|
1965
|
+
}
|
|
1966
|
+
if (bg.band) {
|
|
1967
|
+
const bandPx = bg.band.height * (spec.units === "px" ? 1 : Math.min(w, h));
|
|
1968
|
+
const fromRow = Math.max(0, Math.floor((h - bandPx) / cellH));
|
|
1969
|
+
for (let r = fromRow; r < rows; r++) bgRow[r] = hexLuma(bg.band.color);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
const cells = new Array(cols * rows);
|
|
1973
|
+
for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) cells[r * cols + c] = bgRow[r];
|
|
1974
|
+
const compose = (idx, lum, a, blend) => {
|
|
1975
|
+
const cur = cells[idx];
|
|
1976
|
+
if (blend === "lighter" || blend === "screen") cells[idx] = Math.min(1, cur + lum * a);
|
|
1977
|
+
else if (blend === "multiply") cells[idx] = cur * (1 - a * (1 - lum));
|
|
1978
|
+
else cells[idx] = cur * (1 - a) + lum * a;
|
|
1979
|
+
};
|
|
1980
|
+
for (const { layer, entities } of scene.layers) {
|
|
1981
|
+
const lifeA = lifeAlphaAt(layer.life, t);
|
|
1982
|
+
if (lifeA <= 0) continue;
|
|
1983
|
+
for (const e of entities) {
|
|
1984
|
+
const a = Math.min(1, alphaAt(e, t) * lifeA);
|
|
1985
|
+
if (a <= 4e-3) continue;
|
|
1986
|
+
const lum = spriteLuma(layer, e);
|
|
1987
|
+
const p = posOf(scene, e, t);
|
|
1988
|
+
const sz = sizeAt(e, t);
|
|
1989
|
+
const s = layer.sprite;
|
|
1990
|
+
if (s.kind === "streak") {
|
|
1991
|
+
const heading = headingAt(e, t, w, h) ?? 0;
|
|
1992
|
+
const steps = Math.max(2, Math.ceil(sz / Math.min(cellW, cellH)));
|
|
1993
|
+
const wgt = Math.min(1, (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale / cellH);
|
|
1994
|
+
for (let i = 0; i <= steps; i++) {
|
|
1995
|
+
const k = i / steps;
|
|
1996
|
+
const x = p.x - Math.cos(heading) * sz * (1 - k);
|
|
1997
|
+
const y = p.y - Math.sin(heading) * sz * (1 - k);
|
|
1998
|
+
const c = Math.floor(x / cellW);
|
|
1999
|
+
const r = Math.floor(y / cellH);
|
|
2000
|
+
if (c < 0 || c >= cols || r < 0 || r >= rows) continue;
|
|
2001
|
+
compose(r * cols + c, lum, a * k * wgt, layer.blend);
|
|
2002
|
+
}
|
|
2003
|
+
continue;
|
|
2004
|
+
}
|
|
2005
|
+
let centerX = p.x;
|
|
2006
|
+
let centerY = p.y;
|
|
2007
|
+
let halfX = sz / 2;
|
|
2008
|
+
let halfY = s.kind === "rect" ? (e.size2 ?? sz) * (e.size > 0 ? sz / e.size : 1) / 2 : sz / 2;
|
|
2009
|
+
if (s.kind === "text") {
|
|
2010
|
+
const box = textBox(s, e, p);
|
|
2011
|
+
centerX = box.cx;
|
|
2012
|
+
centerY = box.cy;
|
|
2013
|
+
halfX = box.halfX;
|
|
2014
|
+
halfY = box.halfY;
|
|
2015
|
+
}
|
|
2016
|
+
const c0 = Math.max(0, Math.floor((centerX - halfX) / cellW));
|
|
2017
|
+
const c1 = Math.min(cols - 1, Math.floor((centerX + halfX) / cellW));
|
|
2018
|
+
const r0 = Math.max(0, Math.floor((centerY - halfY) / cellH));
|
|
2019
|
+
const r1 = Math.min(rows - 1, Math.floor((centerY + halfY) / cellH));
|
|
2020
|
+
const circular = s.kind === "circle" || s.kind === "ring";
|
|
2021
|
+
const soft = s.kind === "circle" && s.soft;
|
|
2022
|
+
const inkWeight = s.kind === "text" || s.kind === "emoji" ? 0.55 : 1;
|
|
2023
|
+
for (let r = r0; r <= r1; r++) {
|
|
2024
|
+
for (let c = c0; c <= c1; c++) {
|
|
2025
|
+
const dx = (c + 0.5) * cellW - centerX;
|
|
2026
|
+
const dy = (r + 0.5) * cellH - centerY;
|
|
2027
|
+
if (circular) {
|
|
2028
|
+
const d = Math.sqrt(dx * dx + dy * dy);
|
|
2029
|
+
if (d > halfX + Math.min(cellW, cellH) / 2) continue;
|
|
2030
|
+
if (s.kind === "ring") {
|
|
2031
|
+
const strokeW = (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale;
|
|
2032
|
+
if (d < halfX - strokeW) continue;
|
|
2033
|
+
}
|
|
2034
|
+
const wgt = soft ? Math.max(0.1, 1 - d / Math.max(halfX, 1e-6)) : 1;
|
|
2035
|
+
compose(r * cols + c, lum, a * wgt, layer.blend);
|
|
2036
|
+
} else {
|
|
2037
|
+
compose(r * cols + c, lum, a * inkWeight, layer.blend);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
if (layer.links) {
|
|
2043
|
+
const positions = entities.map((e) => posOf(scene, e, t));
|
|
2044
|
+
const maxDistPx = layer.links.maxDist * scale;
|
|
2045
|
+
const motionWraps = ["drift", "rise", "wander"].includes(layer.motion.type);
|
|
2046
|
+
const edges = linkEdges(layer.links, positions, maxDistPx, layer.wrap !== false && motionWraps, w, h);
|
|
2047
|
+
const la = Math.min(1, (layer.links.alpha ?? 0.6) * lifeA);
|
|
2048
|
+
const lum = layer.links.color ? hexLuma(layer.links.color) : 0.7;
|
|
2049
|
+
const wgt = Math.min(1, (layer.links.width ?? 1) * scale / cellH) * la;
|
|
2050
|
+
for (const edge of edges) {
|
|
2051
|
+
const pi = positions[edge.i];
|
|
2052
|
+
const pj = positions[edge.j];
|
|
2053
|
+
const steps = Math.max(1, Math.ceil(edge.dist / Math.min(cellW, cellH)));
|
|
2054
|
+
for (let i = 0; i <= steps; i++) {
|
|
2055
|
+
const k = i / steps;
|
|
2056
|
+
const c = Math.floor((pi.x + (pj.x - pi.x) * k) / cellW);
|
|
2057
|
+
const r = Math.floor((pi.y + (pj.y - pi.y) * k) / cellH);
|
|
2058
|
+
if (c < 0 || c >= cols || r < 0 || r >= rows) continue;
|
|
2059
|
+
compose(r * cols + c, lum, wgt, layer.blend);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
let sum = 0;
|
|
2065
|
+
let covered = 0;
|
|
2066
|
+
let devSum = 0;
|
|
2067
|
+
let cxAcc = 0;
|
|
2068
|
+
let cyAcc = 0;
|
|
2069
|
+
const rowProfile = new Array(rows).fill(0);
|
|
2070
|
+
const colProfile = new Array(cols).fill(0);
|
|
2071
|
+
for (let r = 0; r < rows; r++) {
|
|
2072
|
+
for (let c = 0; c < cols; c++) {
|
|
2073
|
+
const v = cells[r * cols + c];
|
|
2074
|
+
sum += v;
|
|
2075
|
+
const dev = Math.abs(v - bgRow[r]);
|
|
2076
|
+
if (dev > 0.03) covered++;
|
|
2077
|
+
devSum += dev;
|
|
2078
|
+
cxAcc += dev * (c + 0.5);
|
|
2079
|
+
cyAcc += dev * (r + 0.5);
|
|
2080
|
+
rowProfile[r] += dev;
|
|
2081
|
+
colProfile[c] += dev;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
for (let r = 0; r < rows; r++) rowProfile[r] = rowProfile[r] / cols;
|
|
2085
|
+
for (let c = 0; c < cols; c++) colProfile[c] = colProfile[c] / rows;
|
|
2086
|
+
return {
|
|
2087
|
+
cols,
|
|
2088
|
+
rows,
|
|
2089
|
+
cells,
|
|
2090
|
+
background: bgRow,
|
|
2091
|
+
meanLuminance: sum / (cols * rows),
|
|
2092
|
+
coverage: covered / (cols * rows),
|
|
2093
|
+
centroid: devSum > 1e-6 ? { x: cxAcc / devSum / cols, y: cyAcc / devSum / rows } : null,
|
|
2094
|
+
rowProfile,
|
|
2095
|
+
colProfile
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
var DITHER = [
|
|
2099
|
+
[0.5 / 8, 4.5 / 8],
|
|
2100
|
+
[2.5 / 8, 6.5 / 8],
|
|
2101
|
+
[1.5 / 8, 5.5 / 8],
|
|
2102
|
+
[3.5 / 8, 7.5 / 8]
|
|
2103
|
+
];
|
|
2104
|
+
var DOT_BIT = [
|
|
2105
|
+
[1, 8],
|
|
2106
|
+
[2, 16],
|
|
2107
|
+
[4, 32],
|
|
2108
|
+
[64, 128]
|
|
2109
|
+
];
|
|
2110
|
+
function renderBrailleMap(grid) {
|
|
2111
|
+
const sorted = [...grid.cells].sort((a, b) => a - b);
|
|
2112
|
+
const lo = sorted[0];
|
|
2113
|
+
const hi = Math.max(sorted[Math.floor(sorted.length * 0.98)], lo + 0.08);
|
|
2114
|
+
const expose = (v) => Math.pow(Math.max(0, Math.min(1, (v - lo) / (hi - lo))), 0.8);
|
|
2115
|
+
const outRows = [];
|
|
2116
|
+
for (let br = 0; br < Math.floor(grid.rows / 4); br++) {
|
|
2117
|
+
let line = "";
|
|
2118
|
+
for (let bc = 0; bc < Math.floor(grid.cols / 2); bc++) {
|
|
2119
|
+
let bits = 0;
|
|
2120
|
+
for (let dr = 0; dr < 4; dr++) {
|
|
2121
|
+
for (let dc = 0; dc < 2; dc++) {
|
|
2122
|
+
const v = expose(grid.cells[(br * 4 + dr) * grid.cols + (bc * 2 + dc)]);
|
|
2123
|
+
if (v > DITHER[dr][dc]) bits |= DOT_BIT[dr][dc];
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
line += String.fromCharCode(10240 + bits);
|
|
2127
|
+
}
|
|
2128
|
+
outRows.push(line);
|
|
2129
|
+
}
|
|
2130
|
+
return outRows.join("\n");
|
|
2131
|
+
}
|
|
2132
|
+
function dominanceRanking(spec, opts = {}) {
|
|
2133
|
+
const scene = buildScene(spec, opts);
|
|
2134
|
+
const t = opts.t ?? 5e3;
|
|
2135
|
+
const { w, h, scale } = scene;
|
|
2136
|
+
const bgMean = (() => {
|
|
2137
|
+
const bg = spec.background;
|
|
2138
|
+
if (!bg || bg.type === "solid") return hexLuma(bg?.color ?? "#05050a");
|
|
2139
|
+
return bg.stops.reduce((s, st) => s + hexLuma(st.color), 0) / bg.stops.length;
|
|
2140
|
+
})();
|
|
2141
|
+
const raw = scene.layers.map(({ layer, entities }, layerIndex) => {
|
|
2142
|
+
const lifeA = lifeAlphaAt(layer.life, t);
|
|
2143
|
+
let area = 0;
|
|
2144
|
+
let lumAcc = 0;
|
|
2145
|
+
for (const e of entities) {
|
|
2146
|
+
const a = alphaAt(e, t) * lifeA;
|
|
2147
|
+
const sz = sizeAt(e, t);
|
|
2148
|
+
const s = layer.sprite;
|
|
2149
|
+
let entArea;
|
|
2150
|
+
if (s.kind === "circle") entArea = Math.PI * (sz / 2) * (sz / 2);
|
|
2151
|
+
else if (s.kind === "ring") entArea = Math.PI * sz * ((s.width ?? (scale === 1 ? 2 : 2e-3)) * scale);
|
|
2152
|
+
else if (s.kind === "streak") entArea = sz * ((s.width ?? (scale === 1 ? 2 : 2e-3)) * scale);
|
|
2153
|
+
else if (s.kind === "rect") {
|
|
2154
|
+
const h2 = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz;
|
|
2155
|
+
entArea = sz * h2;
|
|
2156
|
+
} else if (s.kind === "text") {
|
|
2157
|
+
const box = textBox(s, e, { x: 0, y: 0 });
|
|
2158
|
+
entArea = box.halfX * 2 * box.halfY * 2 * 0.55;
|
|
2159
|
+
} else entArea = sz * sz * 0.55;
|
|
2160
|
+
area += entArea * a;
|
|
2161
|
+
lumAcc += spriteLuma(layer, e);
|
|
2162
|
+
}
|
|
2163
|
+
if (layer.links && entities.length > 1) {
|
|
2164
|
+
const positions = entities.map((e) => posOf(scene, e, t));
|
|
2165
|
+
const motionWraps = ["drift", "rise", "wander"].includes(layer.motion.type);
|
|
2166
|
+
const edges = linkEdges(layer.links, positions, layer.links.maxDist * scale, layer.wrap !== false && motionWraps, w, h);
|
|
2167
|
+
const la = (layer.links.alpha ?? 0.6) * lifeA;
|
|
2168
|
+
const lw = (layer.links.width ?? 1) * scale;
|
|
2169
|
+
for (const edge of edges) area += edge.dist * lw * la;
|
|
2170
|
+
}
|
|
2171
|
+
const meanLuma = entities.length ? lumAcc / entities.length : 0;
|
|
2172
|
+
const contrast = Math.abs(meanLuma - bgMean) + 0.1;
|
|
2173
|
+
const blendBoost = layer.blend === "lighter" || layer.blend === "screen" ? 1.3 : layer.blend === "multiply" ? 1.1 : 1;
|
|
2174
|
+
const motionBoost = layer.motion.type === "static" ? 1 : 1.2;
|
|
2175
|
+
const weight = area / (w * h) * contrast * blendBoost * motionBoost;
|
|
2176
|
+
return { layerIndex, key: layer.key, weight, entityCount: entities.length, meanLuma, factors: { area: area / (w * h), contrast, blendBoost, motionBoost } };
|
|
2177
|
+
});
|
|
2178
|
+
const total = raw.reduce((s, r) => s + r.weight, 0) || 1;
|
|
2179
|
+
return raw.slice().sort((a, b) => b.weight - a.weight).map((r, i) => ({
|
|
2180
|
+
rank: i + 1,
|
|
2181
|
+
layerIndex: r.layerIndex,
|
|
2182
|
+
key: r.key,
|
|
2183
|
+
share: r.weight / total,
|
|
2184
|
+
entityCount: r.entityCount,
|
|
2185
|
+
meanLuma: r.meanLuma,
|
|
2186
|
+
factors: r.factors
|
|
2187
|
+
}));
|
|
2188
|
+
}
|
|
2189
|
+
function motionStats(spec, opts = {}) {
|
|
2190
|
+
const scene = buildScene(spec, opts);
|
|
2191
|
+
const t = opts.t ?? 5e3;
|
|
2192
|
+
const dt = opts.dt ?? 500;
|
|
2193
|
+
const { w, h } = scene;
|
|
2194
|
+
return scene.layers.map(({ layer, entities }, layerIndex) => {
|
|
2195
|
+
let acc = 0;
|
|
2196
|
+
let max = 0;
|
|
2197
|
+
let n = 0;
|
|
2198
|
+
for (const e of entities) {
|
|
2199
|
+
const p0 = posOf(scene, e, t);
|
|
2200
|
+
const p1 = posOf(scene, e, t + dt);
|
|
2201
|
+
const dx = p1.x - p0.x;
|
|
2202
|
+
const dy = p1.y - p0.y;
|
|
2203
|
+
if (Math.abs(dx) > w / 2 || Math.abs(dy) > h / 2) continue;
|
|
2204
|
+
const speed = Math.sqrt(dx * dx + dy * dy) / dt * 1e3;
|
|
2205
|
+
acc += speed;
|
|
2206
|
+
max = Math.max(max, speed);
|
|
2207
|
+
n++;
|
|
2208
|
+
}
|
|
2209
|
+
const meanSpeed = n ? acc / n : 0;
|
|
2210
|
+
return { layerIndex, key: layer.key, meanSpeed, maxSpeed: max, moving: meanSpeed > 0.5 };
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
var REGION_NAMES = [
|
|
2214
|
+
"top-left",
|
|
2215
|
+
"top-center",
|
|
2216
|
+
"top-right",
|
|
2217
|
+
"mid-left",
|
|
2218
|
+
"center",
|
|
2219
|
+
"mid-right",
|
|
2220
|
+
"bottom-left",
|
|
2221
|
+
"bottom-center",
|
|
2222
|
+
"bottom-right"
|
|
2223
|
+
];
|
|
2224
|
+
function regionMeans(grid) {
|
|
2225
|
+
const out = new Array(9).fill(0);
|
|
2226
|
+
const cnt = new Array(9).fill(0);
|
|
2227
|
+
for (let r = 0; r < grid.rows; r++) {
|
|
2228
|
+
for (let c = 0; c < grid.cols; c++) {
|
|
2229
|
+
const region = Math.min(2, Math.floor(r / grid.rows * 3)) * 3 + Math.min(2, Math.floor(c / grid.cols * 3));
|
|
2230
|
+
out[region] += Math.abs(grid.cells[r * grid.cols + c] - grid.background[r]);
|
|
2231
|
+
cnt[region]++;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
return out.map((v, i) => cnt[i] ? v / cnt[i] : 0);
|
|
2235
|
+
}
|
|
2236
|
+
function diffScenes(a, b, opts = {}) {
|
|
2237
|
+
const t = opts.t ?? 5e3;
|
|
2238
|
+
const gridA = luminanceGrid(a, opts);
|
|
2239
|
+
const gridB = luminanceGrid(b, opts);
|
|
2240
|
+
const regsA = regionMeans(gridA);
|
|
2241
|
+
const regsB = regionMeans(gridB);
|
|
2242
|
+
const domA = dominanceRanking(a, opts);
|
|
2243
|
+
const domB = dominanceRanking(b, opts);
|
|
2244
|
+
const nameOf = (d) => d.key ?? `layer-${d.layerIndex}`;
|
|
2245
|
+
const names = /* @__PURE__ */ new Set([...domA.map(nameOf), ...domB.map(nameOf)]);
|
|
2246
|
+
const dominance = [...names].map((key) => {
|
|
2247
|
+
const inA = domA.find((d) => nameOf(d) === key);
|
|
2248
|
+
const inB = domB.find((d) => nameOf(d) === key);
|
|
2249
|
+
return {
|
|
2250
|
+
key,
|
|
2251
|
+
rankA: inA?.rank ?? null,
|
|
2252
|
+
rankB: inB?.rank ?? null,
|
|
2253
|
+
shareA: inA?.share ?? 0,
|
|
2254
|
+
shareB: inB?.share ?? 0
|
|
2255
|
+
};
|
|
2256
|
+
}).sort((x, y) => Math.abs(y.shareB - y.shareA) - Math.abs(x.shareB - x.shareA));
|
|
2257
|
+
const codesA = new Set(adviseSpec(a).map((w) => w.code));
|
|
2258
|
+
const codesB = new Set(adviseSpec(b).map((w) => w.code));
|
|
2259
|
+
return {
|
|
2260
|
+
t,
|
|
2261
|
+
coverage: { a: gridA.coverage, b: gridB.coverage, delta: gridB.coverage - gridA.coverage },
|
|
2262
|
+
meanLuminance: { a: gridA.meanLuminance, b: gridB.meanLuminance, delta: gridB.meanLuminance - gridA.meanLuminance },
|
|
2263
|
+
balanceShift: gridA.centroid && gridB.centroid ? { dx: gridB.centroid.x - gridA.centroid.x, dy: gridB.centroid.y - gridA.centroid.y } : null,
|
|
2264
|
+
regions: REGION_NAMES.map((region, i) => ({ region, a: regsA[i], b: regsB[i], delta: regsB[i] - regsA[i] })),
|
|
2265
|
+
dominance,
|
|
2266
|
+
advisories: {
|
|
2267
|
+
added: [...codesB].filter((c) => !codesA.has(c)),
|
|
2268
|
+
removed: [...codesA].filter((c) => !codesB.has(c))
|
|
2269
|
+
}
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
function perceiveScene(spec, opts = {}) {
|
|
2273
|
+
const grid = luminanceGrid(spec, opts);
|
|
2274
|
+
return {
|
|
2275
|
+
t: opts.t ?? 5e3,
|
|
2276
|
+
braille: renderBrailleMap(grid),
|
|
2277
|
+
coverage: grid.coverage,
|
|
2278
|
+
meanLuminance: grid.meanLuminance,
|
|
2279
|
+
centroid: grid.centroid,
|
|
2280
|
+
rowProfile: grid.rowProfile,
|
|
2281
|
+
colProfile: grid.colProfile,
|
|
2282
|
+
dominance: dominanceRanking(spec, opts),
|
|
2283
|
+
motion: motionStats(spec, opts),
|
|
2284
|
+
advisories: adviseSpec(spec, opts.viewport ?? { width: 1920, height: 1080 })
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
|
|
1262
2288
|
// src/examples/aquarium.ts
|
|
1263
2289
|
var AQUARIUM_SPEC = {
|
|
1264
2290
|
schemaVersion: 1,
|
|
@@ -1273,20 +2299,95 @@ var AQUARIUM_SPEC = {
|
|
|
1273
2299
|
{ at: 0.6, color: "#062534" },
|
|
1274
2300
|
{ at: 1, color: "#02141d" }
|
|
1275
2301
|
],
|
|
1276
|
-
band: { color: "#0b1a12", height:
|
|
2302
|
+
band: { color: "#0b1a12", height: 0.02222 }
|
|
1277
2303
|
},
|
|
1278
2304
|
layers: [
|
|
1279
2305
|
{
|
|
1280
2306
|
count: 14,
|
|
1281
2307
|
sprite: { kind: "emoji", glyphs: ["\u{1F41F}", "\u{1F420}", "\u{1F421}", "\u{1F988}", "\u{1F419}"] },
|
|
1282
|
-
size: [
|
|
1283
|
-
motion: { type: "drift", speed: [
|
|
2308
|
+
size: [0.03148, 0.06667],
|
|
2309
|
+
motion: { type: "drift", speed: [0.02778, 0.1019], bidirectional: true, bob: 5556e-6 },
|
|
1284
2310
|
flip: true
|
|
1285
2311
|
},
|
|
1286
2312
|
{
|
|
1287
2313
|
count: 22,
|
|
1288
|
-
sprite: { kind: "circle", radius: [
|
|
1289
|
-
motion: { type: "rise", speed: [
|
|
2314
|
+
sprite: { kind: "circle", radius: [1852e-6, 6481e-6], color: "#c8ebff" },
|
|
2315
|
+
motion: { type: "rise", speed: [0.02222, 0.07222], sway: 7407e-6 }
|
|
2316
|
+
}
|
|
2317
|
+
]
|
|
2318
|
+
};
|
|
2319
|
+
|
|
2320
|
+
// src/examples/aurora.ts
|
|
2321
|
+
var AURORA_SPEC = {
|
|
2322
|
+
schemaVersion: 1,
|
|
2323
|
+
id: "aurora",
|
|
2324
|
+
label: "Aurora",
|
|
2325
|
+
seed: 66,
|
|
2326
|
+
motionIntensity: "calm",
|
|
2327
|
+
ghosting: 0.85,
|
|
2328
|
+
background: {
|
|
2329
|
+
type: "gradient",
|
|
2330
|
+
stops: [
|
|
2331
|
+
{ at: 0, color: "#02030d" },
|
|
2332
|
+
{ at: 0.65, color: "#050a18" },
|
|
2333
|
+
{ at: 1, color: "#0a1220" }
|
|
2334
|
+
],
|
|
2335
|
+
drift: { period: 4e4, amount: 0.1 }
|
|
2336
|
+
},
|
|
2337
|
+
layers: [
|
|
2338
|
+
{
|
|
2339
|
+
key: "stars",
|
|
2340
|
+
count: 110,
|
|
2341
|
+
sprite: { kind: "circle", radius: [5e-4, 15e-4], color: "#e8ecff", soft: true },
|
|
2342
|
+
motion: { type: "static" },
|
|
2343
|
+
region: { y: [0, 0.75] },
|
|
2344
|
+
alpha: [0.2, 0.6],
|
|
2345
|
+
// A slow shimmer travels across the sky instead of stars twinkling independently.
|
|
2346
|
+
pulse: { amp: 0.25, period: 6e3, wave: { wavelength: 0.5, angle: 15 } }
|
|
2347
|
+
},
|
|
2348
|
+
{
|
|
2349
|
+
key: "curtain-green",
|
|
2350
|
+
count: 100,
|
|
2351
|
+
sprite: {
|
|
2352
|
+
kind: "circle",
|
|
2353
|
+
radius: [0.055, 0.11],
|
|
2354
|
+
color: "#1fd68a",
|
|
2355
|
+
soft: true,
|
|
2356
|
+
colors: ["#1fd68a", "#2ee6c8", "#4bd1ff"],
|
|
2357
|
+
colorWeights: [5, 2, 1]
|
|
2358
|
+
},
|
|
2359
|
+
motion: { type: "wander", speed: [4e-3, 0.012], angle: 0, meander: 0.13, coherence: 0.7 },
|
|
2360
|
+
region: { y: [0.1, 0.42] },
|
|
2361
|
+
alpha: [0.04, 0.1],
|
|
2362
|
+
blend: "lighter",
|
|
2363
|
+
grow: { amp: 0.25, period: 9e3 }
|
|
2364
|
+
},
|
|
2365
|
+
{
|
|
2366
|
+
key: "curtain-violet",
|
|
2367
|
+
count: 60,
|
|
2368
|
+
sprite: {
|
|
2369
|
+
kind: "circle",
|
|
2370
|
+
radius: [0.045, 0.09],
|
|
2371
|
+
color: "#8d5bff",
|
|
2372
|
+
soft: true,
|
|
2373
|
+
colors: ["#8d5bff", "#d14bff", "#ff4bd8"],
|
|
2374
|
+
colorWeights: [4, 1.5, 0.5]
|
|
2375
|
+
},
|
|
2376
|
+
motion: { type: "wander", speed: [3e-3, 0.01], angle: 180, meander: 0.1, coherence: 0.55 },
|
|
2377
|
+
region: { y: [0.04, 0.28] },
|
|
2378
|
+
alpha: [0.03, 0.08],
|
|
2379
|
+
blend: "lighter",
|
|
2380
|
+
grow: { amp: 0.3, period: 12e3 }
|
|
2381
|
+
},
|
|
2382
|
+
{
|
|
2383
|
+
key: "fireflies",
|
|
2384
|
+
count: 14,
|
|
2385
|
+
sprite: { kind: "circle", radius: [1e-3, 3e-3], color: "#b8ffe1", soft: true },
|
|
2386
|
+
motion: { type: "wander", speed: [6e-3, 0.02], meander: 0.06 },
|
|
2387
|
+
region: { y: [0.55, 0.95] },
|
|
2388
|
+
alpha: [0.2, 0.55],
|
|
2389
|
+
pulse: { amp: 0.3, period: 3500 },
|
|
2390
|
+
blend: "lighter"
|
|
1290
2391
|
}
|
|
1291
2392
|
]
|
|
1292
2393
|
};
|
|
@@ -1311,7 +2412,7 @@ var COMETS_SPEC = {
|
|
|
1311
2412
|
{
|
|
1312
2413
|
key: "stars",
|
|
1313
2414
|
count: 120,
|
|
1314
|
-
sprite: { kind: "circle", radius: [
|
|
2415
|
+
sprite: { kind: "circle", radius: [463e-6, 1852e-6], color: "#ffffff", soft: true },
|
|
1315
2416
|
motion: { type: "static" },
|
|
1316
2417
|
alpha: [0.3, 0.7],
|
|
1317
2418
|
pulse: { amp: 0.15, period: 3e3 }
|
|
@@ -1319,8 +2420,8 @@ var COMETS_SPEC = {
|
|
|
1319
2420
|
{
|
|
1320
2421
|
key: "comets",
|
|
1321
2422
|
count: 8,
|
|
1322
|
-
sprite: { kind: "circle", radius: [
|
|
1323
|
-
motion: { type: "drift", speed: [
|
|
2423
|
+
sprite: { kind: "circle", radius: [2778e-6, 7407e-6], color: "#88ccff", soft: true },
|
|
2424
|
+
motion: { type: "drift", speed: [0.07407, 0.1852], angle: 225 },
|
|
1324
2425
|
alpha: [0.7, 1],
|
|
1325
2426
|
blend: "lighter",
|
|
1326
2427
|
trail: { length: 1500, fade: 1 }
|
|
@@ -1328,8 +2429,8 @@ var COMETS_SPEC = {
|
|
|
1328
2429
|
{
|
|
1329
2430
|
key: "fireflies",
|
|
1330
2431
|
count: 20,
|
|
1331
|
-
sprite: { kind: "circle", radius: [
|
|
1332
|
-
motion: { type: "drift", speed: [
|
|
2432
|
+
sprite: { kind: "circle", radius: [1852e-6, 463e-5], color: "#ffdd44", soft: true },
|
|
2433
|
+
motion: { type: "drift", speed: [9259e-6, 0.02778], bidirectional: true, bob: 0.01852 },
|
|
1333
2434
|
alpha: [0.4, 0.8],
|
|
1334
2435
|
pulse: { amp: 0.3, period: 2e3 },
|
|
1335
2436
|
blend: "lighter",
|
|
@@ -1357,7 +2458,7 @@ var CONSTELLATION_SPEC = {
|
|
|
1357
2458
|
// Distant static stars (no links, just atmosphere)
|
|
1358
2459
|
{
|
|
1359
2460
|
count: 100,
|
|
1360
|
-
sprite: { kind: "circle", radius: [
|
|
2461
|
+
sprite: { kind: "circle", radius: [9259e-7, 2778e-6], color: "#667799", colors: ["#8899cc", "#99aadd", "#aabbee", "#7788aa"] },
|
|
1361
2462
|
alpha: [0.2, 0.7],
|
|
1362
2463
|
pulse: { amp: 0.2, period: 3e3 },
|
|
1363
2464
|
motion: { type: "static" }
|
|
@@ -1366,19 +2467,19 @@ var CONSTELLATION_SPEC = {
|
|
|
1366
2467
|
{
|
|
1367
2468
|
key: "nodes",
|
|
1368
2469
|
count: 40,
|
|
1369
|
-
sprite: { kind: "circle", radius: [
|
|
2470
|
+
sprite: { kind: "circle", radius: [1852e-6, 463e-5], color: "#88aaff", colors: ["#88aaff", "#aaccff", "#ffcc88", "#ff8866"], soft: true },
|
|
1370
2471
|
alpha: [0.5, 0.9],
|
|
1371
2472
|
blend: "lighter",
|
|
1372
|
-
motion: { type: "drift", speed: [
|
|
1373
|
-
links: { k: 3, maxDist:
|
|
2473
|
+
motion: { type: "drift", speed: [1852e-6, 7407e-6], bidirectional: true, bob: 2778e-6 },
|
|
2474
|
+
links: { k: 3, maxDist: 0.1852, alpha: 0.15, width: 463e-6 }
|
|
1374
2475
|
},
|
|
1375
2476
|
// Cycling asterism markers
|
|
1376
2477
|
{
|
|
1377
2478
|
count: 6,
|
|
1378
2479
|
sprite: { kind: "emoji", glyphs: ["\u2726", "\u2727", "\u22C6", "\u2217"], cycle: { period: 2e3 } },
|
|
1379
|
-
size: [
|
|
2480
|
+
size: [0.01296, 0.02037],
|
|
1380
2481
|
alpha: [0.4, 0.7],
|
|
1381
|
-
motion: { type: "drift", speed: [
|
|
2482
|
+
motion: { type: "drift", speed: [9259e-7, 3704e-6], bidirectional: true }
|
|
1382
2483
|
}
|
|
1383
2484
|
]
|
|
1384
2485
|
};
|
|
@@ -1387,7 +2488,7 @@ var CONSTELLATION_SPEC = {
|
|
|
1387
2488
|
var DASHBOARD_SPEC = {
|
|
1388
2489
|
schemaVersion: 1,
|
|
1389
2490
|
id: "dev-dashboard",
|
|
1390
|
-
label: "
|
|
2491
|
+
label: "Control Center",
|
|
1391
2492
|
seed: 256,
|
|
1392
2493
|
motionIntensity: "calm",
|
|
1393
2494
|
background: {
|
|
@@ -1401,473 +2502,490 @@ var DASHBOARD_SPEC = {
|
|
|
1401
2502
|
layers: [
|
|
1402
2503
|
// --- ambient depth ---
|
|
1403
2504
|
{
|
|
1404
|
-
|
|
1405
|
-
|
|
2505
|
+
key: "dust",
|
|
2506
|
+
count: 40,
|
|
2507
|
+
sprite: { kind: "circle", radius: [1852e-7, 7407e-7], color: "#1a2538" },
|
|
1406
2508
|
alpha: [0.1, 0.25],
|
|
1407
|
-
motion: { type: "drift", speed: [
|
|
2509
|
+
motion: { type: "drift", speed: [2778e-7, 1111e-6], bob: 2778e-7 }
|
|
1408
2510
|
},
|
|
1409
2511
|
{
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
2512
|
+
key: "led-wall",
|
|
2513
|
+
count: 140,
|
|
2514
|
+
sprite: { kind: "circle", radius: [8e-4, 12e-4], color: "#274058" },
|
|
2515
|
+
alpha: [0.05, 0.11],
|
|
2516
|
+
layout: { type: "grid", columns: 20, jitter: 0.15 },
|
|
2517
|
+
// A slow breath travels diagonally across the wall of dots.
|
|
2518
|
+
pulse: { amp: 0.3, period: 5e3, wave: { wavelength: 0.35, angle: 20 } },
|
|
2519
|
+
motion: { type: "static" }
|
|
1416
2520
|
},
|
|
1417
|
-
// --- header ---
|
|
2521
|
+
// --- header (boots first) ---
|
|
1418
2522
|
{
|
|
1419
2523
|
key: "title",
|
|
1420
2524
|
count: 1,
|
|
1421
|
-
position: { x: 0.5, y: 0.
|
|
2525
|
+
position: { x: 0.5, y: 0.045 },
|
|
1422
2526
|
sprite: {
|
|
1423
2527
|
kind: "text",
|
|
1424
|
-
strings: ["
|
|
2528
|
+
strings: ["CONTROL CENTER"],
|
|
1425
2529
|
color: "#556677",
|
|
1426
2530
|
font: "600 13px 'SF Mono', monospace",
|
|
1427
2531
|
align: "center"
|
|
1428
2532
|
},
|
|
1429
|
-
motion: { type: "static" }
|
|
2533
|
+
motion: { type: "static" },
|
|
2534
|
+
life: { enter: 200, fade: 600 }
|
|
1430
2535
|
},
|
|
1431
2536
|
{
|
|
1432
2537
|
key: "status",
|
|
1433
2538
|
count: 1,
|
|
1434
|
-
position: { x: 0.
|
|
2539
|
+
position: { x: 0.94, y: 0.045 },
|
|
1435
2540
|
sprite: {
|
|
1436
2541
|
kind: "text",
|
|
1437
|
-
strings: ["\u25CF
|
|
2542
|
+
strings: ["\u25CF ALL SYSTEMS GO"],
|
|
1438
2543
|
color: "#44cc66",
|
|
1439
2544
|
font: "bold 10px monospace",
|
|
1440
2545
|
align: "right"
|
|
1441
2546
|
},
|
|
1442
|
-
|
|
2547
|
+
pulse: { amp: 0.2, period: 2400 },
|
|
2548
|
+
motion: { type: "static" },
|
|
2549
|
+
life: { enter: 200, fade: 600 }
|
|
1443
2550
|
},
|
|
1444
2551
|
{
|
|
1445
|
-
key: "
|
|
2552
|
+
key: "clock",
|
|
1446
2553
|
count: 1,
|
|
1447
|
-
position: { x: 0.
|
|
2554
|
+
position: { x: 0.06, y: 0.045 },
|
|
1448
2555
|
sprite: {
|
|
1449
2556
|
kind: "text",
|
|
1450
|
-
strings: ["
|
|
1451
|
-
color: "#
|
|
1452
|
-
font: "
|
|
2557
|
+
strings: ["21:47"],
|
|
2558
|
+
color: "#8899aa",
|
|
2559
|
+
font: "bold 12px monospace",
|
|
1453
2560
|
align: "left"
|
|
1454
2561
|
},
|
|
1455
|
-
motion: { type: "static" }
|
|
2562
|
+
motion: { type: "static" },
|
|
2563
|
+
life: { enter: 200, fade: 600 }
|
|
1456
2564
|
},
|
|
1457
|
-
// --- left column:
|
|
2565
|
+
// --- left column: readings (boots second) ---
|
|
1458
2566
|
{
|
|
1459
|
-
key: "
|
|
2567
|
+
key: "power-label",
|
|
1460
2568
|
count: 1,
|
|
1461
|
-
position: { x: 0.06, y: 0.
|
|
2569
|
+
position: { x: 0.06, y: 0.14 },
|
|
1462
2570
|
sprite: {
|
|
1463
2571
|
kind: "text",
|
|
1464
|
-
strings: ["
|
|
2572
|
+
strings: ["POWER"],
|
|
1465
2573
|
color: "#445566",
|
|
1466
2574
|
font: "9px monospace",
|
|
1467
2575
|
align: "left",
|
|
1468
2576
|
baseline: "top"
|
|
1469
2577
|
},
|
|
1470
|
-
motion: { type: "static" }
|
|
2578
|
+
motion: { type: "static" },
|
|
2579
|
+
life: { enter: 700, fade: 600 }
|
|
1471
2580
|
},
|
|
1472
2581
|
{
|
|
1473
|
-
key: "
|
|
2582
|
+
key: "power-value",
|
|
1474
2583
|
count: 1,
|
|
1475
|
-
position: { x: 0.06, y: 0.
|
|
2584
|
+
position: { x: 0.06, y: 0.18 },
|
|
1476
2585
|
sprite: {
|
|
1477
2586
|
kind: "text",
|
|
1478
|
-
strings: ["
|
|
1479
|
-
color: "#
|
|
1480
|
-
font: "bold
|
|
2587
|
+
strings: ["3.2 kW"],
|
|
2588
|
+
color: "#ffd166",
|
|
2589
|
+
font: "bold 26px monospace",
|
|
1481
2590
|
align: "left",
|
|
1482
2591
|
baseline: "top"
|
|
1483
2592
|
},
|
|
1484
|
-
motion: { type: "static" }
|
|
2593
|
+
motion: { type: "static" },
|
|
2594
|
+
life: { enter: 700, fade: 600 }
|
|
1485
2595
|
},
|
|
1486
2596
|
{
|
|
1487
|
-
key: "
|
|
2597
|
+
key: "network-label",
|
|
1488
2598
|
count: 1,
|
|
1489
|
-
position: { x: 0.06, y: 0.
|
|
2599
|
+
position: { x: 0.06, y: 0.28 },
|
|
1490
2600
|
sprite: {
|
|
1491
2601
|
kind: "text",
|
|
1492
|
-
strings: ["
|
|
2602
|
+
strings: ["NETWORK"],
|
|
1493
2603
|
color: "#445566",
|
|
1494
2604
|
font: "9px monospace",
|
|
1495
2605
|
align: "left",
|
|
1496
2606
|
baseline: "top"
|
|
1497
2607
|
},
|
|
1498
|
-
motion: { type: "static" }
|
|
2608
|
+
motion: { type: "static" },
|
|
2609
|
+
life: { enter: 700, fade: 600 }
|
|
1499
2610
|
},
|
|
1500
2611
|
{
|
|
1501
|
-
key: "
|
|
2612
|
+
key: "network-value",
|
|
1502
2613
|
count: 1,
|
|
1503
|
-
position: { x: 0.06, y: 0.
|
|
2614
|
+
position: { x: 0.06, y: 0.32 },
|
|
1504
2615
|
sprite: {
|
|
1505
2616
|
kind: "text",
|
|
1506
|
-
strings: ["
|
|
1507
|
-
color: "#
|
|
1508
|
-
font: "bold
|
|
2617
|
+
strings: ["940 Mb/s"],
|
|
2618
|
+
color: "#44aaff",
|
|
2619
|
+
font: "bold 26px monospace",
|
|
1509
2620
|
align: "left",
|
|
1510
2621
|
baseline: "top"
|
|
1511
2622
|
},
|
|
1512
|
-
motion: { type: "static" }
|
|
2623
|
+
motion: { type: "static" },
|
|
2624
|
+
life: { enter: 700, fade: 600 }
|
|
1513
2625
|
},
|
|
1514
2626
|
{
|
|
1515
|
-
key: "
|
|
2627
|
+
key: "climate-label",
|
|
1516
2628
|
count: 1,
|
|
1517
|
-
position: { x: 0.06, y: 0.
|
|
2629
|
+
position: { x: 0.06, y: 0.42 },
|
|
1518
2630
|
sprite: {
|
|
1519
2631
|
kind: "text",
|
|
1520
|
-
strings: ["
|
|
2632
|
+
strings: ["CLIMATE"],
|
|
1521
2633
|
color: "#445566",
|
|
1522
2634
|
font: "9px monospace",
|
|
1523
2635
|
align: "left",
|
|
1524
2636
|
baseline: "top"
|
|
1525
2637
|
},
|
|
1526
|
-
motion: { type: "static" }
|
|
2638
|
+
motion: { type: "static" },
|
|
2639
|
+
life: { enter: 700, fade: 600 }
|
|
1527
2640
|
},
|
|
1528
2641
|
{
|
|
1529
|
-
key: "
|
|
2642
|
+
key: "climate-value",
|
|
1530
2643
|
count: 1,
|
|
1531
|
-
position: { x: 0.06, y: 0.
|
|
2644
|
+
position: { x: 0.06, y: 0.46 },
|
|
1532
2645
|
sprite: {
|
|
1533
2646
|
kind: "text",
|
|
1534
|
-
strings: ["
|
|
1535
|
-
color: "#
|
|
1536
|
-
font: "bold
|
|
2647
|
+
strings: ["21.5\xB0"],
|
|
2648
|
+
color: "#44ddcc",
|
|
2649
|
+
font: "bold 26px monospace",
|
|
1537
2650
|
align: "left",
|
|
1538
2651
|
baseline: "top"
|
|
1539
2652
|
},
|
|
1540
|
-
motion: { type: "static" }
|
|
2653
|
+
motion: { type: "static" },
|
|
2654
|
+
life: { enter: 700, fade: 600 }
|
|
1541
2655
|
},
|
|
1542
2656
|
{
|
|
1543
|
-
key: "
|
|
2657
|
+
key: "alerts-label",
|
|
1544
2658
|
count: 1,
|
|
1545
|
-
position: { x: 0.06, y: 0.
|
|
2659
|
+
position: { x: 0.06, y: 0.56 },
|
|
1546
2660
|
sprite: {
|
|
1547
2661
|
kind: "text",
|
|
1548
|
-
strings: ["
|
|
2662
|
+
strings: ["ALERTS"],
|
|
1549
2663
|
color: "#445566",
|
|
1550
2664
|
font: "9px monospace",
|
|
1551
2665
|
align: "left",
|
|
1552
2666
|
baseline: "top"
|
|
1553
2667
|
},
|
|
1554
|
-
motion: { type: "static" }
|
|
2668
|
+
motion: { type: "static" },
|
|
2669
|
+
life: { enter: 700, fade: 600 }
|
|
1555
2670
|
},
|
|
1556
2671
|
{
|
|
1557
|
-
key: "
|
|
2672
|
+
key: "alerts-value",
|
|
1558
2673
|
count: 1,
|
|
1559
|
-
position: { x: 0.06, y: 0.
|
|
2674
|
+
position: { x: 0.06, y: 0.6 },
|
|
1560
2675
|
sprite: {
|
|
1561
2676
|
kind: "text",
|
|
1562
|
-
strings: ["
|
|
1563
|
-
color: "#
|
|
1564
|
-
font: "bold
|
|
2677
|
+
strings: ["0"],
|
|
2678
|
+
color: "#44cc66",
|
|
2679
|
+
font: "bold 26px monospace",
|
|
1565
2680
|
align: "left",
|
|
1566
2681
|
baseline: "top"
|
|
1567
2682
|
},
|
|
1568
|
-
motion: { type: "static" }
|
|
2683
|
+
motion: { type: "static" },
|
|
2684
|
+
life: { enter: 700, fade: 600 }
|
|
1569
2685
|
},
|
|
1570
|
-
// --- center
|
|
2686
|
+
// --- center: energy gauge (boots third) ---
|
|
1571
2687
|
{
|
|
1572
|
-
key: "
|
|
2688
|
+
key: "gauge-ring",
|
|
1573
2689
|
count: 1,
|
|
1574
|
-
position: { x: 0.
|
|
1575
|
-
sprite: {
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
font: "9px monospace",
|
|
1580
|
-
align: "center"
|
|
1581
|
-
},
|
|
1582
|
-
motion: { type: "static" }
|
|
2690
|
+
position: { x: 0.4, y: 0.36 },
|
|
2691
|
+
sprite: { kind: "ring", radius: [0.105, 0.105], color: "#1e3a52", width: 28e-4 },
|
|
2692
|
+
alpha: [0.85, 0.85],
|
|
2693
|
+
motion: { type: "static" },
|
|
2694
|
+
life: { enter: 1200, fade: 700 }
|
|
1583
2695
|
},
|
|
1584
2696
|
{
|
|
1585
|
-
key: "
|
|
2697
|
+
key: "gauge-glow",
|
|
1586
2698
|
count: 1,
|
|
1587
|
-
position: { x: 0.
|
|
1588
|
-
sprite: {
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
}
|
|
1595
|
-
motion: { type: "static" }
|
|
2699
|
+
position: { x: 0.4, y: 0.36 },
|
|
2700
|
+
sprite: { kind: "ring", radius: [0.088, 0.088], color: "#2ee6c8", width: 16e-4 },
|
|
2701
|
+
alpha: [0.55, 0.55],
|
|
2702
|
+
blend: "lighter",
|
|
2703
|
+
pulse: { amp: 0.25, period: 4e3 },
|
|
2704
|
+
grow: { amp: 0.05, period: 4e3 },
|
|
2705
|
+
motion: { type: "static" },
|
|
2706
|
+
life: { enter: 1200, fade: 700 }
|
|
1596
2707
|
},
|
|
1597
2708
|
{
|
|
1598
|
-
key: "
|
|
2709
|
+
key: "gauge-label",
|
|
1599
2710
|
count: 1,
|
|
1600
|
-
position: { x: 0.
|
|
2711
|
+
position: { x: 0.4, y: 0.29 },
|
|
1601
2712
|
sprite: {
|
|
1602
2713
|
kind: "text",
|
|
1603
|
-
strings: ["
|
|
2714
|
+
strings: ["ENERGY"],
|
|
1604
2715
|
color: "#445566",
|
|
1605
2716
|
font: "9px monospace",
|
|
1606
2717
|
align: "center"
|
|
1607
2718
|
},
|
|
1608
|
-
motion: { type: "static" }
|
|
1609
|
-
|
|
1610
|
-
{
|
|
1611
|
-
key: "mem-value",
|
|
1612
|
-
count: 1,
|
|
1613
|
-
position: { x: 0.38, y: 0.46 },
|
|
1614
|
-
sprite: {
|
|
1615
|
-
kind: "text",
|
|
1616
|
-
strings: ["4.2 GB"],
|
|
1617
|
-
color: "#ddcc44",
|
|
1618
|
-
font: "bold 36px monospace",
|
|
1619
|
-
align: "center"
|
|
1620
|
-
},
|
|
1621
|
-
motion: { type: "static" }
|
|
2719
|
+
motion: { type: "static" },
|
|
2720
|
+
life: { enter: 1200, fade: 700 }
|
|
1622
2721
|
},
|
|
1623
2722
|
{
|
|
1624
|
-
key: "
|
|
2723
|
+
key: "gauge-value",
|
|
1625
2724
|
count: 1,
|
|
1626
|
-
position: { x: 0.
|
|
2725
|
+
position: { x: 0.4, y: 0.375 },
|
|
1627
2726
|
sprite: {
|
|
1628
2727
|
kind: "text",
|
|
1629
|
-
strings: ["
|
|
1630
|
-
color: "#
|
|
1631
|
-
font: "
|
|
2728
|
+
strings: ["73%"],
|
|
2729
|
+
color: "#2ee6c8",
|
|
2730
|
+
font: "bold 34px monospace",
|
|
1632
2731
|
align: "center"
|
|
1633
2732
|
},
|
|
1634
|
-
motion: { type: "static" }
|
|
2733
|
+
motion: { type: "static" },
|
|
2734
|
+
life: { enter: 1200, fade: 700 }
|
|
1635
2735
|
},
|
|
1636
2736
|
{
|
|
1637
|
-
key: "
|
|
1638
|
-
count:
|
|
1639
|
-
position: { x: 0.38, y: 0.66 },
|
|
2737
|
+
key: "equalizer",
|
|
2738
|
+
count: 22,
|
|
1640
2739
|
sprite: {
|
|
1641
|
-
kind: "
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
2740
|
+
kind: "rect",
|
|
2741
|
+
width: [5e-3, 5e-3],
|
|
2742
|
+
aspect: [0.6, 3.4],
|
|
2743
|
+
color: "#2ee6c8",
|
|
2744
|
+
colors: ["#2ee6c8", "#44aaff", "#aa88ff"],
|
|
2745
|
+
colorWeights: [3, 2, 1]
|
|
1646
2746
|
},
|
|
1647
|
-
|
|
2747
|
+
alpha: [0.35, 0.7],
|
|
2748
|
+
layout: { type: "grid", columns: 22 },
|
|
2749
|
+
region: { x: [0.28, 0.52], y: [0.585, 0.615] },
|
|
2750
|
+
// Wave-phased pulse sweeps along the bars — reads as a live meter.
|
|
2751
|
+
pulse: { amp: 0.4, period: 1600, wave: { wavelength: 0.1, angle: 0 } },
|
|
2752
|
+
motion: { type: "static" },
|
|
2753
|
+
life: { enter: 1200, fade: 700 }
|
|
1648
2754
|
},
|
|
1649
|
-
// --- right column:
|
|
2755
|
+
// --- right column: agenda + devices (boots fourth) ---
|
|
1650
2756
|
{
|
|
1651
|
-
key: "
|
|
2757
|
+
key: "agenda-label",
|
|
1652
2758
|
count: 1,
|
|
1653
|
-
position: { x: 0.
|
|
2759
|
+
position: { x: 0.64, y: 0.14 },
|
|
1654
2760
|
sprite: {
|
|
1655
2761
|
kind: "text",
|
|
1656
|
-
strings: ["
|
|
2762
|
+
strings: ["AGENDA"],
|
|
1657
2763
|
color: "#445566",
|
|
1658
2764
|
font: "9px monospace",
|
|
1659
2765
|
align: "left",
|
|
1660
2766
|
baseline: "top"
|
|
1661
2767
|
},
|
|
1662
|
-
motion: { type: "static" }
|
|
2768
|
+
motion: { type: "static" },
|
|
2769
|
+
life: { enter: 1700, fade: 600 }
|
|
1663
2770
|
},
|
|
1664
2771
|
{
|
|
1665
|
-
key: "
|
|
2772
|
+
key: "agenda-1",
|
|
1666
2773
|
count: 1,
|
|
1667
|
-
position: { x: 0.
|
|
2774
|
+
position: { x: 0.64, y: 0.19 },
|
|
1668
2775
|
sprite: {
|
|
1669
2776
|
kind: "text",
|
|
1670
|
-
strings: ["
|
|
2777
|
+
strings: ["09:00 Standup"],
|
|
1671
2778
|
color: "#8899aa",
|
|
1672
2779
|
font: "11px monospace",
|
|
1673
2780
|
align: "left",
|
|
1674
2781
|
baseline: "top",
|
|
1675
|
-
maxWidth:
|
|
2782
|
+
maxWidth: 0.2593
|
|
1676
2783
|
},
|
|
1677
|
-
motion: { type: "static" }
|
|
2784
|
+
motion: { type: "static" },
|
|
2785
|
+
life: { enter: 1700, fade: 600 }
|
|
1678
2786
|
},
|
|
1679
2787
|
{
|
|
1680
|
-
key: "
|
|
2788
|
+
key: "agenda-2",
|
|
1681
2789
|
count: 1,
|
|
1682
|
-
position: { x: 0.
|
|
2790
|
+
position: { x: 0.64, y: 0.23 },
|
|
1683
2791
|
sprite: {
|
|
1684
2792
|
kind: "text",
|
|
1685
|
-
strings: ["
|
|
2793
|
+
strings: ["12:30 Lunch w/ Kai"],
|
|
1686
2794
|
color: "#778899",
|
|
1687
2795
|
font: "11px monospace",
|
|
1688
2796
|
align: "left",
|
|
1689
2797
|
baseline: "top",
|
|
1690
|
-
maxWidth:
|
|
2798
|
+
maxWidth: 0.2593
|
|
1691
2799
|
},
|
|
1692
|
-
motion: { type: "static" }
|
|
2800
|
+
motion: { type: "static" },
|
|
2801
|
+
life: { enter: 1700, fade: 600 }
|
|
1693
2802
|
},
|
|
1694
2803
|
{
|
|
1695
|
-
key: "
|
|
2804
|
+
key: "agenda-3",
|
|
1696
2805
|
count: 1,
|
|
1697
|
-
position: { x: 0.
|
|
2806
|
+
position: { x: 0.64, y: 0.27 },
|
|
1698
2807
|
sprite: {
|
|
1699
2808
|
kind: "text",
|
|
1700
|
-
strings: ["
|
|
2809
|
+
strings: ["18:00 Gym"],
|
|
1701
2810
|
color: "#667788",
|
|
1702
2811
|
font: "11px monospace",
|
|
1703
2812
|
align: "left",
|
|
1704
2813
|
baseline: "top",
|
|
1705
|
-
maxWidth:
|
|
2814
|
+
maxWidth: 0.2593
|
|
1706
2815
|
},
|
|
1707
|
-
motion: { type: "static" }
|
|
2816
|
+
motion: { type: "static" },
|
|
2817
|
+
life: { enter: 1700, fade: 600 }
|
|
1708
2818
|
},
|
|
1709
2819
|
{
|
|
1710
|
-
key: "
|
|
2820
|
+
key: "devices-label",
|
|
1711
2821
|
count: 1,
|
|
1712
|
-
position: { x: 0.
|
|
2822
|
+
position: { x: 0.64, y: 0.36 },
|
|
1713
2823
|
sprite: {
|
|
1714
2824
|
kind: "text",
|
|
1715
|
-
strings: ["
|
|
1716
|
-
color: "#
|
|
1717
|
-
font: "
|
|
2825
|
+
strings: ["DEVICES"],
|
|
2826
|
+
color: "#445566",
|
|
2827
|
+
font: "9px monospace",
|
|
1718
2828
|
align: "left",
|
|
1719
|
-
baseline: "top"
|
|
1720
|
-
maxWidth: 280
|
|
2829
|
+
baseline: "top"
|
|
1721
2830
|
},
|
|
1722
|
-
motion: { type: "static" }
|
|
2831
|
+
motion: { type: "static" },
|
|
2832
|
+
life: { enter: 1700, fade: 600 }
|
|
1723
2833
|
},
|
|
1724
2834
|
{
|
|
1725
|
-
key: "
|
|
2835
|
+
key: "devices-1",
|
|
1726
2836
|
count: 1,
|
|
1727
|
-
position: { x: 0.
|
|
2837
|
+
position: { x: 0.64, y: 0.41 },
|
|
1728
2838
|
sprite: {
|
|
1729
2839
|
kind: "text",
|
|
1730
|
-
strings: ["
|
|
1731
|
-
color: "#
|
|
2840
|
+
strings: ["Lights 6 on"],
|
|
2841
|
+
color: "#8899aa",
|
|
1732
2842
|
font: "11px monospace",
|
|
1733
2843
|
align: "left",
|
|
1734
|
-
baseline: "top"
|
|
1735
|
-
maxWidth: 280
|
|
2844
|
+
baseline: "top"
|
|
1736
2845
|
},
|
|
1737
|
-
motion: { type: "static" }
|
|
2846
|
+
motion: { type: "static" },
|
|
2847
|
+
life: { enter: 1700, fade: 600 }
|
|
1738
2848
|
},
|
|
1739
|
-
// --- right column: session info ---
|
|
1740
2849
|
{
|
|
1741
|
-
key: "
|
|
2850
|
+
key: "devices-2",
|
|
1742
2851
|
count: 1,
|
|
1743
|
-
position: { x: 0.
|
|
2852
|
+
position: { x: 0.64, y: 0.45 },
|
|
1744
2853
|
sprite: {
|
|
1745
2854
|
kind: "text",
|
|
1746
|
-
strings: ["
|
|
1747
|
-
color: "#
|
|
1748
|
-
font: "
|
|
2855
|
+
strings: ["Thermostat eco"],
|
|
2856
|
+
color: "#778899",
|
|
2857
|
+
font: "11px monospace",
|
|
1749
2858
|
align: "left",
|
|
1750
2859
|
baseline: "top"
|
|
1751
2860
|
},
|
|
1752
|
-
motion: { type: "static" }
|
|
2861
|
+
motion: { type: "static" },
|
|
2862
|
+
life: { enter: 1700, fade: 600 }
|
|
1753
2863
|
},
|
|
1754
2864
|
{
|
|
1755
|
-
key: "
|
|
2865
|
+
key: "devices-3",
|
|
1756
2866
|
count: 1,
|
|
1757
|
-
position: { x: 0.
|
|
2867
|
+
position: { x: 0.64, y: 0.49 },
|
|
1758
2868
|
sprite: {
|
|
1759
2869
|
kind: "text",
|
|
1760
|
-
strings: ["
|
|
1761
|
-
color: "#
|
|
2870
|
+
strings: ["Locks secured"],
|
|
2871
|
+
color: "#667788",
|
|
1762
2872
|
font: "11px monospace",
|
|
1763
2873
|
align: "left",
|
|
1764
2874
|
baseline: "top"
|
|
1765
2875
|
},
|
|
1766
|
-
motion: { type: "static" }
|
|
2876
|
+
motion: { type: "static" },
|
|
2877
|
+
life: { enter: 1700, fade: 600 }
|
|
1767
2878
|
},
|
|
1768
2879
|
{
|
|
1769
|
-
key: "
|
|
2880
|
+
key: "devices-4",
|
|
1770
2881
|
count: 1,
|
|
1771
|
-
position: { x: 0.
|
|
2882
|
+
position: { x: 0.64, y: 0.53 },
|
|
1772
2883
|
sprite: {
|
|
1773
2884
|
kind: "text",
|
|
1774
|
-
strings: ["
|
|
1775
|
-
color: "#
|
|
2885
|
+
strings: ["Media paused"],
|
|
2886
|
+
color: "#556677",
|
|
1776
2887
|
font: "11px monospace",
|
|
1777
2888
|
align: "left",
|
|
1778
2889
|
baseline: "top"
|
|
1779
2890
|
},
|
|
1780
|
-
motion: { type: "static" }
|
|
2891
|
+
motion: { type: "static" },
|
|
2892
|
+
life: { enter: 1700, fade: 600 }
|
|
1781
2893
|
},
|
|
1782
|
-
// --- bottom:
|
|
2894
|
+
// --- bottom: activity feed (boots last) ---
|
|
1783
2895
|
{
|
|
1784
|
-
key: "
|
|
2896
|
+
key: "feed-label",
|
|
1785
2897
|
count: 1,
|
|
1786
|
-
position: { x: 0.06, y: 0.
|
|
2898
|
+
position: { x: 0.06, y: 0.74 },
|
|
1787
2899
|
sprite: {
|
|
1788
2900
|
kind: "text",
|
|
1789
|
-
strings: ["
|
|
2901
|
+
strings: ["ACTIVITY"],
|
|
1790
2902
|
color: "#445566",
|
|
1791
2903
|
font: "9px monospace",
|
|
1792
2904
|
align: "left",
|
|
1793
2905
|
baseline: "top"
|
|
1794
2906
|
},
|
|
1795
|
-
motion: { type: "static" }
|
|
2907
|
+
motion: { type: "static" },
|
|
2908
|
+
life: { enter: 2200, fade: 600 }
|
|
1796
2909
|
},
|
|
1797
2910
|
{
|
|
1798
|
-
key: "
|
|
2911
|
+
key: "feed-1",
|
|
1799
2912
|
count: 1,
|
|
1800
|
-
position: { x: 0.06, y: 0.
|
|
2913
|
+
position: { x: 0.06, y: 0.78 },
|
|
1801
2914
|
sprite: {
|
|
1802
2915
|
kind: "text",
|
|
1803
|
-
strings: ["
|
|
1804
|
-
color: "#
|
|
2916
|
+
strings: ["21:42 Front door locked"],
|
|
2917
|
+
color: "#7a8a7a",
|
|
1805
2918
|
font: "10px monospace",
|
|
1806
2919
|
align: "left",
|
|
1807
2920
|
baseline: "top",
|
|
1808
|
-
maxWidth:
|
|
2921
|
+
maxWidth: 0.5556
|
|
1809
2922
|
},
|
|
1810
|
-
motion: { type: "static" }
|
|
2923
|
+
motion: { type: "static" },
|
|
2924
|
+
life: { enter: 2200, fade: 600 }
|
|
1811
2925
|
},
|
|
1812
2926
|
{
|
|
1813
|
-
key: "
|
|
2927
|
+
key: "feed-2",
|
|
1814
2928
|
count: 1,
|
|
1815
|
-
position: { x: 0.06, y: 0.
|
|
2929
|
+
position: { x: 0.06, y: 0.82 },
|
|
1816
2930
|
sprite: {
|
|
1817
2931
|
kind: "text",
|
|
1818
|
-
strings: ["
|
|
1819
|
-
color: "#
|
|
2932
|
+
strings: ["21:36 Thermostat \u2192 eco"],
|
|
2933
|
+
color: "#6a7a6a",
|
|
1820
2934
|
font: "10px monospace",
|
|
1821
2935
|
align: "left",
|
|
1822
2936
|
baseline: "top",
|
|
1823
|
-
maxWidth:
|
|
2937
|
+
maxWidth: 0.5556
|
|
1824
2938
|
},
|
|
1825
|
-
motion: { type: "static" }
|
|
2939
|
+
motion: { type: "static" },
|
|
2940
|
+
life: { enter: 2200, fade: 600 }
|
|
1826
2941
|
},
|
|
1827
2942
|
{
|
|
1828
|
-
key: "
|
|
2943
|
+
key: "feed-3",
|
|
1829
2944
|
count: 1,
|
|
1830
|
-
position: { x: 0.06, y: 0.
|
|
2945
|
+
position: { x: 0.06, y: 0.86 },
|
|
1831
2946
|
sprite: {
|
|
1832
2947
|
kind: "text",
|
|
1833
|
-
strings: ["14
|
|
1834
|
-
color: "#
|
|
2948
|
+
strings: ["21:14 Backup completed"],
|
|
2949
|
+
color: "#5a6a5a",
|
|
1835
2950
|
font: "10px monospace",
|
|
1836
2951
|
align: "left",
|
|
1837
2952
|
baseline: "top",
|
|
1838
|
-
maxWidth:
|
|
2953
|
+
maxWidth: 0.5556
|
|
1839
2954
|
},
|
|
1840
|
-
motion: { type: "static" }
|
|
2955
|
+
motion: { type: "static" },
|
|
2956
|
+
life: { enter: 2200, fade: 600 }
|
|
1841
2957
|
},
|
|
1842
2958
|
{
|
|
1843
|
-
key: "
|
|
2959
|
+
key: "feed-4",
|
|
1844
2960
|
count: 1,
|
|
1845
|
-
position: { x: 0.06, y: 0.
|
|
2961
|
+
position: { x: 0.06, y: 0.9 },
|
|
1846
2962
|
sprite: {
|
|
1847
2963
|
kind: "text",
|
|
1848
|
-
strings: ["
|
|
1849
|
-
color: "#
|
|
2964
|
+
strings: ["20:58 Motion \xB7 driveway"],
|
|
2965
|
+
color: "#4d5e4d",
|
|
1850
2966
|
font: "10px monospace",
|
|
1851
2967
|
align: "left",
|
|
1852
2968
|
baseline: "top",
|
|
1853
|
-
maxWidth:
|
|
2969
|
+
maxWidth: 0.5556
|
|
1854
2970
|
},
|
|
1855
|
-
motion: { type: "static" }
|
|
2971
|
+
motion: { type: "static" },
|
|
2972
|
+
life: { enter: 2200, fade: 600 }
|
|
1856
2973
|
},
|
|
1857
2974
|
{
|
|
1858
|
-
key: "
|
|
2975
|
+
key: "feed-5",
|
|
1859
2976
|
count: 1,
|
|
1860
|
-
position: { x: 0.06, y: 0.
|
|
2977
|
+
position: { x: 0.06, y: 0.94 },
|
|
1861
2978
|
sprite: {
|
|
1862
2979
|
kind: "text",
|
|
1863
|
-
strings: ["
|
|
2980
|
+
strings: ["20:41 Sunset scene on"],
|
|
1864
2981
|
color: "#3d4e3d",
|
|
1865
2982
|
font: "10px monospace",
|
|
1866
2983
|
align: "left",
|
|
1867
2984
|
baseline: "top",
|
|
1868
|
-
maxWidth:
|
|
2985
|
+
maxWidth: 0.5556
|
|
1869
2986
|
},
|
|
1870
|
-
motion: { type: "static" }
|
|
2987
|
+
motion: { type: "static" },
|
|
2988
|
+
life: { enter: 2200, fade: 600 }
|
|
1871
2989
|
}
|
|
1872
2990
|
]
|
|
1873
2991
|
};
|
|
@@ -1891,58 +3009,147 @@ var LANTERNS_SPEC = {
|
|
|
1891
3009
|
layers: [
|
|
1892
3010
|
{
|
|
1893
3011
|
count: 60,
|
|
1894
|
-
sprite: { kind: "circle", radius: [
|
|
3012
|
+
sprite: { kind: "circle", radius: [463e-6, 1296e-6], color: "#8fa0c8" },
|
|
1895
3013
|
alpha: [0.35, 1],
|
|
1896
3014
|
region: { y: [0, 0.62] },
|
|
1897
|
-
motion: { type: "drift", speed: [
|
|
3015
|
+
motion: { type: "drift", speed: [463e-6, 1852e-6], bob: 9259e-7 }
|
|
1898
3016
|
},
|
|
1899
3017
|
{
|
|
1900
3018
|
count: 36,
|
|
1901
3019
|
sprite: {
|
|
1902
3020
|
kind: "circle",
|
|
1903
|
-
radius: [
|
|
3021
|
+
radius: [1389e-6, 2778e-6],
|
|
1904
3022
|
color: "#b06a2a",
|
|
1905
3023
|
soft: true
|
|
1906
3024
|
},
|
|
1907
3025
|
alpha: [0.5, 0.9],
|
|
1908
3026
|
blend: "lighter",
|
|
1909
3027
|
pulse: { amp: 0.18, period: 2800 },
|
|
1910
|
-
motion: { type: "rise", speed: [6,
|
|
3028
|
+
motion: { type: "rise", speed: [5556e-6, 0.01296], sway: 3704e-6 }
|
|
1911
3029
|
},
|
|
1912
3030
|
{
|
|
1913
3031
|
count: 14,
|
|
1914
3032
|
sprite: {
|
|
1915
3033
|
kind: "circle",
|
|
1916
|
-
radius: [
|
|
3034
|
+
radius: [3241e-6, 6019e-6],
|
|
1917
3035
|
color: "#e08a34",
|
|
1918
3036
|
soft: true
|
|
1919
3037
|
},
|
|
1920
3038
|
alpha: [0.6, 1],
|
|
1921
3039
|
blend: "lighter",
|
|
1922
3040
|
pulse: { amp: 0.22, period: 3400 },
|
|
1923
|
-
motion: { type: "rise", speed: [
|
|
3041
|
+
motion: { type: "rise", speed: [0.01481, 0.02593], sway: 6481e-6 }
|
|
1924
3042
|
},
|
|
1925
3043
|
{
|
|
1926
3044
|
count: 8,
|
|
1927
3045
|
sprite: { kind: "emoji", glyphs: ["\u{1F3EE}"] },
|
|
1928
|
-
size: [
|
|
3046
|
+
size: [0.02407, 0.04074],
|
|
1929
3047
|
alpha: [0.85, 1],
|
|
1930
|
-
motion: { type: "rise", speed: [
|
|
3048
|
+
motion: { type: "rise", speed: [0.02778, 0.04444], sway: 9259e-6 }
|
|
1931
3049
|
},
|
|
1932
3050
|
{
|
|
1933
3051
|
count: 4,
|
|
1934
3052
|
sprite: { kind: "emoji", glyphs: ["\u{1F3EE}"] },
|
|
1935
|
-
size: [
|
|
1936
|
-
motion: { type: "rise", speed: [
|
|
3053
|
+
size: [0.0537, 0.07778],
|
|
3054
|
+
motion: { type: "rise", speed: [0.04815, 0.06852], sway: 0.01296 }
|
|
1937
3055
|
},
|
|
1938
3056
|
{
|
|
1939
3057
|
count: 12,
|
|
1940
|
-
sprite: { kind: "circle", radius: [
|
|
3058
|
+
sprite: { kind: "circle", radius: [1852e-6, 463e-5], color: "#e08a34", soft: true },
|
|
1941
3059
|
alpha: [0.12, 0.35],
|
|
1942
3060
|
blend: "lighter",
|
|
1943
3061
|
region: { y: [0.93, 1] },
|
|
1944
3062
|
pulse: { amp: 0.1, period: 2200 },
|
|
1945
|
-
motion: { type: "drift", speed: [
|
|
3063
|
+
motion: { type: "drift", speed: [1852e-6, 5556e-6], bob: 1852e-6 }
|
|
3064
|
+
}
|
|
3065
|
+
]
|
|
3066
|
+
};
|
|
3067
|
+
|
|
3068
|
+
// src/examples/matrix-rain.ts
|
|
3069
|
+
var MATRIX_RAIN_SPEC = {
|
|
3070
|
+
schemaVersion: 1,
|
|
3071
|
+
id: "matrix-rain",
|
|
3072
|
+
label: "Matrix Rain",
|
|
3073
|
+
seed: 1999,
|
|
3074
|
+
motionIntensity: "moderate",
|
|
3075
|
+
ghosting: 0.88,
|
|
3076
|
+
background: { type: "solid", color: "#010401" },
|
|
3077
|
+
layers: [
|
|
3078
|
+
{
|
|
3079
|
+
key: "rain-far",
|
|
3080
|
+
count: 64,
|
|
3081
|
+
sprite: {
|
|
3082
|
+
kind: "text",
|
|
3083
|
+
strings: ["\u30A2", "\u30AB", "\u30B5", "\u30BF", "\u30CA", "\u30CF", "\u30DE", "\u30E4", "\u30E9", "\u30EF", "0", "1", "7", "Z", "X", "K"],
|
|
3084
|
+
color: "#1c7a2e",
|
|
3085
|
+
font: "bold monospace",
|
|
3086
|
+
cycle: { period: 900 }
|
|
3087
|
+
},
|
|
3088
|
+
size: [0.014, 0.02],
|
|
3089
|
+
layout: { type: "grid", columns: 32, jitter: { y: 1 } },
|
|
3090
|
+
motion: { type: "drift", speed: [0.06, 0.14], angle: 90 },
|
|
3091
|
+
alpha: [0.35, 0.6]
|
|
3092
|
+
},
|
|
3093
|
+
{
|
|
3094
|
+
key: "rain-near",
|
|
3095
|
+
count: 40,
|
|
3096
|
+
sprite: {
|
|
3097
|
+
kind: "text",
|
|
3098
|
+
strings: ["\u30DF", "\u30CD", "\u30EA", "\u30BD", "\u30C4", "\u30CC", "\u30DB", "\u30E6", "3", "8", "9", "V", "N"],
|
|
3099
|
+
color: "#2ecc40",
|
|
3100
|
+
font: "bold monospace",
|
|
3101
|
+
cycle: { period: 700 }
|
|
3102
|
+
},
|
|
3103
|
+
size: [0.022, 0.03],
|
|
3104
|
+
layout: { type: "grid", columns: 20, jitter: { y: 1 } },
|
|
3105
|
+
motion: { type: "drift", speed: [0.16, 0.3], angle: 90 },
|
|
3106
|
+
alpha: [0.6, 0.9]
|
|
3107
|
+
},
|
|
3108
|
+
{
|
|
3109
|
+
key: "heads",
|
|
3110
|
+
count: 12,
|
|
3111
|
+
sprite: {
|
|
3112
|
+
kind: "text",
|
|
3113
|
+
strings: ["\u30B1", "\u30E1", "\u30A8", "\u30E0", "\u30F2", "5", "2"],
|
|
3114
|
+
color: "#c8ffd4",
|
|
3115
|
+
font: "bold monospace",
|
|
3116
|
+
cycle: { period: 500 }
|
|
3117
|
+
},
|
|
3118
|
+
size: [0.024, 0.032],
|
|
3119
|
+
layout: { type: "grid", columns: 12, jitter: { y: 1 } },
|
|
3120
|
+
motion: { type: "drift", speed: [0.2, 0.34], angle: 90 },
|
|
3121
|
+
alpha: [0.9, 1]
|
|
3122
|
+
}
|
|
3123
|
+
]
|
|
3124
|
+
};
|
|
3125
|
+
|
|
3126
|
+
// src/examples/polygons.ts
|
|
3127
|
+
var POLYGONS_SPEC = {
|
|
3128
|
+
schemaVersion: 1,
|
|
3129
|
+
id: "polygons",
|
|
3130
|
+
label: "Polygons",
|
|
3131
|
+
seed: 1995,
|
|
3132
|
+
motionIntensity: "moderate",
|
|
3133
|
+
ghosting: 0.95,
|
|
3134
|
+
background: { type: "solid", color: "#04040a" },
|
|
3135
|
+
layers: [
|
|
3136
|
+
{
|
|
3137
|
+
key: "poly-cyan",
|
|
3138
|
+
count: 4,
|
|
3139
|
+
sprite: { kind: "circle", radius: [25e-4, 25e-4], color: "#37e5d7" },
|
|
3140
|
+
motion: { type: "bounce", speed: [0.14, 0.24] },
|
|
3141
|
+
wrap: false,
|
|
3142
|
+
alpha: [0.9, 0.9],
|
|
3143
|
+
links: { k: 1, maxDist: 3, mode: "chain", closed: true, color: "#37e5d7", alpha: 0.85, width: 18e-4 }
|
|
3144
|
+
},
|
|
3145
|
+
{
|
|
3146
|
+
key: "poly-magenta",
|
|
3147
|
+
count: 4,
|
|
3148
|
+
sprite: { kind: "circle", radius: [25e-4, 25e-4], color: "#e83e9c" },
|
|
3149
|
+
motion: { type: "bounce", speed: [0.1, 0.2] },
|
|
3150
|
+
wrap: false,
|
|
3151
|
+
alpha: [0.9, 0.9],
|
|
3152
|
+
links: { k: 1, maxDist: 3, mode: "chain", closed: true, color: "#e83e9c", alpha: 0.85, width: 18e-4 }
|
|
1946
3153
|
}
|
|
1947
3154
|
]
|
|
1948
3155
|
};
|
|
@@ -1966,7 +3173,7 @@ var ORRERY_SPEC = {
|
|
|
1966
3173
|
// Distant stars
|
|
1967
3174
|
{
|
|
1968
3175
|
count: 80,
|
|
1969
|
-
sprite: { kind: "circle", radius: [
|
|
3176
|
+
sprite: { kind: "circle", radius: [2778e-7, 1111e-6], color: "#8899cc" },
|
|
1970
3177
|
alpha: [0.2, 0.8],
|
|
1971
3178
|
pulse: { amp: 0.3, period: 3e3 },
|
|
1972
3179
|
motion: { type: "static" }
|
|
@@ -1975,7 +3182,7 @@ var ORRERY_SPEC = {
|
|
|
1975
3182
|
{
|
|
1976
3183
|
key: "corona",
|
|
1977
3184
|
count: 1,
|
|
1978
|
-
sprite: { kind: "circle", radius: [
|
|
3185
|
+
sprite: { kind: "circle", radius: [0.05556, 0.05556], color: "#ffaa22", soft: true },
|
|
1979
3186
|
blend: "lighter",
|
|
1980
3187
|
alpha: [0.6, 0.6],
|
|
1981
3188
|
grow: { amp: 0.15, period: 4e3 },
|
|
@@ -1987,7 +3194,7 @@ var ORRERY_SPEC = {
|
|
|
1987
3194
|
key: "sun",
|
|
1988
3195
|
count: 1,
|
|
1989
3196
|
sprite: { kind: "emoji", glyphs: ["\u2600\uFE0F"] },
|
|
1990
|
-
size: [
|
|
3197
|
+
size: [0.04444, 0.04444],
|
|
1991
3198
|
grow: { amp: 0.08, period: 4e3 },
|
|
1992
3199
|
motion: { type: "static" },
|
|
1993
3200
|
position: { x: 0.5, y: 0.5 }
|
|
@@ -1997,36 +3204,114 @@ var ORRERY_SPEC = {
|
|
|
1997
3204
|
key: "inner",
|
|
1998
3205
|
count: 3,
|
|
1999
3206
|
sprite: { kind: "emoji", glyphs: ["\u{1FAA8}", "\u{1F534}", "\u{1F7E0}"] },
|
|
2000
|
-
size: [
|
|
3207
|
+
size: [0.01481, 0.02037],
|
|
2001
3208
|
spin: 30,
|
|
2002
|
-
motion: { type: "orbit", speed: [28, 42], radius: [
|
|
3209
|
+
motion: { type: "orbit", speed: [28, 42], radius: [0.07407, 0.1111] }
|
|
2003
3210
|
},
|
|
2004
3211
|
// Middle ring — gas giants
|
|
2005
3212
|
{
|
|
2006
3213
|
key: "middle",
|
|
2007
3214
|
count: 2,
|
|
2008
3215
|
sprite: { kind: "emoji", glyphs: ["\u{1F7E4}", "\u{1FA90}"] },
|
|
2009
|
-
size: [
|
|
3216
|
+
size: [0.02593, 0.03519],
|
|
2010
3217
|
spin: -15,
|
|
2011
|
-
motion: { type: "orbit", speed: [12, 20], radius: [
|
|
3218
|
+
motion: { type: "orbit", speed: [12, 20], radius: [0.1481, 0.1852] }
|
|
2012
3219
|
},
|
|
2013
3220
|
// Outer ring — ice giants
|
|
2014
3221
|
{
|
|
2015
3222
|
key: "outer",
|
|
2016
3223
|
count: 2,
|
|
2017
3224
|
sprite: { kind: "emoji", glyphs: ["\u{1F535}", "\u{1F48E}"] },
|
|
2018
|
-
size: [
|
|
3225
|
+
size: [0.02037, 0.02778],
|
|
2019
3226
|
spin: 10,
|
|
2020
|
-
motion: { type: "orbit", speed: [5, 10], radius: [
|
|
3227
|
+
motion: { type: "orbit", speed: [5, 10], radius: [0.2222, 0.2778] }
|
|
2021
3228
|
},
|
|
2022
3229
|
// Orbital dust ring (soft glow particles orbiting slowly)
|
|
2023
3230
|
{
|
|
2024
3231
|
count: 24,
|
|
2025
|
-
sprite: { kind: "circle", radius: [
|
|
3232
|
+
sprite: { kind: "circle", radius: [9259e-7, 2778e-6], color: "#ccaa66", soft: true },
|
|
2026
3233
|
alpha: [0.15, 0.4],
|
|
2027
3234
|
blend: "lighter",
|
|
2028
3235
|
pulse: { amp: 0.2, period: 2500 },
|
|
2029
|
-
motion: { type: "orbit", speed: [2, 8], radius: [
|
|
3236
|
+
motion: { type: "orbit", speed: [2, 8], radius: [0.09259, 0.2963] }
|
|
3237
|
+
}
|
|
3238
|
+
]
|
|
3239
|
+
};
|
|
3240
|
+
|
|
3241
|
+
// src/examples/procession.ts
|
|
3242
|
+
var PROCESSION_SPEC = {
|
|
3243
|
+
schemaVersion: 1,
|
|
3244
|
+
id: "procession",
|
|
3245
|
+
label: "Night Procession",
|
|
3246
|
+
seed: 8,
|
|
3247
|
+
motionIntensity: "calm",
|
|
3248
|
+
background: {
|
|
3249
|
+
type: "gradient",
|
|
3250
|
+
stops: [
|
|
3251
|
+
{ at: 0, color: "#060612" },
|
|
3252
|
+
{ at: 0.8, color: "#101024" },
|
|
3253
|
+
{ at: 1, color: "#1a1428" }
|
|
3254
|
+
],
|
|
3255
|
+
drift: { period: 3e4, amount: 0.08 }
|
|
3256
|
+
},
|
|
3257
|
+
layers: [
|
|
3258
|
+
{
|
|
3259
|
+
key: "stars",
|
|
3260
|
+
count: 90,
|
|
3261
|
+
sprite: { kind: "circle", radius: [6e-4, 16e-4], color: "#f0ead8", soft: true },
|
|
3262
|
+
motion: { type: "static" },
|
|
3263
|
+
region: { y: [0, 0.6] },
|
|
3264
|
+
alpha: [0.25, 0.6],
|
|
3265
|
+
pulse: { amp: 0.15, period: 5e3 }
|
|
3266
|
+
},
|
|
3267
|
+
{
|
|
3268
|
+
key: "lanterns",
|
|
3269
|
+
count: 9,
|
|
3270
|
+
sprite: {
|
|
3271
|
+
kind: "rect",
|
|
3272
|
+
width: [0.012, 0.02],
|
|
3273
|
+
aspect: [1.3, 1.7],
|
|
3274
|
+
color: "#ffb347",
|
|
3275
|
+
colors: ["#ffb347", "#ff8c69", "#ffd97a"],
|
|
3276
|
+
colorWeights: [3, 1, 2]
|
|
3277
|
+
},
|
|
3278
|
+
motion: {
|
|
3279
|
+
type: "path",
|
|
3280
|
+
points: [
|
|
3281
|
+
{ x: 0.12, y: 0.72 },
|
|
3282
|
+
{ x: 0.38, y: 0.58 },
|
|
3283
|
+
{ x: 0.62, y: 0.78 },
|
|
3284
|
+
{ x: 0.88, y: 0.62 },
|
|
3285
|
+
{ x: 0.62, y: 0.5 },
|
|
3286
|
+
{ x: 0.38, y: 0.72 }
|
|
3287
|
+
],
|
|
3288
|
+
duration: 36e3,
|
|
3289
|
+
curve: "smooth",
|
|
3290
|
+
closed: true,
|
|
3291
|
+
scatter: 0.02
|
|
3292
|
+
},
|
|
3293
|
+
alpha: [0.65, 0.9],
|
|
3294
|
+
pulse: { amp: 0.15, period: 2600 },
|
|
3295
|
+
life: { enter: 4e3, fade: 2500 },
|
|
3296
|
+
trail: { length: 1200, fade: 1 }
|
|
3297
|
+
},
|
|
3298
|
+
{
|
|
3299
|
+
key: "bearer",
|
|
3300
|
+
count: 1,
|
|
3301
|
+
sprite: { kind: "circle", radius: [0.014, 0.014], color: "#ffe9b0", soft: true },
|
|
3302
|
+
motion: { type: "wander", speed: [8e-3, 0.012], meander: 0.08 },
|
|
3303
|
+
alpha: [0.8, 0.8],
|
|
3304
|
+
blend: "lighter",
|
|
3305
|
+
life: { enter: 4e3, fade: 2500 }
|
|
3306
|
+
},
|
|
3307
|
+
{
|
|
3308
|
+
key: "moths",
|
|
3309
|
+
count: 7,
|
|
3310
|
+
sprite: { kind: "ring", radius: [2e-3, 5e-3], color: "#d8f6ff", width: 1e-3 },
|
|
3311
|
+
motion: { type: "orbit", speed: [20, 55], radius: [0.03, 0.09], center: { layer: "bearer" } },
|
|
3312
|
+
alpha: [0.3, 0.7],
|
|
3313
|
+
life: { enter: 7e3, fade: 2e3 },
|
|
3314
|
+
trail: { length: 600 }
|
|
2030
3315
|
}
|
|
2031
3316
|
]
|
|
2032
3317
|
};
|
|
@@ -2043,8 +3328,8 @@ var RAIN_SPEC = {
|
|
|
2043
3328
|
{
|
|
2044
3329
|
count: 140,
|
|
2045
3330
|
sprite: { kind: "text", strings: ["\u2502"], color: "#9fd0ff" },
|
|
2046
|
-
size: [
|
|
2047
|
-
motion: { type: "drift", angle: 90, speed: [
|
|
3331
|
+
size: [0.01111, 0.02407],
|
|
3332
|
+
motion: { type: "drift", angle: 90, speed: [0.4815, 0.8333] }
|
|
2048
3333
|
}
|
|
2049
3334
|
]
|
|
2050
3335
|
};
|
|
@@ -2061,50 +3346,122 @@ var SAKURA_SPEC = {
|
|
|
2061
3346
|
stops: [
|
|
2062
3347
|
{ at: 0, color: "#1b1d38" },
|
|
2063
3348
|
{ at: 0.45, color: "#3a2645" },
|
|
2064
|
-
{ at: 0.
|
|
2065
|
-
{ at:
|
|
3349
|
+
{ at: 0.78, color: "#7d3d4e" },
|
|
3350
|
+
{ at: 0.92, color: "#4a2438" },
|
|
3351
|
+
{ at: 1, color: "#1c1018" }
|
|
2066
3352
|
],
|
|
2067
|
-
|
|
3353
|
+
drift: { period: 35e3, amount: 0.08 }
|
|
2068
3354
|
},
|
|
2069
3355
|
layers: [
|
|
2070
3356
|
{
|
|
3357
|
+
key: "blossom-glow",
|
|
2071
3358
|
count: 60,
|
|
2072
|
-
sprite: {
|
|
3359
|
+
sprite: {
|
|
3360
|
+
kind: "circle",
|
|
3361
|
+
radius: [5556e-7, 1481e-6],
|
|
3362
|
+
color: "#ffe3ec",
|
|
3363
|
+
soft: true
|
|
3364
|
+
},
|
|
2073
3365
|
alpha: [0.3, 0.9],
|
|
2074
3366
|
blend: "lighter",
|
|
2075
3367
|
region: { y: [0, 0.72] },
|
|
2076
|
-
|
|
2077
|
-
|
|
3368
|
+
// Shimmer travels across the sky instead of orbs twinkling independently.
|
|
3369
|
+
pulse: { amp: 0.22, period: 4e3, wave: { wavelength: 0.45, angle: 10 } },
|
|
3370
|
+
motion: { type: "drift", speed: [2778e-7, 1111e-6], bob: 9259e-7 }
|
|
2078
3371
|
},
|
|
2079
3372
|
{
|
|
3373
|
+
key: "petals-far",
|
|
2080
3374
|
count: 28,
|
|
2081
3375
|
sprite: { kind: "emoji", glyphs: ["\u{1F338}"] },
|
|
2082
|
-
size: [
|
|
3376
|
+
size: [0.01204, 0.01852],
|
|
2083
3377
|
alpha: [0.5, 0.8],
|
|
2084
|
-
|
|
3378
|
+
spin: 16,
|
|
3379
|
+
motion: {
|
|
3380
|
+
type: "wander",
|
|
3381
|
+
speed: [0.011, 0.024],
|
|
3382
|
+
angle: 100,
|
|
3383
|
+
meander: 0.025,
|
|
3384
|
+
coherence: 0.35
|
|
3385
|
+
}
|
|
2085
3386
|
},
|
|
2086
3387
|
{
|
|
3388
|
+
key: "petals-mid",
|
|
2087
3389
|
count: 18,
|
|
2088
3390
|
sprite: { kind: "emoji", glyphs: ["\u{1F338}"] },
|
|
2089
|
-
size: [
|
|
3391
|
+
size: [0.01852, 0.02963],
|
|
2090
3392
|
alpha: [0.7, 0.95],
|
|
2091
|
-
|
|
3393
|
+
spin: 24,
|
|
3394
|
+
motion: {
|
|
3395
|
+
type: "wander",
|
|
3396
|
+
speed: [0.022, 0.041],
|
|
3397
|
+
angle: 100,
|
|
3398
|
+
meander: 0.045,
|
|
3399
|
+
coherence: 0.35
|
|
3400
|
+
}
|
|
2092
3401
|
},
|
|
2093
3402
|
{
|
|
3403
|
+
key: "petals-near",
|
|
2094
3404
|
count: 10,
|
|
2095
3405
|
sprite: { kind: "emoji", glyphs: ["\u{1F338}"] },
|
|
2096
|
-
size: [
|
|
3406
|
+
size: [0.03148, 0.05],
|
|
2097
3407
|
alpha: [0.85, 1],
|
|
2098
|
-
|
|
3408
|
+
spin: 32,
|
|
3409
|
+
motion: {
|
|
3410
|
+
type: "wander",
|
|
3411
|
+
speed: [0.039, 0.063],
|
|
3412
|
+
angle: 100,
|
|
3413
|
+
meander: 0.07,
|
|
3414
|
+
coherence: 0.35
|
|
3415
|
+
}
|
|
3416
|
+
},
|
|
3417
|
+
{
|
|
3418
|
+
key: "gusts",
|
|
3419
|
+
count: 7,
|
|
3420
|
+
sprite: {
|
|
3421
|
+
kind: "streak",
|
|
3422
|
+
length: [0.02, 0.045],
|
|
3423
|
+
width: 12e-4,
|
|
3424
|
+
color: "#ffd9e6"
|
|
3425
|
+
},
|
|
3426
|
+
alpha: [0.1, 0.22],
|
|
3427
|
+
blend: "lighter",
|
|
3428
|
+
motion: { type: "drift", speed: [0.09, 0.14], angle: 100 }
|
|
3429
|
+
},
|
|
3430
|
+
{
|
|
3431
|
+
key: "fireflies",
|
|
3432
|
+
count: 6,
|
|
3433
|
+
sprite: {
|
|
3434
|
+
kind: "circle",
|
|
3435
|
+
radius: [9e-4, 2e-3],
|
|
3436
|
+
color: "#ffe9a8",
|
|
3437
|
+
soft: true
|
|
3438
|
+
},
|
|
3439
|
+
alpha: [0.15, 0.5],
|
|
3440
|
+
blend: "lighter",
|
|
3441
|
+
region: { y: [0.6, 0.9] },
|
|
3442
|
+
pulse: { amp: 0.3, period: 2800 },
|
|
3443
|
+
motion: { type: "wander", speed: [4e-3, 0.01], meander: 0.05 }
|
|
2099
3444
|
},
|
|
2100
3445
|
{
|
|
3446
|
+
key: "fallen-petals",
|
|
2101
3447
|
count: 16,
|
|
2102
|
-
sprite: {
|
|
3448
|
+
sprite: {
|
|
3449
|
+
kind: "circle",
|
|
3450
|
+
radius: [1389e-6, 3704e-6],
|
|
3451
|
+
color: "#ff9bb5",
|
|
3452
|
+
soft: true
|
|
3453
|
+
},
|
|
2103
3454
|
alpha: [0.1, 0.3],
|
|
2104
3455
|
blend: "lighter",
|
|
2105
3456
|
region: { y: [0.9, 1] },
|
|
2106
|
-
|
|
2107
|
-
|
|
3457
|
+
// Settled petals catch the last light in a slow wave along the ground.
|
|
3458
|
+
pulse: { amp: 0.15, period: 3200, wave: { wavelength: 0.3, angle: 0 } },
|
|
3459
|
+
motion: {
|
|
3460
|
+
type: "drift",
|
|
3461
|
+
speed: [2778e-7, 1389e-6],
|
|
3462
|
+
angle: 180,
|
|
3463
|
+
bob: 9259e-7
|
|
3464
|
+
}
|
|
2108
3465
|
}
|
|
2109
3466
|
]
|
|
2110
3467
|
};
|
|
@@ -2127,18 +3484,71 @@ var SNOWFALL_SPEC = {
|
|
|
2127
3484
|
layers: [
|
|
2128
3485
|
{
|
|
2129
3486
|
count: 50,
|
|
2130
|
-
sprite: { kind: "circle", radius: [
|
|
2131
|
-
motion: { type: "drift", angle: 90, speed: [
|
|
3487
|
+
sprite: { kind: "circle", radius: [9259e-7, 2315e-6], color: "#8899aa" },
|
|
3488
|
+
motion: { type: "drift", angle: 90, speed: [0.01389, 0.03704], bob: 2778e-6 }
|
|
2132
3489
|
},
|
|
2133
3490
|
{
|
|
2134
3491
|
count: 35,
|
|
2135
|
-
sprite: { kind: "circle", radius: [
|
|
2136
|
-
motion: { type: "drift", angle: 85, speed: [
|
|
3492
|
+
sprite: { kind: "circle", radius: [1852e-6, 4167e-6], color: "#c0cdd8" },
|
|
3493
|
+
motion: { type: "drift", angle: 85, speed: [0.03241, 0.07407], bob: 5556e-6 }
|
|
2137
3494
|
},
|
|
2138
3495
|
{
|
|
2139
3496
|
count: 15,
|
|
2140
|
-
sprite: { kind: "circle", radius: [
|
|
2141
|
-
motion: { type: "drift", angle: 82, speed: [
|
|
3497
|
+
sprite: { kind: "circle", radius: [3241e-6, 6481e-6], color: "#e8eff5" },
|
|
3498
|
+
motion: { type: "drift", angle: 82, speed: [0.05093, 0.1111], bob: 9259e-6 }
|
|
3499
|
+
}
|
|
3500
|
+
]
|
|
3501
|
+
};
|
|
3502
|
+
|
|
3503
|
+
// src/examples/warp-tunnel.ts
|
|
3504
|
+
var WARP_TUNNEL_SPEC = {
|
|
3505
|
+
schemaVersion: 1,
|
|
3506
|
+
id: "warp-tunnel",
|
|
3507
|
+
label: "Warp Tunnel",
|
|
3508
|
+
seed: 1701,
|
|
3509
|
+
motionIntensity: "energetic",
|
|
3510
|
+
ghosting: 0.55,
|
|
3511
|
+
background: { type: "solid", color: "#020208" },
|
|
3512
|
+
layers: [
|
|
3513
|
+
{
|
|
3514
|
+
key: "far-stars",
|
|
3515
|
+
count: 150,
|
|
3516
|
+
sprite: {
|
|
3517
|
+
kind: "streak",
|
|
3518
|
+
length: [8e-3, 0.02],
|
|
3519
|
+
width: 15e-4,
|
|
3520
|
+
color: "#cfd8ff",
|
|
3521
|
+
colors: ["#cfd8ff", "#ffffff", "#8ab4ff", "#ffd9a0"],
|
|
3522
|
+
colorWeights: [5, 3, 1.5, 0.5]
|
|
3523
|
+
},
|
|
3524
|
+
motion: { type: "warp", speed: [0.06, 0.16] },
|
|
3525
|
+
alpha: [0.5, 0.9],
|
|
3526
|
+
blend: "lighter"
|
|
3527
|
+
},
|
|
3528
|
+
{
|
|
3529
|
+
key: "near-stars",
|
|
3530
|
+
count: 50,
|
|
3531
|
+
sprite: {
|
|
3532
|
+
kind: "streak",
|
|
3533
|
+
length: [0.025, 0.06],
|
|
3534
|
+
width: 25e-4,
|
|
3535
|
+
color: "#ffffff",
|
|
3536
|
+
colors: ["#ffffff", "#a8c8ff"],
|
|
3537
|
+
colorWeights: [3, 1]
|
|
3538
|
+
},
|
|
3539
|
+
motion: { type: "warp", speed: [0.3, 0.55] },
|
|
3540
|
+
alpha: [0.7, 1],
|
|
3541
|
+
blend: "lighter"
|
|
3542
|
+
},
|
|
3543
|
+
{
|
|
3544
|
+
key: "core-glow",
|
|
3545
|
+
count: 1,
|
|
3546
|
+
sprite: { kind: "circle", radius: [0.06, 0.06], color: "#3a4a8f", soft: true },
|
|
3547
|
+
motion: { type: "static" },
|
|
3548
|
+
position: { x: 0.5, y: 0.5 },
|
|
3549
|
+
alpha: [0.35, 0.35],
|
|
3550
|
+
pulse: { amp: 0.15, period: 5e3 },
|
|
3551
|
+
blend: "lighter"
|
|
2142
3552
|
}
|
|
2143
3553
|
]
|
|
2144
3554
|
};
|
|
@@ -2150,10 +3560,15 @@ var SCHEMA_EXAMPLES = [
|
|
|
2150
3560
|
{ id: "snowfall", label: "Snowfall", spec: SNOWFALL_SPEC },
|
|
2151
3561
|
{ id: "lanterns", label: "Night Lanterns", spec: LANTERNS_SPEC },
|
|
2152
3562
|
{ id: "sakura", label: "Sakura Drift", spec: SAKURA_SPEC },
|
|
2153
|
-
{ id: "dev-dashboard", label: "
|
|
3563
|
+
{ id: "dev-dashboard", label: "Control Center", spec: DASHBOARD_SPEC },
|
|
2154
3564
|
{ id: "orrery", label: "Orrery", spec: ORRERY_SPEC },
|
|
2155
3565
|
{ id: "constellation", label: "Constellation", spec: CONSTELLATION_SPEC },
|
|
2156
|
-
{ id: "comets", label: "Comet Shower", spec: COMETS_SPEC }
|
|
3566
|
+
{ id: "comets", label: "Comet Shower", spec: COMETS_SPEC },
|
|
3567
|
+
{ id: "aurora", label: "Aurora", spec: AURORA_SPEC },
|
|
3568
|
+
{ id: "warp-tunnel", label: "Warp Tunnel", spec: WARP_TUNNEL_SPEC },
|
|
3569
|
+
{ id: "polygons", label: "Polygons", spec: POLYGONS_SPEC },
|
|
3570
|
+
{ id: "matrix-rain", label: "Matrix Rain", spec: MATRIX_RAIN_SPEC },
|
|
3571
|
+
{ id: "procession", label: "Night Procession", spec: PROCESSION_SPEC }
|
|
2157
3572
|
];
|
|
2158
3573
|
var EXAMPLE_SPECS = SCHEMA_EXAMPLES.map((e) => e.spec);
|
|
2159
3574
|
var EXAMPLE_BY_ID = Object.fromEntries(
|
|
@@ -2161,6 +3576,7 @@ var EXAMPLE_BY_ID = Object.fromEntries(
|
|
|
2161
3576
|
);
|
|
2162
3577
|
export {
|
|
2163
3578
|
AQUARIUM_SPEC,
|
|
3579
|
+
AURORA_SPEC,
|
|
2164
3580
|
COMETS_SPEC,
|
|
2165
3581
|
CONSTELLATION_SPEC,
|
|
2166
3582
|
DASHBOARD_SPEC,
|
|
@@ -2168,12 +3584,17 @@ export {
|
|
|
2168
3584
|
EXAMPLE_SPECS,
|
|
2169
3585
|
LANTERNS_SPEC,
|
|
2170
3586
|
LIMITS,
|
|
3587
|
+
MATRIX_RAIN_SPEC,
|
|
2171
3588
|
ORRERY_SPEC,
|
|
3589
|
+
POLYGONS_SPEC,
|
|
3590
|
+
PROCESSION_SPEC,
|
|
2172
3591
|
RAIN_SPEC,
|
|
2173
3592
|
SAKURA_SPEC,
|
|
2174
3593
|
SCHEMA_EXAMPLES,
|
|
2175
3594
|
SCHEMA_VERSION,
|
|
2176
3595
|
SNOWFALL_SPEC,
|
|
3596
|
+
WARP_NEAR,
|
|
3597
|
+
WARP_TUNNEL_SPEC,
|
|
2177
3598
|
adviseSpec,
|
|
2178
3599
|
alphaAt,
|
|
2179
3600
|
applyDeltasToSpec,
|
|
@@ -2181,11 +3602,20 @@ export {
|
|
|
2181
3602
|
buildEntities,
|
|
2182
3603
|
compileSaver,
|
|
2183
3604
|
describeScene,
|
|
3605
|
+
diffScenes,
|
|
3606
|
+
dominanceRanking,
|
|
2184
3607
|
easeSmooth,
|
|
3608
|
+
headingAt,
|
|
2185
3609
|
lerpSpec,
|
|
3610
|
+
lifeAlphaAt,
|
|
3611
|
+
linkEdges,
|
|
2186
3612
|
linkPairs,
|
|
3613
|
+
luminanceGrid,
|
|
2187
3614
|
manifestFor,
|
|
3615
|
+
motionStats,
|
|
3616
|
+
perceiveScene,
|
|
2188
3617
|
positionAt,
|
|
3618
|
+
renderBrailleMap,
|
|
2189
3619
|
resolveSpecPath,
|
|
2190
3620
|
rotationAt,
|
|
2191
3621
|
sizeAt,
|