@glowbox/nixie 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 +66 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +196 -0
- package/dist/index.js.map +1 -0
- package/dist/nixie.d.ts +34 -0
- package/package.json +69 -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,66 @@
|
|
|
1
|
+
# @glowbox/nixie
|
|
2
|
+
|
|
3
|
+
A **nixie-tube display component** — a sibling rendering core to
|
|
4
|
+
**[@glowbox/led-grid](../led-grid)**'s LED grid. It renders a single glowing numeral the way a
|
|
5
|
+
real nixie works: a stack of bent-wire cathodes inside a glass tube, only one lit. Each
|
|
6
|
+
digit is a thin geometric **filament** (a single-stroke vector wire) that glows
|
|
7
|
+
warm-orange with a hot core, in front of the full stack of unlit dull-metal cathode
|
|
8
|
+
wires nested behind the honeycomb anode mesh. Zero runtime deps (it borrows only core's
|
|
9
|
+
tree-shaken colour helper).
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
yarn add @glowbox/nixie
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createNixieTube } from '@glowbox/nixie';
|
|
17
|
+
|
|
18
|
+
const tube = createNixieTube(canvas, { value: 7, style: 'classic', glow: 0.8 });
|
|
19
|
+
tube?.setValue(8);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Give it a canvas + a value; it owns the 2D render, glow, and resize. A clock or counter
|
|
23
|
+
is just a **row of tubes** (one component per canvas).
|
|
24
|
+
|
|
25
|
+
## Value
|
|
26
|
+
|
|
27
|
+
A single symbol per tube: a char `0`–`9`, `:`, `-`, or `null` / `''` for all-cathodes-dark.
|
|
28
|
+
`setValue(v)` relights it live.
|
|
29
|
+
|
|
30
|
+
## Options
|
|
31
|
+
|
|
32
|
+
| option | default | notes |
|
|
33
|
+
| ------------ | ----------- | ----------------------------------------------------------------- |
|
|
34
|
+
| `value` | — | the lit symbol (see above) |
|
|
35
|
+
| `style` | `'classic'` | physical tube numeral shape: `'classic'` · `'slim'` · `'tall'` |
|
|
36
|
+
| `color` | warm orange | glow colour — a `Color` (`[r,g,b]` 0..1 or any CSS string) |
|
|
37
|
+
| `glow` | `0.7` | glow strength 0..1 |
|
|
38
|
+
| `background` | near-black | tube glass colour behind the numerals |
|
|
39
|
+
| `mesh` | `true` | draw the honeycomb anode mesh over the tube |
|
|
40
|
+
| `ghost` | `true` | draw the other, unlit cathodes faintly behind — the stacked depth |
|
|
41
|
+
| `pixelRatio` | `2` | cap on devicePixelRatio |
|
|
42
|
+
|
|
43
|
+
All update live via `setOptions(patch)`.
|
|
44
|
+
|
|
45
|
+
## Sizing & backgrounds
|
|
46
|
+
|
|
47
|
+
The render is **size-adaptive**: at large sizes you get the full illusion (a thin
|
|
48
|
+
filament under a heavy bloom, behind the honeycomb mesh and the nested cathode stack); as
|
|
49
|
+
the tube shrinks it switches methods rather than forcing a sub-pixel wire to survive its
|
|
50
|
+
own blur — dropping the stack, then the bloom passes, and fattening the wire, so a tiny
|
|
51
|
+
tube stays a legible bold glyph. The tube draws as a rounded glass module that casts a
|
|
52
|
+
soft drop shadow into a transparent margin, so it floats correctly on **any** backdrop —
|
|
53
|
+
including light/white pages. (A bloom can't read against white, so the glass itself stays
|
|
54
|
+
dark; the shadow + glass rim, not a light glass, are what let it sit right on a light
|
|
55
|
+
surface.) `color` and `background` retint the glow and glass together (e.g. a blue tube:
|
|
56
|
+
`{ color: '#57b6ff', background: '#04121f' }`), and the vignette rim follows the glass
|
|
57
|
+
tint instead of going pure black.
|
|
58
|
+
|
|
59
|
+
## Methods
|
|
60
|
+
|
|
61
|
+
`setValue(v)`, `setOptions(patch)`, `resize()` (after the canvas box changes),
|
|
62
|
+
`snapshot(): string` (PNG data URL), `dispose()`.
|
|
63
|
+
|
|
64
|
+
Part of the **glowbox** family of glowing retro displays — see
|
|
65
|
+
**[@glowbox/led-grid](../led-grid)** (3D LED grid). Live demos:
|
|
66
|
+
<https://eetu.github.io/glowbox/>.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createNixieTube, type NixieOptions, type NixieStyle, type NixieTube } from './nixie';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { parseColor as e } from "@glowbox/led-grid";
|
|
2
|
+
//#region glyphs/0.svg?raw
|
|
3
|
+
var t = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 60 100\" width=\"120\" height=\"200\"><title>glowbox nixie glyph 0</title><path id=\"zero\" d=\"M 28 7 C 14 7 0 19 0 49 C 0 79 14 91 28 91 C 42 91 56 79 56 49 C 56 19 42 7 28 7 Z\" fill=\"none\" stroke=\"#1e1e1e\" stroke-width=\"4.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n", n = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 60 100\" width=\"120\" height=\"200\"><title>glowbox nixie glyph 1</title><path id=\"uno\" d=\"M 28 7 L 28 84\" fill=\"none\" stroke=\"#1e1e1e\" stroke-width=\"4.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n", r = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 60 100\" width=\"120\" height=\"200\"><title>glowbox nixie glyph 2</title><path id=\"dos\" d=\"M 7 28 C 7 14 17.49 7 28 7 C 38.443 7 49 14 49 28 C 49 42 32.068 51.932 21 63 C 14 70 7 77 7 84 L 49 84\" fill=\"none\" stroke=\"#1e1e1e\" stroke-width=\"4.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n", i = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 60 100\" width=\"120\" height=\"200\"><title>glowbox nixie glyph 3</title><path id=\"windows\" d=\"M 14 7 C 28 7 42 7 49 7 C 42 14 28 28 28 28 C 42 28 56 42 56 56 C 56 70 42 84 28 84 C 14 84 0 70 0 56\" fill=\"none\" stroke=\"#1e1e1e\" stroke-width=\"4.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n", a = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 60 100\" width=\"120\" height=\"200\"><title>glowbox nixie glyph 4</title><path id=\"linux\" d=\"M 42 6 L 7 63 L 56 63 M 42 6 L 42 84\" fill=\"none\" stroke=\"#1e1e1e\" stroke-width=\"4.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n", o = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 60 100\" width=\"120\" height=\"200\"><title>glowbox nixie glyph 5</title><path id=\"macos\" d=\"M 56 7 L 21 7 C 21 7 21 7 14 35 C 14 35 21 28 35 28 C 42 28 56 35 56 56 C 56 70 42 84 28 84 C 21 84 7 77 7 63\" fill=\"none\" stroke=\"#1e1e1e\" stroke-width=\"4.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n", s = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 60 100\" width=\"120\" height=\"200\"><title>glowbox nixie glyph 6</title><path id=\"sex\" d=\"M 28 7 C 22.045 12.955 0 49 0 63 C 0 77 14 91 28 91 C 42 91 56 77 56 63 C 56 49 42 35 28 35 C 14 35 0 49.248 0 63\" fill=\"none\" stroke=\"#1e1e1e\" stroke-width=\"4.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n", c = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 60 100\" width=\"120\" height=\"200\"><title>glowbox nixie glyph 7</title><path id=\"nana\" d=\"M 7 7 L 49 7 L 21 91\" fill=\"none\" stroke=\"#1e1e1e\" stroke-width=\"4.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n", l = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 60 100\" width=\"120\" height=\"200\"><title>glowbox nixie glyph 8</title><path id=\"eight ball\" d=\"M 28 35 C 21 35 14 28 14 21 C 14 14 21 7 28 7 C 35 7 42 14 42 21 C 42 28 35 35 28 35 C 14 35 0 49 0 63 C 0 77 14 91 28 91 C 42 91 56 77 56 63 C 56 49 42 35 28 35 Z\" fill=\"none\" stroke=\"#1e1e1e\" stroke-width=\"4.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n", u = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 60 100\" width=\"120\" height=\"200\"><title>glowbox nixie glyph 9</title><path id=\"kyu\" d=\"M 56 35 C 56 21 42 7 28 7 C 14 7 0 21 0 35 C 0 49 14 63 28 63 C 42 63 56 49 56 35 C 56 49 35 77 21 91\" fill=\"none\" stroke=\"#1e1e1e\" stroke-width=\"4.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n", d = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 60 100\" width=\"120\" height=\"200\"><title>glowbox nixie glyph :</title> \n \n \n \n <path id=\"matti\" d=\"M 33 39 C 33 42.314 30.314 45 27 45 C 23.686 45 21 42.314 21 39 C 21 35.686 23.686 33 27 33 C 30.314 33 33 35.686 33 39 Z\" fill=\"none\" stroke=\"#000000\" stroke-width=\"4.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n <path id=\"teppo\" d=\"M 33 63 C 33 66.314 30.314 69 27 69 C 23.686 69 21 66.314 21 63 C 21 59.686 23.686 57 27 57 C 30.314 57 33 59.686 33 63 Z\" fill=\"none\" stroke=\"#000000\" stroke-width=\"4.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n</svg>\n", f = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 60 100\" width=\"120\" height=\"200\"><title>glowbox nixie glyph -</title><path id=\"viiva\" d=\"M 14 49 L 42 49\" fill=\"none\" stroke=\"#1e1e1e\" stroke-width=\"4.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>\n", p = Math.PI * 2, m = 60, h = 100, g = /* #__PURE__ */ Object.assign({
|
|
4
|
+
"../glyphs/0.svg": t,
|
|
5
|
+
"../glyphs/1.svg": n,
|
|
6
|
+
"../glyphs/2.svg": r,
|
|
7
|
+
"../glyphs/3.svg": i,
|
|
8
|
+
"../glyphs/4.svg": a,
|
|
9
|
+
"../glyphs/5.svg": o,
|
|
10
|
+
"../glyphs/6.svg": s,
|
|
11
|
+
"../glyphs/7.svg": c,
|
|
12
|
+
"../glyphs/8.svg": l,
|
|
13
|
+
"../glyphs/9.svg": u,
|
|
14
|
+
"../glyphs/colon.svg": d,
|
|
15
|
+
"../glyphs/dash.svg": f
|
|
16
|
+
}), _ = (e) => [...e.matchAll(/\bd\s*=\s*(["'])([\s\S]*?)\1/g)].map((e) => e[2]).join(" "), v = {}, y = "";
|
|
17
|
+
for (let [e, t] of Object.entries(g)) {
|
|
18
|
+
let n = e.slice(e.lastIndexOf("/") + 1, -4), r = n === "dash" ? "-" : n === "colon" ? ":" : n, i = _(t);
|
|
19
|
+
i && (r === ":" ? y = i : v[r] = i);
|
|
20
|
+
}
|
|
21
|
+
var b = new Path2D(y), x = "1234567890", S = {};
|
|
22
|
+
for (let e = 0; e < 10; e++) S[x[e]] = e;
|
|
23
|
+
var C = (e) => {
|
|
24
|
+
let t = S[e];
|
|
25
|
+
if (t === void 0) return {
|
|
26
|
+
ox: 0,
|
|
27
|
+
oy: 0,
|
|
28
|
+
ds: 1
|
|
29
|
+
};
|
|
30
|
+
let n = (t - 4.5) / 4.5;
|
|
31
|
+
return {
|
|
32
|
+
ox: n * 1.2,
|
|
33
|
+
oy: -n * 1.1 - .4 * Math.sin(t * 1.7),
|
|
34
|
+
ds: 1 - t * .02
|
|
35
|
+
};
|
|
36
|
+
}, w = {
|
|
37
|
+
classic: {
|
|
38
|
+
sx: 1,
|
|
39
|
+
sy: 1,
|
|
40
|
+
lw: 4.2
|
|
41
|
+
},
|
|
42
|
+
slim: {
|
|
43
|
+
sx: .84,
|
|
44
|
+
sy: 1.06,
|
|
45
|
+
lw: 3.2
|
|
46
|
+
},
|
|
47
|
+
tall: {
|
|
48
|
+
sx: .9,
|
|
49
|
+
sy: 1.16,
|
|
50
|
+
lw: 3.7
|
|
51
|
+
}
|
|
52
|
+
}, T = [
|
|
53
|
+
.52,
|
|
54
|
+
.52,
|
|
55
|
+
.56
|
|
56
|
+
], E = /* @__PURE__ */ new Map(), D = (e) => {
|
|
57
|
+
if (e === ":") return b;
|
|
58
|
+
let t = v[e];
|
|
59
|
+
if (!t) return null;
|
|
60
|
+
let n = E.get(e);
|
|
61
|
+
return n || (n = new Path2D(t), E.set(e, n)), n;
|
|
62
|
+
}, O = (e) => e == null ? "" : String(e).slice(0, 1), k = (e) => Math.max(0, Math.min(255, Math.round(e * 255)));
|
|
63
|
+
function A(t, n = {}) {
|
|
64
|
+
let r = t.getContext("2d");
|
|
65
|
+
if (!r) return null;
|
|
66
|
+
let i = O(n.value), a = n.style ?? "classic", o = e(n.color ?? [
|
|
67
|
+
1,
|
|
68
|
+
.45,
|
|
69
|
+
.08
|
|
70
|
+
]), s = n.glow ?? .7, c = e(n.background ?? [
|
|
71
|
+
.03,
|
|
72
|
+
.03,
|
|
73
|
+
.045
|
|
74
|
+
]), l = n.mesh ?? !0, u = n.ghost ?? !0, d = n.pixelRatio ?? 2, f = 0, g = 0, _ = 1, v = (e) => `rgb(${k(e[0])},${k(e[1])},${k(e[2])})`, y = (e, t) => `rgba(${k(e[0])},${k(e[1])},${k(e[2])},${t})`;
|
|
75
|
+
function b(e, t, n) {
|
|
76
|
+
r.beginPath();
|
|
77
|
+
for (let i = 0; i < 6; i++) {
|
|
78
|
+
let a = Math.PI / 6 + i * Math.PI / 3, o = e + n * Math.cos(a), s = t + n * Math.sin(a);
|
|
79
|
+
i ? r.lineTo(o, s) : r.moveTo(o, s);
|
|
80
|
+
}
|
|
81
|
+
r.closePath();
|
|
82
|
+
}
|
|
83
|
+
function E(e, t) {
|
|
84
|
+
r.strokeStyle = "rgba(120,120,135,0.09)", r.lineWidth = .8;
|
|
85
|
+
let n = Math.max(e, t) * .06, i = n * Math.sqrt(3);
|
|
86
|
+
for (let a = -n; a < t + n; a += n * 1.5) {
|
|
87
|
+
let t = Math.round(a / (n * 1.5)) % 2 * (i / 2);
|
|
88
|
+
for (let o = -i; o < e + i; o += i) b(o + t, a, n), r.stroke();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function A(e, t, n, i, a) {
|
|
92
|
+
let o = Math.min(a, n / 2, i / 2), s = r;
|
|
93
|
+
s.beginPath(), s.moveTo(e + o, t), s.arcTo(e + n, t, e + n, t + i, o), s.arcTo(e + n, t + i, e, t + i, o), s.arcTo(e, t + i, e, t, o), s.arcTo(e, t, e + n, t, o), s.closePath();
|
|
94
|
+
}
|
|
95
|
+
function j() {
|
|
96
|
+
if (!f || !g) return;
|
|
97
|
+
let e = r;
|
|
98
|
+
e.setTransform(1, 0, 0, 1, 0, 0), e.clearRect(0, 0, t.width, t.height), e.scale(_, _);
|
|
99
|
+
let n = Math.max(4, f * .08), d = Math.max(4, g * .05), b = f - 2 * n, O = g - 2 * d, k = Math.min(b, O) * .1, j = Math.min(n, d);
|
|
100
|
+
e.save(), e.shadowColor = "rgba(0,0,0,0.42)", e.shadowBlur = j * 1.6, e.shadowOffsetY = d * .3, A(n, d, b, O, k), e.fillStyle = "#000", e.fill(), e.restore(), e.save(), A(n, d, b, O, k), e.clip(), e.translate(n, d);
|
|
101
|
+
let M = e.createRadialGradient(b / 2, O / 2, 0, b / 2, O / 2, Math.max(b, O) * .72);
|
|
102
|
+
M.addColorStop(0, y(c, 1)), M.addColorStop(1, y([
|
|
103
|
+
c[0] * .16,
|
|
104
|
+
c[1] * .16,
|
|
105
|
+
c[2] * .18
|
|
106
|
+
], 1)), e.fillStyle = M, e.fillRect(0, 0, b, O);
|
|
107
|
+
let N = b / 2, P = O / 2, F = w[a], I = i === ":", L = I ? O * .62 : Math.min(b * .6 / m, O * .7 / h) * h, R = L < 32, z = L < 64;
|
|
108
|
+
l && O >= 34 && E(b, O);
|
|
109
|
+
let B = I ? Math.min(O * .62 / h, b * .9 / 24) : z ? Math.min(b * .82 / m, O * .82 / h) : Math.min(b * .72 / m, O * .68 / h), V = h * B, H = R ? 2.4 : z ? 1.5 : 1, U = (e) => Math.min(V * e * s, 160);
|
|
110
|
+
e.lineJoin = "round", e.lineCap = "round";
|
|
111
|
+
let W = (t, n) => {
|
|
112
|
+
let { ox: r, oy: i, ds: a } = C(n);
|
|
113
|
+
e.save(), e.translate(N + r * B, P + i * B), e.scale(B * F.sx * a, B * F.sy * a), e.translate(-60 / 2, -100 / 2), e.stroke(t), e.restore();
|
|
114
|
+
};
|
|
115
|
+
if (u && !z && S[i] !== void 0) {
|
|
116
|
+
e.shadowBlur = 0;
|
|
117
|
+
for (let t of x) {
|
|
118
|
+
if (t === i) continue;
|
|
119
|
+
let n = D(t);
|
|
120
|
+
if (!n) continue;
|
|
121
|
+
let r = S[t] / 9;
|
|
122
|
+
e.strokeStyle = y(T, .3 - r * .19), e.lineWidth = F.lw * .72, W(n, t);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
let G = D(i);
|
|
126
|
+
if (G) {
|
|
127
|
+
let t = R ? [[
|
|
128
|
+
.3,
|
|
129
|
+
.6,
|
|
130
|
+
1.3
|
|
131
|
+
]] : z ? [[
|
|
132
|
+
.42,
|
|
133
|
+
.4,
|
|
134
|
+
1.5
|
|
135
|
+
], [
|
|
136
|
+
.13,
|
|
137
|
+
.85,
|
|
138
|
+
1
|
|
139
|
+
]] : [
|
|
140
|
+
[
|
|
141
|
+
.6,
|
|
142
|
+
.3,
|
|
143
|
+
1.7
|
|
144
|
+
],
|
|
145
|
+
[
|
|
146
|
+
.28,
|
|
147
|
+
.6,
|
|
148
|
+
1.15
|
|
149
|
+
],
|
|
150
|
+
[
|
|
151
|
+
.09,
|
|
152
|
+
.92,
|
|
153
|
+
.85
|
|
154
|
+
]
|
|
155
|
+
];
|
|
156
|
+
e.shadowColor = v(o);
|
|
157
|
+
for (let [n, r, a] of t) e.shadowBlur = U(n), e.strokeStyle = y(o, r), e.lineWidth = F.lw * a * H, W(G, i);
|
|
158
|
+
e.shadowBlur = U(.05), e.strokeStyle = y([
|
|
159
|
+
Math.min(1, o[0] + .55),
|
|
160
|
+
Math.min(1, o[1] + .45),
|
|
161
|
+
Math.min(1, o[2] + .35)
|
|
162
|
+
], .95), e.lineWidth = F.lw * (R ? .62 : .42) * H, W(G, i), e.shadowBlur = 0;
|
|
163
|
+
}
|
|
164
|
+
R || (e.fillStyle = "rgba(255,255,255,0.045)", e.beginPath(), e.ellipse(b * .36, O * .28, b * .2, O * .12, -.5, 0, p), e.fill()), e.restore(), A(n, d, b, O, k), e.lineWidth = 1, e.strokeStyle = "rgba(255,255,255,0.08)", e.stroke();
|
|
165
|
+
}
|
|
166
|
+
function M() {
|
|
167
|
+
let e = d > 0 ? d : 1;
|
|
168
|
+
_ = Math.min(typeof window < "u" && window.devicePixelRatio || 1, e);
|
|
169
|
+
let n = t.getBoundingClientRect();
|
|
170
|
+
f = Math.max(1, n.width || t.clientWidth || 1), g = Math.max(1, n.height || t.clientHeight || 1), Math.abs(f - t.width) < 1 && Math.abs(g - t.height) < 1 && (t.style.width = `${f}px`, t.style.height = `${g}px`), t.width = Math.max(1, Math.round(f * _)), t.height = Math.max(1, Math.round(g * _)), j();
|
|
171
|
+
}
|
|
172
|
+
let N = typeof ResizeObserver < "u" ? new ResizeObserver(() => M()) : null;
|
|
173
|
+
return N?.observe(t), M(), {
|
|
174
|
+
setValue(e) {
|
|
175
|
+
i = O(e), j();
|
|
176
|
+
},
|
|
177
|
+
setOptions(t) {
|
|
178
|
+
if (t.style && (a = t.style), t.color != null && (o = e(t.color)), t.glow != null && (s = t.glow), t.background != null && (c = e(t.background)), t.mesh != null && (l = t.mesh), t.ghost != null && (u = t.ghost), t.value !== void 0 && (i = O(t.value)), t.pixelRatio != null) {
|
|
179
|
+
d = t.pixelRatio, M();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
j();
|
|
183
|
+
},
|
|
184
|
+
resize: M,
|
|
185
|
+
snapshot() {
|
|
186
|
+
return j(), t.toDataURL("image/png");
|
|
187
|
+
},
|
|
188
|
+
dispose() {
|
|
189
|
+
N?.disconnect();
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
export { A as createNixieTube };
|
|
195
|
+
|
|
196
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/nixie.ts"],"sourcesContent":["// @glowbox/nixie — a nixie-tube display *component*, a sibling rendering core to\n// @glowbox/led-grid's LED grid. It renders a single glowing numeral the way a real nixie\n// works: a stack of bent-wire cathodes inside a glass tube, only one lit, warm-orange,\n// behind a honeycomb anode mesh. Give it a canvas + a value; drive it with\n// setValue/setOptions. The `style` prop picks the physical tube's numeral shape.\n//\n// The numerals are hand-authored *single-stroke* SVG paths (a viewBox of 60×100),\n// stroked as one constant-width filament with round caps — a real nixie digit is one\n// continuous bent wire, not a filled glyph. Each digit also carries a small fixed\n// NUDGE: in a real tube the cathodes sit at slightly different positions/depths, so the\n// glowing number shifts a hair off dead-centre and jitters as the value changes.\nimport { type Color, parseColor } from '@glowbox/led-grid';\n\nconst TAU = Math.PI * 2;\nconst VB_W = 60;\nconst VB_H = 100;\n\n// Single-stroke wire numerals (viewBox 0 0 60 100, y-down): each is one thin geometric\n// filament — the cathode wire — drawn round-capped, in a light geometric grotesque (à la\n// TT Chocolates Extra Light). The shapes live as editable SVGs in ../glyphs (their single\n// source of truth); Vite inlines each at build time and we lift the path data out, so\n// tuning a numeral is just editing its .svg — no second copy to keep in sync. (`dash.svg`\n// → '-', `colon.svg` → the two-dot separator.) Every `d=` attribute in a file is read\n// (single- or double-quoted) and concatenated, so layers without a `d` — guide rects, a\n// `<title>`, etc. — are ignored; but any extra `<path>` is drawn too, so keep each glyph to\n// the strokes you want.\nconst rawSvgs = import.meta.glob('../glyphs/*.svg', {\n\tquery: '?raw',\n\timport: 'default',\n\teager: true\n}) as Record<string, string>;\nconst pathData = (svg: string): string =>\n\t[...svg.matchAll(/\\bd\\s*=\\s*([\"'])([\\s\\S]*?)\\1/g)].map((m) => m[2]).join(' ');\nconst GLYPHS: Record<string, string> = {};\nlet colonD = '';\nfor (const [file, svg] of Object.entries(rawSvgs)) {\n\tconst name = file.slice(file.lastIndexOf('/') + 1, -'.svg'.length);\n\tconst ch = name === 'dash' ? '-' : name === 'colon' ? ':' : name;\n\tconst d = pathData(svg);\n\tif (!d) continue;\n\tif (ch === ':') colonD = d;\n\telse GLYPHS[ch] = d;\n}\n// Colon separator: two circles, stroked as glowing wire rings like the numerals.\nconst COLON = new Path2D(colonD);\n\n// Stack order, front → back (the physical cathode order of an IN-14-style tube). Depth\n// index gives each numeral a slightly different position/scale, so the glowing digit\n// sits a hair off dead-centre and set back among the wire stack.\nconst STACK = '1234567890';\nconst DEPTH: Record<string, number> = {};\nfor (let i = 0; i < STACK.length; i++) DEPTH[STACK[i]] = i;\n\n// A digit's parallax offset + scale from its depth in the stack (viewBox units). The\n// cathodes sit nearly concentric — mostly set *back* (smaller) with a small sideways\n// drift — so they read as a nested stack, not a scattered spread. Symbols that aren't part\n// of the numeral stack (the `:` and `-` separators) get no parallax — they render centred.\nconst placement = (ch: string): { ox: number; oy: number; ds: number } => {\n\tconst d = DEPTH[ch];\n\tif (d === undefined) return { ox: 0, oy: 0, ds: 1 };\n\tconst t = (d - 4.5) / 4.5; // -1 (front) .. +1 (back)\n\treturn { ox: t * 1.2, oy: -t * 1.1 - 0.4 * Math.sin(d * 1.7), ds: 1 - d * 0.02 };\n};\n\n// Per-style wire proportions: horizontal/vertical squash and filament width (viewBox units).\nconst STYLES: Record<NixieStyle, { sx: number; sy: number; lw: number }> = {\n\tclassic: { sx: 1, sy: 1, lw: 4.2 },\n\tslim: { sx: 0.84, sy: 1.06, lw: 3.2 },\n\ttall: { sx: 0.9, sy: 1.16, lw: 3.7 }\n};\n// Dull-metal colour of the unlit cathode wires behind the glass (dim nickel).\nconst WIRE: number[] = [0.52, 0.52, 0.56];\n\nconst glyphCache = new Map<string, Path2D>();\nconst pathFor = (ch: string): Path2D | null => {\n\tif (ch === ':') return COLON;\n\tconst d = GLYPHS[ch];\n\tif (!d) return null;\n\tlet p = glyphCache.get(ch);\n\tif (!p) {\n\t\tp = new Path2D(d);\n\t\tglyphCache.set(ch, p);\n\t}\n\treturn p;\n};\n\n/** Physical tube style — changes the numeral shape (and proportions). */\nexport type NixieStyle = 'classic' | 'slim' | 'tall';\n\nexport interface NixieOptions {\n\t/** The symbol to light: a single char `0–9`, `:`, `-`, or null/'' = all cathodes dark. */\n\tvalue?: string | number | null;\n\t/** Tube style (default 'classic'). */\n\tstyle?: NixieStyle;\n\t/** Glow colour (default warm nixie orange). */\n\tcolor?: Color;\n\t/** Glow strength 0..1 (default 0.7). */\n\tglow?: number;\n\t/** Tube glass colour behind the numerals (default near-black). */\n\tbackground?: Color;\n\t/** Draw the honeycomb anode mesh over the tube (default true). */\n\tmesh?: boolean;\n\t/** Draw the other, unlit cathodes faintly behind — the stacked-numeral depth (default true). */\n\tghost?: boolean;\n\t/** Cap on devicePixelRatio (default 2). */\n\tpixelRatio?: number;\n}\n\nexport interface NixieTube {\n\t/** Light a new symbol (single char `0–9`, `:`, `-`, or null = off). */\n\tsetValue(v: string | number | null): void;\n\t/** Live-update any option. */\n\tsetOptions(patch: Partial<NixieOptions>): void;\n\t/** Redraw at the current canvas box size (call after the canvas resizes). */\n\tresize(): void;\n\t/** Return the current frame as a PNG data URL. */\n\tsnapshot(): string;\n\tdispose(): void;\n}\n\nconst norm = (v: string | number | null | undefined): string =>\n\tv == null ? '' : String(v).slice(0, 1);\nconst c255 = (v: number) => Math.max(0, Math.min(255, Math.round(v * 255)));\n\n/** Create a nixie tube on a 2D canvas. Returns null if a 2D context is unavailable. */\nexport function createNixieTube(\n\tcanvas: HTMLCanvasElement,\n\topts: NixieOptions = {}\n): NixieTube | null {\n\tconst ctx = canvas.getContext('2d');\n\tif (!ctx) return null;\n\n\tlet value = norm(opts.value);\n\tlet style: NixieStyle = opts.style ?? 'classic';\n\tlet color = parseColor(opts.color ?? [1, 0.45, 0.08]);\n\tlet glow = opts.glow ?? 0.7;\n\tlet bg = parseColor(opts.background ?? [0.03, 0.03, 0.045]);\n\tlet mesh = opts.mesh ?? true;\n\tlet ghost = opts.ghost ?? true;\n\tlet pixelRatio = opts.pixelRatio ?? 2;\n\tlet w = 0;\n\tlet h = 0;\n\tlet dpr = 1;\n\n\tconst rgb = (c: number[]) => `rgb(${c255(c[0])},${c255(c[1])},${c255(c[2])})`;\n\tconst rgba = (c: number[], a: number) => `rgba(${c255(c[0])},${c255(c[1])},${c255(c[2])},${a})`;\n\n\tfunction hex(cx: number, cy: number, r: number) {\n\t\tctx!.beginPath();\n\t\tfor (let i = 0; i < 6; i++) {\n\t\t\tconst a = Math.PI / 6 + (i * Math.PI) / 3;\n\t\t\tconst px = cx + r * Math.cos(a);\n\t\t\tconst py = cy + r * Math.sin(a);\n\t\t\tif (i) ctx!.lineTo(px, py);\n\t\t\telse ctx!.moveTo(px, py);\n\t\t}\n\t\tctx!.closePath();\n\t}\n\tfunction drawMesh(bw: number, bh: number) {\n\t\tctx!.strokeStyle = 'rgba(120,120,135,0.09)';\n\t\tctx!.lineWidth = 0.8;\n\t\tconst r = Math.max(bw, bh) * 0.06;\n\t\tconst hw = r * Math.sqrt(3);\n\t\tfor (let y = -r; y < bh + r; y += r * 1.5) {\n\t\t\tconst off = (Math.round(y / (r * 1.5)) % 2) * (hw / 2);\n\t\t\tfor (let x = -hw; x < bw + hw; x += hw) {\n\t\t\t\thex(x + off, y, r);\n\t\t\t\tctx!.stroke();\n\t\t\t}\n\t\t}\n\t}\n\t// The glass envelope shape — its shadow, fill/clip, and rim all trace this rounded rect.\n\tfunction roundRect(x: number, y: number, rw: number, rh: number, r: number) {\n\t\tconst rr = Math.min(r, rw / 2, rh / 2);\n\t\tconst g = ctx!;\n\t\tg.beginPath();\n\t\tg.moveTo(x + rr, y);\n\t\tg.arcTo(x + rw, y, x + rw, y + rh, rr);\n\t\tg.arcTo(x + rw, y + rh, x, y + rh, rr);\n\t\tg.arcTo(x, y + rh, x, y, rr);\n\t\tg.arcTo(x, y, x + rw, y, rr);\n\t\tg.closePath();\n\t}\n\n\tfunction draw() {\n\t\tif (!w || !h) return;\n\t\tconst g = ctx!;\n\t\tg.setTransform(1, 0, 0, 1, 0, 0);\n\t\t// Clear to *transparent* — the page background shows through the margin around the\n\t\t// glass. The glass is drawn as a rounded module inset from the canvas edge so it can\n\t\t// cast a soft shadow into that margin: that depth is what lets the tube sit right on\n\t\t// ANY backdrop, including white. (A bloom can't read against white, so the glass\n\t\t// itself must stay dark; the shadow + rim, not a light glass, are what carry it.)\n\t\tg.clearRect(0, 0, canvas.width, canvas.height);\n\t\tg.scale(dpr, dpr);\n\t\t// Shadow/glass margin — horizontal tracks width, vertical tracks height, so tubes\n\t\t// that share a canvas height get the SAME glass height (a narrow separator tube stays\n\t\t// exactly as tall as the digit tubes beside it, instead of gaining height from a\n\t\t// smaller margin).\n\t\tconst padX = Math.max(4, w * 0.08);\n\t\tconst padY = Math.max(4, h * 0.05);\n\t\tconst bw = w - 2 * padX;\n\t\tconst bh = h - 2 * padY;\n\t\tconst rad = Math.min(bw, bh) * 0.1;\n\t\tconst sPad = Math.min(padX, padY);\n\n\t\t// Drop shadow (invisible on a dark page, a soft lift on a light one).\n\t\tg.save();\n\t\tg.shadowColor = 'rgba(0,0,0,0.42)';\n\t\tg.shadowBlur = sPad * 1.6;\n\t\tg.shadowOffsetY = padY * 0.3;\n\t\troundRect(padX, padY, bw, bh, rad);\n\t\tg.fillStyle = '#000';\n\t\tg.fill();\n\t\tg.restore();\n\n\t\t// Clip to the glass and work in glass-local coordinates.\n\t\tg.save();\n\t\troundRect(padX, padY, bw, bh, rad);\n\t\tg.clip();\n\t\tg.translate(padX, padY);\n\n\t\t// Tube glass: a radial vignette from the glass colour out to a darkened tint of it\n\t\t// (not pure black), so coloured/tinted tubes keep a coloured — not black — rim.\n\t\tconst grad = g.createRadialGradient(bw / 2, bh / 2, 0, bw / 2, bh / 2, Math.max(bw, bh) * 0.72);\n\t\tgrad.addColorStop(0, rgba(bg, 1));\n\t\tgrad.addColorStop(1, rgba([bg[0] * 0.16, bg[1] * 0.16, bg[2] * 0.18], 1));\n\t\tg.fillStyle = grad;\n\t\tg.fillRect(0, 0, bw, bh);\n\n\t\tconst cx = bw / 2;\n\t\tconst cy = bh / 2;\n\t\tconst st = STYLES[style];\n\t\tconst isColon = value === ':';\n\t\t// Level of detail from the rendered numeral height (css px). The filament illusion —\n\t\t// a *thin* wire under a heavy bloom, behind a honeycomb mesh and a stack of cathode\n\t\t// wires — reads down to ~64px. Smaller than that, forcing it makes a sub-pixel wire\n\t\t// vanish into its own blur, so we switch methods: fatten the filament, drop the mesh\n\t\t// + stack, and use fewer/tighter bloom passes. Tiny tubes become a plain bold glyph.\n\t\t// The colon's ink is narrow (two dots), so gauge it by tube *height* — otherwise its\n\t\t// slim separator tube would wrongly demote it below its digit neighbours.\n\t\tconst est = isColon ? bh * 0.62 : Math.min((bw * 0.6) / VB_W, (bh * 0.7) / VB_H) * VB_H;\n\t\tconst micro = est < 32; // tiny — a legible bold glyph, minimal glow\n\t\tconst compact = est < 64; // small numeral — no cathode stack, lighter bloom\n\t\t// The mesh belongs to the glass, so it tracks glass *height* — uniform across a row —\n\t\t// which keeps a narrow separator tube's mesh matching its wider digit neighbours (a\n\t\t// min(bw,bh) gauge would single the slim colon out). The cathode stack, below, tracks\n\t\t// numeral size instead (it needs room to read).\n\t\tif (mesh && bh >= 34) drawMesh(bw, bh);\n\n\t\t// Fit the numeral into the glass. The digit viewBox already carries side margin, so\n\t\t// the width factor can run fairly high — the glyph fills most of the glass width\n\t\t// (the bloom is clipped to the glass anyway) — which lets a narrow tube still show a\n\t\t// full-size numeral. Small tubes fill even more (legibility trumps bloom room). The\n\t\t// colon is fit by height (its ink is narrow), falling back to width only in an\n\t\t// extremely slim tube, so the dots stay sized like the digits beside them.\n\t\tconst s = isColon\n\t\t\t? Math.min((bh * 0.62) / VB_H, (bw * 0.9) / 24)\n\t\t\t: compact\n\t\t\t\t? Math.min((bw * 0.82) / VB_W, (bh * 0.82) / VB_H)\n\t\t\t\t: Math.min((bw * 0.72) / VB_W, (bh * 0.68) / VB_H);\n\t\tconst sz = VB_H * s; // filament height in css px — the glow scale reference\n\t\tconst weight = micro ? 2.4 : compact ? 1.5 : 1; // fatten the thin wire as it shrinks\n\t\tconst blur = (f: number) => Math.min(sz * f * glow, 160); // cap: perf + huge-size sanity\n\t\tg.lineJoin = 'round';\n\t\tg.lineCap = 'round';\n\n\t\t// Position a glyph at its depth in the cathode stack (parallax offset + scale),\n\t\t// under the style's squash, then stroke it. Everything is a stroked filament — the\n\t\t// colon's circles included (they render as glowing wire rings, matching the digits).\n\t\tconst place = (path: Path2D, ch: string) => {\n\t\t\tconst { ox, oy, ds } = placement(ch);\n\t\t\tg.save();\n\t\t\tg.translate(cx + ox * s, cy + oy * s);\n\t\t\tg.scale(s * st.sx * ds, s * st.sy * ds);\n\t\t\tg.translate(-VB_W / 2, -VB_H / 2);\n\t\t\tg.stroke(path);\n\t\t\tg.restore();\n\t\t};\n\n\t\t// The physical cathode stack: every numeral as a thin dull-metal wire, back-to-front\n\t\t// so nearer digits overlap farther ones — the wire stack you see behind the glass\n\t\t// even unlit. Only on a digit tube (a separator tube — `:`/`-` — has no digit\n\t\t// cathodes, so it shows none) and only when big enough to read (else a smudge).\n\t\tif (ghost && !compact && DEPTH[value] !== undefined) {\n\t\t\tg.shadowBlur = 0;\n\t\t\tfor (const ch of STACK) {\n\t\t\t\tif (ch === value) continue; // the lit numeral is drawn glowing below\n\t\t\t\tconst p = pathFor(ch);\n\t\t\t\tif (!p) continue;\n\t\t\t\tconst back = DEPTH[ch] / 9; // 0 front .. 1 back → farther wires dimmer\n\t\t\t\tg.strokeStyle = rgba(WIRE, 0.3 - back * 0.19);\n\t\t\t\tg.lineWidth = st.lw * 0.72;\n\t\t\t\tplace(p, ch);\n\t\t\t}\n\t\t}\n\n\t\t// The lit cathode: an outer neon bloom (fewer passes as the tube shrinks), then a\n\t\t// hot thin filament core.\n\t\tconst path = pathFor(value);\n\t\tif (path) {\n\t\t\tconst layers = micro\n\t\t\t\t? [[0.3, 0.6, 1.3]]\n\t\t\t\t: compact\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t[0.42, 0.4, 1.5],\n\t\t\t\t\t\t\t[0.13, 0.85, 1]\n\t\t\t\t\t\t]\n\t\t\t\t\t: [\n\t\t\t\t\t\t\t[0.6, 0.3, 1.7],\n\t\t\t\t\t\t\t[0.28, 0.6, 1.15],\n\t\t\t\t\t\t\t[0.09, 0.92, 0.85]\n\t\t\t\t\t\t];\n\t\t\tg.shadowColor = rgb(color);\n\t\t\tfor (const [blurF, alpha, lwF] of layers) {\n\t\t\t\tg.shadowBlur = blur(blurF);\n\t\t\t\tg.strokeStyle = rgba(color, alpha);\n\t\t\t\tg.lineWidth = st.lw * lwF * weight;\n\t\t\t\tplace(path, value);\n\t\t\t}\n\t\t\t// Hot core — a thinner, near-white filament down the centre of the wire.\n\t\t\tg.shadowBlur = blur(0.05);\n\t\t\tg.strokeStyle = rgba(\n\t\t\t\t[Math.min(1, color[0] + 0.55), Math.min(1, color[1] + 0.45), Math.min(1, color[2] + 0.35)],\n\t\t\t\t0.95\n\t\t\t);\n\t\t\tg.lineWidth = st.lw * (micro ? 0.62 : 0.42) * weight;\n\t\t\tplace(path, value);\n\t\t\tg.shadowBlur = 0;\n\t\t}\n\t\t// Glass highlight (skip on tiny tubes — it's just noise there).\n\t\tif (!micro) {\n\t\t\tg.fillStyle = 'rgba(255,255,255,0.045)';\n\t\t\tg.beginPath();\n\t\t\tg.ellipse(bw * 0.36, bh * 0.28, bw * 0.2, bh * 0.12, -0.5, 0, TAU);\n\t\t\tg.fill();\n\t\t}\n\t\tg.restore(); // end glass clip\n\n\t\t// Glass rim: a hairline bright edge so the envelope reads as a curved-glass tube even\n\t\t// against a light background.\n\t\troundRect(padX, padY, bw, bh, rad);\n\t\tg.lineWidth = 1;\n\t\tg.strokeStyle = 'rgba(255,255,255,0.08)';\n\t\tg.stroke();\n\t}\n\n\tfunction resize() {\n\t\t// pixelRatio caps the device ratio; guard a non-positive value (fall back to 1×) so a\n\t\t// stray 0 can't zero the backing store below.\n\t\tconst cap = pixelRatio > 0 ? pixelRatio : 1;\n\t\tdpr = Math.min(typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1, cap);\n\t\tconst r = canvas.getBoundingClientRect();\n\t\t// CSS (layout) size — never derive it from canvas.width (that's the *device*\n\t\t// backing size; on retina it's dpr× the display size and reusing it here would\n\t\t// inflate the tube every frame).\n\t\tw = Math.max(1, r.width || canvas.clientWidth || 1);\n\t\th = Math.max(1, r.height || canvas.clientHeight || 1);\n\t\t// If the element has no CSS size of its own, its layout box is driven by the\n\t\t// width/height *attributes* — so writing a HiDPI backing store below would enlarge\n\t\t// the box and re-fire the ResizeObserver, an unbounded loop on retina. Pin the\n\t\t// display size in CSS px once so the backing-store write can't feed back into\n\t\t// layout. A CSS-sized canvas (width:100%, fixed px, …) reports a box that differs\n\t\t// from the attribute, so it's left alone and stays responsive.\n\t\tif (Math.abs(w - canvas.width) < 1 && Math.abs(h - canvas.height) < 1) {\n\t\t\tcanvas.style.width = `${w}px`;\n\t\t\tcanvas.style.height = `${h}px`;\n\t\t}\n\t\t// Floor at 1px: a sub-1 dpr on a tiny element must not round to a 0-size (blank) canvas.\n\t\tcanvas.width = Math.max(1, Math.round(w * dpr));\n\t\tcanvas.height = Math.max(1, Math.round(h * dpr));\n\t\tdraw();\n\t}\n\n\tconst ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(() => resize()) : null;\n\tro?.observe(canvas);\n\tresize();\n\n\treturn {\n\t\tsetValue(v) {\n\t\t\tvalue = norm(v);\n\t\t\tdraw();\n\t\t},\n\t\tsetOptions(patch) {\n\t\t\tif (patch.style) style = patch.style;\n\t\t\tif (patch.color != null) color = parseColor(patch.color);\n\t\t\tif (patch.glow != null) glow = patch.glow;\n\t\t\tif (patch.background != null) bg = parseColor(patch.background);\n\t\t\tif (patch.mesh != null) mesh = patch.mesh;\n\t\t\tif (patch.ghost != null) ghost = patch.ghost;\n\t\t\tif (patch.value !== undefined) value = norm(patch.value);\n\t\t\tif (patch.pixelRatio != null) {\n\t\t\t\tpixelRatio = patch.pixelRatio;\n\t\t\t\tresize();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tdraw();\n\t\t},\n\t\tresize,\n\t\tsnapshot() {\n\t\t\tdraw();\n\t\t\treturn canvas.toDataURL('image/png');\n\t\t},\n\t\tdispose() {\n\t\t\tro?.disconnect();\n\t\t}\n\t};\n}\n"],"mappings":"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"}
|
package/dist/nixie.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Color } from '@glowbox/led-grid';
|
|
2
|
+
/** Physical tube style — changes the numeral shape (and proportions). */
|
|
3
|
+
export type NixieStyle = 'classic' | 'slim' | 'tall';
|
|
4
|
+
export interface NixieOptions {
|
|
5
|
+
/** The symbol to light: a single char `0–9`, `:`, `-`, or null/'' = all cathodes dark. */
|
|
6
|
+
value?: string | number | null;
|
|
7
|
+
/** Tube style (default 'classic'). */
|
|
8
|
+
style?: NixieStyle;
|
|
9
|
+
/** Glow colour (default warm nixie orange). */
|
|
10
|
+
color?: Color;
|
|
11
|
+
/** Glow strength 0..1 (default 0.7). */
|
|
12
|
+
glow?: number;
|
|
13
|
+
/** Tube glass colour behind the numerals (default near-black). */
|
|
14
|
+
background?: Color;
|
|
15
|
+
/** Draw the honeycomb anode mesh over the tube (default true). */
|
|
16
|
+
mesh?: boolean;
|
|
17
|
+
/** Draw the other, unlit cathodes faintly behind — the stacked-numeral depth (default true). */
|
|
18
|
+
ghost?: boolean;
|
|
19
|
+
/** Cap on devicePixelRatio (default 2). */
|
|
20
|
+
pixelRatio?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface NixieTube {
|
|
23
|
+
/** Light a new symbol (single char `0–9`, `:`, `-`, or null = off). */
|
|
24
|
+
setValue(v: string | number | null): void;
|
|
25
|
+
/** Live-update any option. */
|
|
26
|
+
setOptions(patch: Partial<NixieOptions>): void;
|
|
27
|
+
/** Redraw at the current canvas box size (call after the canvas resizes). */
|
|
28
|
+
resize(): void;
|
|
29
|
+
/** Return the current frame as a PNG data URL. */
|
|
30
|
+
snapshot(): string;
|
|
31
|
+
dispose(): void;
|
|
32
|
+
}
|
|
33
|
+
/** Create a nixie tube on a 2D canvas. Returns null if a 2D context is unavailable. */
|
|
34
|
+
export declare function createNixieTube(canvas: HTMLCanvasElement, opts?: NixieOptions): NixieTube | null;
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@glowbox/nixie",
|
|
3
|
+
"version": "1.0.0-rc.2",
|
|
4
|
+
"description": "Nixie-tube display component — a glowing vector-numeral rendering core in the glowbox family.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nixie",
|
|
7
|
+
"tube",
|
|
8
|
+
"retro",
|
|
9
|
+
"display",
|
|
10
|
+
"clock",
|
|
11
|
+
"glow"
|
|
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/nixie"
|
|
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 (own code; core external)",
|
|
48
|
+
"path": "dist/index.js",
|
|
49
|
+
"ignore": [
|
|
50
|
+
"@glowbox/led-grid"
|
|
51
|
+
],
|
|
52
|
+
"limit": "3 kB"
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@glowbox/led-grid": "^1.0.0-rc.2"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@anarkisti/eslint-config": "^1",
|
|
60
|
+
"@vitest/browser": "^4.1",
|
|
61
|
+
"@vitest/browser-playwright": "^4.1",
|
|
62
|
+
"eslint": "^10",
|
|
63
|
+
"playwright": "^1.61",
|
|
64
|
+
"typescript": "^6",
|
|
65
|
+
"vite": "^8.1",
|
|
66
|
+
"vite-plugin-dts": "^5",
|
|
67
|
+
"vitest": "^4.1"
|
|
68
|
+
}
|
|
69
|
+
}
|