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