@glowbox/led-grid 1.0.0-rc.2
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/LICENSE +21 -0
- package/README.md +111 -0
- package/dist/color.d.ts +9 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +577 -0
- package/dist/index.js.map +1 -0
- package/dist/led-display.d.ts +142 -0
- package/dist/renderer.d.ts +50 -0
- package/dist/voxel-grid.d.ts +47 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eetu Sutinen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# @glowbox/led-grid
|
|
2
|
+
|
|
3
|
+
Framework-agnostic **3D LED-grid display** — an nx×ny×nz lattice of glowing
|
|
4
|
+
"LEDs" you draw on like a tiny 3D canvas, rendered in WebGL and orbitable
|
|
5
|
+
(auto-spin + drag + zoom). Zero dependencies.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
yarn add @glowbox/led-grid
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createLedDisplay } from '@glowbox/led-grid';
|
|
13
|
+
|
|
14
|
+
const d = createLedDisplay(canvas, {
|
|
15
|
+
size: [8, 8, 8],
|
|
16
|
+
led: { glow: 3, offColor: '#0a0a12' },
|
|
17
|
+
color: { background: '#000', gain: 1.1 },
|
|
18
|
+
camera: { autoOrbit: true },
|
|
19
|
+
interaction: { zoom: true }
|
|
20
|
+
});
|
|
21
|
+
d?.onFrame((d, dt) => {
|
|
22
|
+
d.clear();
|
|
23
|
+
d.sphere([4, 4, 4], 3, '#00aaff'); // filled=false → a hollow shell
|
|
24
|
+
d.plot(2, 5, 1, [1.4, 0.8, 0]); // array form; >1 blooms
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The display owns the WebGL render, orbit, zoom, resize and the animation loop —
|
|
29
|
+
you just write voxels each frame. It holds **no** content of its own.
|
|
30
|
+
|
|
31
|
+
## Colours
|
|
32
|
+
|
|
33
|
+
Everywhere a colour is accepted (`background`, `offColor`, `tint`, and every draw
|
|
34
|
+
call) the type is `Color`:
|
|
35
|
+
|
|
36
|
+
- `[r, g, b]` numbers in `0..1` — values `>1` are **extra-bright** (more bloom / HDR headroom).
|
|
37
|
+
- any CSS string — `'#0a0a12'`, `'#f80'`, `'rgb(0 128 255)'`, `'tomato'`, `'hsl(…)'`.
|
|
38
|
+
(Strings are `0..255` → `0..1`, so only the array form can exceed 1.)
|
|
39
|
+
|
|
40
|
+
**Unlit LEDs** draw nothing by default (`offColor` is black) — only lit LEDs show,
|
|
41
|
+
so the display reads as floating light with no dark lattice. To hint the physical
|
|
42
|
+
grid, set `offColor` to a faint colour: it renders a tiny speck (`led.offSize`,
|
|
43
|
+
default 0.35 of the glow) at each off node, the size of a real LED package.
|
|
44
|
+
|
|
45
|
+
## Rendering (`led.style`)
|
|
46
|
+
|
|
47
|
+
Two looks, both reading on **any** background (dark or light):
|
|
48
|
+
|
|
49
|
+
- **`hologram`** _(default)_ — LEDs as real emitters: each lit LED is a bright point
|
|
50
|
+
with a soft glow **halo**, tone-mapped and composited _in front of_ the background.
|
|
51
|
+
Internally an HDR (half-float) bloom pipeline (emissive → blur → ACES tone-map →
|
|
52
|
+
over-composite), falling back to an LDR over-composite when the half-float WebGL
|
|
53
|
+
extensions are unavailable.
|
|
54
|
+
- **`comic`** — flat **cel-shaded** pop-art: each lit LED is a solid disc (or square)
|
|
55
|
+
with an optional bold ink outline — a comic-book / Ben-Day look. Brightness is
|
|
56
|
+
posterized into a few bands (keeping each LED's hue **and** relative brightness), so
|
|
57
|
+
tonal content — images, fading trails — reads faithfully. Set `led.vivid` for the
|
|
58
|
+
punchy flat variant instead: every lit LED at the **full value** of its hue (dim
|
|
59
|
+
voxels read vivid) — great for bold graphic content. Opaque and depth-tested; pairs
|
|
60
|
+
well with a light "paper" background.
|
|
61
|
+
|
|
62
|
+
`glow`/`offColor`/`offSize` apply to the hologram style; `outline`/`outlineColor` to
|
|
63
|
+
`comic`. `shape: 'square'` makes LEDs tile gap-free (no bg between them); `'round'` is
|
|
64
|
+
the dotty LED-grid look.
|
|
65
|
+
|
|
66
|
+
**Performance.** Only **lit** voxels are packed and drawn each frame, so cost scales
|
|
67
|
+
with how many LEDs are _on_, not with the grid volume — a thin shape in a huge grid
|
|
68
|
+
stays cheap. (Setting `offColor` to a non-black lattice necessarily draws every node,
|
|
69
|
+
so leave it black for big sparse grids.) The `display.stats` object exposes live
|
|
70
|
+
`fps` / `frameMs` / `drawMs` / `renderMs` for profiling.
|
|
71
|
+
|
|
72
|
+
## Voxel API
|
|
73
|
+
|
|
74
|
+
`plot / add / get / clear / fill / line / box / sphere`, plus `index` / `inBounds`
|
|
75
|
+
and the raw `leds` `Float32Array`. `sphere`/`box` take a `filled` flag (shell/edges
|
|
76
|
+
otherwise). Draw voxels headlessly with `createVoxelGrid(nx, ny, nz)` — same API,
|
|
77
|
+
no canvas.
|
|
78
|
+
|
|
79
|
+
## Options (all optional except `size`)
|
|
80
|
+
|
|
81
|
+
| group | fields (defaults) |
|
|
82
|
+
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
83
|
+
| `led` | `style` `'hologram'\|'comic'` · `shape` `'round'\|'square'` · `stagger` (false; brick lattice — offset every other row half a cell, also cuts moiré) · `rgb` (false; render each LED as R/G/B sub-emitters) · `rgbLayout` (`auto`\|`triad`\|`quad`\|`stripe`; sub-die packing) · `vivid` (false; comic: flat full-value pop-art vs cel-shade) · `outline` (0.25; comic ink border, 0 = off) · `outlineColor` (dark) · `size` (0.6) · `glow` (2.2) · `offColor` (black = no lattice; set faint to hint the grid) · `offSize` (0.35) |
|
|
84
|
+
| `color` | `background` (`[.01,.01,.02]`) · `gain` (1) · `tint` (`[1,1,1]`) |
|
|
85
|
+
| `camera` | `yaw`/`pitch` (.6/.4) · `distance` (3.6) · `fov` (.9) · `projection` `'perspective'\|'orthographic'` · `autoOrbit` (true) · `orbitSpeed` (.45) · `pitchLimits` (±1.4) |
|
|
86
|
+
| `interaction` | `drag` (true) · `dragSpeed` (.01) · `zoom` (false) · `zoomLimits` (`[1.5,10]`) |
|
|
87
|
+
| `quality` | `pixelRatio` (2) · `antialias` (true) · `paused` (false) · `fps` (uncapped; cap the loop — see below) |
|
|
88
|
+
|
|
89
|
+
## Display methods
|
|
90
|
+
|
|
91
|
+
`onFrame(cb) → stop()`, `render()`, `setGain(g)`, `setPaused(b)` (freeze → render
|
|
92
|
+
on demand), `setOptions(patch)` (live-update anything but `size`), `setCamera({yaw,
|
|
93
|
+
pitch,distance})`, `resize(size?)`, `snapshot(): string` (PNG data URL — for previews /
|
|
94
|
+
LED-cube frame export), `dispose()`.
|
|
95
|
+
|
|
96
|
+
**Frame-rate cap.** `quality.fps` limits the render loop (e.g. `{ quality: { fps: 30 } }`);
|
|
97
|
+
omit it for the display's native refresh rate. It's a **power / cadence** knob — lower it
|
|
98
|
+
for an always-on ambient display to cut GPU/battery, or match a hardware LED-cube's
|
|
99
|
+
refresh rate. It is _not_ a speed-up: each frame still costs the same, so it won't rescue a
|
|
100
|
+
GPU-bound scene (watch `display.stats.fps` for the actual rate).
|
|
101
|
+
|
|
102
|
+
`resize()` recomputes the drawing buffer from the canvas box; `resize([nx, ny, nz])`
|
|
103
|
+
also **changes the grid size in place** — reallocating on the same canvas (no context
|
|
104
|
+
loss), preserving camera + options. The display also **recovers from WebGL context
|
|
105
|
+
loss** on its own (tab backgrounded, driver reset): it rebuilds the renderer on restore
|
|
106
|
+
and repaints your content.
|
|
107
|
+
|
|
108
|
+
Framework wrappers build on this core — see **[@glowbox/svelte](../svelte)**,
|
|
109
|
+
**[@glowbox/react](../react)** and **[@glowbox/vue](../vue)**; content helpers (GIF /
|
|
110
|
+
image / text) live in **[@glowbox/extras](../extras)**. Live demos:
|
|
111
|
+
<https://eetu.github.io/glowbox/>.
|
package/dist/color.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type RGB = [number, number, number];
|
|
2
|
+
export type Vec3 = [number, number, number];
|
|
3
|
+
export type Color = RGB | string;
|
|
4
|
+
/** Normalise a `Color` to an `RGB` triple. Arrays pass through untouched (so bloom
|
|
5
|
+
* values >1 survive); strings are parsed (and memoised). */
|
|
6
|
+
export declare function parseColor(c: Color): RGB;
|
|
7
|
+
/** Like parseColor but clamps to 0..1 — for config colours (background/off) where
|
|
8
|
+
* bloom is meaningless and out-of-range would misbehave in the shader. */
|
|
9
|
+
export declare function parseColor01(c: Color): RGB;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { VoxelGrid as PublicVoxelGrid } from './voxel-grid';
|
|
2
|
+
export type { CameraOptions, ColorOptions, DisplayStats, InteractionOptions, LedDisplay, LedDisplayOptions, LedDisplayPatch, LedOptions, LedShape, LedStyle, Projection, QualityOptions, RGB, RgbLayout, Vec3, VoxelGrid } from './led-display';
|
|
3
|
+
export { createLedDisplay } from './led-display';
|
|
4
|
+
export { type Color, parseColor } from './color';
|
|
5
|
+
export declare const createVoxelGrid: (nx: number, ny: number, nz: number, leds?: Float32Array) => PublicVoxelGrid;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
//#region src/color.ts
|
|
2
|
+
var e = /* @__PURE__ */ new Map(), t, n = (e) => e < 0 ? 0 : e > 1 ? 1 : e;
|
|
3
|
+
function r(e) {
|
|
4
|
+
let t = /^#([0-9a-f]{3,8})$/i.exec(e);
|
|
5
|
+
if (!t) return null;
|
|
6
|
+
let n = t[1];
|
|
7
|
+
return n.length === 3 || n.length === 4 ? [
|
|
8
|
+
parseInt(n[0] + n[0], 16) / 255,
|
|
9
|
+
parseInt(n[1] + n[1], 16) / 255,
|
|
10
|
+
parseInt(n[2] + n[2], 16) / 255
|
|
11
|
+
] : n.length === 6 || n.length === 8 ? [
|
|
12
|
+
parseInt(n.slice(0, 2), 16) / 255,
|
|
13
|
+
parseInt(n.slice(2, 4), 16) / 255,
|
|
14
|
+
parseInt(n.slice(4, 6), 16) / 255
|
|
15
|
+
] : null;
|
|
16
|
+
}
|
|
17
|
+
function i(e) {
|
|
18
|
+
if (t === void 0) {
|
|
19
|
+
let e = typeof document < "u" ? document.createElement("canvas") : null;
|
|
20
|
+
t = e ? e.getContext("2d") : null;
|
|
21
|
+
}
|
|
22
|
+
if (!t) return null;
|
|
23
|
+
t.fillStyle = "#000", t.fillStyle = e;
|
|
24
|
+
let n = t.fillStyle;
|
|
25
|
+
if (t.fillStyle = "#fff", t.fillStyle = e, n !== t.fillStyle) return null;
|
|
26
|
+
let i = r(n);
|
|
27
|
+
if (i) return i;
|
|
28
|
+
let a = /rgba?\(([^)]+)\)/i.exec(n);
|
|
29
|
+
if (a) {
|
|
30
|
+
let e = a[1].split(",").map((e) => parseFloat(e));
|
|
31
|
+
return [
|
|
32
|
+
(e[0] || 0) / 255,
|
|
33
|
+
(e[1] || 0) / 255,
|
|
34
|
+
(e[2] || 0) / 255
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function a(t) {
|
|
40
|
+
let n = t.trim(), a = e.get(n);
|
|
41
|
+
if (a) return a;
|
|
42
|
+
let o = r(n) ?? i(n) ?? [
|
|
43
|
+
0,
|
|
44
|
+
0,
|
|
45
|
+
0
|
|
46
|
+
];
|
|
47
|
+
return e.set(n, o), o;
|
|
48
|
+
}
|
|
49
|
+
function o(e) {
|
|
50
|
+
return typeof e == "string" ? a(e) : e;
|
|
51
|
+
}
|
|
52
|
+
function s(e) {
|
|
53
|
+
let [t, r, i] = o(e);
|
|
54
|
+
return [
|
|
55
|
+
n(t),
|
|
56
|
+
n(r),
|
|
57
|
+
n(i)
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/renderer.ts
|
|
62
|
+
var c = () => /* @__PURE__ */ new Float32Array(16);
|
|
63
|
+
function l(e) {
|
|
64
|
+
return e.fill(0), e[0] = e[5] = e[10] = e[15] = 1, e;
|
|
65
|
+
}
|
|
66
|
+
function u(e, t, n) {
|
|
67
|
+
for (let r = 0; r < 4; r++) for (let i = 0; i < 4; i++) {
|
|
68
|
+
let a = 0;
|
|
69
|
+
for (let e = 0; e < 4; e++) a += t[e * 4 + i] * n[r * 4 + e];
|
|
70
|
+
e[r * 4 + i] = a;
|
|
71
|
+
}
|
|
72
|
+
return e;
|
|
73
|
+
}
|
|
74
|
+
function d(e, t, n, r, i) {
|
|
75
|
+
let a = 1 / Math.tan(t / 2);
|
|
76
|
+
return e.fill(0), e[0] = a / n, e[5] = a, e[10] = (i + r) / (r - i), e[11] = -1, e[14] = 2 * i * r / (r - i), e;
|
|
77
|
+
}
|
|
78
|
+
function f(e, t, n, r, i) {
|
|
79
|
+
let a = t * n;
|
|
80
|
+
return e.fill(0), e[0] = 1 / a, e[5] = 1 / t, e[10] = -2 / (i - r), e[14] = -(i + r) / (i - r), e[15] = 1, e;
|
|
81
|
+
}
|
|
82
|
+
function p(e, t, n, r) {
|
|
83
|
+
let i = Math.cos(t), a = Math.sin(t), o = Math.cos(n), s = Math.sin(n), d = c();
|
|
84
|
+
l(d), d[0] = i, d[2] = -a, d[8] = a, d[10] = i;
|
|
85
|
+
let f = c();
|
|
86
|
+
l(f), f[5] = o, f[6] = s, f[9] = -s, f[10] = o;
|
|
87
|
+
let p = c();
|
|
88
|
+
u(p, f, d), l(e), e[14] = -r;
|
|
89
|
+
let m = c();
|
|
90
|
+
return u(m, e, p), e.set(m), e;
|
|
91
|
+
}
|
|
92
|
+
var m = "\n attribute vec3 aPos;\n attribute vec3 aColor;\n uniform mat4 uMVP;\n uniform float uPointScale;\n varying vec3 vColor;\n void main() {\n vColor = aColor;\n vec4 p = uMVP * vec4(aPos, 1.0);\n gl_Position = p;\n gl_PointSize = clamp(uPointScale / p.w, 2.0, 64.0); // nearer LEDs bigger → depth cue\n }\n", h = "\n precision highp float;\n uniform float uGain;\n uniform float uGlow;\n uniform float uDotSize;\n uniform float uOutline; // comic ink-border thickness (0 = none)\n uniform vec3 uOff;\n uniform vec3 uTint;\n uniform vec3 uOutlineColor;\n uniform int uStyle; // 0 = hologram (emissive), 1 = comic (opaque, cel/vivid)\n uniform int uShape; // 0 = round, 1 = square\n uniform int uVivid; // comic: 0 = cel-shade (keep brightness), 1 = flat vivid (full value)\n uniform int uRgb; // 1 = split each LED into R/G/B sub-emitters (real RGB-LED look)\n uniform int uRgbLayout; // 0 = triad, 1 = quad (RGGB), 2 = stripe\n varying vec3 vColor;\n void main() {\n vec2 q = gl_PointCoord * 2.0 - 1.0; // sprite coords [-1,1]\n\n // RGB mode: the pixel is a real RGB-LED module — separate single-colour dies whose\n // channels are DIVIDED across them, grouped inside ONE led's footprint so it reads\n // as one blended led at a distance but resolves into its dies up close. Three\n // packings, each matched to a footprint (see RgbLayout): a delta TRIAD (round),\n // an RGGB QUAD (square, ~full fill + double green), or the classic R|G|B STRIPE.\n if (uRgb == 1) {\n float cR, cG, cB; // per-channel coverage 0..1\n if (uRgbLayout == 1) {\n // QUAD (RGGB): 2×2 square sub-cells, two greens (eye is most green-sensitive).\n float h = 0.5;\n cR = smoothstep(1.0, 0.78, max(abs(q.x + 0.5), abs(q.y - 0.5)) / h);\n cG = max(smoothstep(1.0, 0.78, max(abs(q.x - 0.5), abs(q.y - 0.5)) / h),\n smoothstep(1.0, 0.78, max(abs(q.x + 0.5), abs(q.y + 0.5)) / h));\n cB = smoothstep(1.0, 0.78, max(abs(q.x - 0.5), abs(q.y + 0.5)) / h);\n } else if (uRgbLayout == 2) {\n // STRIPE: three full-height R|G|B bars (retro LCD look).\n cR = smoothstep(0.33, 0.02, abs(q.x + 0.66));\n cG = smoothstep(0.33, 0.02, abs(q.x));\n cB = smoothstep(0.33, 0.02, abs(q.x - 0.66));\n } else {\n // TRIAD: three round dies in a tight, overlapping triangle (round default).\n float s = 1.5;\n cR = smoothstep(1.0, 0.0, length(q - vec2(0.0, 0.28)) * s);\n cG = smoothstep(1.0, 0.0, length(q - vec2(-0.24, -0.16)) * s);\n cB = smoothstep(1.0, 0.0, length(q - vec2(0.24, -0.16)) * s);\n }\n if (uStyle == 1) {\n // Comic dies: vivid (full value) or cel-shaded (quantized), hue kept; gaps drop.\n vec3 base = vColor * uTint;\n float lum = max(max(base.r, base.g), base.b);\n if (lum < 0.02) discard;\n float scale = uVivid == 1 ? (1.0 / lum) : (ceil(min(lum, 1.0) * 4.0) / 4.0 / lum);\n vec3 full = min(base * scale * uGain, 1.0);\n vec3 col = vec3(full.r * step(0.5, cR), full.g * step(0.5, cG), full.b * step(0.5, cB));\n if (max(max(col.r, col.g), col.b) < 0.02) discard; // outside / all-off → gap\n gl_FragColor = vec4(col, 1.0);\n return;\n }\n // Hologram: each die glows its own channel; the glows ADD, so overlaps blend and\n // the module fuses to the true colour as it shrinks on screen.\n vec3 e = vec3(vColor.r * pow(cR, uGlow),\n vColor.g * pow(cG, uGlow),\n vColor.b * pow(cB, uGlow)) * uTint * uGain;\n gl_FragColor = vec4(e, clamp(max(max(e.r, e.g), e.b), 0.0, 1.0));\n return;\n }\n\n float d = (uShape == 1) ? max(abs(q.x), abs(q.y)) : length(q); // 0 centre → 1 edge\n if (uStyle == 1) {\n // Comic: bold, flat cel-shaded fill (+ optional ink border), round or square.\n // Brightness is QUANTIZED into a few bands (posterized) but the LED's actual\n // brightness + hue are kept — so tonal content (images, fading trails) reads\n // faithfully, not maxed to full value. Faint/unlit LEDs drop out entirely.\n if (d > 1.0) discard;\n vec3 base = vColor * uTint;\n float lum = max(max(base.r, base.g), base.b);\n if (lum < 0.02) discard; // dim / unlit → transparent\n // vivid: flatten to the full value of the hue (punchy flat pop-art). cel: keep\n // brightness, quantized into a few bands (posterized → tonal content reads).\n float scale = uVivid == 1 ? (1.0 / lum) : (ceil(min(lum, 1.0) * 4.0) / 4.0 / lum);\n vec3 col = min(base * scale * uGain, 1.0);\n col = mix(col, uOutlineColor, smoothstep(1.0 - uOutline, 1.0, d));\n gl_FragColor = vec4(col, 1.0);\n return;\n }\n // Hologram (emissive): soft glow + a tiny physical off-speck; alpha tracks\n // brightness so unlit LEDs are transparent. Square shape → a boxy glow.\n float core = smoothstep(1.0, 0.0, d);\n float glow = pow(core, uGlow);\n vec3 emit = vColor * uTint * glow * uGain;\n vec3 dot = uOff * smoothstep(uDotSize, 0.0, d);\n vec3 c = emit + dot;\n float cov = clamp(max(max(c.r, c.g), c.b), 0.0, 1.0);\n gl_FragColor = vec4(c, cov);\n }\n", g = "\n attribute vec2 aQuad;\n varying vec2 vUv;\n void main() {\n vUv = aQuad * 0.5 + 0.5;\n gl_Position = vec4(aQuad, 0.0, 1.0);\n }\n", _ = "\n precision highp float;\n uniform sampler2D uScene;\n uniform vec3 uBg;\n varying vec2 vUv;\n void main() {\n vec4 s = texture2D(uScene, vUv);\n gl_FragColor = vec4(uBg * (1.0 - clamp(s.a, 0.0, 1.0)) + s.rgb, 1.0);\n }\n", v = "\n precision highp float;\n uniform sampler2D uSrc;\n uniform vec2 uDir;\n varying vec2 vUv;\n void main() {\n float w0 = 0.227027, w1 = 0.1945946, w2 = 0.1216216, w3 = 0.054054, w4 = 0.016216;\n vec3 c = texture2D(uSrc, vUv).rgb * w0;\n c += texture2D(uSrc, vUv + uDir * 1.0).rgb * w1;\n c += texture2D(uSrc, vUv - uDir * 1.0).rgb * w1;\n c += texture2D(uSrc, vUv + uDir * 2.0).rgb * w2;\n c += texture2D(uSrc, vUv - uDir * 2.0).rgb * w2;\n c += texture2D(uSrc, vUv + uDir * 3.0).rgb * w3;\n c += texture2D(uSrc, vUv - uDir * 3.0).rgb * w3;\n c += texture2D(uSrc, vUv + uDir * 4.0).rgb * w4;\n c += texture2D(uSrc, vUv - uDir * 4.0).rgb * w4;\n gl_FragColor = vec4(c, 1.0);\n }\n", y = "\n precision highp float;\n uniform sampler2D uScene; // emissive HDR: rgb = light, a = coverage\n uniform sampler2D uBloom; // blurred halo\n uniform vec3 uBg;\n uniform float uStrength;\n varying vec2 vUv;\n vec3 aces(vec3 x) { return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0); }\n void main() {\n vec4 e = texture2D(uScene, vUv);\n vec3 halo = texture2D(uBloom, vUv).rgb * uStrength;\n vec3 light = e.rgb + halo;\n float cov = clamp(max(e.a, max(halo.r, max(halo.g, halo.b))), 0.0, 1.0);\n // Tone-map only the emitted light; the background is preserved and the light\n // is over-composited onto it (like a real emitter in front of a surface).\n gl_FragColor = vec4(mix(uBg, aces(light), cov), 1.0);\n }\n";
|
|
93
|
+
function b(e, t, n, r, i, a = new Float32Array(t * n * r * 3)) {
|
|
94
|
+
let o = e.getContext("webgl", {
|
|
95
|
+
antialias: i.antialias,
|
|
96
|
+
alpha: !1,
|
|
97
|
+
premultipliedAlpha: !1,
|
|
98
|
+
depth: !0
|
|
99
|
+
});
|
|
100
|
+
if (!o) return null;
|
|
101
|
+
let s = o.getExtension("OES_texture_half_float"), l = o.getExtension("EXT_color_buffer_half_float"), b = o.getExtension("OES_texture_half_float_linear"), x = s ? s.HALF_FLOAT_OES : 0, S = !!(s && l), C = b ? o.LINEAR : o.NEAREST, w = i.background, T = i.offColor, E = i.tint, ee = i.glow, D = i.ledSize, O = i.offSize, k = i.style, A = i.shape, te = i.outline, j = i.outlineColor, ne = i.stagger, M = i.rgb, N = i.rgbLayout, P = i.vivid;
|
|
102
|
+
function F(e, t) {
|
|
103
|
+
let n = o.createShader(e);
|
|
104
|
+
return n ? (o.shaderSource(n, t), o.compileShader(n), o.getShaderParameter(n, o.COMPILE_STATUS) ? n : (console.warn("led-grid shader:", o.getShaderInfoLog(n)), null)) : null;
|
|
105
|
+
}
|
|
106
|
+
function I(e, t) {
|
|
107
|
+
let n = F(o.VERTEX_SHADER, e), r = F(o.FRAGMENT_SHADER, t), i = o.createProgram();
|
|
108
|
+
return !n || !r || !i ? (n && o.deleteShader(n), r && o.deleteShader(r), i && o.deleteProgram(i), null) : (o.attachShader(i, n), o.attachShader(i, r), o.linkProgram(i), o.deleteShader(n), o.deleteShader(r), o.getProgramParameter(i, o.LINK_STATUS) ? i : (console.warn("led-grid link:", o.getProgramInfoLog(i)), o.deleteProgram(i), null));
|
|
109
|
+
}
|
|
110
|
+
let L = I(m, h);
|
|
111
|
+
if (!L) return null;
|
|
112
|
+
o.useProgram(L);
|
|
113
|
+
let R = t * n * r, z = Math.max(t, n, r), B = z > 1 ? 2 / (z - 1) : 0, V = new Float32Array(R * 3);
|
|
114
|
+
function re() {
|
|
115
|
+
let e = 0;
|
|
116
|
+
for (let i = 0; i < r; i++) for (let a = 0; a < n; a++) {
|
|
117
|
+
let o = ne && a & 1 ? B * .5 : 0;
|
|
118
|
+
for (let s = 0; s < t; s++) V[e * 3] = (s - (t - 1) / 2) * B + o, V[e * 3 + 1] = (a - (n - 1) / 2) * B, V[e * 3 + 2] = (i - (r - 1) / 2) * B, e++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
re();
|
|
122
|
+
let H = new Float32Array(R * 6), ie = o.createBuffer(), ae = o.getAttribLocation(L, "aPos"), U = o.getAttribLocation(L, "aColor"), W = o.getUniformLocation(L, "uMVP"), G = o.getUniformLocation(L, "uPointScale"), oe = o.getUniformLocation(L, "uGain"), se = o.getUniformLocation(L, "uGlow"), K = o.getUniformLocation(L, "uDotSize"), ce = o.getUniformLocation(L, "uOff"), q = o.getUniformLocation(L, "uTint"), le = o.getUniformLocation(L, "uOutline"), ue = o.getUniformLocation(L, "uOutlineColor"), de = o.getUniformLocation(L, "uStyle"), fe = o.getUniformLocation(L, "uShape"), pe = o.getUniformLocation(L, "uRgb"), me = o.getUniformLocation(L, "uRgbLayout"), he = o.getUniformLocation(L, "uVivid");
|
|
123
|
+
function ge() {
|
|
124
|
+
o.disable(o.DEPTH_TEST), o.enable(o.BLEND), o.blendFunc(o.ONE, o.ONE);
|
|
125
|
+
}
|
|
126
|
+
ge();
|
|
127
|
+
let J = null;
|
|
128
|
+
function _e() {
|
|
129
|
+
J || (J = o.createBuffer(), o.bindBuffer(o.ARRAY_BUFFER, J), o.bufferData(o.ARRAY_BUFFER, new Float32Array([
|
|
130
|
+
-1,
|
|
131
|
+
-1,
|
|
132
|
+
1,
|
|
133
|
+
-1,
|
|
134
|
+
-1,
|
|
135
|
+
1,
|
|
136
|
+
-1,
|
|
137
|
+
1,
|
|
138
|
+
1,
|
|
139
|
+
-1,
|
|
140
|
+
1,
|
|
141
|
+
1
|
|
142
|
+
]), o.STATIC_DRAW));
|
|
143
|
+
}
|
|
144
|
+
function ve(e) {
|
|
145
|
+
o.bindBuffer(o.ARRAY_BUFFER, J), o.enableVertexAttribArray(e), o.vertexAttribPointer(e, 2, o.FLOAT, !1, 0, 0), o.drawArrays(o.TRIANGLES, 0, 6);
|
|
146
|
+
}
|
|
147
|
+
function ye(e, t, n, r) {
|
|
148
|
+
let i = o, a = i.createTexture();
|
|
149
|
+
return i.bindTexture(i.TEXTURE_2D, a), i.texImage2D(i.TEXTURE_2D, 0, i.RGBA, e, t, 0, i.RGBA, n, null), i.texParameteri(i.TEXTURE_2D, i.TEXTURE_MIN_FILTER, r), i.texParameteri(i.TEXTURE_2D, i.TEXTURE_MAG_FILTER, r), i.texParameteri(i.TEXTURE_2D, i.TEXTURE_WRAP_S, i.CLAMP_TO_EDGE), i.texParameteri(i.TEXTURE_2D, i.TEXTURE_WRAP_T, i.CLAMP_TO_EDGE), a;
|
|
150
|
+
}
|
|
151
|
+
function be(e, t) {
|
|
152
|
+
let n = o;
|
|
153
|
+
n.bindFramebuffer(n.FRAMEBUFFER, e), n.framebufferTexture2D(n.FRAMEBUFFER, n.COLOR_ATTACHMENT0, n.TEXTURE_2D, t, 0);
|
|
154
|
+
let r = n.checkFramebufferStatus(n.FRAMEBUFFER) === n.FRAMEBUFFER_COMPLETE;
|
|
155
|
+
return n.bindFramebuffer(n.FRAMEBUFFER, null), r;
|
|
156
|
+
}
|
|
157
|
+
let Y = null, xe = null, X = null, Se = -1, Ce = null, we = null, Te = 0, Ee = 0;
|
|
158
|
+
function De(e, t) {
|
|
159
|
+
let n = o;
|
|
160
|
+
if (!Y) {
|
|
161
|
+
if (Y = I(g, _), !Y) return !1;
|
|
162
|
+
_e(), Se = n.getAttribLocation(Y, "aQuad"), Ce = n.getUniformLocation(Y, "uScene"), we = n.getUniformLocation(Y, "uBg"), xe = n.createFramebuffer();
|
|
163
|
+
}
|
|
164
|
+
if (e !== Te || t !== Ee) {
|
|
165
|
+
if (X && n.deleteTexture(X), X = ye(e, t, n.UNSIGNED_BYTE, n.LINEAR), !be(xe, X)) return !1;
|
|
166
|
+
Te = e, Ee = t;
|
|
167
|
+
}
|
|
168
|
+
return !0;
|
|
169
|
+
}
|
|
170
|
+
let Z = null, Q = null, Oe = !1, ke = null, Ae = null, je = null, $ = null, Me = null, Ne = null, Pe = -1, Fe = -1, Ie = null, Le = null, Re = null, ze = null, Be = null, Ve = null, He = 0, Ue = 0;
|
|
171
|
+
function We(e, t) {
|
|
172
|
+
if (Oe || !S) return !1;
|
|
173
|
+
let n = o;
|
|
174
|
+
if (!Q) {
|
|
175
|
+
if (Z = I(g, v), Q = I(g, y), !Z || !Q) return Oe = !0, !1;
|
|
176
|
+
_e(), Pe = n.getAttribLocation(Z, "aQuad"), Ie = n.getUniformLocation(Z, "uSrc"), Le = n.getUniformLocation(Z, "uDir"), Fe = n.getAttribLocation(Q, "aQuad"), Re = n.getUniformLocation(Q, "uScene"), ze = n.getUniformLocation(Q, "uBloom"), Be = n.getUniformLocation(Q, "uBg"), Ve = n.getUniformLocation(Q, "uStrength"), ke = n.createFramebuffer(), Ae = n.createFramebuffer(), je = n.createFramebuffer();
|
|
177
|
+
}
|
|
178
|
+
if (e !== He || t !== Ue) {
|
|
179
|
+
let r = Math.max(1, Math.ceil(e / 4)), i = Math.max(1, Math.ceil(t / 4));
|
|
180
|
+
for (let e of [
|
|
181
|
+
$,
|
|
182
|
+
Me,
|
|
183
|
+
Ne
|
|
184
|
+
]) e && n.deleteTexture(e);
|
|
185
|
+
if ($ = ye(e, t, x, C), Me = ye(r, i, x, C), Ne = ye(r, i, x, C), !be(ke, $) || !be(Ae, Me) || !be(je, Ne)) return Oe = !0, !1;
|
|
186
|
+
He = e, Ue = t;
|
|
187
|
+
}
|
|
188
|
+
return !0;
|
|
189
|
+
}
|
|
190
|
+
let Ge = c(), Ke = c(), qe = c(), Je;
|
|
191
|
+
function Ye(e, t) {
|
|
192
|
+
let n = t * 6;
|
|
193
|
+
H[n] = V[e], H[n + 1] = V[e + 1], H[n + 2] = V[e + 2], H[n + 3] = a[e], H[n + 4] = a[e + 1], H[n + 5] = a[e + 2];
|
|
194
|
+
}
|
|
195
|
+
function Xe() {
|
|
196
|
+
let e = o, t = T[0] > 0 || T[1] > 0 || T[2] > 0, n = 0;
|
|
197
|
+
if (Je && !Je.all && !t) {
|
|
198
|
+
let e = Je.list, t = Je.count;
|
|
199
|
+
for (let r = 0; r < t; r++) {
|
|
200
|
+
let t = e[r] * 3;
|
|
201
|
+
(a[t] > 0 || a[t + 1] > 0 || a[t + 2] > 0) && Ye(t, n++);
|
|
202
|
+
}
|
|
203
|
+
} else for (let e = 0; e < R; e++) {
|
|
204
|
+
let r = e * 3;
|
|
205
|
+
(t || a[r] > 0 || a[r + 1] > 0 || a[r + 2] > 0) && Ye(r, n++);
|
|
206
|
+
}
|
|
207
|
+
e.bindBuffer(e.ARRAY_BUFFER, ie), e.bufferData(e.ARRAY_BUFFER, H.subarray(0, n * 6), e.DYNAMIC_DRAW), e.enableVertexAttribArray(ae), e.vertexAttribPointer(ae, 3, e.FLOAT, !1, 24, 0), e.enableVertexAttribArray(U), e.vertexAttribPointer(U, 3, e.FLOAT, !1, 24, 12), e.drawArrays(e.POINTS, 0, n);
|
|
208
|
+
}
|
|
209
|
+
function Ze(t, n) {
|
|
210
|
+
let r = o;
|
|
211
|
+
Je = n;
|
|
212
|
+
let i = e.width, a = e.height;
|
|
213
|
+
p(Ke, t.yaw, t.pitch, t.dist);
|
|
214
|
+
let s = i / a, c = s < 1 ? 2 * Math.atan(Math.tan(t.fov / 2) / s) : t.fov, l;
|
|
215
|
+
if (t.projection === "orthographic") {
|
|
216
|
+
let e = t.dist * Math.tan(c / 2);
|
|
217
|
+
f(Ge, e, s, .1, 100), l = a / 2 * B / e * D;
|
|
218
|
+
} else d(Ge, c, s, .1, 100), l = a / 2 * B * (1 / Math.tan(c / 2)) * D;
|
|
219
|
+
u(qe, Ge, Ke), r.useProgram(L), r.uniformMatrix4fv(W, !1, qe), r.uniform1f(G, l), r.uniform1f(oe, t.gain), r.uniform1f(se, ee), r.uniform1f(K, O), r.uniform3fv(ce, T), r.uniform3fv(q, E), r.uniform1f(le, te), r.uniform3fv(ue, j), r.uniform1i(de, +(k === "comic")), r.uniform1i(fe, +(A === "square")), r.uniform1i(pe, +!!M);
|
|
220
|
+
let m = N === "auto" ? A === "square" ? "quad" : "triad" : N;
|
|
221
|
+
if (r.uniform1i(me, m === "quad" ? 1 : m === "stripe" ? 2 : 0), r.uniform1i(he, +!!P), k === "comic") {
|
|
222
|
+
Qe(i, a);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
ge(), We(i, a) ? et(i, a) : De(i, a) && $e(i, a);
|
|
226
|
+
}
|
|
227
|
+
function Qe(e, t) {
|
|
228
|
+
let n = o;
|
|
229
|
+
n.bindFramebuffer(n.FRAMEBUFFER, null), n.viewport(0, 0, e, t), n.disable(n.BLEND), n.enable(n.DEPTH_TEST), n.clearColor(w[0], w[1], w[2], 1), n.clear(n.COLOR_BUFFER_BIT | n.DEPTH_BUFFER_BIT), Xe();
|
|
230
|
+
}
|
|
231
|
+
function $e(e, t) {
|
|
232
|
+
let n = o;
|
|
233
|
+
n.bindFramebuffer(n.FRAMEBUFFER, xe), n.viewport(0, 0, e, t), n.clearColor(0, 0, 0, 0), n.clear(n.COLOR_BUFFER_BIT), Xe(), n.bindFramebuffer(n.FRAMEBUFFER, null), n.viewport(0, 0, e, t), n.disable(n.BLEND), n.useProgram(Y), n.activeTexture(n.TEXTURE0), n.bindTexture(n.TEXTURE_2D, X), n.uniform1i(Ce, 0), n.uniform3fv(we, w), ve(Se), n.useProgram(L), ge();
|
|
234
|
+
}
|
|
235
|
+
function et(e, t) {
|
|
236
|
+
let n = o, r = Math.max(1, Math.ceil(e / 4)), i = Math.max(1, Math.ceil(t / 4));
|
|
237
|
+
n.bindFramebuffer(n.FRAMEBUFFER, ke), n.viewport(0, 0, e, t), n.clearColor(0, 0, 0, 0), n.clear(n.COLOR_BUFFER_BIT), Xe(), n.disable(n.BLEND), n.useProgram(Z), n.bindFramebuffer(n.FRAMEBUFFER, Ae), n.viewport(0, 0, r, i), n.activeTexture(n.TEXTURE0), n.bindTexture(n.TEXTURE_2D, $), n.uniform1i(Ie, 0), n.uniform2f(Le, 1 / e, 0), ve(Pe), n.bindFramebuffer(n.FRAMEBUFFER, je), n.viewport(0, 0, r, i), n.bindTexture(n.TEXTURE_2D, Me), n.uniform2f(Le, 0, 1 / i), ve(Pe), n.bindFramebuffer(n.FRAMEBUFFER, null), n.viewport(0, 0, e, t), n.useProgram(Q), n.activeTexture(n.TEXTURE0), n.bindTexture(n.TEXTURE_2D, $), n.uniform1i(Re, 0), n.activeTexture(n.TEXTURE1), n.bindTexture(n.TEXTURE_2D, Ne), n.uniform1i(ze, 1), n.uniform3fv(Be, w), n.uniform1f(Ve, 1), ve(Fe), n.activeTexture(n.TEXTURE0), n.useProgram(L), ge();
|
|
238
|
+
}
|
|
239
|
+
function tt(e) {
|
|
240
|
+
e.background && (w = e.background), e.offColor && (T = e.offColor), e.tint && (E = e.tint), e.glow !== void 0 && (ee = e.glow), e.ledSize !== void 0 && (D = e.ledSize), e.offSize !== void 0 && (O = e.offSize), e.style && (k = e.style), e.shape && (A = e.shape), e.outline !== void 0 && (te = e.outline), e.outlineColor && (j = e.outlineColor), e.stagger !== void 0 && e.stagger !== ne && (ne = e.stagger, re()), e.rgb !== void 0 && (M = e.rgb), e.rgbLayout && (N = e.rgbLayout), e.vivid !== void 0 && (P = e.vivid);
|
|
241
|
+
}
|
|
242
|
+
function nt() {
|
|
243
|
+
let e = o;
|
|
244
|
+
e.deleteBuffer(ie), e.deleteProgram(L), Y && e.deleteProgram(Y), Z && e.deleteProgram(Z), Q && e.deleteProgram(Q), J && e.deleteBuffer(J);
|
|
245
|
+
for (let t of [
|
|
246
|
+
X,
|
|
247
|
+
$,
|
|
248
|
+
Me,
|
|
249
|
+
Ne
|
|
250
|
+
]) t && e.deleteTexture(t);
|
|
251
|
+
for (let t of [
|
|
252
|
+
xe,
|
|
253
|
+
ke,
|
|
254
|
+
Ae,
|
|
255
|
+
je
|
|
256
|
+
]) t && e.deleteFramebuffer(t);
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
leds: a,
|
|
260
|
+
render: Ze,
|
|
261
|
+
configure: tt,
|
|
262
|
+
dispose: nt
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/voxel-grid.ts
|
|
267
|
+
function x(e, t, n, r = new Float32Array(e * t * n * 3)) {
|
|
268
|
+
let i = e * t * n, a = (n, r, i) => ((n | 0) + e * ((r | 0) + t * (i | 0))) * 3, s = (r, i, a) => (r |= 0, i |= 0, a |= 0, r >= 0 && r < e && i >= 0 && i < t && a >= 0 && a < n), c = {
|
|
269
|
+
list: new Uint32Array(i),
|
|
270
|
+
count: 0,
|
|
271
|
+
all: !1
|
|
272
|
+
}, l = new Uint32Array(i), u = 1, d = !1;
|
|
273
|
+
function f(e) {
|
|
274
|
+
l[e] !== u && (l[e] = u, c.list[c.count++] = e);
|
|
275
|
+
}
|
|
276
|
+
function p(n, i, a, o) {
|
|
277
|
+
if (!s(n, i, a)) return;
|
|
278
|
+
let c = (n | 0) + e * ((i | 0) + t * (a | 0)), l = c * 3;
|
|
279
|
+
r[l] = o[0], r[l + 1] = o[1], r[l + 2] = o[2], f(c);
|
|
280
|
+
}
|
|
281
|
+
function m(e, t, n, r) {
|
|
282
|
+
p(e, t, n, o(r));
|
|
283
|
+
}
|
|
284
|
+
function h(n, i, a, c) {
|
|
285
|
+
if (!s(n, i, a)) return;
|
|
286
|
+
let l = o(c), u = (n | 0) + e * ((i | 0) + t * (a | 0)), d = u * 3;
|
|
287
|
+
r[d] += l[0], r[d + 1] += l[1], r[d + 2] += l[2], f(u);
|
|
288
|
+
}
|
|
289
|
+
function g(e, t, n) {
|
|
290
|
+
if (!s(e, t, n)) return [
|
|
291
|
+
0,
|
|
292
|
+
0,
|
|
293
|
+
0
|
|
294
|
+
];
|
|
295
|
+
let i = a(e, t, n);
|
|
296
|
+
return [
|
|
297
|
+
r[i],
|
|
298
|
+
r[i + 1],
|
|
299
|
+
r[i + 2]
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
function _(e = [
|
|
303
|
+
0,
|
|
304
|
+
0,
|
|
305
|
+
0
|
|
306
|
+
]) {
|
|
307
|
+
let t = o(e);
|
|
308
|
+
if (t[0] === 0 && t[1] === 0 && t[2] === 0) {
|
|
309
|
+
if (d) r.fill(0), d = !1;
|
|
310
|
+
else for (let e = 0; e < c.count; e++) {
|
|
311
|
+
let t = c.list[e] * 3;
|
|
312
|
+
r[t] = r[t + 1] = r[t + 2] = 0;
|
|
313
|
+
}
|
|
314
|
+
c.count = 0, c.all = !1, u++;
|
|
315
|
+
} else {
|
|
316
|
+
for (let e = 0; e < r.length; e += 3) r[e] = t[0], r[e + 1] = t[1], r[e + 2] = t[2];
|
|
317
|
+
c.count = 0, c.all = !0, d = !0, u++;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function v() {
|
|
321
|
+
c.all = !0, d = !0;
|
|
322
|
+
}
|
|
323
|
+
function y(e, t, n) {
|
|
324
|
+
let r = o(n), i = e[0] | 0, a = e[1] | 0, s = e[2] | 0, c = t[0] | 0, l = t[1] | 0, u = t[2] | 0, d = Math.abs(c - i), f = Math.abs(l - a), m = Math.abs(u - s), h = i < c ? 1 : -1, g = a < l ? 1 : -1, _ = s < u ? 1 : -1, v = Math.max(d, f, m);
|
|
325
|
+
if (v === 0) return p(i, a, s, r);
|
|
326
|
+
let y = v / 2, b = v / 2, x = v / 2;
|
|
327
|
+
for (let e = 0; e <= v; e++) p(i, a, s, r), y -= d, y < 0 && (y += v, i += h), b -= f, b < 0 && (b += v, a += g), x -= m, x < 0 && (x += v, s += _);
|
|
328
|
+
}
|
|
329
|
+
function b(r, i, a, s = !1) {
|
|
330
|
+
let c = o(a), [l, u, d] = [
|
|
331
|
+
Math.min(r[0], i[0]),
|
|
332
|
+
Math.min(r[1], i[1]),
|
|
333
|
+
Math.min(r[2], i[2])
|
|
334
|
+
], [f, m, h] = [
|
|
335
|
+
Math.max(r[0], i[0]),
|
|
336
|
+
Math.max(r[1], i[1]),
|
|
337
|
+
Math.max(r[2], i[2])
|
|
338
|
+
], g = Math.max(d, 0), _ = Math.min(h, n - 1), v = Math.max(u, 0), y = Math.min(m, t - 1), b = Math.max(l, 0), x = Math.min(f, e - 1);
|
|
339
|
+
for (let e = g; e <= _; e++) for (let t = v; t <= y; t++) for (let n = b; n <= x; n++) if (s) p(n, t, e, c);
|
|
340
|
+
else {
|
|
341
|
+
let r = 0;
|
|
342
|
+
(n === l || n === f) && r++, (t === u || t === m) && r++, (e === d || e === h) && r++, r >= 2 && p(n, t, e, c);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function x(r, i, a, s = !1) {
|
|
346
|
+
let c = o(a), l = i * i, u = Math.max(0, Math.floor(r[0] - i)), d = Math.min(e - 1, Math.ceil(r[0] + i)), f = Math.max(0, Math.floor(r[1] - i)), m = Math.min(t - 1, Math.ceil(r[1] + i)), h = Math.max(0, Math.floor(r[2] - i)), g = Math.min(n - 1, Math.ceil(r[2] + i));
|
|
347
|
+
for (let e = h; e <= g; e++) for (let t = f; t <= m; t++) for (let n = u; n <= d; n++) {
|
|
348
|
+
let a = (n - r[0]) ** 2 + (t - r[1]) ** 2 + (e - r[2]) ** 2;
|
|
349
|
+
(s ? a <= l : Math.abs(Math.sqrt(a) - i) <= .5) && p(n, t, e, c);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
nx: e,
|
|
354
|
+
ny: t,
|
|
355
|
+
nz: n,
|
|
356
|
+
leds: r,
|
|
357
|
+
active: c,
|
|
358
|
+
markAll: v,
|
|
359
|
+
index: a,
|
|
360
|
+
inBounds: s,
|
|
361
|
+
plot: m,
|
|
362
|
+
add: h,
|
|
363
|
+
get: g,
|
|
364
|
+
clear: _,
|
|
365
|
+
fill: (e) => _(e),
|
|
366
|
+
line: y,
|
|
367
|
+
box: b,
|
|
368
|
+
sphere: x
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
//#endregion
|
|
372
|
+
//#region src/led-display.ts
|
|
373
|
+
var S = (e, t, n) => e < t ? t : e > n ? n : e, C = 4e6, w = (e, t, n) => {
|
|
374
|
+
let r = e * t * n;
|
|
375
|
+
return Number.isFinite(r) && r >= 1 && r <= C;
|
|
376
|
+
};
|
|
377
|
+
function T(e, t) {
|
|
378
|
+
let [n, r, i] = t.size;
|
|
379
|
+
if (!w(n, r, i)) return null;
|
|
380
|
+
let a = t.led ?? {}, c = t.color ?? {}, l = t.camera ?? {}, u = t.interaction ?? {}, d = t.quality ?? {}, f = {
|
|
381
|
+
background: s(c.background ?? [
|
|
382
|
+
.01,
|
|
383
|
+
.01,
|
|
384
|
+
.02
|
|
385
|
+
]),
|
|
386
|
+
offColor: s(a.offColor ?? [
|
|
387
|
+
0,
|
|
388
|
+
0,
|
|
389
|
+
0
|
|
390
|
+
]),
|
|
391
|
+
tint: o(c.tint ?? [
|
|
392
|
+
1,
|
|
393
|
+
1,
|
|
394
|
+
1
|
|
395
|
+
]),
|
|
396
|
+
glow: a.glow ?? 2.2,
|
|
397
|
+
ledSize: a.size ?? .6,
|
|
398
|
+
offSize: a.offSize ?? .35,
|
|
399
|
+
style: a.style ?? "hologram",
|
|
400
|
+
shape: a.shape ?? "round",
|
|
401
|
+
outline: a.outline ?? .25,
|
|
402
|
+
outlineColor: s(a.outlineColor ?? [
|
|
403
|
+
.02,
|
|
404
|
+
.02,
|
|
405
|
+
.02
|
|
406
|
+
]),
|
|
407
|
+
stagger: a.stagger ?? !1,
|
|
408
|
+
rgb: a.rgb ?? !1,
|
|
409
|
+
rgbLayout: a.rgbLayout ?? "auto",
|
|
410
|
+
vivid: a.vivid ?? !1,
|
|
411
|
+
antialias: d.antialias ?? !0
|
|
412
|
+
}, p = new Float32Array(n * r * i * 3), m = b(e, n, r, i, f, p);
|
|
413
|
+
if (!m) return null;
|
|
414
|
+
let h = m, g = x(n, r, i, p), _ = l.yaw ?? .6, v = l.pitch ?? .4, y = l.distance ?? 3.6, C = l.fov ?? .9, T = l.projection ?? "perspective", E = l.autoOrbit ?? !0, ee = l.orbitSpeed ?? .45, D = l.pitchLimits ?? [-1.4, 1.4], O = c.gain ?? 1, k = u.drag ?? !0, A = u.dragSpeed ?? .01, te = u.zoom ?? !1, j = u.zoomLimits ?? [1.5, 10], ne = d.pixelRatio ?? 2, M = d.paused ?? !1, N = d.fps ?? 0, P = /* @__PURE__ */ new Map(), F = 0, I = () => {
|
|
415
|
+
let [e, t] = [...P.values()];
|
|
416
|
+
return Math.hypot(e.x - t.x, e.y - t.y);
|
|
417
|
+
}, L = (t) => {
|
|
418
|
+
P.set(t.pointerId, {
|
|
419
|
+
x: t.clientX,
|
|
420
|
+
y: t.clientY
|
|
421
|
+
}), e.setPointerCapture?.(t.pointerId), P.size === 2 && (F = I());
|
|
422
|
+
}, R = (e) => {
|
|
423
|
+
let t = P.get(e.pointerId);
|
|
424
|
+
if (!t) return;
|
|
425
|
+
let n = e.clientX - t.x, r = e.clientY - t.y;
|
|
426
|
+
if (t.x = e.clientX, t.y = e.clientY, P.size === 1 && k) _ += n * A, v = S(v + r * A, D[0], D[1]), W();
|
|
427
|
+
else if (P.size === 2 && te) {
|
|
428
|
+
let e = I();
|
|
429
|
+
F > 0 && e > 0 && (y = S(y * F / e, j[0], j[1]), W()), F = e;
|
|
430
|
+
}
|
|
431
|
+
}, z = (e) => {
|
|
432
|
+
P.delete(e.pointerId), F = 0;
|
|
433
|
+
}, B = (e) => {
|
|
434
|
+
te && (e.preventDefault(), y = S(y * Math.exp(e.deltaY * .001), j[0], j[1]), W());
|
|
435
|
+
};
|
|
436
|
+
e.addEventListener("pointerdown", L), e.addEventListener("pointermove", R), e.addEventListener("pointerup", z), e.addEventListener("pointercancel", z), e.addEventListener("wheel", B, { passive: !1 });
|
|
437
|
+
function V(t) {
|
|
438
|
+
if (t && w(t[0], t[1], t[2]) && (t[0] !== n || t[1] !== r || t[2] !== i)) {
|
|
439
|
+
let [a, o, s] = t, c = new Float32Array(a * o * s * 3), l = b(e, a, o, s, f, c);
|
|
440
|
+
l && (h.dispose(), h = l, p = c, g = x(a, o, s, p), [n, r, i] = t, K?.(me, 0));
|
|
441
|
+
}
|
|
442
|
+
let a = Math.min(typeof window < "u" && window.devicePixelRatio || 1, ne), o = e.getBoundingClientRect();
|
|
443
|
+
e.width = Math.max(1, Math.round(o.width * a)), e.height = Math.max(1, Math.round(o.height * a)), M && U();
|
|
444
|
+
}
|
|
445
|
+
let re = new ResizeObserver(() => V());
|
|
446
|
+
re.observe(e);
|
|
447
|
+
let H = !1, ie = (e) => {
|
|
448
|
+
e.preventDefault(), H = !0, de();
|
|
449
|
+
}, ae = () => {
|
|
450
|
+
let t = b(e, n, r, i, f, p);
|
|
451
|
+
t && (h = t, H = !1, g.markAll(), V(), M ? U() : ue());
|
|
452
|
+
};
|
|
453
|
+
e.addEventListener("webglcontextlost", ie), e.addEventListener("webglcontextrestored", ae);
|
|
454
|
+
function U() {
|
|
455
|
+
H || h.render({
|
|
456
|
+
yaw: _,
|
|
457
|
+
pitch: v,
|
|
458
|
+
dist: y,
|
|
459
|
+
fov: C,
|
|
460
|
+
gain: O,
|
|
461
|
+
projection: T
|
|
462
|
+
}, g.active);
|
|
463
|
+
}
|
|
464
|
+
function W() {
|
|
465
|
+
M && U();
|
|
466
|
+
}
|
|
467
|
+
let G = {
|
|
468
|
+
fps: 0,
|
|
469
|
+
frameMs: 0,
|
|
470
|
+
drawMs: 0,
|
|
471
|
+
renderMs: 0
|
|
472
|
+
}, oe = () => typeof performance < "u" ? performance.now() : 0, se = (e, t) => e === 0 ? t : e + (t - e) * .1, K = null, ce = 0, q = 0, le = (e) => {
|
|
473
|
+
if (ce = requestAnimationFrame(le), typeof document < "u" && document.hidden) return;
|
|
474
|
+
if (N > 0 && q) {
|
|
475
|
+
let t = 1e3 / N;
|
|
476
|
+
if (e - q < t - Math.min(1, t * .1)) return;
|
|
477
|
+
}
|
|
478
|
+
let t = q ? e - q : 0, n = q ? Math.min(.05, t / 1e3) : 0;
|
|
479
|
+
q = e, E && P.size === 0 && (_ += n * ee);
|
|
480
|
+
let r = oe();
|
|
481
|
+
K?.(me, n);
|
|
482
|
+
let i = oe();
|
|
483
|
+
U();
|
|
484
|
+
let a = oe();
|
|
485
|
+
G.drawMs = se(G.drawMs, i - r), G.renderMs = se(G.renderMs, a - i), t > 0 && (G.frameMs = se(G.frameMs, t), G.fps = 1e3 / G.frameMs);
|
|
486
|
+
};
|
|
487
|
+
function ue() {
|
|
488
|
+
ce ||= (q = 0, requestAnimationFrame(le));
|
|
489
|
+
}
|
|
490
|
+
function de() {
|
|
491
|
+
ce &&= (cancelAnimationFrame(ce), 0);
|
|
492
|
+
}
|
|
493
|
+
function fe(e) {
|
|
494
|
+
e !== M && (M = e, M ? (de(), U()) : ue());
|
|
495
|
+
}
|
|
496
|
+
function pe(e) {
|
|
497
|
+
if (e.led || e.color) {
|
|
498
|
+
let t = {
|
|
499
|
+
background: e.color?.background == null ? void 0 : s(e.color.background),
|
|
500
|
+
offColor: e.led?.offColor == null ? void 0 : s(e.led.offColor),
|
|
501
|
+
tint: e.color?.tint == null ? void 0 : o(e.color.tint),
|
|
502
|
+
glow: e.led?.glow,
|
|
503
|
+
ledSize: e.led?.size,
|
|
504
|
+
offSize: e.led?.offSize,
|
|
505
|
+
style: e.led?.style,
|
|
506
|
+
shape: e.led?.shape,
|
|
507
|
+
stagger: e.led?.stagger,
|
|
508
|
+
rgb: e.led?.rgb,
|
|
509
|
+
rgbLayout: e.led?.rgbLayout,
|
|
510
|
+
vivid: e.led?.vivid,
|
|
511
|
+
outline: e.led?.outline,
|
|
512
|
+
outlineColor: e.led?.outlineColor == null ? void 0 : s(e.led.outlineColor)
|
|
513
|
+
};
|
|
514
|
+
h.configure(t), Object.assign(f, Object.fromEntries(Object.entries(t).filter(([, e]) => e !== void 0))), e.color?.gain != null && (O = e.color.gain);
|
|
515
|
+
}
|
|
516
|
+
let t = e.camera;
|
|
517
|
+
t && (t.yaw != null && (_ = t.yaw), t.pitch != null && (v = t.pitch), t.distance != null && (y = t.distance), t.fov != null && (C = t.fov), t.projection && (T = t.projection), t.autoOrbit != null && (E = t.autoOrbit), t.orbitSpeed != null && (ee = t.orbitSpeed), t.pitchLimits && (D = t.pitchLimits));
|
|
518
|
+
let n = e.interaction;
|
|
519
|
+
n && (n.drag != null && (k = n.drag), n.dragSpeed != null && (A = n.dragSpeed), n.zoom != null && (te = n.zoom), n.zoomLimits && (j = n.zoomLimits)), e.quality?.pixelRatio != null && (ne = e.quality.pixelRatio, V()), e.quality?.fps != null && (N = e.quality.fps), e.quality?.paused != null && fe(e.quality.paused), W();
|
|
520
|
+
}
|
|
521
|
+
M ? U() : ue();
|
|
522
|
+
let me = {
|
|
523
|
+
get nx() {
|
|
524
|
+
return g.nx;
|
|
525
|
+
},
|
|
526
|
+
get ny() {
|
|
527
|
+
return g.ny;
|
|
528
|
+
},
|
|
529
|
+
get nz() {
|
|
530
|
+
return g.nz;
|
|
531
|
+
},
|
|
532
|
+
get leds() {
|
|
533
|
+
return g.leds;
|
|
534
|
+
},
|
|
535
|
+
markAll: () => g.markAll(),
|
|
536
|
+
index: (e, t, n) => g.index(e, t, n),
|
|
537
|
+
inBounds: (e, t, n) => g.inBounds(e, t, n),
|
|
538
|
+
plot: (e, t, n, r) => g.plot(e, t, n, r),
|
|
539
|
+
add: (e, t, n, r) => g.add(e, t, n, r),
|
|
540
|
+
get: (e, t, n) => g.get(e, t, n),
|
|
541
|
+
clear: (e) => g.clear(e),
|
|
542
|
+
fill: (e) => g.fill(e),
|
|
543
|
+
line: (e, t, n) => g.line(e, t, n),
|
|
544
|
+
box: (e, t, n, r) => g.box(e, t, n, r),
|
|
545
|
+
sphere: (e, t, n, r) => g.sphere(e, t, n, r),
|
|
546
|
+
stats: G,
|
|
547
|
+
onFrame(e) {
|
|
548
|
+
return K = e, W(), () => {
|
|
549
|
+
K === e && (K = null);
|
|
550
|
+
};
|
|
551
|
+
},
|
|
552
|
+
render: U,
|
|
553
|
+
setGain(e) {
|
|
554
|
+
O = e, W();
|
|
555
|
+
},
|
|
556
|
+
setPaused: fe,
|
|
557
|
+
setOptions: pe,
|
|
558
|
+
setCamera(e) {
|
|
559
|
+
e.yaw != null && (_ = e.yaw), e.pitch != null && (v = S(e.pitch, D[0], D[1])), e.distance != null && (y = S(e.distance, j[0], j[1])), W();
|
|
560
|
+
},
|
|
561
|
+
resize: V,
|
|
562
|
+
snapshot() {
|
|
563
|
+
return U(), e.toDataURL("image/png");
|
|
564
|
+
},
|
|
565
|
+
dispose() {
|
|
566
|
+
de(), re.disconnect(), e.removeEventListener("pointerdown", L), e.removeEventListener("pointermove", R), e.removeEventListener("pointerup", z), e.removeEventListener("pointercancel", z), e.removeEventListener("wheel", B), e.removeEventListener("webglcontextlost", ie), e.removeEventListener("webglcontextrestored", ae), h.dispose();
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
return me;
|
|
570
|
+
}
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region src/index.ts
|
|
573
|
+
var E = x;
|
|
574
|
+
//#endregion
|
|
575
|
+
export { T as createLedDisplay, E as createVoxelGrid, o as parseColor };
|
|
576
|
+
|
|
577
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/color.ts","../src/renderer.ts","../src/voxel-grid.ts","../src/led-display.ts","../src/index.ts"],"sourcesContent":["// Colour handling for @glowbox/led-grid. A `Color` is either a numeric RGB triple in\n// 0..1 (values >1 are allowed and bloom under the additive glow) or any CSS colour\n// string ('#ff8800', 'rgb(0 128 255)', 'tomato', 'hsl(...)', …). Everything is\n// normalised to a plain `RGB` before it reaches the LED buffer.\n//\n// Note: only the array form can exceed 1 (bloom); CSS strings are 0..255 → 0..1.\n\nexport type RGB = [number, number, number];\nexport type Vec3 = [number, number, number];\nexport type Color = RGB | string;\n\n// Parsed strings are memoised — draw callbacks may pass the same literal thousands\n// of times per frame, so this must be O(1) after the first sighting.\nconst cache = new Map<string, RGB>();\n\n// A throwaway 2D context normalises any CSS colour the browser understands. Created\n// lazily and only when a non-hex string is seen; `null` when there's no DOM (SSR /\n// pure-node), in which case non-hex strings fall back to black.\nlet ctx: CanvasRenderingContext2D | null | undefined;\n\nconst clamp01 = (n: number) => (n < 0 ? 0 : n > 1 ? 1 : n);\n\n/** #rgb / #rgba / #rrggbb / #rrggbbaa → RGB (alpha ignored), else null. */\nfunction parseHex(s: string): RGB | null {\n\tconst m = /^#([0-9a-f]{3,8})$/i.exec(s);\n\tif (!m) return null;\n\tconst h = m[1];\n\tif (h.length === 3 || h.length === 4) {\n\t\treturn [\n\t\t\tparseInt(h[0] + h[0], 16) / 255,\n\t\t\tparseInt(h[1] + h[1], 16) / 255,\n\t\t\tparseInt(h[2] + h[2], 16) / 255\n\t\t];\n\t}\n\tif (h.length === 6 || h.length === 8) {\n\t\treturn [\n\t\t\tparseInt(h.slice(0, 2), 16) / 255,\n\t\t\tparseInt(h.slice(2, 4), 16) / 255,\n\t\t\tparseInt(h.slice(4, 6), 16) / 255\n\t\t];\n\t}\n\treturn null;\n}\n\n/** Normalise via a 2D canvas: assigning an invalid colour leaves fillStyle unchanged,\n * so two different sentinels detect an unparseable input. Returns null if no DOM. */\nfunction parseViaCanvas(s: string): RGB | null {\n\tif (ctx === undefined) {\n\t\tconst canvas = typeof document !== 'undefined' ? document.createElement('canvas') : null;\n\t\tctx = canvas ? canvas.getContext('2d') : null;\n\t}\n\tif (!ctx) return null;\n\n\tctx.fillStyle = '#000';\n\tctx.fillStyle = s;\n\tconst a = ctx.fillStyle;\n\tctx.fillStyle = '#fff';\n\tctx.fillStyle = s;\n\tconst b = ctx.fillStyle;\n\tif (a !== b) return null; // input took neither sentinel → invalid\n\n\tconst hex = parseHex(a);\n\tif (hex) return hex;\n\t// Non-hex normal form is 'rgba(r, g, b, a)' (or 'rgb(r, g, b)').\n\tconst m = /rgba?\\(([^)]+)\\)/i.exec(a);\n\tif (m) {\n\t\tconst parts = m[1].split(',').map((p) => parseFloat(p));\n\t\treturn [(parts[0] || 0) / 255, (parts[1] || 0) / 255, (parts[2] || 0) / 255];\n\t}\n\treturn null;\n}\n\nfunction parseString(s: string): RGB {\n\tconst key = s.trim();\n\tconst hit = cache.get(key);\n\tif (hit) return hit;\n\tconst rgb = parseHex(key) ?? parseViaCanvas(key) ?? [0, 0, 0];\n\tcache.set(key, rgb);\n\treturn rgb;\n}\n\n/** Normalise a `Color` to an `RGB` triple. Arrays pass through untouched (so bloom\n * values >1 survive); strings are parsed (and memoised). */\nexport function parseColor(c: Color): RGB {\n\treturn typeof c === 'string' ? parseString(c) : c;\n}\n\n/** Like parseColor but clamps to 0..1 — for config colours (background/off) where\n * bloom is meaningless and out-of-range would misbehave in the shader. */\nexport function parseColor01(c: Color): RGB {\n\tconst [r, g, b] = parseColor(c);\n\treturn [clamp01(r), clamp01(g), clamp01(b)];\n}\n","// Internal WebGL renderer for the LED grid — an nx×ny×nz lattice of point-light\n// \"LEDs\" rendered as real emitters (the \"hologram\" look): each lit LED is a bright\n// point with a soft glow halo; unlit LEDs are a tiny transparent speck. It reads on\n// ANY background (dark or light) because the light is over-composited in front of it.\n//\n// Primary path: emissive → half-float HDR buffer → separable blur (halos) → ACES\n// tone-map → over-composite onto the background. When the half-float extensions are\n// unavailable it falls back to an RGBA8 over-composite (no HDR/blur) — same shape,\n// slightly flatter. Not exported — createLedDisplay wraps it with the draw API.\nimport type { RGB } from './color';\nimport type { ActiveSet } from './voxel-grid';\n\nexport type Projection = 'perspective' | 'orthographic';\n/** Visual look: 'hologram' (emissive glow, default) or 'comic' (cel-shaded + ink outline). */\nexport type LedStyle = 'hologram' | 'comic';\n/** LED sprite shape: 'round' (default) or 'square' (tiles gap-free). */\nexport type LedShape = 'round' | 'square';\n/** RGB sub-die arrangement (when `rgb` is on). `auto` picks the packing that best\n * fits the shape: a delta **triad** for round dies, an RGGB **quad** for square.\n * `stripe` is the classic R|G|B LCD-stripe look. */\nexport type RgbLayout = 'auto' | 'triad' | 'quad' | 'stripe';\n\n/** Live-updatable appearance (all except `antialias`, which is fixed at context creation). */\nexport type RendererParams = {\n\tbackground: RGB;\n\toffColor: RGB;\n\ttint: RGB;\n\tglow: number;\n\tledSize: number;\n\t/** Radius of the tiny \"physical LED\" speck shown when off, as a fraction of the sprite (0..1). */\n\toffSize: number;\n\tstyle: LedStyle;\n\tshape: LedShape;\n\t/** Comic ink-border thickness, 0..1 of the sprite radius (0 = no border). */\n\toutline: number;\n\toutlineColor: RGB;\n\t/** Brick lattice: offset every other row by half a cell (breaks the regular grid → less moiré). */\n\tstagger: boolean;\n\t/** Render each LED as three R/G/B sub-emitters (real RGB-LED look). */\n\trgb: boolean;\n\t/** Sub-die packing when `rgb` is on (default 'auto' — triad for round, quad for square). */\n\trgbLayout: RgbLayout;\n\t/** Comic brightness: false = cel-shade (keep tone), true = flat vivid (full value). */\n\tvivid: boolean;\n\tantialias: boolean;\n};\n\nexport type RenderView = {\n\tyaw: number;\n\tpitch: number;\n\tdist: number;\n\tfov: number;\n\tgain: number;\n\tprojection: Projection;\n};\n\nexport type Renderer = {\n\tleds: Float32Array; // nx*ny*nz*3, RGB ≥0. Write then render().\n\trender(view: RenderView, active?: ActiveSet): void;\n\tconfigure(p: Partial<RendererParams>): void;\n\tdispose(): void;\n};\n\n// --- tiny column-major mat4 -------------------------------------------------\ntype M4 = Float32Array;\nconst m4 = (): M4 => new Float32Array(16);\nfunction ident(o: M4): M4 {\n\to.fill(0);\n\to[0] = o[5] = o[10] = o[15] = 1;\n\treturn o;\n}\nfunction mul(o: M4, a: M4, b: M4): M4 {\n\tfor (let c = 0; c < 4; c++)\n\t\tfor (let r = 0; r < 4; r++) {\n\t\t\tlet s = 0;\n\t\t\tfor (let k = 0; k < 4; k++) s += a[k * 4 + r] * b[c * 4 + k];\n\t\t\to[c * 4 + r] = s;\n\t\t}\n\treturn o;\n}\nfunction perspective(o: M4, fov: number, aspect: number, near: number, far: number): M4 {\n\tconst f = 1 / Math.tan(fov / 2);\n\to.fill(0);\n\to[0] = f / aspect;\n\to[5] = f;\n\to[10] = (far + near) / (near - far);\n\to[11] = -1;\n\to[14] = (2 * far * near) / (near - far);\n\treturn o;\n}\nfunction ortho(o: M4, halfH: number, aspect: number, near: number, far: number): M4 {\n\tconst halfW = halfH * aspect;\n\to.fill(0);\n\to[0] = 1 / halfW;\n\to[5] = 1 / halfH;\n\to[10] = -2 / (far - near);\n\to[14] = -(far + near) / (far - near);\n\to[15] = 1;\n\treturn o;\n}\nfunction orbit(o: M4, yaw: number, pitch: number, dist: number): M4 {\n\t// view = translate(0,0,-dist) · Rx(pitch) · Ry(yaw)\n\tconst cy = Math.cos(yaw),\n\t\tsy = Math.sin(yaw);\n\tconst cx = Math.cos(pitch),\n\t\tsx = Math.sin(pitch);\n\tconst ry = m4();\n\tident(ry);\n\try[0] = cy;\n\try[2] = -sy;\n\try[8] = sy;\n\try[10] = cy;\n\tconst rx = m4();\n\tident(rx);\n\trx[5] = cx;\n\trx[6] = sx;\n\trx[9] = -sx;\n\trx[10] = cx;\n\tconst rot = m4();\n\tmul(rot, rx, ry);\n\tident(o);\n\to[14] = -dist;\n\tconst view = m4();\n\tmul(view, o, rot);\n\to.set(view);\n\treturn o;\n}\n\nconst VERT = `\n attribute vec3 aPos;\n attribute vec3 aColor;\n uniform mat4 uMVP;\n uniform float uPointScale;\n varying vec3 vColor;\n void main() {\n vColor = aColor;\n vec4 p = uMVP * vec4(aPos, 1.0);\n gl_Position = p;\n gl_PointSize = clamp(uPointScale / p.w, 2.0, 64.0); // nearer LEDs bigger → depth cue\n }\n`;\n// Emissive LEDs, drawn into an offscreen buffer. The emitted light is a large soft\n// glow; the physical LED is a tiny dot shown even when off (uDotSize ≪ 1). Alpha\n// tracks brightness, so unlit LEDs are transparent (a real off LED is a dark speck).\nconst FRAG = `\n precision highp float;\n uniform float uGain;\n uniform float uGlow;\n uniform float uDotSize;\n uniform float uOutline; // comic ink-border thickness (0 = none)\n uniform vec3 uOff;\n uniform vec3 uTint;\n uniform vec3 uOutlineColor;\n uniform int uStyle; // 0 = hologram (emissive), 1 = comic (opaque, cel/vivid)\n uniform int uShape; // 0 = round, 1 = square\n uniform int uVivid; // comic: 0 = cel-shade (keep brightness), 1 = flat vivid (full value)\n uniform int uRgb; // 1 = split each LED into R/G/B sub-emitters (real RGB-LED look)\n uniform int uRgbLayout; // 0 = triad, 1 = quad (RGGB), 2 = stripe\n varying vec3 vColor;\n void main() {\n vec2 q = gl_PointCoord * 2.0 - 1.0; // sprite coords [-1,1]\n\n // RGB mode: the pixel is a real RGB-LED module — separate single-colour dies whose\n // channels are DIVIDED across them, grouped inside ONE led's footprint so it reads\n // as one blended led at a distance but resolves into its dies up close. Three\n // packings, each matched to a footprint (see RgbLayout): a delta TRIAD (round),\n // an RGGB QUAD (square, ~full fill + double green), or the classic R|G|B STRIPE.\n if (uRgb == 1) {\n float cR, cG, cB; // per-channel coverage 0..1\n if (uRgbLayout == 1) {\n // QUAD (RGGB): 2×2 square sub-cells, two greens (eye is most green-sensitive).\n float h = 0.5;\n cR = smoothstep(1.0, 0.78, max(abs(q.x + 0.5), abs(q.y - 0.5)) / h);\n cG = max(smoothstep(1.0, 0.78, max(abs(q.x - 0.5), abs(q.y - 0.5)) / h),\n smoothstep(1.0, 0.78, max(abs(q.x + 0.5), abs(q.y + 0.5)) / h));\n cB = smoothstep(1.0, 0.78, max(abs(q.x - 0.5), abs(q.y + 0.5)) / h);\n } else if (uRgbLayout == 2) {\n // STRIPE: three full-height R|G|B bars (retro LCD look).\n cR = smoothstep(0.33, 0.02, abs(q.x + 0.66));\n cG = smoothstep(0.33, 0.02, abs(q.x));\n cB = smoothstep(0.33, 0.02, abs(q.x - 0.66));\n } else {\n // TRIAD: three round dies in a tight, overlapping triangle (round default).\n float s = 1.5;\n cR = smoothstep(1.0, 0.0, length(q - vec2(0.0, 0.28)) * s);\n cG = smoothstep(1.0, 0.0, length(q - vec2(-0.24, -0.16)) * s);\n cB = smoothstep(1.0, 0.0, length(q - vec2(0.24, -0.16)) * s);\n }\n if (uStyle == 1) {\n // Comic dies: vivid (full value) or cel-shaded (quantized), hue kept; gaps drop.\n vec3 base = vColor * uTint;\n float lum = max(max(base.r, base.g), base.b);\n if (lum < 0.02) discard;\n float scale = uVivid == 1 ? (1.0 / lum) : (ceil(min(lum, 1.0) * 4.0) / 4.0 / lum);\n vec3 full = min(base * scale * uGain, 1.0);\n vec3 col = vec3(full.r * step(0.5, cR), full.g * step(0.5, cG), full.b * step(0.5, cB));\n if (max(max(col.r, col.g), col.b) < 0.02) discard; // outside / all-off → gap\n gl_FragColor = vec4(col, 1.0);\n return;\n }\n // Hologram: each die glows its own channel; the glows ADD, so overlaps blend and\n // the module fuses to the true colour as it shrinks on screen.\n vec3 e = vec3(vColor.r * pow(cR, uGlow),\n vColor.g * pow(cG, uGlow),\n vColor.b * pow(cB, uGlow)) * uTint * uGain;\n gl_FragColor = vec4(e, clamp(max(max(e.r, e.g), e.b), 0.0, 1.0));\n return;\n }\n\n float d = (uShape == 1) ? max(abs(q.x), abs(q.y)) : length(q); // 0 centre → 1 edge\n if (uStyle == 1) {\n // Comic: bold, flat cel-shaded fill (+ optional ink border), round or square.\n // Brightness is QUANTIZED into a few bands (posterized) but the LED's actual\n // brightness + hue are kept — so tonal content (images, fading trails) reads\n // faithfully, not maxed to full value. Faint/unlit LEDs drop out entirely.\n if (d > 1.0) discard;\n vec3 base = vColor * uTint;\n float lum = max(max(base.r, base.g), base.b);\n if (lum < 0.02) discard; // dim / unlit → transparent\n // vivid: flatten to the full value of the hue (punchy flat pop-art). cel: keep\n // brightness, quantized into a few bands (posterized → tonal content reads).\n float scale = uVivid == 1 ? (1.0 / lum) : (ceil(min(lum, 1.0) * 4.0) / 4.0 / lum);\n vec3 col = min(base * scale * uGain, 1.0);\n col = mix(col, uOutlineColor, smoothstep(1.0 - uOutline, 1.0, d));\n gl_FragColor = vec4(col, 1.0);\n return;\n }\n // Hologram (emissive): soft glow + a tiny physical off-speck; alpha tracks\n // brightness so unlit LEDs are transparent. Square shape → a boxy glow.\n float core = smoothstep(1.0, 0.0, d);\n float glow = pow(core, uGlow);\n vec3 emit = vColor * uTint * glow * uGain;\n vec3 dot = uOff * smoothstep(uDotSize, 0.0, d);\n vec3 c = emit + dot;\n float cov = clamp(max(max(c.r, c.g), c.b), 0.0, 1.0);\n gl_FragColor = vec4(c, cov);\n }\n`;\n\n// Fullscreen-quad shaders (share COMP_VERT).\nconst COMP_VERT = `\n attribute vec2 aQuad;\n varying vec2 vUv;\n void main() {\n vUv = aQuad * 0.5 + 0.5;\n gl_Position = vec4(aQuad, 0.0, 1.0);\n }\n`;\n// Fallback (RGBA8): over-composite the accumulated light onto the background.\nconst COMP_FRAG = `\n precision highp float;\n uniform sampler2D uScene;\n uniform vec3 uBg;\n varying vec2 vUv;\n void main() {\n vec4 s = texture2D(uScene, vUv);\n gl_FragColor = vec4(uBg * (1.0 - clamp(s.a, 0.0, 1.0)) + s.rgb, 1.0);\n }\n`;\n// 9-tap separable Gaussian blur of the emissive HDR buffer → soft light halos.\nconst BLUR_FRAG = `\n precision highp float;\n uniform sampler2D uSrc;\n uniform vec2 uDir;\n varying vec2 vUv;\n void main() {\n float w0 = 0.227027, w1 = 0.1945946, w2 = 0.1216216, w3 = 0.054054, w4 = 0.016216;\n vec3 c = texture2D(uSrc, vUv).rgb * w0;\n c += texture2D(uSrc, vUv + uDir * 1.0).rgb * w1;\n c += texture2D(uSrc, vUv - uDir * 1.0).rgb * w1;\n c += texture2D(uSrc, vUv + uDir * 2.0).rgb * w2;\n c += texture2D(uSrc, vUv - uDir * 2.0).rgb * w2;\n c += texture2D(uSrc, vUv + uDir * 3.0).rgb * w3;\n c += texture2D(uSrc, vUv - uDir * 3.0).rgb * w3;\n c += texture2D(uSrc, vUv + uDir * 4.0).rgb * w4;\n c += texture2D(uSrc, vUv - uDir * 4.0).rgb * w4;\n gl_FragColor = vec4(c, 1.0);\n }\n`;\n// HDR: tone-map the light (emissive core + blurred halo) and over-composite on bg.\nconst BLOOM_FRAG = `\n precision highp float;\n uniform sampler2D uScene; // emissive HDR: rgb = light, a = coverage\n uniform sampler2D uBloom; // blurred halo\n uniform vec3 uBg;\n uniform float uStrength;\n varying vec2 vUv;\n vec3 aces(vec3 x) { return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0); }\n void main() {\n vec4 e = texture2D(uScene, vUv);\n vec3 halo = texture2D(uBloom, vUv).rgb * uStrength;\n vec3 light = e.rgb + halo;\n float cov = clamp(max(e.a, max(halo.r, max(halo.g, halo.b))), 0.0, 1.0);\n // Tone-map only the emitted light; the background is preserved and the light\n // is over-composited onto it (like a real emitter in front of a surface).\n gl_FragColor = vec4(mix(uBg, aces(light), cov), 1.0);\n }\n`;\n\nexport function createRenderer(\n\tcanvas: HTMLCanvasElement,\n\tnx: number,\n\tny: number,\n\tnz: number,\n\tparams: RendererParams,\n\t// The frame buffer is owned by the display (so it survives a context rebuild /\n\t// resize). nx*ny*nz*3 RGB. If omitted the renderer allocates its own.\n\tleds: Float32Array = new Float32Array(nx * ny * nz * 3)\n): Renderer | null {\n\tconst gl = canvas.getContext('webgl', {\n\t\tantialias: params.antialias,\n\t\talpha: false,\n\t\tpremultipliedAlpha: false,\n\t\tdepth: true // the comic style is opaque + depth-tested (hologram ignores it)\n\t});\n\tif (!gl) return null;\n\n\t// Half-float support for the HDR path (else the RGBA8 over-composite fallback).\n\tconst extHF = gl.getExtension('OES_texture_half_float');\n\tconst extCBF = gl.getExtension('EXT_color_buffer_half_float');\n\tconst extHFL = gl.getExtension('OES_texture_half_float_linear');\n\tconst HALF_FLOAT = extHF ? extHF.HALF_FLOAT_OES : 0;\n\tconst canHdr = !!(extHF && extCBF);\n\tconst hdrFilter = extHFL ? gl.LINEAR : gl.NEAREST;\n\n\t// Mutable appearance (updated by configure()).\n\tlet bg = params.background;\n\tlet off = params.offColor;\n\tlet tint = params.tint;\n\tlet glow = params.glow;\n\tlet ledSize = params.ledSize;\n\tlet offSize = params.offSize;\n\tlet style = params.style;\n\tlet shape = params.shape;\n\tlet outline = params.outline;\n\tlet outlineCol = params.outlineColor;\n\tlet stagger = params.stagger;\n\tlet rgb = params.rgb;\n\tlet rgbLayout = params.rgbLayout;\n\tlet vivid = params.vivid;\n\n\tfunction compile(type: number, src: string): WebGLShader | null {\n\t\tconst sh = gl!.createShader(type);\n\t\tif (!sh) return null;\n\t\tgl!.shaderSource(sh, src);\n\t\tgl!.compileShader(sh);\n\t\tif (!gl!.getShaderParameter(sh, gl!.COMPILE_STATUS)) {\n\t\t\tconsole.warn('led-grid shader:', gl!.getShaderInfoLog(sh));\n\t\t\treturn null;\n\t\t}\n\t\treturn sh;\n\t}\n\tfunction linkProg(vsSrc: string, fsSrc: string): WebGLProgram | null {\n\t\tconst vs = compile(gl!.VERTEX_SHADER, vsSrc);\n\t\tconst fs = compile(gl!.FRAGMENT_SHADER, fsSrc);\n\t\tconst p = gl!.createProgram();\n\t\tif (!vs || !fs || !p) {\n\t\t\tif (vs) gl!.deleteShader(vs);\n\t\t\tif (fs) gl!.deleteShader(fs);\n\t\t\tif (p) gl!.deleteProgram(p);\n\t\t\treturn null;\n\t\t}\n\t\tgl!.attachShader(p, vs);\n\t\tgl!.attachShader(p, fs);\n\t\tgl!.linkProgram(p);\n\t\t// The shaders are linked into the program now; flag the standalone shader objects\n\t\t// for deletion so they don't linger (otherwise one leak per renderer rebuild —\n\t\t// resolution changes / context-restore each build a fresh renderer).\n\t\tgl!.deleteShader(vs);\n\t\tgl!.deleteShader(fs);\n\t\tif (!gl!.getProgramParameter(p, gl!.LINK_STATUS)) {\n\t\t\tconsole.warn('led-grid link:', gl!.getProgramInfoLog(p));\n\t\t\tgl!.deleteProgram(p);\n\t\t\treturn null;\n\t\t}\n\t\treturn p;\n\t}\n\tconst prog = linkProg(VERT, FRAG);\n\tif (!prog) return null;\n\tgl.useProgram(prog);\n\n\tconst count = nx * ny * nz;\n\t// Uniform LED spacing so a non-cube grid isn't stretched: the largest axis\n\t// spans [-1,1], the others keep the same spacing, centred.\n\tconst maxDim = Math.max(nx, ny, nz);\n\tconst spacing = maxDim > 1 ? 2 / (maxDim - 1) : 0;\n\tconst positions = new Float32Array(count * 3);\n\t// Static lattice positions (index order z→y→x, matching the leds buffer). When\n\t// `stagger`, odd rows shift +half a cell in x for a brick layout — this also\n\t// perturbs the regular grid, which reduces the view-dependent moiré.\n\tfunction computePositions() {\n\t\tlet i = 0;\n\t\tfor (let z = 0; z < nz; z++)\n\t\t\tfor (let y = 0; y < ny; y++) {\n\t\t\t\tconst dx = stagger && y & 1 ? spacing * 0.5 : 0;\n\t\t\t\tfor (let x = 0; x < nx; x++) {\n\t\t\t\t\tpositions[i * 3] = (x - (nx - 1) / 2) * spacing + dx;\n\t\t\t\t\tpositions[i * 3 + 1] = (y - (ny - 1) / 2) * spacing;\n\t\t\t\t\tpositions[i * 3 + 2] = (z - (nz - 1) / 2) * spacing;\n\t\t\t\t\ti++;\n\t\t\t\t}\n\t\t\t}\n\t}\n\tcomputePositions();\n\n\t// Culling: each frame we pack only the LIT voxels into an interleaved\n\t// [x,y,z, r,g,b] buffer and draw just those — so a sparse shape in a big grid\n\t// rasterizes (and uploads) a handful of sprites instead of all nx·ny·nz.\n\tconst packed = new Float32Array(count * 6);\n\tconst packedBuf = gl.createBuffer();\n\n\tconst aPos = gl.getAttribLocation(prog, 'aPos');\n\tconst aColor = gl.getAttribLocation(prog, 'aColor');\n\tconst uMVP = gl.getUniformLocation(prog, 'uMVP');\n\tconst uPointScale = gl.getUniformLocation(prog, 'uPointScale');\n\tconst uGain = gl.getUniformLocation(prog, 'uGain');\n\tconst uGlow = gl.getUniformLocation(prog, 'uGlow');\n\tconst uDotSize = gl.getUniformLocation(prog, 'uDotSize');\n\tconst uOff = gl.getUniformLocation(prog, 'uOff');\n\tconst uTint = gl.getUniformLocation(prog, 'uTint');\n\tconst uOutline = gl.getUniformLocation(prog, 'uOutline');\n\tconst uOutlineColor = gl.getUniformLocation(prog, 'uOutlineColor');\n\tconst uStyle = gl.getUniformLocation(prog, 'uStyle');\n\tconst uShape = gl.getUniformLocation(prog, 'uShape');\n\tconst uRgb = gl.getUniformLocation(prog, 'uRgb');\n\tconst uRgbLayout = gl.getUniformLocation(prog, 'uRgbLayout');\n\tconst uVivid = gl.getUniformLocation(prog, 'uVivid');\n\n\t// LEDs accumulate additively into the offscreen buffer (order-independent).\n\tfunction setEmissiveState() {\n\t\tgl!.disable(gl!.DEPTH_TEST);\n\t\tgl!.enable(gl!.BLEND);\n\t\tgl!.blendFunc(gl!.ONE, gl!.ONE);\n\t}\n\tsetEmissiveState();\n\n\t// --- shared fullscreen quad ---\n\tlet quadBuf: WebGLBuffer | null = null;\n\tfunction ensureQuad() {\n\t\tif (quadBuf) return;\n\t\tquadBuf = gl!.createBuffer();\n\t\tgl!.bindBuffer(gl!.ARRAY_BUFFER, quadBuf);\n\t\tgl!.bufferData(\n\t\t\tgl!.ARRAY_BUFFER,\n\t\t\tnew Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),\n\t\t\tgl!.STATIC_DRAW\n\t\t);\n\t}\n\tfunction drawQuad(aLoc: number) {\n\t\tgl!.bindBuffer(gl!.ARRAY_BUFFER, quadBuf);\n\t\tgl!.enableVertexAttribArray(aLoc);\n\t\tgl!.vertexAttribPointer(aLoc, 2, gl!.FLOAT, false, 0, 0);\n\t\tgl!.drawArrays(gl!.TRIANGLES, 0, 6);\n\t}\n\tfunction makeTex(w: number, h: number, type: number, filter: number): WebGLTexture | null {\n\t\tconst g = gl!;\n\t\tconst t = g.createTexture();\n\t\tg.bindTexture(g.TEXTURE_2D, t);\n\t\tg.texImage2D(g.TEXTURE_2D, 0, g.RGBA, w, h, 0, g.RGBA, type, null);\n\t\tg.texParameteri(g.TEXTURE_2D, g.TEXTURE_MIN_FILTER, filter);\n\t\tg.texParameteri(g.TEXTURE_2D, g.TEXTURE_MAG_FILTER, filter);\n\t\tg.texParameteri(g.TEXTURE_2D, g.TEXTURE_WRAP_S, g.CLAMP_TO_EDGE);\n\t\tg.texParameteri(g.TEXTURE_2D, g.TEXTURE_WRAP_T, g.CLAMP_TO_EDGE);\n\t\treturn t;\n\t}\n\tfunction attach(fbo: WebGLFramebuffer | null, tex: WebGLTexture | null): boolean {\n\t\tconst g = gl!;\n\t\tg.bindFramebuffer(g.FRAMEBUFFER, fbo);\n\t\tg.framebufferTexture2D(g.FRAMEBUFFER, g.COLOR_ATTACHMENT0, g.TEXTURE_2D, tex, 0);\n\t\tconst ok = g.checkFramebufferStatus(g.FRAMEBUFFER) === g.FRAMEBUFFER_COMPLETE;\n\t\tg.bindFramebuffer(g.FRAMEBUFFER, null);\n\t\treturn ok;\n\t}\n\n\t// --- RGBA8 fallback resources (over-composite, no blur) ---\n\tlet compProg: WebGLProgram | null = null;\n\tlet ldrFbo: WebGLFramebuffer | null = null;\n\tlet ldrTex: WebGLTexture | null = null;\n\tlet aQuadComp = -1;\n\tlet uCompScene: WebGLUniformLocation | null = null;\n\tlet uCompBg: WebGLUniformLocation | null = null;\n\tlet ldrW = 0;\n\tlet ldrH = 0;\n\tfunction ensureLdr(w: number, h: number): boolean {\n\t\tconst g = gl!;\n\t\tif (!compProg) {\n\t\t\tcompProg = linkProg(COMP_VERT, COMP_FRAG);\n\t\t\tif (!compProg) return false;\n\t\t\tensureQuad();\n\t\t\taQuadComp = g.getAttribLocation(compProg, 'aQuad');\n\t\t\tuCompScene = g.getUniformLocation(compProg, 'uScene');\n\t\t\tuCompBg = g.getUniformLocation(compProg, 'uBg');\n\t\t\tldrFbo = g.createFramebuffer();\n\t\t}\n\t\tif (w !== ldrW || h !== ldrH) {\n\t\t\tif (ldrTex) g.deleteTexture(ldrTex);\n\t\t\tldrTex = makeTex(w, h, g.UNSIGNED_BYTE, g.LINEAR);\n\t\t\tif (!attach(ldrFbo, ldrTex)) return false;\n\t\t\tldrW = w;\n\t\t\tldrH = h;\n\t\t}\n\t\treturn true;\n\t}\n\n\t// --- HDR bloom resources (half-float scene + blur + tone-map) ---\n\tlet blurProg: WebGLProgram | null = null;\n\tlet bloomProg: WebGLProgram | null = null;\n\tlet hdrFailed = false;\n\tlet sceneFbo: WebGLFramebuffer | null = null;\n\tlet bloomFboA: WebGLFramebuffer | null = null;\n\tlet bloomFboB: WebGLFramebuffer | null = null;\n\tlet sceneTex: WebGLTexture | null = null;\n\tlet bloomTexA: WebGLTexture | null = null;\n\tlet bloomTexB: WebGLTexture | null = null;\n\tlet aQuadBlur = -1;\n\tlet aQuadBloom = -1;\n\tlet uBlurSrc: WebGLUniformLocation | null = null;\n\tlet uBlurDir: WebGLUniformLocation | null = null;\n\tlet uBloomScene: WebGLUniformLocation | null = null;\n\tlet uBloomHalo: WebGLUniformLocation | null = null;\n\tlet uBloomBg: WebGLUniformLocation | null = null;\n\tlet uBloomStrength: WebGLUniformLocation | null = null;\n\tlet hdrW = 0;\n\tlet hdrH = 0;\n\tfunction ensureHdr(w: number, h: number): boolean {\n\t\tif (hdrFailed || !canHdr) return false;\n\t\tconst g = gl!;\n\t\tif (!bloomProg) {\n\t\t\tblurProg = linkProg(COMP_VERT, BLUR_FRAG);\n\t\t\tbloomProg = linkProg(COMP_VERT, BLOOM_FRAG);\n\t\t\tif (!blurProg || !bloomProg) {\n\t\t\t\thdrFailed = true;\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tensureQuad();\n\t\t\taQuadBlur = g.getAttribLocation(blurProg, 'aQuad');\n\t\t\tuBlurSrc = g.getUniformLocation(blurProg, 'uSrc');\n\t\t\tuBlurDir = g.getUniformLocation(blurProg, 'uDir');\n\t\t\taQuadBloom = g.getAttribLocation(bloomProg, 'aQuad');\n\t\t\tuBloomScene = g.getUniformLocation(bloomProg, 'uScene');\n\t\t\tuBloomHalo = g.getUniformLocation(bloomProg, 'uBloom');\n\t\t\tuBloomBg = g.getUniformLocation(bloomProg, 'uBg');\n\t\t\tuBloomStrength = g.getUniformLocation(bloomProg, 'uStrength');\n\t\t\tsceneFbo = g.createFramebuffer();\n\t\t\tbloomFboA = g.createFramebuffer();\n\t\t\tbloomFboB = g.createFramebuffer();\n\t\t}\n\t\tif (w !== hdrW || h !== hdrH) {\n\t\t\tconst hw = Math.max(1, Math.ceil(w / 4));\n\t\t\tconst hh = Math.max(1, Math.ceil(h / 4));\n\t\t\tfor (const t of [sceneTex, bloomTexA, bloomTexB]) if (t) g.deleteTexture(t);\n\t\t\tsceneTex = makeTex(w, h, HALF_FLOAT, hdrFilter);\n\t\t\tbloomTexA = makeTex(hw, hh, HALF_FLOAT, hdrFilter);\n\t\t\tbloomTexB = makeTex(hw, hh, HALF_FLOAT, hdrFilter);\n\t\t\tif (\n\t\t\t\t!attach(sceneFbo, sceneTex) ||\n\t\t\t\t!attach(bloomFboA, bloomTexA) ||\n\t\t\t\t!attach(bloomFboB, bloomTexB)\n\t\t\t) {\n\t\t\t\thdrFailed = true; // driver can't render to half-float → use the fallback\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\thdrW = w;\n\t\t\thdrH = h;\n\t\t}\n\t\treturn true;\n\t}\n\n\tconst proj = m4();\n\tconst vm = m4();\n\tconst mvp = m4();\n\tlet activeSrc: ActiveSet | undefined;\n\n\tfunction pack(j: number, k: number) {\n\t\tconst o = k * 6;\n\t\tpacked[o] = positions[j];\n\t\tpacked[o + 1] = positions[j + 1];\n\t\tpacked[o + 2] = positions[j + 2];\n\t\tpacked[o + 3] = leds[j];\n\t\tpacked[o + 4] = leds[j + 1];\n\t\tpacked[o + 5] = leds[j + 2];\n\t}\n\n\tfunction drawPoints() {\n\t\tconst g = gl!;\n\t\t// Pack lit voxels (position + colour) into one interleaved buffer. If a\n\t\t// lattice is requested (offColor != black) the fragment draws a speck at\n\t\t// *every* node, so we must keep unlit voxels too — no culling then.\n\t\tconst keepUnlit = off[0] > 0 || off[1] > 0 || off[2] > 0;\n\t\tlet k = 0;\n\t\tif (activeSrc && !activeSrc.all && !keepUnlit) {\n\t\t\t// Fast path: iterate only the cells lit since the last clear (O(lit)).\n\t\t\tconst list = activeSrc.list;\n\t\t\tconst n = activeSrc.count;\n\t\t\tfor (let a = 0; a < n; a++) {\n\t\t\t\tconst j = list[a] * 3;\n\t\t\t\tif (leds[j] > 0 || leds[j + 1] > 0 || leds[j + 2] > 0) pack(j, k++);\n\t\t\t}\n\t\t} else {\n\t\t\t// Full scan (fill / lattice / raw markAll / no tracking).\n\t\t\tfor (let idx = 0; idx < count; idx++) {\n\t\t\t\tconst j = idx * 3;\n\t\t\t\tif (keepUnlit || leds[j] > 0 || leds[j + 1] > 0 || leds[j + 2] > 0) pack(j, k++);\n\t\t\t}\n\t\t}\n\t\tg.bindBuffer(g.ARRAY_BUFFER, packedBuf);\n\t\tg.bufferData(g.ARRAY_BUFFER, packed.subarray(0, k * 6), g.DYNAMIC_DRAW);\n\t\tg.enableVertexAttribArray(aPos);\n\t\tg.vertexAttribPointer(aPos, 3, g.FLOAT, false, 24, 0);\n\t\tg.enableVertexAttribArray(aColor);\n\t\tg.vertexAttribPointer(aColor, 3, g.FLOAT, false, 24, 12);\n\t\tg.drawArrays(g.POINTS, 0, k);\n\t}\n\n\tfunction render(view: RenderView, active?: ActiveSet) {\n\t\tconst g = gl!;\n\t\tactiveSrc = active;\n\t\tconst w = canvas.width,\n\t\t\th = canvas.height;\n\n\t\torbit(vm, view.yaw, view.pitch, view.dist);\n\t\tconst aspect = w / h;\n\t\t// `view.fov` is the vertical FOV, which frames landscape well but lets a\n\t\t// portrait/narrow viewport clip the grid's sides. On aspect < 1 widen the FOV\n\t\t// so the grid fits the *width* (fit-to-narrower-dimension) — landscape is\n\t\t// unchanged. pointScale uses the same effective FOV so sprites scale with it.\n\t\tconst fov = aspect < 1 ? 2 * Math.atan(Math.tan(view.fov / 2) / aspect) : view.fov;\n\t\tlet pointScale: number;\n\t\tif (view.projection === 'orthographic') {\n\t\t\tconst halfH = view.dist * Math.tan(fov / 2);\n\t\t\tortho(proj, halfH, aspect, 0.1, 100);\n\t\t\tpointScale = ((spacing * (h / 2)) / halfH) * ledSize; // w==1, constant size\n\t\t} else {\n\t\t\tperspective(proj, fov, aspect, 0.1, 100);\n\t\t\tpointScale = spacing * (h / 2) * (1 / Math.tan(fov / 2)) * ledSize;\n\t\t}\n\t\tmul(mvp, proj, vm);\n\n\t\tg.useProgram(prog);\n\t\tg.uniformMatrix4fv(uMVP, false, mvp);\n\t\tg.uniform1f(uPointScale, pointScale);\n\t\tg.uniform1f(uGain, view.gain);\n\t\tg.uniform1f(uGlow, glow);\n\t\tg.uniform1f(uDotSize, offSize);\n\t\tg.uniform3fv(uOff, off);\n\t\tg.uniform3fv(uTint, tint);\n\t\tg.uniform1f(uOutline, outline);\n\t\tg.uniform3fv(uOutlineColor, outlineCol);\n\t\tg.uniform1i(uStyle, style === 'comic' ? 1 : 0);\n\t\tg.uniform1i(uShape, shape === 'square' ? 1 : 0);\n\t\tg.uniform1i(uRgb, rgb ? 1 : 0);\n\t\t// 'auto' → quad on square LEDs, triad on round (best packing per footprint).\n\t\tconst layout = rgbLayout === 'auto' ? (shape === 'square' ? 'quad' : 'triad') : rgbLayout;\n\t\tg.uniform1i(uRgbLayout, layout === 'quad' ? 1 : layout === 'stripe' ? 2 : 0);\n\t\tg.uniform1i(uVivid, vivid ? 1 : 0);\n\n\t\tif (style === 'comic') {\n\t\t\trenderComic(w, h);\n\t\t\treturn;\n\t\t}\n\t\tsetEmissiveState(); // additive for the offscreen point pass (may follow a comic frame)\n\t\tif (ensureHdr(w, h)) renderBloom(w, h);\n\t\telse if (ensureLdr(w, h)) renderLdr(w, h);\n\t}\n\n\t// Comic: opaque, depth-tested LEDs drawn straight to screen over the background.\n\tfunction renderComic(w: number, h: number) {\n\t\tconst g = gl!;\n\t\tg.bindFramebuffer(g.FRAMEBUFFER, null);\n\t\tg.viewport(0, 0, w, h);\n\t\tg.disable(g.BLEND);\n\t\tg.enable(g.DEPTH_TEST);\n\t\tg.clearColor(bg[0], bg[1], bg[2], 1);\n\t\tg.clear(g.COLOR_BUFFER_BIT | g.DEPTH_BUFFER_BIT);\n\t\tdrawPoints();\n\t}\n\n\t// RGBA8 fallback: accumulate light, then over-composite onto the background.\n\tfunction renderLdr(w: number, h: number) {\n\t\tconst g = gl!;\n\t\tg.bindFramebuffer(g.FRAMEBUFFER, ldrFbo);\n\t\tg.viewport(0, 0, w, h);\n\t\tg.clearColor(0, 0, 0, 0);\n\t\tg.clear(g.COLOR_BUFFER_BIT);\n\t\tdrawPoints();\n\t\tg.bindFramebuffer(g.FRAMEBUFFER, null);\n\t\tg.viewport(0, 0, w, h);\n\t\tg.disable(g.BLEND);\n\t\tg.useProgram(compProg);\n\t\tg.activeTexture(g.TEXTURE0);\n\t\tg.bindTexture(g.TEXTURE_2D, ldrTex);\n\t\tg.uniform1i(uCompScene, 0);\n\t\tg.uniform3fv(uCompBg, bg);\n\t\tdrawQuad(aQuadComp);\n\t\tg.useProgram(prog);\n\t\tsetEmissiveState();\n\t}\n\n\t// HDR: emissive → blur (halos) → tone-map + over-composite.\n\tfunction renderBloom(w: number, h: number) {\n\t\tconst g = gl!;\n\t\tconst hw = Math.max(1, Math.ceil(w / 4));\n\t\tconst hh = Math.max(1, Math.ceil(h / 4));\n\n\t\tg.bindFramebuffer(g.FRAMEBUFFER, sceneFbo);\n\t\tg.viewport(0, 0, w, h);\n\t\tg.clearColor(0, 0, 0, 0);\n\t\tg.clear(g.COLOR_BUFFER_BIT);\n\t\tdrawPoints();\n\n\t\tg.disable(g.BLEND);\n\t\tg.useProgram(blurProg);\n\t\tg.bindFramebuffer(g.FRAMEBUFFER, bloomFboA);\n\t\tg.viewport(0, 0, hw, hh);\n\t\tg.activeTexture(g.TEXTURE0);\n\t\tg.bindTexture(g.TEXTURE_2D, sceneTex);\n\t\tg.uniform1i(uBlurSrc, 0);\n\t\tg.uniform2f(uBlurDir, 1 / w, 0);\n\t\tdrawQuad(aQuadBlur);\n\t\tg.bindFramebuffer(g.FRAMEBUFFER, bloomFboB);\n\t\tg.viewport(0, 0, hw, hh);\n\t\tg.bindTexture(g.TEXTURE_2D, bloomTexA);\n\t\tg.uniform2f(uBlurDir, 0, 1 / hh);\n\t\tdrawQuad(aQuadBlur);\n\n\t\tg.bindFramebuffer(g.FRAMEBUFFER, null);\n\t\tg.viewport(0, 0, w, h);\n\t\tg.useProgram(bloomProg);\n\t\tg.activeTexture(g.TEXTURE0);\n\t\tg.bindTexture(g.TEXTURE_2D, sceneTex);\n\t\tg.uniform1i(uBloomScene, 0);\n\t\tg.activeTexture(g.TEXTURE1);\n\t\tg.bindTexture(g.TEXTURE_2D, bloomTexB);\n\t\tg.uniform1i(uBloomHalo, 1);\n\t\tg.uniform3fv(uBloomBg, bg);\n\t\tg.uniform1f(uBloomStrength, 1.0);\n\t\tdrawQuad(aQuadBloom);\n\t\tg.activeTexture(g.TEXTURE0);\n\n\t\tg.useProgram(prog);\n\t\tsetEmissiveState();\n\t}\n\n\tfunction configure(p: Partial<RendererParams>) {\n\t\tif (p.background) bg = p.background;\n\t\tif (p.offColor) off = p.offColor;\n\t\tif (p.tint) tint = p.tint;\n\t\tif (p.glow !== undefined) glow = p.glow;\n\t\tif (p.ledSize !== undefined) ledSize = p.ledSize;\n\t\tif (p.offSize !== undefined) offSize = p.offSize;\n\t\tif (p.style) style = p.style;\n\t\tif (p.shape) shape = p.shape;\n\t\tif (p.outline !== undefined) outline = p.outline;\n\t\tif (p.outlineColor) outlineCol = p.outlineColor;\n\t\tif (p.stagger !== undefined && p.stagger !== stagger) {\n\t\t\tstagger = p.stagger;\n\t\t\tcomputePositions(); // O(count), rare (only on toggle)\n\t\t}\n\t\tif (p.rgb !== undefined) rgb = p.rgb;\n\t\tif (p.rgbLayout) rgbLayout = p.rgbLayout;\n\t\tif (p.vivid !== undefined) vivid = p.vivid;\n\t}\n\n\t// Frees GL objects only — does NOT lose the context. The display owns the\n\t// canvas/context lifetime (so it can rebuild a renderer on the same canvas after\n\t// a resize or a context-restore); it loses the context itself on final teardown.\n\tfunction dispose() {\n\t\tconst g = gl!;\n\t\tg.deleteBuffer(packedBuf);\n\t\tg.deleteProgram(prog);\n\t\tif (compProg) g.deleteProgram(compProg);\n\t\tif (blurProg) g.deleteProgram(blurProg);\n\t\tif (bloomProg) g.deleteProgram(bloomProg);\n\t\tif (quadBuf) g.deleteBuffer(quadBuf);\n\t\tfor (const t of [ldrTex, sceneTex, bloomTexA, bloomTexB]) if (t) g.deleteTexture(t);\n\t\tfor (const f of [ldrFbo, sceneFbo, bloomFboA, bloomFboB]) if (f) g.deleteFramebuffer(f);\n\t}\n\n\treturn { leds, render, configure, dispose };\n}\n","// The canvas-like voxel API of @glowbox/led-grid, factored out of the WebGL display\n// so the geometry is usable — and unit-testable — with no GL context. A grid is\n// \"a tiny 3D canvas\": an nx×ny×nz lattice of RGB LEDs backed by a flat\n// Float32Array (x + nx*(y + ny*z), ×3). Colours accept a `Color` (an [r,g,b]\n// triple 0..1 — values >1 bloom under the display's additive glow — or any CSS\n// colour string); reads return a plain `RGB`. Integer coords; out of range is\n// silently ignored.\n//\n// createLedDisplay composes a grid over the renderer's LED buffer; you can also\n// use a grid on its own (e.g. to compute frames headlessly).\nimport { type Color, parseColor, type RGB, type Vec3 } from './color';\n\nexport type { Color, RGB, Vec3 } from './color';\n\n/** The set of *lit* cells since the last clear, so the renderer (and clear) can\n * work in O(lit) instead of O(volume). `all` = treat every cell as lit (after a\n * fill, or markAll for raw-buffer writers). `list[0..count]` holds cell indices.\n * Package-internal (the culling wiring) — not part of the public surface. */\nexport type ActiveSet = { list: Uint32Array; count: number; all: boolean };\n\n/** A 3D LED lattice you draw voxels onto. Pure state — no rendering. */\nexport interface VoxelGrid {\n\treadonly nx: number;\n\treadonly ny: number;\n\treadonly nz: number;\n\t/** Raw LED buffer (nx*ny*nz*3 RGB). You can read it freely; if you *write* it\n\t * directly, call markAll() so culling/clear don't skip your cells. */\n\treadonly leds: Float32Array;\n\t/** Force every cell to be treated as lit (for raw-buffer writers). */\n\tmarkAll(): void;\n\t/** Flat buffer offset of (x,y,z) — start of its 3 RGB floats. */\n\tindex(x: number, y: number, z: number): number;\n\tinBounds(x: number, y: number, z: number): boolean;\n\n\t// --- canvas-like voxel drawing (integer coords; out-of-range is ignored) ---\n\tplot(x: number, y: number, z: number, color: Color): void;\n\tadd(x: number, y: number, z: number, color: Color): void;\n\tget(x: number, y: number, z: number): RGB;\n\t/** Clear the whole grid (default black). */\n\tclear(color?: Color): void;\n\t/** Fill every LED with a colour (alias of clear(color)). */\n\tfill(color: Color): void;\n\tline(a: Vec3, b: Vec3, color: Color): void;\n\t/** Axis-aligned box between two corners; `filled` fills it, else wireframe edges. */\n\tbox(min: Vec3, max: Vec3, color: Color, filled?: boolean): void;\n\t/** `filled` fills the ball, else a ~1-voxel-thick shell. */\n\tsphere(center: Vec3, radius: number, color: Color, filled?: boolean): void;\n}\n\n/** A grid plus its internal culling handle — what `createVoxelGrid` actually\n * returns; the display and renderer consume `active`, but it isn't part of the\n * public `VoxelGrid` surface. */\nexport interface VoxelGridInternal extends VoxelGrid {\n\t/** Lit-cell tracking (used by the display to cull). */\n\treadonly active: ActiveSet;\n}\n\n/** Create a voxel grid. Pass an existing `leds` buffer (e.g. the display's) to\n * draw straight into it; otherwise a fresh nx*ny*nz*3 Float32Array is allocated. */\nexport function createVoxelGrid(\n\tnx: number,\n\tny: number,\n\tnz: number,\n\tleds: Float32Array = new Float32Array(nx * ny * nz * 3)\n): VoxelGridInternal {\n\tconst cap = nx * ny * nz;\n\tconst idx = (x: number, y: number, z: number) => ((x | 0) + nx * ((y | 0) + ny * (z | 0))) * 3;\n\tconst inBounds = (x: number, y: number, z: number) => {\n\t\tx |= 0;\n\t\ty |= 0;\n\t\tz |= 0;\n\t\treturn x >= 0 && x < nx && y >= 0 && y < ny && z >= 0 && z < nz;\n\t};\n\n\t// Lit-cell tracking. `seen[cell]` stores the epoch a cell was last marked;\n\t// bumping `epoch` on clear invalidates all marks in O(1) (no array reset).\n\tconst active: ActiveSet = { list: new Uint32Array(cap), count: 0, all: false };\n\tconst seen = new Uint32Array(cap);\n\tlet epoch = 1;\n\tlet filled = false; // buffer has cells set outside the active list (fill/markAll)\n\tfunction mark(cell: number) {\n\t\tif (seen[cell] !== epoch) {\n\t\t\tseen[cell] = epoch;\n\t\t\tactive.list[active.count++] = cell;\n\t\t}\n\t}\n\n\t// Internal writers take an already-parsed RGB so geometry helpers parse once.\n\tfunction put(x: number, y: number, z: number, c: RGB) {\n\t\tif (!inBounds(x, y, z)) return;\n\t\tconst cell = (x | 0) + nx * ((y | 0) + ny * (z | 0));\n\t\tconst i = cell * 3;\n\t\tleds[i] = c[0];\n\t\tleds[i + 1] = c[1];\n\t\tleds[i + 2] = c[2];\n\t\tmark(cell);\n\t}\n\n\tfunction plot(x: number, y: number, z: number, color: Color) {\n\t\tput(x, y, z, parseColor(color));\n\t}\n\tfunction add(x: number, y: number, z: number, color: Color) {\n\t\tif (!inBounds(x, y, z)) return;\n\t\tconst c = parseColor(color);\n\t\tconst cell = (x | 0) + nx * ((y | 0) + ny * (z | 0));\n\t\tconst i = cell * 3;\n\t\tleds[i] += c[0];\n\t\tleds[i + 1] += c[1];\n\t\tleds[i + 2] += c[2];\n\t\tmark(cell);\n\t}\n\tfunction get(x: number, y: number, z: number): RGB {\n\t\tif (!inBounds(x, y, z)) return [0, 0, 0];\n\t\tconst i = idx(x, y, z);\n\t\treturn [leds[i], leds[i + 1], leds[i + 2]];\n\t}\n\tfunction clear(color: Color = [0, 0, 0]) {\n\t\tconst c = parseColor(color);\n\t\tif (c[0] === 0 && c[1] === 0 && c[2] === 0) {\n\t\t\t// Fast clear: zero only the cells lit since the last clear (O(lit)).\n\t\t\t// If the buffer was fully written (fill/markAll) we must zero it all once.\n\t\t\tif (filled) {\n\t\t\t\tleds.fill(0);\n\t\t\t\tfilled = false;\n\t\t\t} else {\n\t\t\t\tfor (let a = 0; a < active.count; a++) {\n\t\t\t\t\tconst i = active.list[a] * 3;\n\t\t\t\t\tleds[i] = leds[i + 1] = leds[i + 2] = 0;\n\t\t\t\t}\n\t\t\t}\n\t\t\tactive.count = 0;\n\t\t\tactive.all = false;\n\t\t\tepoch++;\n\t\t} else {\n\t\t\t// Fill every cell with a colour (rare) → whole buffer, marked \"all\".\n\t\t\tfor (let i = 0; i < leds.length; i += 3) {\n\t\t\t\tleds[i] = c[0];\n\t\t\t\tleds[i + 1] = c[1];\n\t\t\t\tleds[i + 2] = c[2];\n\t\t\t}\n\t\t\tactive.count = 0;\n\t\t\tactive.all = true;\n\t\t\tfilled = true;\n\t\t\tepoch++;\n\t\t}\n\t}\n\tfunction markAll() {\n\t\tactive.all = true;\n\t\tfilled = true;\n\t}\n\tfunction line(a: Vec3, b: Vec3, color: Color) {\n\t\t// 3D Bresenham: step the driving (longest) axis, accumulate error on the\n\t\t// other two → a contiguous voxel line, no gaps.\n\t\tconst c = parseColor(color);\n\t\tlet x0 = a[0] | 0,\n\t\t\ty0 = a[1] | 0,\n\t\t\tz0 = a[2] | 0;\n\t\tconst x1 = b[0] | 0,\n\t\t\ty1 = b[1] | 0,\n\t\t\tz1 = b[2] | 0;\n\t\tconst dx = Math.abs(x1 - x0),\n\t\t\tdy = Math.abs(y1 - y0),\n\t\t\tdz = Math.abs(z1 - z0);\n\t\tconst sx = x0 < x1 ? 1 : -1,\n\t\t\tsy = y0 < y1 ? 1 : -1,\n\t\t\tsz = z0 < z1 ? 1 : -1;\n\t\tconst dm = Math.max(dx, dy, dz);\n\t\tif (dm === 0) return put(x0, y0, z0, c);\n\t\tlet ex = dm / 2,\n\t\t\tey = dm / 2,\n\t\t\tez = dm / 2;\n\t\tfor (let s = 0; s <= dm; s++) {\n\t\t\tput(x0, y0, z0, c);\n\t\t\tex -= dx;\n\t\t\tif (ex < 0) {\n\t\t\t\tex += dm;\n\t\t\t\tx0 += sx;\n\t\t\t}\n\t\t\tey -= dy;\n\t\t\tif (ey < 0) {\n\t\t\t\tey += dm;\n\t\t\t\ty0 += sy;\n\t\t\t}\n\t\t\tez -= dz;\n\t\t\tif (ez < 0) {\n\t\t\t\tez += dm;\n\t\t\t\tz0 += sz;\n\t\t\t}\n\t\t}\n\t}\n\tfunction box(min: Vec3, max: Vec3, color: Color, filled = false) {\n\t\tconst c = parseColor(color);\n\t\tconst [x0, y0, z0] = [\n\t\t\tMath.min(min[0], max[0]),\n\t\t\tMath.min(min[1], max[1]),\n\t\t\tMath.min(min[2], max[2])\n\t\t];\n\t\tconst [x1, y1, z1] = [\n\t\t\tMath.max(min[0], max[0]),\n\t\t\tMath.max(min[1], max[1]),\n\t\t\tMath.max(min[2], max[2])\n\t\t];\n\t\t// Clamp the iteration to the grid — put() bounds-checks per voxel, but an off-grid or\n\t\t// very large box would otherwise spin over up to billions of no-op cells (a main-\n\t\t// thread hang). Edge tests below keep the *true* bounds, so off-grid faces are omitted.\n\t\tconst zLo = Math.max(z0, 0);\n\t\tconst zHi = Math.min(z1, nz - 1);\n\t\tconst yLo = Math.max(y0, 0);\n\t\tconst yHi = Math.min(y1, ny - 1);\n\t\tconst xLo = Math.max(x0, 0);\n\t\tconst xHi = Math.min(x1, nx - 1);\n\t\tfor (let z = zLo; z <= zHi; z++)\n\t\t\tfor (let y = yLo; y <= yHi; y++)\n\t\t\t\tfor (let x = xLo; x <= xHi; x++) {\n\t\t\t\t\tif (filled) {\n\t\t\t\t\t\tput(x, y, z, c);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// wireframe: on a face-boundary in ≥2 axes = an edge\n\t\t\t\t\t\tlet edges = 0;\n\t\t\t\t\t\tif (x === x0 || x === x1) edges++;\n\t\t\t\t\t\tif (y === y0 || y === y1) edges++;\n\t\t\t\t\t\tif (z === z0 || z === z1) edges++;\n\t\t\t\t\t\tif (edges >= 2) put(x, y, z, c);\n\t\t\t\t\t}\n\t\t\t\t}\n\t}\n\tfunction sphere(center: Vec3, radius: number, color: Color, filled = false) {\n\t\tconst c = parseColor(color);\n\t\tconst r2 = radius * radius;\n\t\t// Clamp the bounding box to the grid so a huge radius / off-grid centre can't spin\n\t\t// over billions of no-op voxels (put() bounds-checks each, but the loop wouldn't).\n\t\tconst x0 = Math.max(0, Math.floor(center[0] - radius)),\n\t\t\tx1 = Math.min(nx - 1, Math.ceil(center[0] + radius));\n\t\tconst y0 = Math.max(0, Math.floor(center[1] - radius)),\n\t\t\ty1 = Math.min(ny - 1, Math.ceil(center[1] + radius));\n\t\tconst z0 = Math.max(0, Math.floor(center[2] - radius)),\n\t\t\tz1 = Math.min(nz - 1, Math.ceil(center[2] + radius));\n\t\tfor (let z = z0; z <= z1; z++)\n\t\t\tfor (let y = y0; y <= y1; y++)\n\t\t\t\tfor (let x = x0; x <= x1; x++) {\n\t\t\t\t\tconst d2 = (x - center[0]) ** 2 + (y - center[1]) ** 2 + (z - center[2]) ** 2;\n\t\t\t\t\tif (filled ? d2 <= r2 : Math.abs(Math.sqrt(d2) - radius) <= 0.5) put(x, y, z, c);\n\t\t\t\t}\n\t}\n\n\treturn {\n\t\tnx,\n\t\tny,\n\t\tnz,\n\t\tleds,\n\t\tactive,\n\t\tmarkAll,\n\t\tindex: idx,\n\t\tinBounds,\n\t\tplot,\n\t\tadd,\n\t\tget,\n\t\tclear,\n\t\tfill: (color) => clear(color),\n\t\tline,\n\t\tbox,\n\t\tsphere\n\t};\n}\n","// Plain-JS core of @glowbox/led-grid: a 3D LED-grid *display* you can embed in any\n// SPA (framework wrappers live in sibling packages). Give it a canvas + a size\n// and it owns everything — WebGL rendering, orbit (auto + drag), zoom, resize and\n// the animation loop. You draw voxels each frame through the canvas-like API\n// (see voxel-grid.ts):\n//\n// const d = createLedDisplay(canvas, { size: [8, 8, 8] });\n// d.onFrame((d, dt) => { d.clear(); d.sphere([4, 4, 4], 3, [0, 0.6, 1]); });\n//\n// Colours (background, offColor, tint, and every draw call) are a `Color`: an\n// [r,g,b] triple 0..1 (values >1 bloom) or a CSS string. Appearance, camera and\n// interaction are grouped options, all live-updatable via setOptions().\nimport { parseColor, parseColor01 } from './color';\nimport {\n\tcreateRenderer,\n\ttype LedShape,\n\ttype LedStyle,\n\ttype Projection,\n\ttype RendererParams,\n\ttype RgbLayout\n} from './renderer';\nimport { type Color, createVoxelGrid, type VoxelGrid } from './voxel-grid';\n\nexport type { LedShape, LedStyle, Projection, RgbLayout } from './renderer';\nexport type { Color, RGB, Vec3, VoxelGrid } from './voxel-grid';\n\n/** LED appearance. */\nexport interface LedOptions {\n\t/** Visual look: 'hologram' (emissive glow, default) or 'comic' (cel-shaded + ink outline). */\n\tstyle?: LedStyle;\n\t/** Sprite shape: 'round' (default) or 'square' (tiles gap-free). */\n\tshape?: LedShape;\n\t/** Comic ink-border thickness, 0..1 of the sprite radius (default 0.25; 0 = no border). */\n\toutline?: number;\n\t/** Comic ink-border colour (default near-black). */\n\toutlineColor?: Color;\n\t/** Brick lattice: offset every other row by half a cell (default false). Also\n\t * breaks up the regular grid, reducing view-dependent moiré. */\n\tstagger?: boolean;\n\t/** Render each LED as three R/G/B sub-emitters — a real RGB-LED package look\n\t * (default false). Up close you see the R/G/B dies; they blend at a distance. */\n\trgb?: boolean;\n\t/** Sub-die packing when `rgb` is on (default 'auto'): a delta `triad` for round\n\t * LEDs, an RGGB `quad` for square, or the classic R\\|G\\|B `stripe`. */\n\trgbLayout?: RgbLayout;\n\t/** Comic brightness (default false): `false` = cel-shade — keep each LED's\n\t * brightness (posterized), so tonal content reads; `true` = flat **vivid**\n\t * pop-art — every lit LED at the full value of its hue. Comic style only. */\n\tvivid?: boolean;\n\t/** Sprite size relative to LED spacing (default 0.6). */\n\tsize?: number;\n\t/** Glow tightness — the falloff exponent; higher = tighter (default 2.2). Hologram only. */\n\tglow?: number;\n\t/** Colour of the tiny speck shown at an *unlit* LED node. Default black (no\n\t * lattice — only lit LEDs show). Set e.g. `[0.03,0.03,0.03]` to hint the grid. Hologram only. */\n\toffColor?: Color;\n\t/** Size of the tiny \"physical LED\" speck shown when off, 0..1 of the glow sprite\n\t * (default 0.35). A real off LED is a small dark dot, not a full-size disc. Hologram only. */\n\toffSize?: number;\n}\n\n/** Global colour / brightness. */\nexport interface ColorOptions {\n\t/** Clear colour behind the LEDs (default near-black [0.01,0.01,0.02]). */\n\tbackground?: Color;\n\t/** Overall brightness multiplier (default 1). */\n\tgain?: number;\n\t/** Multiplies every lit colour — handy for theming (default white [1,1,1]). */\n\ttint?: Color;\n}\n\n/** Camera & projection. */\nexport interface CameraOptions {\n\t/** Initial orbit angles (radians; default yaw 0.6, pitch 0.4). */\n\tyaw?: number;\n\tpitch?: number;\n\t/** Camera distance (default 3.6); also the zoom target. */\n\tdistance?: number;\n\t/** Vertical field of view in radians (default 0.9). */\n\tfov?: number;\n\tprojection?: Projection; // 'perspective' (default) | 'orthographic'\n\t/** Slow auto-spin, paused while interacting (default true). */\n\tautoOrbit?: boolean;\n\t/** Auto-spin speed in rad/s (default 0.45). */\n\torbitSpeed?: number;\n\t/** Clamp for drag pitch (default [-1.4, 1.4]). */\n\tpitchLimits?: [number, number];\n}\n\n/** Pointer interaction. */\nexport interface InteractionOptions {\n\t/** Pointer-drag to orbit (default true). */\n\tdrag?: boolean;\n\t/** Drag sensitivity in radians per pixel (default 0.01). */\n\tdragSpeed?: number;\n\t/** Wheel + pinch to zoom (default false). */\n\tzoom?: boolean;\n\t/** Min/max camera distance for zoom (default [1.5, 10]). */\n\tzoomLimits?: [number, number];\n}\n\n/** Rendering quality / lifecycle. */\nexport interface QualityOptions {\n\t/** Cap on devicePixelRatio (default 2). */\n\tpixelRatio?: number;\n\t/** MSAA — fixed at creation, ignored by setOptions (default true). */\n\tantialias?: boolean;\n\t/** Freeze the render loop; interaction + render()/snapshot() still repaint (default false). */\n\tpaused?: boolean;\n\t/** Cap the render loop to at most this many frames/sec (default: uncapped, i.e.\n\t * the display's refresh rate). A power/cadence knob — good for an always-on\n\t * ambient display (lower = less GPU/battery) or matching a hardware LED-cube's\n\t * refresh rate. Not a speed-up: each frame still costs the same. */\n\tfps?: number;\n}\n\nexport interface LedDisplayOptions {\n\t/** Grid size [nx, ny, nz]. */\n\tsize: [number, number, number];\n\tled?: LedOptions;\n\tcolor?: ColorOptions;\n\tcamera?: CameraOptions;\n\tinteraction?: InteractionOptions;\n\tquality?: QualityOptions;\n}\n\n/** Everything except `size` — live-updatable. */\nexport type LedDisplayPatch = Omit<LedDisplayOptions, 'size'>;\n\n/** Rolling per-frame performance stats (EMA-smoothed). `drawMs`/`renderMs` are\n * CPU time in the frame callback vs issuing the GL commands; `frameMs`/`fps` are\n * the true end-to-end rate. If fps is low but drawMs+renderMs are small, you're\n * GPU/vsync-bound (CPU timers don't capture GPU execution). */\nexport type DisplayStats = { fps: number; frameMs: number; drawMs: number; renderMs: number };\n\n/** A live LED-grid display: the voxel-drawing API (VoxelGrid) plus the loop,\n * camera control and lifecycle. */\nexport interface LedDisplay extends VoxelGrid {\n\t/** Live, EMA-smoothed frame timings (updated while the loop runs). */\n\treadonly stats: DisplayStats;\n\t/** Run `cb(display, dt)` every animation frame (auto-pauses when hidden, and\n\t * does not fire while `paused` — drive updates yourself with `render()` then).\n\t * Returns a stop() that clears the callback. */\n\tonFrame(cb: (d: LedDisplay, dt: number) => void): () => void;\n\t/** Draw the current LED buffer once (also called each frame by the loop). */\n\trender(): void;\n\t/** Set the brightness multiplier (shortcut for setOptions({ color: { gain } })). */\n\tsetGain(gain: number): void;\n\t/** Freeze/unfreeze the loop (render-on-demand when frozen). */\n\tsetPaused(paused: boolean): void;\n\t/** Live-update any option except `size`. */\n\tsetOptions(patch: LedDisplayPatch): void;\n\t/** Imperatively move the camera. */\n\tsetCamera(c: { yaw?: number; pitch?: number; distance?: number }): void;\n\t/** Recompute the drawing-buffer size from the canvas box + pixelRatio, then render.\n\t * Pass a `[nx, ny, nz]` to also change the grid size in place — reallocating the\n\t * buffer on the *same* canvas (no context loss), preserving camera + options. */\n\tresize(size?: [number, number, number]): void;\n\t/** Render once and return a PNG data URL (for previews / frame export). */\n\tsnapshot(): string;\n\tdispose(): void;\n}\n\nconst clamp = (n: number, lo: number, hi: number) => (n < lo ? lo : n > hi ? hi : n);\n\n// Upper bound on the LED count (nx·ny·nz). Past this — or for a non-finite / < 1 count —\n// createLedDisplay returns null and resize() is a no-op, rather than throwing on a failed\n// TypedArray allocation. ~4M (≈158³) keeps the frame buffer + GPU packing to sane memory.\nconst MAX_LEDS = 4_000_000;\nconst withinCap = (x: number, y: number, z: number): boolean => {\n\tconst n = x * y * z;\n\treturn Number.isFinite(n) && n >= 1 && n <= MAX_LEDS;\n};\n\nexport function createLedDisplay(\n\tcanvas: HTMLCanvasElement,\n\topts: LedDisplayOptions\n): LedDisplay | null {\n\tlet [nx, ny, nz] = opts.size; // mutable: resize() can change the grid in place\n\tif (!withinCap(nx, ny, nz)) return null;\n\tconst led = opts.led ?? {};\n\tconst col = opts.color ?? {};\n\tconst cam = opts.camera ?? {};\n\tconst it = opts.interaction ?? {};\n\tconst q = opts.quality ?? {};\n\n\tconst rendererParams: RendererParams = {\n\t\tbackground: parseColor01(col.background ?? [0.01, 0.01, 0.02]),\n\t\toffColor: parseColor01(led.offColor ?? [0, 0, 0]),\n\t\ttint: parseColor(col.tint ?? [1, 1, 1]),\n\t\tglow: led.glow ?? 2.2,\n\t\tledSize: led.size ?? 0.6,\n\t\toffSize: led.offSize ?? 0.35,\n\t\tstyle: led.style ?? 'hologram',\n\t\tshape: led.shape ?? 'round',\n\t\toutline: led.outline ?? 0.25,\n\t\toutlineColor: parseColor01(led.outlineColor ?? [0.02, 0.02, 0.02]),\n\t\tstagger: led.stagger ?? false,\n\t\trgb: led.rgb ?? false,\n\t\trgbLayout: led.rgbLayout ?? 'auto',\n\t\tvivid: led.vivid ?? false,\n\t\tantialias: q.antialias ?? true\n\t};\n\n\t// The frame buffer is owned by the *display* (not the renderer) so it survives a\n\t// context rebuild (context-loss restore) and can be reallocated on resize().\n\tlet leds = new Float32Array(nx * ny * nz * 3);\n\tconst maybe = createRenderer(canvas, nx, ny, nz, rendererParams, leds);\n\tif (!maybe) return null;\n\tlet renderer = maybe; // rebuilt on resize()/context-restore; closures read this let\n\n\t// Draw straight into the shared LED buffer: the grid writes, render() uploads.\n\tlet grid = createVoxelGrid(nx, ny, nz, leds);\n\n\t// --- mutable view / interaction / lifecycle state ---\n\tlet yaw = cam.yaw ?? 0.6;\n\tlet pitch = cam.pitch ?? 0.4;\n\tlet distance = cam.distance ?? 3.6;\n\tlet fov = cam.fov ?? 0.9;\n\tlet projection: Projection = cam.projection ?? 'perspective';\n\tlet autoOrbit = cam.autoOrbit ?? true;\n\tlet orbitSpeed = cam.orbitSpeed ?? 0.45;\n\tlet pitchLimits: [number, number] = cam.pitchLimits ?? [-1.4, 1.4];\n\tlet gain = col.gain ?? 1;\n\n\tlet drag = it.drag ?? true;\n\tlet dragSpeed = it.dragSpeed ?? 0.01;\n\tlet zoom = it.zoom ?? false;\n\tlet zoomLimits: [number, number] = it.zoomLimits ?? [1.5, 10];\n\n\tlet pixelRatio = q.pixelRatio ?? 2;\n\tlet paused = q.paused ?? false;\n\tlet fpsCap = q.fps ?? 0; // 0 = uncapped (render every animation frame)\n\n\t// --- pointer interaction (drag = orbit, two-finger = pinch-zoom) ---\n\tconst pointers = new Map<number, { x: number; y: number }>();\n\tlet pinchDist = 0;\n\tconst twoPointerDist = () => {\n\t\tconst [a, b] = [...pointers.values()];\n\t\treturn Math.hypot(a.x - b.x, a.y - b.y);\n\t};\n\tconst down = (e: PointerEvent) => {\n\t\tpointers.set(e.pointerId, { x: e.clientX, y: e.clientY });\n\t\tcanvas.setPointerCapture?.(e.pointerId);\n\t\tif (pointers.size === 2) pinchDist = twoPointerDist();\n\t};\n\tconst move = (e: PointerEvent) => {\n\t\tconst p = pointers.get(e.pointerId);\n\t\tif (!p) return;\n\t\tconst dx = e.clientX - p.x;\n\t\tconst dy = e.clientY - p.y;\n\t\tp.x = e.clientX;\n\t\tp.y = e.clientY;\n\t\tif (pointers.size === 1 && drag) {\n\t\t\tyaw += dx * dragSpeed;\n\t\t\tpitch = clamp(pitch + dy * dragSpeed, pitchLimits[0], pitchLimits[1]);\n\t\t\trequestRender();\n\t\t} else if (pointers.size === 2 && zoom) {\n\t\t\tconst d = twoPointerDist();\n\t\t\tif (pinchDist > 0 && d > 0) {\n\t\t\t\tdistance = clamp((distance * pinchDist) / d, zoomLimits[0], zoomLimits[1]);\n\t\t\t\trequestRender();\n\t\t\t}\n\t\t\tpinchDist = d;\n\t\t}\n\t};\n\tconst upCancel = (e: PointerEvent) => {\n\t\tpointers.delete(e.pointerId);\n\t\tpinchDist = 0;\n\t};\n\tconst wheel = (e: WheelEvent) => {\n\t\tif (!zoom) return;\n\t\te.preventDefault();\n\t\tdistance = clamp(distance * Math.exp(e.deltaY * 0.001), zoomLimits[0], zoomLimits[1]);\n\t\trequestRender();\n\t};\n\tcanvas.addEventListener('pointerdown', down);\n\tcanvas.addEventListener('pointermove', move);\n\tcanvas.addEventListener('pointerup', upCancel);\n\tcanvas.addEventListener('pointercancel', upCancel);\n\tcanvas.addEventListener('wheel', wheel, { passive: false });\n\n\t// --- resize (drawing buffer, and optionally the grid dims) ---\n\tfunction resize(size?: [number, number, number]) {\n\t\t// Change the grid in place: reallocate the buffer + rebuild renderer/grid on\n\t\t// the SAME canvas (no context loss), preserving camera + appearance options.\n\t\tif (\n\t\t\tsize &&\n\t\t\twithinCap(size[0], size[1], size[2]) &&\n\t\t\t(size[0] !== nx || size[1] !== ny || size[2] !== nz)\n\t\t) {\n\t\t\tconst [mx, my, mz] = size;\n\t\t\tconst nextLeds = new Float32Array(mx * my * mz * 3);\n\t\t\tconst next = createRenderer(canvas, mx, my, mz, rendererParams, nextLeds);\n\t\t\tif (next) {\n\t\t\t\trenderer.dispose(); // free the old GL objects (context stays alive)\n\t\t\t\trenderer = next;\n\t\t\t\tleds = nextLeds;\n\t\t\t\tgrid = createVoxelGrid(mx, my, mz, leds);\n\t\t\t\t[nx, ny, nz] = size;\n\t\t\t\tframeCb?.(display, 0); // repaint content at the new size before showing\n\t\t\t}\n\t\t}\n\t\tconst dpr = Math.min(\n\t\t\ttypeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,\n\t\t\tpixelRatio\n\t\t);\n\t\tconst r = canvas.getBoundingClientRect();\n\t\tcanvas.width = Math.max(1, Math.round(r.width * dpr));\n\t\tcanvas.height = Math.max(1, Math.round(r.height * dpr));\n\t\tif (paused) render();\n\t}\n\tconst ro = new ResizeObserver(() => resize());\n\tro.observe(canvas);\n\n\t// --- WebGL context-loss recovery ---\n\t// The GPU can drop the context (tab backgrounded, driver reset, too many\n\t// contexts). Prevent the default (which would make it unrecoverable), pause,\n\t// and rebuild the renderer on restore — the display-owned `leds` + grid survive,\n\t// so content and wiring come back intact.\n\tlet contextLost = false;\n\tconst onContextLost = (e: Event) => {\n\t\te.preventDefault();\n\t\tcontextLost = true;\n\t\tstop();\n\t};\n\tconst onContextRestored = () => {\n\t\tconst next = createRenderer(canvas, nx, ny, nz, rendererParams, leds);\n\t\tif (!next) return; // couldn't rebuild; stay lost (nothing more we can do)\n\t\trenderer = next;\n\t\tcontextLost = false;\n\t\tgrid.markAll(); // repaint all existing content into the fresh context\n\t\tresize(); // re-establish the drawing buffer + offscreen targets\n\t\tif (!paused) start();\n\t\telse render();\n\t};\n\tcanvas.addEventListener('webglcontextlost', onContextLost);\n\tcanvas.addEventListener('webglcontextrestored', onContextRestored);\n\n\tfunction render() {\n\t\tif (contextLost) return; // nothing to draw into until the context is restored\n\t\trenderer.render({ yaw, pitch, dist: distance, fov, gain, projection }, grid.active);\n\t}\n\t// When frozen, repaint on demand (interaction / resize); otherwise the loop covers it.\n\tfunction requestRender() {\n\t\tif (paused) render();\n\t}\n\n\t// --- loop + perf stats ---\n\tconst stats: DisplayStats = { fps: 0, frameMs: 0, drawMs: 0, renderMs: 0 };\n\tconst nowMs = () => (typeof performance !== 'undefined' ? performance.now() : 0);\n\tconst ema = (prev: number, sample: number) =>\n\t\tprev === 0 ? sample : prev + (sample - prev) * 0.1;\n\tlet frameCb: ((d: LedDisplay, dt: number) => void) | null = null;\n\tlet raf = 0;\n\tlet lastFrame = 0; // timestamp of the last *executed* frame (0 = none yet)\n\tconst loop = (now: number) => {\n\t\traf = requestAnimationFrame(loop);\n\t\tif (typeof document !== 'undefined' && document.hidden) return;\n\t\t// fps cap: keep the rAF loop running (so it still pauses when hidden) but only do\n\t\t// the frame work at the target cadence. The tolerance is a fraction of the target\n\t\t// interval (not a fixed 1ms, which would grow disproportionate at high caps and make\n\t\t// a >=1000 cap effectively uncapped) so it absorbs sub-ms vsync jitter at any cap.\n\t\t// dt spans executed frames, so orbit/animation speed stays correct at any cap.\n\t\tif (fpsCap > 0 && lastFrame) {\n\t\t\tconst target = 1000 / fpsCap;\n\t\t\tif (now - lastFrame < target - Math.min(1, target * 0.1)) return;\n\t\t}\n\t\tconst interval = lastFrame ? now - lastFrame : 0;\n\t\tconst dt = lastFrame ? Math.min(0.05, interval / 1000) : 0;\n\t\tlastFrame = now;\n\t\tif (autoOrbit && pointers.size === 0) yaw += dt * orbitSpeed;\n\t\tconst t0 = nowMs();\n\t\tframeCb?.(display, dt);\n\t\tconst t1 = nowMs();\n\t\trender();\n\t\tconst t2 = nowMs();\n\t\tstats.drawMs = ema(stats.drawMs, t1 - t0);\n\t\tstats.renderMs = ema(stats.renderMs, t2 - t1);\n\t\tif (interval > 0) {\n\t\t\tstats.frameMs = ema(stats.frameMs, interval);\n\t\t\tstats.fps = 1000 / stats.frameMs;\n\t\t}\n\t};\n\tfunction start() {\n\t\tif (!raf) {\n\t\t\tlastFrame = 0;\n\t\t\traf = requestAnimationFrame(loop);\n\t\t}\n\t}\n\tfunction stop() {\n\t\tif (raf) {\n\t\t\tcancelAnimationFrame(raf);\n\t\t\traf = 0;\n\t\t}\n\t}\n\tfunction setPaused(b: boolean) {\n\t\tif (b === paused) return;\n\t\tpaused = b;\n\t\tif (paused) {\n\t\t\tstop();\n\t\t\trender();\n\t\t} else {\n\t\t\tstart();\n\t\t}\n\t}\n\n\tfunction setOptions(patch: LedDisplayPatch) {\n\t\tif (patch.led || patch.color) {\n\t\t\tconst cfg: Partial<RendererParams> = {\n\t\t\t\tbackground:\n\t\t\t\t\tpatch.color?.background != null ? parseColor01(patch.color.background) : undefined,\n\t\t\t\toffColor: patch.led?.offColor != null ? parseColor01(patch.led.offColor) : undefined,\n\t\t\t\ttint: patch.color?.tint != null ? parseColor(patch.color.tint) : undefined,\n\t\t\t\tglow: patch.led?.glow,\n\t\t\t\tledSize: patch.led?.size,\n\t\t\t\toffSize: patch.led?.offSize,\n\t\t\t\tstyle: patch.led?.style,\n\t\t\t\tshape: patch.led?.shape,\n\t\t\t\tstagger: patch.led?.stagger,\n\t\t\t\trgb: patch.led?.rgb,\n\t\t\t\trgbLayout: patch.led?.rgbLayout,\n\t\t\t\tvivid: patch.led?.vivid,\n\t\t\t\toutline: patch.led?.outline,\n\t\t\t\toutlineColor:\n\t\t\t\t\tpatch.led?.outlineColor != null ? parseColor01(patch.led.outlineColor) : undefined\n\t\t\t};\n\t\t\trenderer.configure(cfg);\n\t\t\t// Keep rendererParams current so a later resize()/context-restore rebuilds\n\t\t\t// the renderer with the LIVE appearance, not the creation-time values.\n\t\t\tObject.assign(\n\t\t\t\trendererParams,\n\t\t\t\tObject.fromEntries(Object.entries(cfg).filter(([, v]) => v !== undefined))\n\t\t\t);\n\t\t\tif (patch.color?.gain != null) gain = patch.color.gain;\n\t\t}\n\t\tconst c = patch.camera;\n\t\tif (c) {\n\t\t\tif (c.yaw != null) yaw = c.yaw;\n\t\t\tif (c.pitch != null) pitch = c.pitch;\n\t\t\tif (c.distance != null) distance = c.distance;\n\t\t\tif (c.fov != null) fov = c.fov;\n\t\t\tif (c.projection) projection = c.projection;\n\t\t\tif (c.autoOrbit != null) autoOrbit = c.autoOrbit;\n\t\t\tif (c.orbitSpeed != null) orbitSpeed = c.orbitSpeed;\n\t\t\tif (c.pitchLimits) pitchLimits = c.pitchLimits;\n\t\t}\n\t\tconst i = patch.interaction;\n\t\tif (i) {\n\t\t\tif (i.drag != null) drag = i.drag;\n\t\t\tif (i.dragSpeed != null) dragSpeed = i.dragSpeed;\n\t\t\tif (i.zoom != null) zoom = i.zoom;\n\t\t\tif (i.zoomLimits) zoomLimits = i.zoomLimits;\n\t\t}\n\t\tif (patch.quality?.pixelRatio != null) {\n\t\t\tpixelRatio = patch.quality.pixelRatio;\n\t\t\tresize();\n\t\t}\n\t\tif (patch.quality?.fps != null) fpsCap = patch.quality.fps;\n\t\tif (patch.quality?.paused != null) setPaused(patch.quality.paused);\n\t\trequestRender();\n\t}\n\n\tif (!paused) start();\n\telse render();\n\n\t// Delegate the VoxelGrid surface to the *current* grid (resize()/context-restore\n\t// swap `grid` for a new instance, so we can't spread a captured one).\n\tconst display: LedDisplay = {\n\t\tget nx() {\n\t\t\treturn grid.nx;\n\t\t},\n\t\tget ny() {\n\t\t\treturn grid.ny;\n\t\t},\n\t\tget nz() {\n\t\t\treturn grid.nz;\n\t\t},\n\t\tget leds() {\n\t\t\treturn grid.leds;\n\t\t},\n\t\tmarkAll: () => grid.markAll(),\n\t\tindex: (x, y, z) => grid.index(x, y, z),\n\t\tinBounds: (x, y, z) => grid.inBounds(x, y, z),\n\t\tplot: (x, y, z, c) => grid.plot(x, y, z, c),\n\t\tadd: (x, y, z, c) => grid.add(x, y, z, c),\n\t\tget: (x, y, z) => grid.get(x, y, z),\n\t\tclear: (c) => grid.clear(c),\n\t\tfill: (c) => grid.fill(c),\n\t\tline: (a, b, c) => grid.line(a, b, c),\n\t\tbox: (min, max, c, filled) => grid.box(min, max, c, filled),\n\t\tsphere: (center, radius, c, filled) => grid.sphere(center, radius, c, filled),\n\t\tstats,\n\t\tonFrame(cb) {\n\t\t\tframeCb = cb;\n\t\t\trequestRender();\n\t\t\treturn () => {\n\t\t\t\tif (frameCb === cb) frameCb = null;\n\t\t\t};\n\t\t},\n\t\trender,\n\t\tsetGain(g) {\n\t\t\tgain = g;\n\t\t\trequestRender();\n\t\t},\n\t\tsetPaused,\n\t\tsetOptions,\n\t\tsetCamera(c) {\n\t\t\tif (c.yaw != null) yaw = c.yaw;\n\t\t\tif (c.pitch != null) pitch = clamp(c.pitch, pitchLimits[0], pitchLimits[1]);\n\t\t\tif (c.distance != null) distance = clamp(c.distance, zoomLimits[0], zoomLimits[1]);\n\t\t\trequestRender();\n\t\t},\n\t\tresize,\n\t\tsnapshot() {\n\t\t\trender();\n\t\t\treturn canvas.toDataURL('image/png');\n\t\t},\n\t\tdispose() {\n\t\t\tstop();\n\t\t\tro.disconnect();\n\t\t\tcanvas.removeEventListener('pointerdown', down);\n\t\t\tcanvas.removeEventListener('pointermove', move);\n\t\t\tcanvas.removeEventListener('pointerup', upCancel);\n\t\t\tcanvas.removeEventListener('pointercancel', upCancel);\n\t\t\tcanvas.removeEventListener('wheel', wheel);\n\t\t\tcanvas.removeEventListener('webglcontextlost', onContextLost);\n\t\t\tcanvas.removeEventListener('webglcontextrestored', onContextRestored);\n\t\t\t// Free the GL objects but leave the context intact — forcing `loseContext()`\n\t\t\t// here poisons the canvas for reuse, which breaks re-creating a display on the\n\t\t\t// same element (e.g. React StrictMode's mount → unmount → remount in dev). The\n\t\t\t// context is released normally once the canvas is dropped/GC'd.\n\t\t\trenderer.dispose();\n\t\t}\n\t};\n\treturn display;\n}\n","// @glowbox/led-grid — framework-agnostic 3D LED-grid display. Embed in any SPA:\n// import { createLedDisplay } from \"@glowbox/led-grid\";\n// Framework wrappers are separate packages (e.g. \"@glowbox/svelte\").\nexport type {\n\tCameraOptions,\n\tColorOptions,\n\tDisplayStats,\n\tInteractionOptions,\n\tLedDisplay,\n\tLedDisplayOptions,\n\tLedDisplayPatch,\n\tLedOptions,\n\tLedShape,\n\tLedStyle,\n\tProjection,\n\tQualityOptions,\n\tRGB,\n\tRgbLayout,\n\tVec3,\n\tVoxelGrid\n} from './led-display';\nexport { createLedDisplay } from './led-display';\n// Colours: [r,g,b] 0..1 (>1 blooms) or any CSS string. parseColor normalizes either.\nexport { type Color, parseColor } from './color';\n// The pure voxel API (no WebGL) — draw/compute frames headlessly, or reuse the\n// canvas-like drawing primitives on your own buffer. Re-typed to the public `VoxelGrid`\n// so the internal culling handle (`active`/`ActiveSet`) stays off the published surface.\nimport {\n\tcreateVoxelGrid as createVoxelGridImpl,\n\ttype VoxelGrid as PublicVoxelGrid\n} from './voxel-grid';\nexport const createVoxelGrid: (\n\tnx: number,\n\tny: number,\n\tnz: number,\n\tleds?: Float32Array\n) => PublicVoxelGrid = createVoxelGridImpl;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { LedShape, LedStyle, Projection, RgbLayout } from './renderer';
|
|
2
|
+
import { Color, VoxelGrid } from './voxel-grid';
|
|
3
|
+
export type { LedShape, LedStyle, Projection, RgbLayout } from './renderer';
|
|
4
|
+
export type { Color, RGB, Vec3, VoxelGrid } from './voxel-grid';
|
|
5
|
+
/** LED appearance. */
|
|
6
|
+
export interface LedOptions {
|
|
7
|
+
/** Visual look: 'hologram' (emissive glow, default) or 'comic' (cel-shaded + ink outline). */
|
|
8
|
+
style?: LedStyle;
|
|
9
|
+
/** Sprite shape: 'round' (default) or 'square' (tiles gap-free). */
|
|
10
|
+
shape?: LedShape;
|
|
11
|
+
/** Comic ink-border thickness, 0..1 of the sprite radius (default 0.25; 0 = no border). */
|
|
12
|
+
outline?: number;
|
|
13
|
+
/** Comic ink-border colour (default near-black). */
|
|
14
|
+
outlineColor?: Color;
|
|
15
|
+
/** Brick lattice: offset every other row by half a cell (default false). Also
|
|
16
|
+
* breaks up the regular grid, reducing view-dependent moiré. */
|
|
17
|
+
stagger?: boolean;
|
|
18
|
+
/** Render each LED as three R/G/B sub-emitters — a real RGB-LED package look
|
|
19
|
+
* (default false). Up close you see the R/G/B dies; they blend at a distance. */
|
|
20
|
+
rgb?: boolean;
|
|
21
|
+
/** Sub-die packing when `rgb` is on (default 'auto'): a delta `triad` for round
|
|
22
|
+
* LEDs, an RGGB `quad` for square, or the classic R\|G\|B `stripe`. */
|
|
23
|
+
rgbLayout?: RgbLayout;
|
|
24
|
+
/** Comic brightness (default false): `false` = cel-shade — keep each LED's
|
|
25
|
+
* brightness (posterized), so tonal content reads; `true` = flat **vivid**
|
|
26
|
+
* pop-art — every lit LED at the full value of its hue. Comic style only. */
|
|
27
|
+
vivid?: boolean;
|
|
28
|
+
/** Sprite size relative to LED spacing (default 0.6). */
|
|
29
|
+
size?: number;
|
|
30
|
+
/** Glow tightness — the falloff exponent; higher = tighter (default 2.2). Hologram only. */
|
|
31
|
+
glow?: number;
|
|
32
|
+
/** Colour of the tiny speck shown at an *unlit* LED node. Default black (no
|
|
33
|
+
* lattice — only lit LEDs show). Set e.g. `[0.03,0.03,0.03]` to hint the grid. Hologram only. */
|
|
34
|
+
offColor?: Color;
|
|
35
|
+
/** Size of the tiny "physical LED" speck shown when off, 0..1 of the glow sprite
|
|
36
|
+
* (default 0.35). A real off LED is a small dark dot, not a full-size disc. Hologram only. */
|
|
37
|
+
offSize?: number;
|
|
38
|
+
}
|
|
39
|
+
/** Global colour / brightness. */
|
|
40
|
+
export interface ColorOptions {
|
|
41
|
+
/** Clear colour behind the LEDs (default near-black [0.01,0.01,0.02]). */
|
|
42
|
+
background?: Color;
|
|
43
|
+
/** Overall brightness multiplier (default 1). */
|
|
44
|
+
gain?: number;
|
|
45
|
+
/** Multiplies every lit colour — handy for theming (default white [1,1,1]). */
|
|
46
|
+
tint?: Color;
|
|
47
|
+
}
|
|
48
|
+
/** Camera & projection. */
|
|
49
|
+
export interface CameraOptions {
|
|
50
|
+
/** Initial orbit angles (radians; default yaw 0.6, pitch 0.4). */
|
|
51
|
+
yaw?: number;
|
|
52
|
+
pitch?: number;
|
|
53
|
+
/** Camera distance (default 3.6); also the zoom target. */
|
|
54
|
+
distance?: number;
|
|
55
|
+
/** Vertical field of view in radians (default 0.9). */
|
|
56
|
+
fov?: number;
|
|
57
|
+
projection?: Projection;
|
|
58
|
+
/** Slow auto-spin, paused while interacting (default true). */
|
|
59
|
+
autoOrbit?: boolean;
|
|
60
|
+
/** Auto-spin speed in rad/s (default 0.45). */
|
|
61
|
+
orbitSpeed?: number;
|
|
62
|
+
/** Clamp for drag pitch (default [-1.4, 1.4]). */
|
|
63
|
+
pitchLimits?: [number, number];
|
|
64
|
+
}
|
|
65
|
+
/** Pointer interaction. */
|
|
66
|
+
export interface InteractionOptions {
|
|
67
|
+
/** Pointer-drag to orbit (default true). */
|
|
68
|
+
drag?: boolean;
|
|
69
|
+
/** Drag sensitivity in radians per pixel (default 0.01). */
|
|
70
|
+
dragSpeed?: number;
|
|
71
|
+
/** Wheel + pinch to zoom (default false). */
|
|
72
|
+
zoom?: boolean;
|
|
73
|
+
/** Min/max camera distance for zoom (default [1.5, 10]). */
|
|
74
|
+
zoomLimits?: [number, number];
|
|
75
|
+
}
|
|
76
|
+
/** Rendering quality / lifecycle. */
|
|
77
|
+
export interface QualityOptions {
|
|
78
|
+
/** Cap on devicePixelRatio (default 2). */
|
|
79
|
+
pixelRatio?: number;
|
|
80
|
+
/** MSAA — fixed at creation, ignored by setOptions (default true). */
|
|
81
|
+
antialias?: boolean;
|
|
82
|
+
/** Freeze the render loop; interaction + render()/snapshot() still repaint (default false). */
|
|
83
|
+
paused?: boolean;
|
|
84
|
+
/** Cap the render loop to at most this many frames/sec (default: uncapped, i.e.
|
|
85
|
+
* the display's refresh rate). A power/cadence knob — good for an always-on
|
|
86
|
+
* ambient display (lower = less GPU/battery) or matching a hardware LED-cube's
|
|
87
|
+
* refresh rate. Not a speed-up: each frame still costs the same. */
|
|
88
|
+
fps?: number;
|
|
89
|
+
}
|
|
90
|
+
export interface LedDisplayOptions {
|
|
91
|
+
/** Grid size [nx, ny, nz]. */
|
|
92
|
+
size: [number, number, number];
|
|
93
|
+
led?: LedOptions;
|
|
94
|
+
color?: ColorOptions;
|
|
95
|
+
camera?: CameraOptions;
|
|
96
|
+
interaction?: InteractionOptions;
|
|
97
|
+
quality?: QualityOptions;
|
|
98
|
+
}
|
|
99
|
+
/** Everything except `size` — live-updatable. */
|
|
100
|
+
export type LedDisplayPatch = Omit<LedDisplayOptions, 'size'>;
|
|
101
|
+
/** Rolling per-frame performance stats (EMA-smoothed). `drawMs`/`renderMs` are
|
|
102
|
+
* CPU time in the frame callback vs issuing the GL commands; `frameMs`/`fps` are
|
|
103
|
+
* the true end-to-end rate. If fps is low but drawMs+renderMs are small, you're
|
|
104
|
+
* GPU/vsync-bound (CPU timers don't capture GPU execution). */
|
|
105
|
+
export type DisplayStats = {
|
|
106
|
+
fps: number;
|
|
107
|
+
frameMs: number;
|
|
108
|
+
drawMs: number;
|
|
109
|
+
renderMs: number;
|
|
110
|
+
};
|
|
111
|
+
/** A live LED-grid display: the voxel-drawing API (VoxelGrid) plus the loop,
|
|
112
|
+
* camera control and lifecycle. */
|
|
113
|
+
export interface LedDisplay extends VoxelGrid {
|
|
114
|
+
/** Live, EMA-smoothed frame timings (updated while the loop runs). */
|
|
115
|
+
readonly stats: DisplayStats;
|
|
116
|
+
/** Run `cb(display, dt)` every animation frame (auto-pauses when hidden, and
|
|
117
|
+
* does not fire while `paused` — drive updates yourself with `render()` then).
|
|
118
|
+
* Returns a stop() that clears the callback. */
|
|
119
|
+
onFrame(cb: (d: LedDisplay, dt: number) => void): () => void;
|
|
120
|
+
/** Draw the current LED buffer once (also called each frame by the loop). */
|
|
121
|
+
render(): void;
|
|
122
|
+
/** Set the brightness multiplier (shortcut for setOptions({ color: { gain } })). */
|
|
123
|
+
setGain(gain: number): void;
|
|
124
|
+
/** Freeze/unfreeze the loop (render-on-demand when frozen). */
|
|
125
|
+
setPaused(paused: boolean): void;
|
|
126
|
+
/** Live-update any option except `size`. */
|
|
127
|
+
setOptions(patch: LedDisplayPatch): void;
|
|
128
|
+
/** Imperatively move the camera. */
|
|
129
|
+
setCamera(c: {
|
|
130
|
+
yaw?: number;
|
|
131
|
+
pitch?: number;
|
|
132
|
+
distance?: number;
|
|
133
|
+
}): void;
|
|
134
|
+
/** Recompute the drawing-buffer size from the canvas box + pixelRatio, then render.
|
|
135
|
+
* Pass a `[nx, ny, nz]` to also change the grid size in place — reallocating the
|
|
136
|
+
* buffer on the *same* canvas (no context loss), preserving camera + options. */
|
|
137
|
+
resize(size?: [number, number, number]): void;
|
|
138
|
+
/** Render once and return a PNG data URL (for previews / frame export). */
|
|
139
|
+
snapshot(): string;
|
|
140
|
+
dispose(): void;
|
|
141
|
+
}
|
|
142
|
+
export declare function createLedDisplay(canvas: HTMLCanvasElement, opts: LedDisplayOptions): LedDisplay | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { RGB } from './color';
|
|
2
|
+
import { ActiveSet } from './voxel-grid';
|
|
3
|
+
export type Projection = 'perspective' | 'orthographic';
|
|
4
|
+
/** Visual look: 'hologram' (emissive glow, default) or 'comic' (cel-shaded + ink outline). */
|
|
5
|
+
export type LedStyle = 'hologram' | 'comic';
|
|
6
|
+
/** LED sprite shape: 'round' (default) or 'square' (tiles gap-free). */
|
|
7
|
+
export type LedShape = 'round' | 'square';
|
|
8
|
+
/** RGB sub-die arrangement (when `rgb` is on). `auto` picks the packing that best
|
|
9
|
+
* fits the shape: a delta **triad** for round dies, an RGGB **quad** for square.
|
|
10
|
+
* `stripe` is the classic R|G|B LCD-stripe look. */
|
|
11
|
+
export type RgbLayout = 'auto' | 'triad' | 'quad' | 'stripe';
|
|
12
|
+
/** Live-updatable appearance (all except `antialias`, which is fixed at context creation). */
|
|
13
|
+
export type RendererParams = {
|
|
14
|
+
background: RGB;
|
|
15
|
+
offColor: RGB;
|
|
16
|
+
tint: RGB;
|
|
17
|
+
glow: number;
|
|
18
|
+
ledSize: number;
|
|
19
|
+
/** Radius of the tiny "physical LED" speck shown when off, as a fraction of the sprite (0..1). */
|
|
20
|
+
offSize: number;
|
|
21
|
+
style: LedStyle;
|
|
22
|
+
shape: LedShape;
|
|
23
|
+
/** Comic ink-border thickness, 0..1 of the sprite radius (0 = no border). */
|
|
24
|
+
outline: number;
|
|
25
|
+
outlineColor: RGB;
|
|
26
|
+
/** Brick lattice: offset every other row by half a cell (breaks the regular grid → less moiré). */
|
|
27
|
+
stagger: boolean;
|
|
28
|
+
/** Render each LED as three R/G/B sub-emitters (real RGB-LED look). */
|
|
29
|
+
rgb: boolean;
|
|
30
|
+
/** Sub-die packing when `rgb` is on (default 'auto' — triad for round, quad for square). */
|
|
31
|
+
rgbLayout: RgbLayout;
|
|
32
|
+
/** Comic brightness: false = cel-shade (keep tone), true = flat vivid (full value). */
|
|
33
|
+
vivid: boolean;
|
|
34
|
+
antialias: boolean;
|
|
35
|
+
};
|
|
36
|
+
export type RenderView = {
|
|
37
|
+
yaw: number;
|
|
38
|
+
pitch: number;
|
|
39
|
+
dist: number;
|
|
40
|
+
fov: number;
|
|
41
|
+
gain: number;
|
|
42
|
+
projection: Projection;
|
|
43
|
+
};
|
|
44
|
+
export type Renderer = {
|
|
45
|
+
leds: Float32Array;
|
|
46
|
+
render(view: RenderView, active?: ActiveSet): void;
|
|
47
|
+
configure(p: Partial<RendererParams>): void;
|
|
48
|
+
dispose(): void;
|
|
49
|
+
};
|
|
50
|
+
export declare function createRenderer(canvas: HTMLCanvasElement, nx: number, ny: number, nz: number, params: RendererParams, leds?: Float32Array): Renderer | null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Color, RGB, Vec3 } from './color';
|
|
2
|
+
export type { Color, RGB, Vec3 } from './color';
|
|
3
|
+
/** The set of *lit* cells since the last clear, so the renderer (and clear) can
|
|
4
|
+
* work in O(lit) instead of O(volume). `all` = treat every cell as lit (after a
|
|
5
|
+
* fill, or markAll for raw-buffer writers). `list[0..count]` holds cell indices.
|
|
6
|
+
* Package-internal (the culling wiring) — not part of the public surface. */
|
|
7
|
+
export type ActiveSet = {
|
|
8
|
+
list: Uint32Array;
|
|
9
|
+
count: number;
|
|
10
|
+
all: boolean;
|
|
11
|
+
};
|
|
12
|
+
/** A 3D LED lattice you draw voxels onto. Pure state — no rendering. */
|
|
13
|
+
export interface VoxelGrid {
|
|
14
|
+
readonly nx: number;
|
|
15
|
+
readonly ny: number;
|
|
16
|
+
readonly nz: number;
|
|
17
|
+
/** Raw LED buffer (nx*ny*nz*3 RGB). You can read it freely; if you *write* it
|
|
18
|
+
* directly, call markAll() so culling/clear don't skip your cells. */
|
|
19
|
+
readonly leds: Float32Array;
|
|
20
|
+
/** Force every cell to be treated as lit (for raw-buffer writers). */
|
|
21
|
+
markAll(): void;
|
|
22
|
+
/** Flat buffer offset of (x,y,z) — start of its 3 RGB floats. */
|
|
23
|
+
index(x: number, y: number, z: number): number;
|
|
24
|
+
inBounds(x: number, y: number, z: number): boolean;
|
|
25
|
+
plot(x: number, y: number, z: number, color: Color): void;
|
|
26
|
+
add(x: number, y: number, z: number, color: Color): void;
|
|
27
|
+
get(x: number, y: number, z: number): RGB;
|
|
28
|
+
/** Clear the whole grid (default black). */
|
|
29
|
+
clear(color?: Color): void;
|
|
30
|
+
/** Fill every LED with a colour (alias of clear(color)). */
|
|
31
|
+
fill(color: Color): void;
|
|
32
|
+
line(a: Vec3, b: Vec3, color: Color): void;
|
|
33
|
+
/** Axis-aligned box between two corners; `filled` fills it, else wireframe edges. */
|
|
34
|
+
box(min: Vec3, max: Vec3, color: Color, filled?: boolean): void;
|
|
35
|
+
/** `filled` fills the ball, else a ~1-voxel-thick shell. */
|
|
36
|
+
sphere(center: Vec3, radius: number, color: Color, filled?: boolean): void;
|
|
37
|
+
}
|
|
38
|
+
/** A grid plus its internal culling handle — what `createVoxelGrid` actually
|
|
39
|
+
* returns; the display and renderer consume `active`, but it isn't part of the
|
|
40
|
+
* public `VoxelGrid` surface. */
|
|
41
|
+
export interface VoxelGridInternal extends VoxelGrid {
|
|
42
|
+
/** Lit-cell tracking (used by the display to cull). */
|
|
43
|
+
readonly active: ActiveSet;
|
|
44
|
+
}
|
|
45
|
+
/** Create a voxel grid. Pass an existing `leds` buffer (e.g. the display's) to
|
|
46
|
+
* draw straight into it; otherwise a fresh nx*ny*nz*3 Float32Array is allocated. */
|
|
47
|
+
export declare function createVoxelGrid(nx: number, ny: number, nz: number, leds?: Float32Array): VoxelGridInternal;
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@glowbox/led-grid",
|
|
3
|
+
"version": "1.0.0-rc.2",
|
|
4
|
+
"description": "Framework-agnostic 3D LED-grid display — a tiny WebGL voxel canvas you draw on.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"led",
|
|
7
|
+
"led-cube",
|
|
8
|
+
"webgl",
|
|
9
|
+
"voxel",
|
|
10
|
+
"3d",
|
|
11
|
+
"visualization"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"module": "./dist/index.js",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/eetu/glowbox.git",
|
|
33
|
+
"directory": "packages/led-grid"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://eetu.github.io/glowbox/",
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "vite build",
|
|
38
|
+
"prepack": "vite build",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"lint": "eslint .",
|
|
41
|
+
"lint:fix": "eslint . --fix",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"size": "size-limit"
|
|
44
|
+
},
|
|
45
|
+
"size-limit": [
|
|
46
|
+
{
|
|
47
|
+
"name": "esm (bundled + minified + brotli)",
|
|
48
|
+
"path": "dist/index.js",
|
|
49
|
+
"limit": "10 kB"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@anarkisti/eslint-config": "^1",
|
|
54
|
+
"@vitest/browser": "^4.1",
|
|
55
|
+
"@vitest/browser-playwright": "^4.1",
|
|
56
|
+
"eslint": "^10",
|
|
57
|
+
"playwright": "^1.61",
|
|
58
|
+
"typescript": "^6",
|
|
59
|
+
"vite": "^8.1",
|
|
60
|
+
"vite-plugin-dts": "^5",
|
|
61
|
+
"vitest": "^4.1"
|
|
62
|
+
}
|
|
63
|
+
}
|