@idle-screens/schema 2.3.0 → 3.0.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/dist/index.js CHANGED
@@ -250,8 +250,15 @@ function validateLayer(layer, path, err, warn, spec) {
250
250
  }
251
251
  }
252
252
  if (layer.spin !== void 0) {
253
- if (!isNum(layer.spin)) err(`${path}.spin`, "must be a number (degrees/sec)");
254
- else if (Math.abs(layer.spin) > LIMITS.maxSpin) err(`${path}.spin`, `must be within \xB1${LIMITS.maxSpin} deg/sec`);
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
+ }
255
262
  }
256
263
  if (layer.grow !== void 0) {
257
264
  if (!isObj(layer.grow)) err(`${path}.grow`, "must be an object");
@@ -749,8 +756,10 @@ function buildEntities(layer, rng, w, h, scale = 1, countScale = 1) {
749
756
  pulsePeriod: layer.pulse?.period ?? 1e3,
750
757
  pulsePhase: layer.pulse ? rng.range(0, Math.PI * 2) : 0,
751
758
  // New draws AFTER pulsePhase — guarded so existing specs keep identical streams.
752
- spinSpeed: layer.spin ? layer.spin * Math.PI / 180 : 0,
753
- // deg/sec → rad/sec
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,
754
763
  spinPhase: layer.spin ? rng.range(0, Math.PI * 2) : 0,
755
764
  growAmp: layer.grow?.amp ?? 0,
756
765
  growPeriod: layer.grow?.period ?? 1e3,
@@ -1178,6 +1187,11 @@ function hexToRgba(hex, alpha) {
1178
1187
  return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${alpha})`;
1179
1188
  }
1180
1189
  var FONT_PREFIX_RE = /^((?:(?:italic|oblique|bold|bolder|lighter|normal|\d{3})\s+)+)/;
1190
+ var FONT_PX_RE = /(\d*\.?\d+)px/;
1191
+ function scaleFontPx(font, factor) {
1192
+ if (factor === 1) return font;
1193
+ return font.replace(FONT_PX_RE, (_m, n) => `${(Number(n) * factor).toFixed(2)}px`);
1194
+ }
1181
1195
  function composeFontShorthand(sz, font) {
1182
1196
  const m = FONT_PREFIX_RE.exec(font);
1183
1197
  if (m) return `${m[1].trim()} ${sz}px ${font.slice(m[0].length)}`;
@@ -1249,6 +1263,11 @@ var SpecInstance = class {
1249
1263
  /** Active glide between two resolved specs (live setParam/applyTrack). */
1250
1264
  transition = null;
1251
1265
  lastStructural = "";
1266
+ /** Viewport factor for absolute px sizes — 1 for `units: 'px'` specs. */
1267
+ fontScale() {
1268
+ if (this.effSpec.units === "px") return 1;
1269
+ return Math.min(this.w, this.h) / (this.effSpec.referenceViewport ?? LIMITS.referenceViewport);
1270
+ }
1252
1271
  sizeCanvas() {
1253
1272
  const dpr = Math.min(this.saverCtx.dpr, 2);
1254
1273
  this.canvas.width = Math.max(1, Math.round(this.w * dpr));
@@ -1463,7 +1482,7 @@ var SpecInstance = class {
1463
1482
  } else {
1464
1483
  ctx.textAlign = sprite.align ?? "center";
1465
1484
  ctx.textBaseline = sprite.baseline ?? "middle";
1466
- ctx.font = sprite.font ? /\dpx/.test(sprite.font) ? sprite.font : composeFontShorthand(sz, sprite.font) : `${sz}px system-ui, sans-serif`;
1485
+ ctx.font = sprite.font ? FONT_PX_RE.test(sprite.font) ? scaleFontPx(sprite.font, this.fontScale()) : composeFontShorthand(sz, sprite.font) : `${sz}px system-ui, sans-serif`;
1467
1486
  ctx.fillStyle = sprite.color ?? "#e6e8ef";
1468
1487
  const idx = spriteIndexAt(e, t, sprite.strings.length);
1469
1488
  const text = sprite.strings[idx] ?? sprite.strings[0];
@@ -1875,6 +1894,8 @@ function describeScene(spec, options = {}) {
1875
1894
 
1876
1895
  // src/perceive.ts
1877
1896
  import { createRng as createRng4 } from "@idle-screens/core";
1897
+ var GLOW_SPREAD = 2.4;
1898
+ var LINE_SALIENCE = 3.5;
1878
1899
  function buildScene(spec, opts) {
1879
1900
  const { width: w, height: h } = opts.viewport ?? { width: 1920, height: 1080 };
1880
1901
  const seed = opts.seed ?? spec.seed ?? 42;
@@ -2013,12 +2034,16 @@ function luminanceGrid(spec, opts = {}) {
2013
2034
  halfX = box.halfX;
2014
2035
  halfY = box.halfY;
2015
2036
  }
2016
- const c0 = Math.max(0, Math.floor((centerX - halfX) / cellW));
2017
- const c1 = Math.min(cols - 1, Math.floor((centerX + halfX) / cellW));
2018
- const r0 = Math.max(0, Math.floor((centerY - halfY) / cellH));
2019
- const r1 = Math.min(rows - 1, Math.floor((centerY + halfY) / cellH));
2020
2037
  const circular = s.kind === "circle" || s.kind === "ring";
2021
- const soft = s.kind === "circle" && s.soft;
2038
+ const soft = s.kind === "circle" && !!s.soft;
2039
+ const additive = layer.blend === "lighter" || layer.blend === "screen";
2040
+ const glow = soft && additive;
2041
+ const reachX = glow ? halfX * GLOW_SPREAD : halfX;
2042
+ const reachY = glow ? halfY * GLOW_SPREAD : halfY;
2043
+ const c0 = Math.max(0, Math.floor((centerX - reachX) / cellW));
2044
+ const c1 = Math.min(cols - 1, Math.floor((centerX + reachX) / cellW));
2045
+ const r0 = Math.max(0, Math.floor((centerY - reachY) / cellH));
2046
+ const r1 = Math.min(rows - 1, Math.floor((centerY + reachY) / cellH));
2022
2047
  const inkWeight = s.kind === "text" || s.kind === "emoji" ? 0.55 : 1;
2023
2048
  for (let r = r0; r <= r1; r++) {
2024
2049
  for (let c = c0; c <= c1; c++) {
@@ -2026,12 +2051,23 @@ function luminanceGrid(spec, opts = {}) {
2026
2051
  const dy = (r + 0.5) * cellH - centerY;
2027
2052
  if (circular) {
2028
2053
  const d = Math.sqrt(dx * dx + dy * dy);
2029
- if (d > halfX + Math.min(cellW, cellH) / 2) continue;
2054
+ const reach = glow ? reachX : halfX;
2055
+ if (d > reach + Math.min(cellW, cellH) / 2) continue;
2030
2056
  if (s.kind === "ring") {
2031
2057
  const strokeW = (s.width ?? (scale === 1 ? 2 : 2e-3)) * scale;
2032
2058
  if (d < halfX - strokeW) continue;
2033
2059
  }
2034
- const wgt = soft ? Math.max(0.1, 1 - d / Math.max(halfX, 1e-6)) : 1;
2060
+ let wgt;
2061
+ if (glow) {
2062
+ const core = halfX;
2063
+ if (d <= core) wgt = 1;
2064
+ else {
2065
+ const f = Math.max(0, 1 - (d - core) / Math.max(reachX - core, 1e-6));
2066
+ wgt = f * f;
2067
+ }
2068
+ } else {
2069
+ wgt = soft ? Math.max(0.1, 1 - d / Math.max(halfX, 1e-6)) : 1;
2070
+ }
2035
2071
  compose(r * cols + c, lum, a * wgt, layer.blend);
2036
2072
  } else {
2037
2073
  compose(r * cols + c, lum, a * inkWeight, layer.blend);
@@ -2107,11 +2143,14 @@ var DOT_BIT = [
2107
2143
  [4, 32],
2108
2144
  [64, 128]
2109
2145
  ];
2110
- function renderBrailleMap(grid) {
2146
+ function autoExpose(grid) {
2111
2147
  const sorted = [...grid.cells].sort((a, b) => a - b);
2112
2148
  const lo = sorted[0];
2113
2149
  const hi = Math.max(sorted[Math.floor(sorted.length * 0.98)], lo + 0.08);
2114
- const expose = (v) => Math.pow(Math.max(0, Math.min(1, (v - lo) / (hi - lo))), 0.8);
2150
+ return (v) => Math.pow(Math.max(0, Math.min(1, (v - lo) / (hi - lo))), 0.8);
2151
+ }
2152
+ function renderBrailleMap(grid) {
2153
+ const expose = autoExpose(grid);
2115
2154
  const outRows = [];
2116
2155
  for (let br = 0; br < Math.floor(grid.rows / 4); br++) {
2117
2156
  let line = "";
@@ -2129,6 +2168,34 @@ function renderBrailleMap(grid) {
2129
2168
  }
2130
2169
  return outRows.join("\n");
2131
2170
  }
2171
+ var DENSITY_RAMP = " .:-=+*#%@";
2172
+ function renderDensityMap(grid) {
2173
+ const expose = autoExpose(grid);
2174
+ const ramp = DENSITY_RAMP;
2175
+ const lines = [];
2176
+ for (let r = 0; r < grid.rows; r++) {
2177
+ let line = "";
2178
+ for (let c = 0; c < grid.cols; c++) {
2179
+ const v = expose(grid.cells[r * grid.cols + c]);
2180
+ line += ramp[Math.min(ramp.length - 1, Math.floor(v * ramp.length))];
2181
+ }
2182
+ lines.push(line);
2183
+ }
2184
+ return lines.join("\n");
2185
+ }
2186
+ function textSprites(spec, opts = {}) {
2187
+ const scene = buildScene(spec, opts);
2188
+ const t = opts.t ?? 5e3;
2189
+ const out = [];
2190
+ scene.layers.forEach(({ layer, entities }, layerIndex) => {
2191
+ if (layer.sprite.kind !== "text") return;
2192
+ const s = layer.sprite;
2193
+ const m = s.font ? /(\d+(?:\.\d+)?)px/.exec(s.font) : null;
2194
+ const sizePx = m ? Number(m[1]) : entities[0] ? sizeAt(entities[0], t) : 0;
2195
+ out.push({ layerIndex, key: layer.key, strings: [...s.strings], count: entities.length, sizePx: Math.round(sizePx) });
2196
+ });
2197
+ return out;
2198
+ }
2132
2199
  function dominanceRanking(spec, opts = {}) {
2133
2200
  const scene = buildScene(spec, opts);
2134
2201
  const t = opts.t ?? 5e3;
@@ -2148,8 +2215,8 @@ function dominanceRanking(spec, opts = {}) {
2148
2215
  const s = layer.sprite;
2149
2216
  let entArea;
2150
2217
  if (s.kind === "circle") entArea = Math.PI * (sz / 2) * (sz / 2);
2151
- else if (s.kind === "ring") entArea = Math.PI * sz * ((s.width ?? (scale === 1 ? 2 : 2e-3)) * scale);
2152
- else if (s.kind === "streak") entArea = sz * ((s.width ?? (scale === 1 ? 2 : 2e-3)) * scale);
2218
+ else if (s.kind === "ring") entArea = Math.PI * sz * ((s.width ?? (scale === 1 ? 2 : 2e-3)) * scale) * LINE_SALIENCE;
2219
+ else if (s.kind === "streak") entArea = sz * ((s.width ?? (scale === 1 ? 2 : 2e-3)) * scale) * LINE_SALIENCE;
2153
2220
  else if (s.kind === "rect") {
2154
2221
  const h2 = e.size2 !== void 0 ? e.size2 * (e.size > 0 ? sz / e.size : 1) : sz;
2155
2222
  entArea = sz * h2;
@@ -2166,7 +2233,7 @@ function dominanceRanking(spec, opts = {}) {
2166
2233
  const edges = linkEdges(layer.links, positions, layer.links.maxDist * scale, layer.wrap !== false && motionWraps, w, h);
2167
2234
  const la = (layer.links.alpha ?? 0.6) * lifeA;
2168
2235
  const lw = (layer.links.width ?? 1) * scale;
2169
- for (const edge of edges) area += edge.dist * lw * la;
2236
+ for (const edge of edges) area += edge.dist * lw * la * LINE_SALIENCE;
2170
2237
  }
2171
2238
  const meanLuma = entities.length ? lumAcc / entities.length : 0;
2172
2239
  const contrast = Math.abs(meanLuma - bgMean) + 0.1;
@@ -2274,6 +2341,7 @@ function perceiveScene(spec, opts = {}) {
2274
2341
  return {
2275
2342
  t: opts.t ?? 5e3,
2276
2343
  braille: renderBrailleMap(grid),
2344
+ density: renderDensityMap(grid),
2277
2345
  coverage: grid.coverage,
2278
2346
  meanLuminance: grid.meanLuminance,
2279
2347
  centroid: grid.centroid,
@@ -2281,6 +2349,7 @@ function perceiveScene(spec, opts = {}) {
2281
2349
  colProfile: grid.colProfile,
2282
2350
  dominance: dominanceRanking(spec, opts),
2283
2351
  motion: motionStats(spec, opts),
2352
+ text: textSprites(spec, opts),
2284
2353
  advisories: adviseSpec(spec, opts.viewport ?? { width: 1920, height: 1080 })
2285
2354
  };
2286
2355
  }
@@ -2494,84 +2563,113 @@ var DASHBOARD_SPEC = {
2494
2563
  background: {
2495
2564
  type: "gradient",
2496
2565
  stops: [
2497
- { at: 0, color: "#06060c" },
2498
- { at: 0.5, color: "#0a0a14" },
2499
- { at: 1, color: "#0c0c18" }
2566
+ { at: 0, color: "#05070d" },
2567
+ { at: 0.55, color: "#080b14" },
2568
+ { at: 1, color: "#0a0d18" }
2500
2569
  ]
2501
2570
  },
2502
2571
  layers: [
2503
- // --- ambient depth ---
2504
- {
2505
- key: "dust",
2506
- count: 40,
2507
- sprite: { kind: "circle", radius: [1852e-7, 7407e-7], color: "#1a2538" },
2508
- alpha: [0.1, 0.25],
2509
- motion: { type: "drift", speed: [2778e-7, 1111e-6], bob: 2778e-7 }
2510
- },
2572
+ // ---- ambient: the LED wall behind everything ----
2511
2573
  {
2512
2574
  key: "led-wall",
2513
- count: 140,
2514
- sprite: { kind: "circle", radius: [8e-4, 12e-4], color: "#274058" },
2515
- alpha: [0.05, 0.11],
2516
- layout: { type: "grid", columns: 20, jitter: 0.15 },
2517
- // A slow breath travels diagonally across the wall of dots.
2518
- pulse: { amp: 0.3, period: 5e3, wave: { wavelength: 0.35, angle: 20 } },
2575
+ count: 240,
2576
+ sprite: { kind: "circle", radius: [7e-4, 11e-4], color: "#22344c" },
2577
+ alpha: [0.04, 0.1],
2578
+ layout: { type: "grid", columns: 24, jitter: 0.1 },
2579
+ pulse: { amp: 0.3, period: 6200, wave: { wavelength: 0.4, angle: 24 } },
2519
2580
  motion: { type: "static" }
2520
2581
  },
2521
- // --- header (boots first) ---
2582
+ // ---- frame chrome (boots first) ----
2583
+ {
2584
+ key: "rule-header",
2585
+ count: 1,
2586
+ position: { x: 0.5, y: 0.085 },
2587
+ sprite: { kind: "rect", width: [0.93, 0.93], aspect: [12e-4, 12e-4], color: "#24384f" },
2588
+ alpha: [0.8, 0.8],
2589
+ motion: { type: "static" },
2590
+ life: { enter: 100, fade: 400 }
2591
+ },
2592
+ {
2593
+ key: "rule-footer",
2594
+ count: 1,
2595
+ position: { x: 0.5, y: 0.915 },
2596
+ sprite: { kind: "rect", width: [0.93, 0.93], aspect: [12e-4, 12e-4], color: "#24384f" },
2597
+ alpha: [0.8, 0.8],
2598
+ motion: { type: "static" },
2599
+ life: { enter: 100, fade: 400 }
2600
+ },
2601
+ {
2602
+ key: "rule-col-left",
2603
+ count: 1,
2604
+ position: { x: 0.265, y: 0.5 },
2605
+ sprite: { kind: "rect", width: [9e-4, 9e-4], aspect: [620, 620], color: "#1c2c40" },
2606
+ alpha: [0.7, 0.7],
2607
+ motion: { type: "static" },
2608
+ life: { enter: 100, fade: 400 }
2609
+ },
2610
+ {
2611
+ key: "rule-col-right",
2612
+ count: 1,
2613
+ position: { x: 0.645, y: 0.5 },
2614
+ sprite: { kind: "rect", width: [9e-4, 9e-4], aspect: [620, 620], color: "#1c2c40" },
2615
+ alpha: [0.7, 0.7],
2616
+ motion: { type: "static" },
2617
+ life: { enter: 100, fade: 400 }
2618
+ },
2619
+ // ---- header band ----
2522
2620
  {
2523
2621
  key: "title",
2524
2622
  count: 1,
2525
- position: { x: 0.5, y: 0.045 },
2623
+ position: { x: 0.035, y: 0.052 },
2526
2624
  sprite: {
2527
2625
  kind: "text",
2528
- strings: ["CONTROL CENTER"],
2529
- color: "#556677",
2626
+ strings: ["VIREO-9 \xB7 TRANS-LUNAR RELAY \xB7 OPS WALL 03"],
2627
+ color: "#7f93ab",
2530
2628
  font: "600 13px 'SF Mono', monospace",
2531
- align: "center"
2629
+ align: "left"
2532
2630
  },
2533
2631
  motion: { type: "static" },
2534
2632
  life: { enter: 200, fade: 600 }
2535
2633
  },
2536
2634
  {
2537
- key: "status",
2635
+ key: "clock",
2538
2636
  count: 1,
2539
- position: { x: 0.94, y: 0.045 },
2637
+ position: { x: 0.5, y: 0.058 },
2540
2638
  sprite: {
2541
2639
  kind: "text",
2542
- strings: ["\u25CF ALL SYSTEMS GO"],
2543
- color: "#44cc66",
2544
- font: "bold 10px monospace",
2545
- align: "right"
2640
+ strings: ["04:17:26 UTC"],
2641
+ color: "#c8d4e4",
2642
+ font: "600 20px 'SF Mono', monospace",
2643
+ align: "center"
2546
2644
  },
2547
- pulse: { amp: 0.2, period: 2400 },
2548
2645
  motion: { type: "static" },
2549
2646
  life: { enter: 200, fade: 600 }
2550
2647
  },
2551
2648
  {
2552
- key: "clock",
2649
+ key: "status",
2553
2650
  count: 1,
2554
- position: { x: 0.06, y: 0.045 },
2651
+ position: { x: 0.965, y: 0.052 },
2555
2652
  sprite: {
2556
2653
  kind: "text",
2557
- strings: ["21:47"],
2558
- color: "#8899aa",
2559
- font: "bold 12px monospace",
2560
- align: "left"
2654
+ strings: ["\u25CF UPLINK LOCKED \xB7 WATCH C"],
2655
+ color: "#46d17a",
2656
+ font: "bold 11px 'SF Mono', monospace",
2657
+ align: "right"
2561
2658
  },
2659
+ pulse: { amp: 0.18, period: 2600 },
2562
2660
  motion: { type: "static" },
2563
2661
  life: { enter: 200, fade: 600 }
2564
2662
  },
2565
- // --- left column: readings (boots second) ---
2663
+ // ---- left column: arrays (boots second) ----
2566
2664
  {
2567
- key: "power-label",
2665
+ key: "dish1",
2568
2666
  count: 1,
2569
- position: { x: 0.06, y: 0.14 },
2667
+ position: { x: 0.035, y: 0.148 },
2570
2668
  sprite: {
2571
2669
  kind: "text",
2572
- strings: ["POWER"],
2573
- color: "#445566",
2574
- font: "9px monospace",
2670
+ strings: ["DSA-1 EAST AZ 214.6\xB0 EL 38.2\xB0"],
2671
+ color: "#9fb2c8",
2672
+ font: "11px 'SF Mono', monospace",
2575
2673
  align: "left",
2576
2674
  baseline: "top"
2577
2675
  },
@@ -2579,267 +2677,245 @@ var DASHBOARD_SPEC = {
2579
2677
  life: { enter: 700, fade: 600 }
2580
2678
  },
2581
2679
  {
2582
- key: "power-value",
2583
- count: 1,
2584
- position: { x: 0.06, y: 0.18 },
2585
- sprite: {
2586
- kind: "text",
2587
- strings: ["3.2 kW"],
2588
- color: "#ffd166",
2589
- font: "bold 26px monospace",
2590
- align: "left",
2591
- baseline: "top"
2592
- },
2680
+ key: "dish1-bars",
2681
+ count: 14,
2682
+ sprite: { kind: "rect", width: [38e-4, 38e-4], aspect: [0.5, 2.6], color: "#2ee6c8" },
2683
+ alpha: [0.3, 0.65],
2684
+ layout: { type: "grid", columns: 14 },
2685
+ region: { x: [0.035, 0.15], y: [0.183, 0.203] },
2686
+ pulse: { amp: 0.42, period: 1500, wave: { wavelength: 0.09, angle: 0 } },
2593
2687
  motion: { type: "static" },
2594
2688
  life: { enter: 700, fade: 600 }
2595
2689
  },
2596
2690
  {
2597
- key: "network-label",
2691
+ key: "dish2",
2598
2692
  count: 1,
2599
- position: { x: 0.06, y: 0.28 },
2693
+ position: { x: 0.035, y: 0.243 },
2600
2694
  sprite: {
2601
2695
  kind: "text",
2602
- strings: ["NETWORK"],
2603
- color: "#445566",
2604
- font: "9px monospace",
2696
+ strings: ["DSA-2 WEST AZ 097.1\xB0 EL 61.7\xB0"],
2697
+ color: "#9fb2c8",
2698
+ font: "11px 'SF Mono', monospace",
2605
2699
  align: "left",
2606
2700
  baseline: "top"
2607
2701
  },
2608
2702
  motion: { type: "static" },
2609
- life: { enter: 700, fade: 600 }
2703
+ life: { enter: 850, fade: 600 }
2610
2704
  },
2611
2705
  {
2612
- key: "network-value",
2613
- count: 1,
2614
- position: { x: 0.06, y: 0.32 },
2615
- sprite: {
2616
- kind: "text",
2617
- strings: ["940 Mb/s"],
2618
- color: "#44aaff",
2619
- font: "bold 26px monospace",
2620
- align: "left",
2621
- baseline: "top"
2622
- },
2706
+ key: "dish2-bars",
2707
+ count: 14,
2708
+ sprite: { kind: "rect", width: [38e-4, 38e-4], aspect: [0.5, 2.2], color: "#44aaff" },
2709
+ alpha: [0.3, 0.6],
2710
+ layout: { type: "grid", columns: 14 },
2711
+ region: { x: [0.035, 0.15], y: [0.278, 0.298] },
2712
+ pulse: { amp: 0.4, period: 1900, wave: { wavelength: 0.11, angle: 0 } },
2623
2713
  motion: { type: "static" },
2624
- life: { enter: 700, fade: 600 }
2714
+ life: { enter: 850, fade: 600 }
2625
2715
  },
2626
2716
  {
2627
- key: "climate-label",
2717
+ key: "dish3",
2628
2718
  count: 1,
2629
- position: { x: 0.06, y: 0.42 },
2719
+ position: { x: 0.035, y: 0.338 },
2630
2720
  sprite: {
2631
2721
  kind: "text",
2632
- strings: ["CLIMATE"],
2633
- color: "#445566",
2634
- font: "9px monospace",
2722
+ strings: ["DSA-3 POLAR AZ 002.4\xB0 EL 12.9\xB0 \u25B3 WIND"],
2723
+ color: "#5d7089",
2724
+ font: "11px 'SF Mono', monospace",
2635
2725
  align: "left",
2636
2726
  baseline: "top"
2637
2727
  },
2638
2728
  motion: { type: "static" },
2639
- life: { enter: 700, fade: 600 }
2729
+ life: { enter: 1e3, fade: 600 }
2640
2730
  },
2641
2731
  {
2642
- key: "climate-value",
2643
- count: 1,
2644
- position: { x: 0.06, y: 0.46 },
2645
- sprite: {
2646
- kind: "text",
2647
- strings: ["21.5\xB0"],
2648
- color: "#44ddcc",
2649
- font: "bold 26px monospace",
2650
- align: "left",
2651
- baseline: "top"
2652
- },
2732
+ key: "dish3-bars",
2733
+ count: 14,
2734
+ sprite: { kind: "rect", width: [38e-4, 38e-4], aspect: [0.4, 1.1], color: "#5d7089" },
2735
+ alpha: [0.2, 0.4],
2736
+ layout: { type: "grid", columns: 14 },
2737
+ region: { x: [0.035, 0.15], y: [0.373, 0.393] },
2738
+ pulse: { amp: 0.3, period: 2600, wave: { wavelength: 0.14, angle: 0 } },
2653
2739
  motion: { type: "static" },
2654
- life: { enter: 700, fade: 600 }
2740
+ life: { enter: 1e3, fade: 600 }
2655
2741
  },
2656
2742
  {
2657
- key: "alerts-label",
2743
+ key: "dish-leds",
2744
+ count: 3,
2745
+ sprite: { kind: "circle", radius: [4e-3, 4e-3], color: "#46d17a" },
2746
+ alpha: [0.9, 0.9],
2747
+ layout: { type: "grid", columns: 1 },
2748
+ region: { x: [0.238, 0.246], y: [0.15, 0.35] },
2749
+ pulse: { amp: 0.25, period: 2100 },
2750
+ motion: { type: "static" },
2751
+ life: { enter: 1e3, fade: 600 }
2752
+ },
2753
+ // carrier + doppler + station env
2754
+ {
2755
+ key: "carrier-wave",
2756
+ count: 30,
2757
+ sprite: { kind: "rect", width: [32e-4, 32e-4], aspect: [0.4, 3.6], color: "#2ee6c8" },
2758
+ alpha: [0.25, 0.6],
2759
+ layout: { type: "grid", columns: 30 },
2760
+ region: { x: [0.035, 0.245], y: [0.47, 0.5] },
2761
+ pulse: { amp: 0.5, period: 2100, wave: { wavelength: 0.16, angle: 0 } },
2762
+ motion: { type: "static" },
2763
+ life: { enter: 1150, fade: 600 }
2764
+ },
2765
+ {
2766
+ key: "doppler",
2658
2767
  count: 1,
2659
- position: { x: 0.06, y: 0.56 },
2768
+ position: { x: 0.035, y: 0.535 },
2660
2769
  sprite: {
2661
2770
  kind: "text",
2662
- strings: ["ALERTS"],
2663
- color: "#445566",
2664
- font: "9px monospace",
2771
+ strings: ["CARRIER X-BAND \xB7 DOPPLER +2.114 kHz \xB7 DRIFT 0.03"],
2772
+ color: "#9fb2c8",
2773
+ font: "10px 'SF Mono', monospace",
2665
2774
  align: "left",
2666
2775
  baseline: "top"
2667
2776
  },
2668
2777
  motion: { type: "static" },
2669
- life: { enter: 700, fade: 600 }
2778
+ life: { enter: 1150, fade: 600 }
2670
2779
  },
2671
2780
  {
2672
- key: "alerts-value",
2781
+ key: "env-line",
2673
2782
  count: 1,
2674
- position: { x: 0.06, y: 0.6 },
2783
+ position: { x: 0.035, y: 0.6 },
2675
2784
  sprite: {
2676
2785
  kind: "text",
2677
- strings: ["0"],
2678
- color: "#44cc66",
2679
- font: "bold 26px monospace",
2786
+ strings: ["STATION \xB7 PWR 28.1 V \xB7 THERM 312 K \xB7 GYRO NOM \xB7 PROP 81%"],
2787
+ color: "#5d7089",
2788
+ font: "10px 'SF Mono', monospace",
2680
2789
  align: "left",
2681
2790
  baseline: "top"
2682
2791
  },
2683
2792
  motion: { type: "static" },
2684
- life: { enter: 700, fade: 600 }
2793
+ life: { enter: 1300, fade: 600 }
2685
2794
  },
2686
- // --- center: energy gauge (boots third) ---
2795
+ // ---- center: the orbital plot (boots third) ----
2687
2796
  {
2688
- key: "gauge-ring",
2797
+ key: "moon",
2689
2798
  count: 1,
2690
- position: { x: 0.4, y: 0.36 },
2691
- sprite: { kind: "ring", radius: [0.105, 0.105], color: "#1e3a52", width: 28e-4 },
2692
- alpha: [0.85, 0.85],
2799
+ position: { x: 0.455, y: 0.4 },
2800
+ sprite: { kind: "circle", radius: [0.028, 0.028], color: "#8fa3bd", soft: true },
2801
+ alpha: [0.9, 0.9],
2693
2802
  motion: { type: "static" },
2694
2803
  life: { enter: 1200, fade: 700 }
2695
2804
  },
2696
2805
  {
2697
- key: "gauge-glow",
2698
- count: 1,
2699
- position: { x: 0.4, y: 0.36 },
2700
- sprite: { kind: "ring", radius: [0.088, 0.088], color: "#2ee6c8", width: 16e-4 },
2806
+ key: "orbit-guides",
2807
+ count: 3,
2808
+ // All three guides pin to the plot centre; the seeded radius range
2809
+ // spreads them into concentric orbits (`position` is count:1-only).
2810
+ region: { x: [0.455, 0.455], y: [0.4, 0.4] },
2811
+ sprite: { kind: "ring", radius: [0.075, 0.185], color: "#1e3a52", width: 11e-4 },
2701
2812
  alpha: [0.55, 0.55],
2702
- blend: "lighter",
2703
- pulse: { amp: 0.25, period: 4e3 },
2704
- grow: { amp: 0.05, period: 4e3 },
2705
2813
  motion: { type: "static" },
2706
2814
  life: { enter: 1200, fade: 700 }
2707
2815
  },
2708
2816
  {
2709
- key: "gauge-label",
2710
- count: 1,
2711
- position: { x: 0.4, y: 0.29 },
2817
+ key: "fleet",
2818
+ count: 5,
2712
2819
  sprite: {
2713
- kind: "text",
2714
- strings: ["ENERGY"],
2715
- color: "#445566",
2716
- font: "9px monospace",
2717
- align: "center"
2718
- },
2719
- motion: { type: "static" },
2720
- life: { enter: 1200, fade: 700 }
2721
- },
2722
- {
2723
- key: "gauge-value",
2724
- count: 1,
2725
- position: { x: 0.4, y: 0.375 },
2726
- sprite: {
2727
- kind: "text",
2728
- strings: ["73%"],
2729
- color: "#2ee6c8",
2730
- font: "bold 34px monospace",
2731
- align: "center"
2732
- },
2733
- motion: { type: "static" },
2734
- life: { enter: 1200, fade: 700 }
2735
- },
2736
- {
2737
- key: "equalizer",
2738
- count: 22,
2739
- sprite: {
2740
- kind: "rect",
2741
- width: [5e-3, 5e-3],
2742
- aspect: [0.6, 3.4],
2820
+ kind: "circle",
2821
+ radius: [45e-4, 65e-4],
2743
2822
  color: "#2ee6c8",
2744
- colors: ["#2ee6c8", "#44aaff", "#aa88ff"],
2745
- colorWeights: [3, 2, 1]
2823
+ colors: ["#2ee6c8", "#44aaff", "#ffd166", "#aa88ff", "#ff9f43"]
2746
2824
  },
2747
- alpha: [0.35, 0.7],
2748
- layout: { type: "grid", columns: 22 },
2749
- region: { x: [0.28, 0.52], y: [0.585, 0.615] },
2750
- // Wave-phased pulse sweeps along the bars — reads as a live meter.
2751
- pulse: { amp: 0.4, period: 1600, wave: { wavelength: 0.1, angle: 0 } },
2752
- motion: { type: "static" },
2753
- life: { enter: 1200, fade: 700 }
2825
+ alpha: [0.85, 0.95],
2826
+ blend: "lighter",
2827
+ motion: { type: "orbit", speed: [4, 11], radius: [0.075, 0.185], center: { x: 0.455, y: 0.4 } },
2828
+ trail: { length: 9, fade: 0.75 },
2829
+ // The relay web: each craft holds a comm chain to its neighbours.
2830
+ links: { k: 2, maxDist: 0.5, color: "#2ee6c8", alpha: 0.2, width: 12e-4, mode: "chain" },
2831
+ life: { enter: 1350, fade: 700 }
2754
2832
  },
2755
- // --- right column: agenda + devices (boots fourth) ---
2756
2833
  {
2757
- key: "agenda-label",
2834
+ key: "fleet-legend",
2758
2835
  count: 1,
2759
- position: { x: 0.64, y: 0.14 },
2836
+ position: { x: 0.455, y: 0.63 },
2760
2837
  sprite: {
2761
2838
  kind: "text",
2762
- strings: ["AGENDA"],
2763
- color: "#445566",
2764
- font: "9px monospace",
2765
- align: "left",
2839
+ strings: ["TRACKING 5 \xB7 KESTREL TIDE-2 LIMELIGHT CATWALK-1 SLIPSTREAM"],
2840
+ color: "#5d7089",
2841
+ font: "9px 'SF Mono', monospace",
2842
+ align: "center",
2766
2843
  baseline: "top"
2767
2844
  },
2768
2845
  motion: { type: "static" },
2769
- life: { enter: 1700, fade: 600 }
2846
+ life: { enter: 1500, fade: 700 }
2770
2847
  },
2848
+ // pass schedule under the plot
2771
2849
  {
2772
- key: "agenda-1",
2850
+ key: "pass-label",
2773
2851
  count: 1,
2774
- position: { x: 0.64, y: 0.19 },
2852
+ position: { x: 0.3, y: 0.69 },
2775
2853
  sprite: {
2776
2854
  kind: "text",
2777
- strings: ["09:00 Standup"],
2778
- color: "#8899aa",
2779
- font: "11px monospace",
2855
+ strings: ["PASS SCHEDULE"],
2856
+ color: "#41536b",
2857
+ font: "9px 'SF Mono', monospace",
2780
2858
  align: "left",
2781
- baseline: "top",
2782
- maxWidth: 0.2593
2859
+ baseline: "top"
2783
2860
  },
2784
2861
  motion: { type: "static" },
2785
- life: { enter: 1700, fade: 600 }
2862
+ life: { enter: 1650, fade: 600 }
2786
2863
  },
2787
2864
  {
2788
- key: "agenda-2",
2865
+ key: "pass-1",
2789
2866
  count: 1,
2790
- position: { x: 0.64, y: 0.23 },
2867
+ position: { x: 0.3, y: 0.72 },
2791
2868
  sprite: {
2792
2869
  kind: "text",
2793
- strings: ["12:30 Lunch w/ Kai"],
2794
- color: "#778899",
2795
- font: "11px monospace",
2870
+ strings: ["04:41 CATWALK-1 ACQUIRE \xB7 DSA-1 \u25B2 12 MIN"],
2871
+ color: "#c8d4e4",
2872
+ font: "11px 'SF Mono', monospace",
2796
2873
  align: "left",
2797
- baseline: "top",
2798
- maxWidth: 0.2593
2874
+ baseline: "top"
2799
2875
  },
2800
2876
  motion: { type: "static" },
2801
- life: { enter: 1700, fade: 600 }
2877
+ life: { enter: 1650, fade: 600 }
2802
2878
  },
2803
2879
  {
2804
- key: "agenda-3",
2880
+ key: "pass-2",
2805
2881
  count: 1,
2806
- position: { x: 0.64, y: 0.27 },
2882
+ position: { x: 0.3, y: 0.757 },
2807
2883
  sprite: {
2808
2884
  kind: "text",
2809
- strings: ["18:00 Gym"],
2810
- color: "#667788",
2811
- font: "11px monospace",
2885
+ strings: ["05:03 TIDE-2 OCCULT EXIT \xB7 DSA-2"],
2886
+ color: "#7f93ab",
2887
+ font: "11px 'SF Mono', monospace",
2812
2888
  align: "left",
2813
- baseline: "top",
2814
- maxWidth: 0.2593
2889
+ baseline: "top"
2815
2890
  },
2816
2891
  motion: { type: "static" },
2817
- life: { enter: 1700, fade: 600 }
2892
+ life: { enter: 1750, fade: 600 }
2818
2893
  },
2819
2894
  {
2820
- key: "devices-label",
2895
+ key: "pass-3",
2821
2896
  count: 1,
2822
- position: { x: 0.64, y: 0.36 },
2897
+ position: { x: 0.3, y: 0.794 },
2823
2898
  sprite: {
2824
2899
  kind: "text",
2825
- strings: ["DEVICES"],
2826
- color: "#445566",
2827
- font: "9px monospace",
2900
+ strings: ["05:47 KESTREL RANGING \xB7 DSA-1+2"],
2901
+ color: "#7f93ab",
2902
+ font: "11px 'SF Mono', monospace",
2828
2903
  align: "left",
2829
2904
  baseline: "top"
2830
2905
  },
2831
2906
  motion: { type: "static" },
2832
- life: { enter: 1700, fade: 600 }
2907
+ life: { enter: 1850, fade: 600 }
2833
2908
  },
2909
+ // ---- right column: telemetry (boots fourth) ----
2834
2910
  {
2835
- key: "devices-1",
2911
+ key: "signal-label",
2836
2912
  count: 1,
2837
- position: { x: 0.64, y: 0.41 },
2913
+ position: { x: 0.665, y: 0.115 },
2838
2914
  sprite: {
2839
2915
  kind: "text",
2840
- strings: ["Lights 6 on"],
2841
- color: "#8899aa",
2842
- font: "11px monospace",
2916
+ strings: ["PRIMARY CARRIER \xB7 CATWALK-1"],
2917
+ color: "#41536b",
2918
+ font: "9px 'SF Mono', monospace",
2843
2919
  align: "left",
2844
2920
  baseline: "top"
2845
2921
  },
@@ -2847,14 +2923,14 @@ var DASHBOARD_SPEC = {
2847
2923
  life: { enter: 1700, fade: 600 }
2848
2924
  },
2849
2925
  {
2850
- key: "devices-2",
2926
+ key: "signal-value",
2851
2927
  count: 1,
2852
- position: { x: 0.64, y: 0.45 },
2928
+ position: { x: 0.665, y: 0.14 },
2853
2929
  sprite: {
2854
2930
  kind: "text",
2855
- strings: ["Thermostat eco"],
2856
- color: "#778899",
2857
- font: "11px monospace",
2931
+ strings: ["-112.4 dBm"],
2932
+ color: "#ffd166",
2933
+ font: "bold 30px 'SF Mono', monospace",
2858
2934
  align: "left",
2859
2935
  baseline: "top"
2860
2936
  },
@@ -2862,130 +2938,130 @@ var DASHBOARD_SPEC = {
2862
2938
  life: { enter: 1700, fade: 600 }
2863
2939
  },
2864
2940
  {
2865
- key: "devices-3",
2941
+ key: "telemetry-row",
2866
2942
  count: 1,
2867
- position: { x: 0.64, y: 0.49 },
2943
+ position: { x: 0.665, y: 0.215 },
2868
2944
  sprite: {
2869
2945
  kind: "text",
2870
- strings: ["Locks secured"],
2871
- color: "#667788",
2872
- font: "11px monospace",
2946
+ strings: ["RANGE 1.28 LS DOWNLINK 2.048 Mb/s BUFFER 38%"],
2947
+ color: "#9fb2c8",
2948
+ font: "11px 'SF Mono', monospace",
2873
2949
  align: "left",
2874
2950
  baseline: "top"
2875
2951
  },
2876
2952
  motion: { type: "static" },
2877
- life: { enter: 1700, fade: 600 }
2953
+ life: { enter: 1800, fade: 600 }
2878
2954
  },
2955
+ // event log
2879
2956
  {
2880
- key: "devices-4",
2957
+ key: "feed-1",
2881
2958
  count: 1,
2882
- position: { x: 0.64, y: 0.53 },
2959
+ position: { x: 0.665, y: 0.3 },
2883
2960
  sprite: {
2884
2961
  kind: "text",
2885
- strings: ["Media paused"],
2886
- color: "#556677",
2887
- font: "11px monospace",
2962
+ strings: ["04:15:02 DSA-1 LOCK RE-ACQUIRED"],
2963
+ color: "#9fb2c8",
2964
+ font: "10px 'SF Mono', monospace",
2888
2965
  align: "left",
2889
2966
  baseline: "top"
2890
2967
  },
2891
2968
  motion: { type: "static" },
2892
- life: { enter: 1700, fade: 600 }
2969
+ life: { enter: 1950, fade: 600 }
2893
2970
  },
2894
- // --- bottom: activity feed (boots last) ---
2895
2971
  {
2896
- key: "feed-label",
2972
+ key: "feed-2",
2897
2973
  count: 1,
2898
- position: { x: 0.06, y: 0.74 },
2974
+ position: { x: 0.665, y: 0.334 },
2899
2975
  sprite: {
2900
2976
  kind: "text",
2901
- strings: ["ACTIVITY"],
2902
- color: "#445566",
2903
- font: "9px monospace",
2977
+ strings: ["04:12:47 CATWALK-1 WHEEL DESAT OK"],
2978
+ color: "#7f93ab",
2979
+ font: "10px 'SF Mono', monospace",
2904
2980
  align: "left",
2905
2981
  baseline: "top"
2906
2982
  },
2907
2983
  motion: { type: "static" },
2908
- life: { enter: 2200, fade: 600 }
2984
+ life: { enter: 2050, fade: 600 }
2909
2985
  },
2910
2986
  {
2911
- key: "feed-1",
2987
+ key: "feed-3",
2912
2988
  count: 1,
2913
- position: { x: 0.06, y: 0.78 },
2989
+ position: { x: 0.665, y: 0.368 },
2914
2990
  sprite: {
2915
2991
  kind: "text",
2916
- strings: ["21:42 Front door locked"],
2917
- color: "#7a8a7a",
2918
- font: "10px monospace",
2992
+ strings: ["04:09:31 RANGING RESIDUAL 0.4 m"],
2993
+ color: "#5d7089",
2994
+ font: "10px 'SF Mono', monospace",
2919
2995
  align: "left",
2920
- baseline: "top",
2921
- maxWidth: 0.5556
2996
+ baseline: "top"
2922
2997
  },
2923
2998
  motion: { type: "static" },
2924
- life: { enter: 2200, fade: 600 }
2999
+ life: { enter: 2150, fade: 600 }
2925
3000
  },
3001
+ // alert + relay load
2926
3002
  {
2927
- key: "feed-2",
3003
+ key: "alert",
2928
3004
  count: 1,
2929
- position: { x: 0.06, y: 0.82 },
3005
+ position: { x: 0.665, y: 0.49 },
2930
3006
  sprite: {
2931
3007
  kind: "text",
2932
- strings: ["21:36 Thermostat \u2192 eco"],
2933
- color: "#6a7a6a",
2934
- font: "10px monospace",
3008
+ strings: ["\u25B3 SOLAR FLUX ELEVATED \u2014 MONITORING"],
3009
+ color: "#ff9f43",
3010
+ font: "bold 11px 'SF Mono', monospace",
2935
3011
  align: "left",
2936
- baseline: "top",
2937
- maxWidth: 0.5556
3012
+ baseline: "top"
2938
3013
  },
3014
+ pulse: { amp: 0.22, period: 3400 },
2939
3015
  motion: { type: "static" },
2940
- life: { enter: 2200, fade: 600 }
3016
+ life: { enter: 2450, fade: 600 }
2941
3017
  },
2942
3018
  {
2943
- key: "feed-3",
2944
- count: 1,
2945
- position: { x: 0.06, y: 0.86 },
3019
+ key: "load-bars",
3020
+ count: 24,
2946
3021
  sprite: {
2947
- kind: "text",
2948
- strings: ["21:14 Backup completed"],
2949
- color: "#5a6a5a",
2950
- font: "10px monospace",
2951
- align: "left",
2952
- baseline: "top",
2953
- maxWidth: 0.5556
3022
+ kind: "rect",
3023
+ width: [42e-4, 42e-4],
3024
+ aspect: [0.5, 3.2],
3025
+ color: "#aa88ff",
3026
+ colors: ["#aa88ff", "#44aaff", "#2ee6c8"],
3027
+ colorWeights: [2, 2, 3]
2954
3028
  },
3029
+ alpha: [0.3, 0.65],
3030
+ layout: { type: "grid", columns: 24 },
3031
+ region: { x: [0.665, 0.965], y: [0.58, 0.615] },
3032
+ pulse: { amp: 0.45, period: 1800, wave: { wavelength: 0.12, angle: 0 } },
2955
3033
  motion: { type: "static" },
2956
- life: { enter: 2200, fade: 600 }
3034
+ life: { enter: 2550, fade: 600 }
2957
3035
  },
2958
3036
  {
2959
- key: "feed-4",
3037
+ key: "load-note",
2960
3038
  count: 1,
2961
- position: { x: 0.06, y: 0.9 },
3039
+ position: { x: 0.665, y: 0.65 },
2962
3040
  sprite: {
2963
3041
  kind: "text",
2964
- strings: ["20:58 Motion \xB7 driveway"],
2965
- color: "#4d5e4d",
2966
- font: "10px monospace",
3042
+ strings: ["RELAY LOAD \xB7 QUEUE 14 FRAMES \xB7 NOMINAL"],
3043
+ color: "#5d7089",
3044
+ font: "10px 'SF Mono', monospace",
2967
3045
  align: "left",
2968
- baseline: "top",
2969
- maxWidth: 0.5556
3046
+ baseline: "top"
2970
3047
  },
2971
3048
  motion: { type: "static" },
2972
- life: { enter: 2200, fade: 600 }
3049
+ life: { enter: 2550, fade: 600 }
2973
3050
  },
3051
+ // ---- footer band ----
2974
3052
  {
2975
- key: "feed-5",
3053
+ key: "footer",
2976
3054
  count: 1,
2977
- position: { x: 0.06, y: 0.94 },
3055
+ position: { x: 0.035, y: 0.95 },
2978
3056
  sprite: {
2979
3057
  kind: "text",
2980
- strings: ["20:41 Sunset scene on"],
2981
- color: "#3d4e3d",
2982
- font: "10px monospace",
2983
- align: "left",
2984
- baseline: "top",
2985
- maxWidth: 0.5556
3058
+ strings: ["NET STATE GREEN \xB7 3 ARRAYS \xB7 5 CARRIERS \xB7 DSN HANDOVER 23:34 \xB7 NEXT WINDOW 04:41:00"],
3059
+ color: "#41536b",
3060
+ font: "9px 'SF Mono', monospace",
3061
+ align: "left"
2986
3062
  },
2987
3063
  motion: { type: "static" },
2988
- life: { enter: 2200, fade: 600 }
3064
+ life: { enter: 2750, fade: 600 }
2989
3065
  }
2990
3066
  ]
2991
3067
  };
@@ -3123,6 +3199,120 @@ var MATRIX_RAIN_SPEC = {
3123
3199
  ]
3124
3200
  };
3125
3201
 
3202
+ // src/examples/nostalghia-candle.ts
3203
+ var NOSTALGHIA_CANDLE_SPEC = {
3204
+ schemaVersion: 1,
3205
+ id: "nostalghia-candle",
3206
+ label: "Nostalghia's Candle",
3207
+ seed: 1983,
3208
+ motionIntensity: "calm",
3209
+ background: {
3210
+ type: "gradient",
3211
+ stops: [
3212
+ { at: 0, color: "#050302" },
3213
+ { at: 0.55, color: "#0a0704" },
3214
+ { at: 1, color: "#120c08" }
3215
+ ]
3216
+ },
3217
+ layers: [
3218
+ {
3219
+ key: "halo",
3220
+ count: 1,
3221
+ position: { x: 0.5, y: 0.52 },
3222
+ sprite: {
3223
+ kind: "circle",
3224
+ radius: [0.11, 0.11],
3225
+ color: "#c4782a",
3226
+ soft: true
3227
+ },
3228
+ // Base 0.04 + amp 0.02 → breathes ~0.02..0.06; never clamps to 0.
3229
+ // (pulse.amp is ADDED to alpha, not multiplied.) Kept subordinate to the flame.
3230
+ alpha: [0.04, 0.04],
3231
+ blend: "lighter",
3232
+ pulse: { amp: 0.02, period: 1e4 },
3233
+ grow: { amp: 0.08, period: 1e4 },
3234
+ motion: { type: "static" }
3235
+ },
3236
+ {
3237
+ key: "smoke",
3238
+ count: 3,
3239
+ sprite: {
3240
+ kind: "circle",
3241
+ radius: [0.01, 0.022],
3242
+ color: "#6a6258",
3243
+ soft: true
3244
+ },
3245
+ alpha: [0.02, 0.04],
3246
+ blend: "lighter",
3247
+ region: { x: [0.46, 0.54], y: [0.36, 0.48] },
3248
+ pulse: { amp: 0.015, period: 4500 },
3249
+ motion: { type: "rise", speed: [2e-3, 4e-3], sway: 8e-3 }
3250
+ },
3251
+ {
3252
+ key: "flame-glow",
3253
+ count: 1,
3254
+ sprite: {
3255
+ kind: "circle",
3256
+ radius: [0.028, 0.028],
3257
+ color: "#ffb040",
3258
+ soft: true
3259
+ },
3260
+ alpha: [0.32, 0.32],
3261
+ blend: "lighter",
3262
+ pulse: { amp: 0.12, period: 1400 },
3263
+ grow: { amp: 0.4, period: 1600 },
3264
+ // Tiny orbit = positional lean without drifting off-center (wander has vx).
3265
+ motion: {
3266
+ type: "orbit",
3267
+ speed: [18, 18],
3268
+ radius: [3e-3, 3e-3],
3269
+ center: { x: 0.5, y: 0.52 }
3270
+ }
3271
+ },
3272
+ {
3273
+ key: "flame-core",
3274
+ count: 1,
3275
+ sprite: {
3276
+ kind: "circle",
3277
+ radius: [0.014, 0.014],
3278
+ color: "#fff6e0",
3279
+ soft: true
3280
+ },
3281
+ alpha: [0.95, 0.95],
3282
+ blend: "lighter",
3283
+ pulse: { amp: 0.25, period: 900 },
3284
+ grow: { amp: 0.55, period: 1100 },
3285
+ motion: {
3286
+ type: "orbit",
3287
+ speed: [22, 22],
3288
+ radius: [25e-4, 25e-4],
3289
+ center: { x: 0.5, y: 0.52 }
3290
+ }
3291
+ },
3292
+ {
3293
+ key: "embers",
3294
+ count: 7,
3295
+ sprite: {
3296
+ kind: "circle",
3297
+ radius: [18e-4, 4e-3],
3298
+ color: "#e08a34",
3299
+ soft: true
3300
+ },
3301
+ alpha: [0.4, 0.8],
3302
+ blend: "lighter",
3303
+ region: { x: [0.35, 0.65], y: [0.4, 0.7] },
3304
+ pulse: { amp: 0.2, period: 2200 },
3305
+ trail: { length: 2200, fade: 1 },
3306
+ motion: {
3307
+ type: "drift",
3308
+ speed: [4e-3, 0.012],
3309
+ angle: 270,
3310
+ bob: 8e-3
3311
+ }
3312
+ }
3313
+ ]
3314
+ };
3315
+
3126
3316
  // src/examples/polygons.ts
3127
3317
  var POLYGONS_SPEC = {
3128
3318
  schemaVersion: 1,
@@ -3568,7 +3758,8 @@ var SCHEMA_EXAMPLES = [
3568
3758
  { id: "warp-tunnel", label: "Warp Tunnel", spec: WARP_TUNNEL_SPEC },
3569
3759
  { id: "polygons", label: "Polygons", spec: POLYGONS_SPEC },
3570
3760
  { id: "matrix-rain", label: "Matrix Rain", spec: MATRIX_RAIN_SPEC },
3571
- { id: "procession", label: "Night Procession", spec: PROCESSION_SPEC }
3761
+ { id: "procession", label: "Night Procession", spec: PROCESSION_SPEC },
3762
+ { id: "nostalghia-candle", label: "Nostalghia's Candle", spec: NOSTALGHIA_CANDLE_SPEC }
3572
3763
  ];
3573
3764
  var EXAMPLE_SPECS = SCHEMA_EXAMPLES.map((e) => e.spec);
3574
3765
  var EXAMPLE_BY_ID = Object.fromEntries(
@@ -3585,6 +3776,7 @@ export {
3585
3776
  LANTERNS_SPEC,
3586
3777
  LIMITS,
3587
3778
  MATRIX_RAIN_SPEC,
3779
+ NOSTALGHIA_CANDLE_SPEC,
3588
3780
  ORRERY_SPEC,
3589
3781
  POLYGONS_SPEC,
3590
3782
  PROCESSION_SPEC,
@@ -3616,6 +3808,7 @@ export {
3616
3808
  perceiveScene,
3617
3809
  positionAt,
3618
3810
  renderBrailleMap,
3811
+ renderDensityMap,
3619
3812
  resolveSpecPath,
3620
3813
  rotationAt,
3621
3814
  sizeAt,
@@ -3623,6 +3816,7 @@ export {
3623
3816
  spriteVariants,
3624
3817
  steerablePaths,
3625
3818
  structuralSignature,
3819
+ textSprites,
3626
3820
  validateSpec
3627
3821
  };
3628
3822
  //# sourceMappingURL=index.js.map