@idle-screens/schema 3.6.0 → 3.6.1
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 +135 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +29 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/saver-spec.schema.json +55 -1
package/FORMAT.md
CHANGED
|
@@ -105,9 +105,11 @@ must exist, have `count: 1`, and not themselves orbit a layer.
|
|
|
105
105
|
|
|
106
106
|
With `units: "viewport"` (the default) every dimensional value — sizes, radii,
|
|
107
107
|
speeds, distances, wavelengths, stroke widths — is a fraction of
|
|
108
|
-
`min(width, height)`, so specs scale to any display.
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
`min(width, height)`, so specs scale to any display. Text sprites with an
|
|
109
|
+
explicit `px` size in their `font` string (e.g. `"bold 14px monospace"`) are no
|
|
110
|
+
exception: the compiler rescales that number by
|
|
111
|
+
`min(width, height) / referenceViewport` so the text holds its apparent size,
|
|
112
|
+
and only `units: "px"` specs get it verbatim. See **Scale** below.
|
|
111
113
|
|
|
112
114
|
`ghosting` paints each frame over a faded copy of the previous one instead of
|
|
113
115
|
clearing: moving entities leave decaying after-images (Mystify smears, Matrix
|
|
@@ -169,7 +171,8 @@ with distance, removing pop-in at the cutoff.
|
|
|
169
171
|
- `{ "kind": "emoji", "glyphs": ["🐟", "🐠"], "cycle": { "period": 800 } }` —
|
|
170
172
|
glyph picked per entity (seeded); optional `cycle` rotates variants over time
|
|
171
173
|
- `{ "kind": "text", "strings": ["HELLO"], "color": "#e6e8ef", "font": "bold monospace", "align": "center", "baseline": "middle", "maxWidth": 300, "cycle": ... }`
|
|
172
|
-
— a `font` **with** a px size
|
|
174
|
+
— a `font` **with** a px size keeps that shorthand, its number rescaled to the
|
|
175
|
+
viewport (verbatim only under `units: "px"`); a family/weight only
|
|
173
176
|
(`"bold monospace"`) composes with the seeded per-entity `size`
|
|
174
177
|
- `{ "kind": "circle", "radius": [0.001, 0.003], "color": "#ffffff", "soft": true }` —
|
|
175
178
|
`soft` renders a radial-falloff glow orb instead of a hard disc
|
|
@@ -304,6 +307,134 @@ an event, where the same ring pulsing forever reads as wallpaper. Long
|
|
|
304
307
|
drift a little, settle, fade" is one layer). `clock` is the other half: two
|
|
305
308
|
layers on the same clock are a duet, not a coincidence. The shipped `pings`
|
|
306
309
|
example is the whole idea in two layers.
|
|
310
|
+
## Scale — what the numbers actually look like
|
|
311
|
+
|
|
312
|
+
Every dimensional value is a fraction of `min(width, height)`. On a 1920×1080
|
|
313
|
+
display that divisor is **1080**, so `0.01` is 10.8 px there and the same
|
|
314
|
+
fraction of the short side on a phone or a 4K wall. Nothing below is a rule —
|
|
315
|
+
they are measured landmarks, because the most common authoring failure is not
|
|
316
|
+
a bad idea, it is an idea rendered two orders of magnitude too small or too
|
|
317
|
+
large to see. The px conversions and coverage figures are measured
|
|
318
|
+
(`perceiveScene` at 1920×1080, 40 opaque white entities on black, unless a row
|
|
319
|
+
says otherwise); the "reads as" readings and the recipe advice are judgment.
|
|
320
|
+
|
|
321
|
+
### `circle.radius` (a **radius**, so a dot is twice this wide)
|
|
322
|
+
|
|
323
|
+
| radius | px radius @1080p | px across | coverage (40) | reads as |
|
|
324
|
+
|---|---|---|---|---|
|
|
325
|
+
| `0.0005` | 0.5 | 1 | 0.9 % | star grain — texture, never the subject |
|
|
326
|
+
| `0.002` | 2.2 | 4 | 1.0 % | dust, embers, distant snow |
|
|
327
|
+
| `0.005` | 5.4 | 11 | 1.6 % | a distinct dot; graph node, firefly |
|
|
328
|
+
| `0.02` | 21.6 | 43 | 6.2 % | a clear disc — bokeh, a planet |
|
|
329
|
+
| `0.05` | 54 | 108 | 21 % | a focal object, or a soft glow field |
|
|
330
|
+
| `0.1` | 108 | 216 | 52 % | dominant form — sun, moon, one per scene |
|
|
331
|
+
| `0.2` | 216 | 432 | 88 % | 40 % of the short side; only as a soft glow plate |
|
|
332
|
+
|
|
333
|
+
**Read the coverage column with its floor in mind.** `perceiveScene` samples an
|
|
334
|
+
80 × 48 grid, so a cell is ~24 × 22 px at 1080p and anything smaller registers as
|
|
335
|
+
one cell per entity. That is why the top two rungs both land near 1 % despite a
|
|
336
|
+
16 × difference in area — below `radius ≈ 0.01` the column counts entities, not
|
|
337
|
+
pixels. It becomes an area measurement from `0.02` up.
|
|
338
|
+
|
|
339
|
+
The shipped examples put entity *fields* almost entirely in `0.0003 – 0.007`
|
|
340
|
+
and large forms in `0.03 – 0.185` (the biggest is a `ring` gauge in
|
|
341
|
+
`dev-dashboard`). The gap between is thinly populated
|
|
342
|
+
but not empty, and the exception is instructive: `aurora` runs 60–100-entity
|
|
343
|
+
layers at `radius [0.045, 0.11]`, because a soft additive orb that large is
|
|
344
|
+
atmosphere rather than an object. Within one scene, `lanterns`' three parallax
|
|
345
|
+
layers step by ~2.2 × and ~2.4 × per layer; steps much smaller than that read
|
|
346
|
+
as one layer with a wide size range rather than as depth.
|
|
347
|
+
|
|
348
|
+
The bottom two rungs are texture, not content, and `adviseSpec` says so: a
|
|
349
|
+
`0.002` field stops raising `sparse-scene` at ~70 entities, while a `0.0005`
|
|
350
|
+
field would need over a thousand — far past `LIMITS.maxPerLayer` (400) — so it
|
|
351
|
+
raises `sparse-scene` at every legal count and always needs a brighter layer
|
|
352
|
+
above it.
|
|
353
|
+
|
|
354
|
+
### `size` (emoji), `fontSize` (textBlock), `size` (text sprites)
|
|
355
|
+
|
|
356
|
+
`0.02` ≈ 22 px, `0.035` ≈ 38 px, `0.05` ≈ 54 px, `0.1` ≈ 108 px at 1080p.
|
|
357
|
+
Emoji fields in the examples sit at `0.011 – 0.078`; `haiku`, the one textBlock
|
|
358
|
+
example, sets `fontSize: 0.05`.
|
|
359
|
+
|
|
360
|
+
An explicit px size inside a `font` string (`"bold 14px monospace"`) is **not**
|
|
361
|
+
absolute: under the default `viewport` units the compiler rescales it by
|
|
362
|
+
`min(w, h) / referenceViewport`, so it holds its apparent size across displays
|
|
363
|
+
(14 px at 1080p becomes 28 px at 4K and 5 px on a phone). Only `units: "px"`
|
|
364
|
+
specs get the number verbatim. Two consequences: a font pinned for a 4K wall
|
|
365
|
+
becomes illegibly small on a phone-sized viewport, and a spec that genuinely
|
|
366
|
+
wants fixed pixels must say `units: "px"`.
|
|
367
|
+
|
|
368
|
+
### `speed` (viewport fractions per second)
|
|
369
|
+
|
|
370
|
+
| speed | px/s @1080p | crosses the short side | crosses a 16:9 width |
|
|
371
|
+
|---|---|---|---|
|
|
372
|
+
| `0.001` | 1.1 | 17 min | 30 min |
|
|
373
|
+
| `0.005` | 5.4 | 3.3 min | 5.9 min |
|
|
374
|
+
| `0.02` | 21.6 | 50 s | 89 s |
|
|
375
|
+
| `0.06` | 65 | 17 s | 30 s |
|
|
376
|
+
| `0.15` | 162 | 7 s | 12 s |
|
|
377
|
+
|
|
378
|
+
An ambient scene meant to live on a wall for hours wants its background layers
|
|
379
|
+
under `0.005` — anything faster resolves as traffic rather than atmosphere.
|
|
380
|
+
Above `0.06` a layer reads as an event (rain, warp, a passing comet). Speeds
|
|
381
|
+
are ranges and each entity draws its own, so a `[0.002, 0.008]` layer already
|
|
382
|
+
carries internal parallax.
|
|
383
|
+
|
|
384
|
+
### `links.maxDist`
|
|
385
|
+
|
|
386
|
+
Link density is governed by `maxDist` relative to the mean spacing between
|
|
387
|
+
entities, `sqrt(w·h / count) / min(w, h)` — for 40 entities at 1920×1080 that
|
|
388
|
+
is `0.21` (228 px), for 100 it is `0.13` (144 px). Measured at `k: 3`, count 40,
|
|
389
|
+
averaged over 15 seeds:
|
|
390
|
+
|
|
391
|
+
| `maxDist` | edges drawn (of `k·count`) | graph |
|
|
392
|
+
|---|---|---|
|
|
393
|
+
| 0.5 × spacing | ~12 % | scattered pairs; ~45 % of nodes isolated |
|
|
394
|
+
| 1.0 × spacing | ~45 % | a constellation with a few loners |
|
|
395
|
+
| 1.5 × spacing | ~60 % | usually one connected component, rarely an isolate |
|
|
396
|
+
| 2.0 × spacing | ~62 % | saturated — `k` is now the binding cap |
|
|
397
|
+
|
|
398
|
+
Past ~2 × spacing, raising `maxDist` costs distance tests and buys nothing;
|
|
399
|
+
raise `k` instead. `describeScene` reports `linksDrawn` / `linksExpected` and
|
|
400
|
+
`isolatedNodes`, so connectivity is checkable per spec rather than guessed —
|
|
401
|
+
worth doing, since the seed moves these by several points either way.
|
|
402
|
+
|
|
403
|
+
### Recipes
|
|
404
|
+
|
|
405
|
+
**Parallax depth.** Three or four layers where size and speed rise together —
|
|
406
|
+
scaling size alone reads as a size distribution, not as distance. `lanterns`'
|
|
407
|
+
ladder, which the numbers above are drawn from: `radius [0.0005, 0.0013]` /
|
|
408
|
+
`speed [0.0005, 0.002]` / `alpha [0.35, 1]` far, `radius [0.0014, 0.0028]` /
|
|
409
|
+
`speed [0.006, 0.013]` / `alpha [0.5, 0.9]` mid, `radius [0.0032, 0.006]` /
|
|
410
|
+
`speed [0.015, 0.026]` / `alpha [0.6, 1]` near. Note that alpha *narrows*
|
|
411
|
+
toward the front rather than simply rising — the far layer spans the whole
|
|
412
|
+
range because distance is what varies it; the near layer is uniformly present.
|
|
413
|
+
|
|
414
|
+
**Glow stacking.** `soft: true` + `blend: "lighter"` + `alpha` around 0.5–0.9,
|
|
415
|
+
with `pulse: { amp: 0.2, period: 3000 }` for breathing. Glow reads only against
|
|
416
|
+
a dark plate — over a pale background use `"screen"`. Do not assume a soft orb
|
|
417
|
+
covers what a hard disc of the same radius would: `perceiveScene` models the
|
|
418
|
+
halo as reaching 2.4 × the radius, so a large soft additive layer measures far
|
|
419
|
+
*more* coverage than its hard equivalent — ~2.5 × at `radius 0.05` — while a
|
|
420
|
+
small one measures *less*, ~0.5 × at `radius 0.005`, because the faint outer
|
|
421
|
+
halo falls under the per-cell visibility threshold. The crossover sits near
|
|
422
|
+
`radius 0.01`. Re-measure after switching `soft` or `blend` rather than
|
|
423
|
+
compensating by a rule of thumb.
|
|
424
|
+
|
|
425
|
+
**Graph web.** `count 40–100`, `radius [0.002, 0.005]`, a slow `drift`
|
|
426
|
+
(`speed [0.002, 0.007]`) so the topology keeps re-forming, and `links` with
|
|
427
|
+
`k: 3`, `maxDist` at 1.5 × mean spacing (`0.3` for 40 entities, `0.2` for 100),
|
|
428
|
+
`alpha: 0.15`, `width: 0.0005`. Link alpha wants to be much lower than the
|
|
429
|
+
nodes' — at 0.15 the web is a suggestion, at 0.5 it is a diagram. Add
|
|
430
|
+
`falloff: true` to fade edges toward the cutoff instead of popping them.
|
|
431
|
+
|
|
432
|
+
**Focal pin.** One `count: 1` layer with `motion: { type: "static" }` and an
|
|
433
|
+
explicit `position`, `radius` in the `0.05 – 0.15` band, over a field of
|
|
434
|
+
`0.002`-scale entities. The size ratio between the two is what makes it read as
|
|
435
|
+
a subject rather than the largest member of the crowd — an order of magnitude
|
|
436
|
+
or more, which is the gap the ladder above shows between the field rungs and
|
|
437
|
+
the focal ones.
|
|
307
438
|
|
|
308
439
|
## Determinism contract
|
|
309
440
|
|
package/dist/index.d.ts
CHANGED
|
@@ -719,6 +719,8 @@ interface Entity {
|
|
|
719
719
|
ordered?: true;
|
|
720
720
|
/** Orbit parent layer key (motion.center = { layer }). Center resolved at render time. */
|
|
721
721
|
orbitParent?: string;
|
|
722
|
+
/** Present only when the layer explicitly disables edge wrapping. */
|
|
723
|
+
wrapDisabled?: true;
|
|
722
724
|
/** Harmonic drift params. Only set for wander motion. */
|
|
723
725
|
wander?: {
|
|
724
726
|
own: WanderOsc;
|
package/dist/index.js
CHANGED
|
@@ -83,12 +83,13 @@ var LIMITS = {
|
|
|
83
83
|
|
|
84
84
|
// src/steer.ts
|
|
85
85
|
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
86
|
+
var STEERABLE_ROOT_KEYS = /* @__PURE__ */ new Set(["ghosting", "referenceViewport"]);
|
|
86
87
|
function resolveSpecPath(spec, path) {
|
|
87
88
|
if (!spec || typeof spec !== "object" || !path) return null;
|
|
88
89
|
const parts = path.split(".");
|
|
89
90
|
if (parts.some((p) => UNSAFE_KEYS.has(p))) return null;
|
|
90
91
|
const s = spec;
|
|
91
|
-
if (parts[0] !== "layers" && parts[0] !== "background" && Array.isArray(s.layers)) {
|
|
92
|
+
if (parts[0] !== "layers" && parts[0] !== "background" && !STEERABLE_ROOT_KEYS.has(parts[0]) && Array.isArray(s.layers)) {
|
|
92
93
|
const idx = s.layers.findIndex((l) => l && l.key === parts[0]);
|
|
93
94
|
if (idx === -1) return null;
|
|
94
95
|
parts.splice(0, 1, "layers", String(idx));
|
|
@@ -150,7 +151,7 @@ function lerpSpec(from, to, k) {
|
|
|
150
151
|
}
|
|
151
152
|
function steerablePaths(spec) {
|
|
152
153
|
if (!spec || typeof spec !== "object") return [];
|
|
153
|
-
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "mode", "curve", "layer"]);
|
|
154
|
+
const SKIP = /* @__PURE__ */ new Set(["kind", "type", "key", "schemaVersion", "id", "label", "seed", "units", "motionIntensity", "mode", "curve", "layer"]);
|
|
154
155
|
const INDEXED = /* @__PURE__ */ new Set(["layers", "stops"]);
|
|
155
156
|
const out = [];
|
|
156
157
|
const walk = (node, prefix, key) => {
|
|
@@ -181,6 +182,7 @@ function easeSmooth(k) {
|
|
|
181
182
|
function structuralSignature(spec) {
|
|
182
183
|
return JSON.stringify([
|
|
183
184
|
spec.units,
|
|
185
|
+
spec.referenceViewport,
|
|
184
186
|
spec.layers.map((l) => {
|
|
185
187
|
const s = l.sprite;
|
|
186
188
|
return [
|
|
@@ -660,6 +662,9 @@ function validateSprite(sprite, path, err, warn) {
|
|
|
660
662
|
err(`${path}.strings`, "must be a non-empty array of strings");
|
|
661
663
|
}
|
|
662
664
|
if (sprite.color !== void 0) color(sprite.color, `${path}.color`, err);
|
|
665
|
+
if (sprite.font !== void 0 && !isStr(sprite.font)) {
|
|
666
|
+
err(`${path}.font`, "must be a string");
|
|
667
|
+
}
|
|
663
668
|
if (sprite.align !== void 0 && !["left", "center", "right"].includes(sprite.align)) {
|
|
664
669
|
err(`${path}.align`, "must be left | center | right");
|
|
665
670
|
}
|
|
@@ -1255,7 +1260,8 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1255
1260
|
Math.round(layer.count * countScale),
|
|
1256
1261
|
layer.links ? LIMITS.maxLinkLayerCount : LIMITS.maxPerLayer
|
|
1257
1262
|
));
|
|
1258
|
-
const [
|
|
1263
|
+
const defaultSize = scale === 1 ? [20, 40] : [20 / LIMITS.referenceViewport, 40 / LIMITS.referenceViewport];
|
|
1264
|
+
const [smin, smax] = layer.size ?? defaultSize;
|
|
1259
1265
|
const variants = spriteVariants(layer.sprite);
|
|
1260
1266
|
const sprite = layer.sprite;
|
|
1261
1267
|
const spriteColors = isShapedSprite(sprite) ? sprite.colors : void 0;
|
|
@@ -1444,6 +1450,7 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
|
|
|
1444
1450
|
...sprite.kind === "bar" ? { barIndex: i } : {},
|
|
1445
1451
|
...ordered ? { ordered: true } : {},
|
|
1446
1452
|
...orbitParent ? { orbitParent } : {},
|
|
1453
|
+
...layer.wrap === false ? { wrapDisabled: true } : {},
|
|
1447
1454
|
...m.type === "wander" ? {
|
|
1448
1455
|
wander: {
|
|
1449
1456
|
own: drawOsc(rng, meanderPx),
|
|
@@ -1635,9 +1642,10 @@ function positionAt(e, t, w, h) {
|
|
|
1635
1642
|
};
|
|
1636
1643
|
}
|
|
1637
1644
|
if (e.motion === "rise") {
|
|
1645
|
+
const y2 = e.y0 + e.vy * dt;
|
|
1638
1646
|
return {
|
|
1639
1647
|
x: e.x0 + (e.bob ? e.bob * Math.sin(t / 700 + e.phase) : 0),
|
|
1640
|
-
y:
|
|
1648
|
+
y: e.wrapDisabled ? y2 : wrap(y2, -m, h + m),
|
|
1641
1649
|
flip: false
|
|
1642
1650
|
};
|
|
1643
1651
|
}
|
|
@@ -1657,14 +1665,18 @@ function positionAt(e, t, w, h) {
|
|
|
1657
1665
|
}
|
|
1658
1666
|
}
|
|
1659
1667
|
const margin = m + wp.margin;
|
|
1668
|
+
const x2 = e.x0 + e.vx * dt + hx;
|
|
1669
|
+
const y2 = e.y0 + e.vy * dt + hy;
|
|
1660
1670
|
return {
|
|
1661
|
-
x:
|
|
1662
|
-
y:
|
|
1671
|
+
x: e.wrapDisabled ? x2 : wrap(x2, -margin, w + margin),
|
|
1672
|
+
y: e.wrapDisabled ? y2 : wrap(y2, -margin, h + margin),
|
|
1663
1673
|
flip: e.headingLeft
|
|
1664
1674
|
};
|
|
1665
1675
|
}
|
|
1666
|
-
const
|
|
1667
|
-
|
|
1676
|
+
const rawX = e.x0 + e.vx * dt;
|
|
1677
|
+
const x = e.wrapDisabled ? rawX : wrap(rawX, -m, w + m);
|
|
1678
|
+
const rawY = e.y0 + e.vy * dt;
|
|
1679
|
+
let y = e.vy !== 0 && !e.wrapDisabled ? wrap(rawY, -m, h + m) : rawY;
|
|
1668
1680
|
if (e.bob) y += e.bob * Math.sin(t / 500 + e.phase);
|
|
1669
1681
|
return { x, y, flip: e.headingLeft };
|
|
1670
1682
|
}
|
|
@@ -2472,7 +2484,9 @@ var SpecInstance = class {
|
|
|
2472
2484
|
const positions = built.entities.map((e) => this.entityPos(e, t, parentE));
|
|
2473
2485
|
const maxDistPx = links.maxDist * (this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h));
|
|
2474
2486
|
const edges = linkEdges(links, positions, maxDistPx, wrap2, this.w, this.h);
|
|
2475
|
-
const
|
|
2487
|
+
const unitScale = this.effSpec.units === "px" ? 1 : Math.min(this.w, this.h);
|
|
2488
|
+
const defaultWidth = unitScale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
2489
|
+
const lw = (links.width ?? defaultWidth) * unitScale;
|
|
2476
2490
|
ctx.lineWidth = lw;
|
|
2477
2491
|
ctx.lineCap = "butt";
|
|
2478
2492
|
for (const { i, j, dist } of edges) {
|
|
@@ -2758,7 +2772,6 @@ var SequenceInstance = class {
|
|
|
2758
2772
|
child.hotSwapSpec(this.childScene(index));
|
|
2759
2773
|
} else {
|
|
2760
2774
|
child = this.ensureChild(index);
|
|
2761
|
-
if (useMorphSeed) child.hotSwapSpec(this.childScene(index));
|
|
2762
2775
|
}
|
|
2763
2776
|
child.renderFrame(localT, effSeed);
|
|
2764
2777
|
}
|
|
@@ -3222,7 +3235,8 @@ function adviseSpec(spec, viewport = { width: 1920, height: 1080 }, opts = {}) {
|
|
|
3222
3235
|
const positions = entities.map((e) => positionAt(e, 0, w, h));
|
|
3223
3236
|
const maxDistPx = layer.links.maxDist * scale;
|
|
3224
3237
|
const edges = linkEdges(layer.links, positions, maxDistPx, layer.wrap !== false, w, h);
|
|
3225
|
-
const
|
|
3238
|
+
const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
3239
|
+
const lwPx = (layer.links.width ?? defaultWidth) * scale;
|
|
3226
3240
|
const la = layer.links.alpha ?? 1;
|
|
3227
3241
|
for (const edge of edges) totalCoverage += edge.dist * lwPx * la / (w * h);
|
|
3228
3242
|
}
|
|
@@ -3826,7 +3840,8 @@ function luminanceGrid(spec, opts = {}) {
|
|
|
3826
3840
|
const edges = linkEdges(layer.links, positions, maxDistPx, layer.wrap !== false && motionWraps, w, h);
|
|
3827
3841
|
const la = Math.min(1, (layer.links.alpha ?? 0.6) * lifeA);
|
|
3828
3842
|
const lum = layer.links.color ? hexLuma(layer.links.color) : 0.7;
|
|
3829
|
-
const
|
|
3843
|
+
const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
3844
|
+
const wgt = Math.min(1, (layer.links.width ?? defaultWidth) * scale / cellH) * la * alphaScale;
|
|
3830
3845
|
for (const edge of edges) {
|
|
3831
3846
|
const pi = positions[edge.i];
|
|
3832
3847
|
const pj = positions[edge.j];
|
|
@@ -4034,7 +4049,8 @@ function dominanceRanking(spec, opts = {}) {
|
|
|
4034
4049
|
const motionWraps = ["drift", "rise", "wander"].includes(layer.motion.type);
|
|
4035
4050
|
const edges = linkEdges(layer.links, positions, layer.links.maxDist * scale, layer.wrap !== false && motionWraps, w, h);
|
|
4036
4051
|
const la = (layer.links.alpha ?? 0.6) * lifeA;
|
|
4037
|
-
const
|
|
4052
|
+
const defaultWidth = scale === 1 ? 1 : 1 / LIMITS.referenceViewport;
|
|
4053
|
+
const lw = (layer.links.width ?? defaultWidth) * scale;
|
|
4038
4054
|
for (const edge of edges) area += edge.dist * lw * la * LINE_SALIENCE;
|
|
4039
4055
|
}
|
|
4040
4056
|
const meanLuma = entities.length ? lumAcc / entities.length : 0;
|