@feelgxxd/textel 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,22 +2,9 @@
2
2
 
3
3
  Turn 3D models into animated ASCII art, in the terminal and on the web.
4
4
 
5
- ```
6
- /-----------------\
7
- /---/-----------------\----\
8
- /--//-------------------------\\--\
9
- ////-----%@@@@@@@@@@@@##########----||
10
- /##%//%%%%@@@@@@@@@@@@@%############\|
11
- \\***##%@@@@@@@@@@@@@@@@%#####*++++==|
12
- \\\**##############%%%%+==========//
13
- \-\\*###############*====://////
14
- \\\----------------///--///
15
- \\\----------------//-/
16
- \--\-----------///
17
- \-\------//-/
18
- \-\--///
19
- \--/
20
- ```
5
+ <p align="center"><img src="docs/gem.svg" width="480" alt="A gem spinning, drawn in ASCII characters"></p>
6
+
7
+ <p align="center"><sub>An animated SVG made with <code>textel render --example gem --size 80x40 --frames 80 --fps 20 -o gem.svg</code></sub></p>
21
8
 
22
9
  ```sh
23
10
  npx @feelgxxd/textel model.glb # spins in your terminal
@@ -29,11 +16,22 @@ Install it once with `npm install -g @feelgxxd/textel` and the command is just `
29
16
 
30
17
  Reads `.glb`, `.gltf`, `.obj` and `.stl`. Everything runs locally; nothing is uploaded.
31
18
 
19
+ ## Playground
20
+
21
+ **[feelgxxd.github.io/textel](https://feelgxxd.github.io/textel/)**: drop a model into the page, tune the
22
+ look with a live preview, and take it with you. Nothing to install, and your files never leave the browser.
23
+
24
+ - Start from a preset (terminal green, amber CRT, blueprint, ink on paper, blocks) or build your own look:
25
+ characters, color and gradients, outlines, shading, light, camera and motion, and per-material styles
26
+ - **Copy link** puts the whole look in the URL, so a link opens the same look (the model itself stays with you)
27
+ - Export it as a viewer embed for a page, a recorded `.textel.json` loop with a player embed, the matching
28
+ `textel` command for the terminal, or a preset JSON file
29
+
32
30
  ## Terminal
33
31
 
34
32
  ```
35
33
  textel <model> [options] spin it in the terminal (same as textel preview)
36
- textel render <model> [-o f] record a spinning loop into a .textel.json file
34
+ textel render <model> [-o f] record a spinning loop into a .textel.json file or an animated .svg
37
35
  textel play <file> play a recorded file
38
36
  textel list built-in examples and charsets
39
37
  textel help <command> all options of a command
@@ -86,6 +84,8 @@ Two elements, both without dependencies and both loaded with one script tag:
86
84
  | dragging | scrubs through the loop | turns the model any way, with a flick |
87
85
  | best for | a spinning logo | something people play with |
88
86
 
87
+ Where scripts cannot run, such as a GitHub README, use an animated SVG instead (see below).
88
+
89
89
  ### `<textel-player>`
90
90
 
91
91
  Record once, then play the file: no 3D code in the page.
@@ -95,7 +95,7 @@ textel render logo.glb --aspect 1.5 -o logo.textel.json
95
95
  ```
96
96
 
97
97
  ```html
98
- <script type="module" src="https://cdn.jsdelivr.net/npm/@feelgxxd/textel@0.2/dist/textel-player.js"></script>
98
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@feelgxxd/textel@0.5/dist/textel-player.js"></script>
99
99
  <textel-player src="logo.textel.json"></textel-player>
100
100
  ```
101
101
 
@@ -121,7 +121,7 @@ or `::part(screen)`. From JavaScript: `player.play()`, `player.pause()`, `player
121
121
  ### `<textel-viewer>`
122
122
 
123
123
  ```html
124
- <script type="module" src="https://cdn.jsdelivr.net/npm/@feelgxxd/textel@0.3/dist/textel-viewer.js"></script>
124
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@feelgxxd/textel@0.5/dist/textel-viewer.js"></script>
125
125
  <textel-viewer src="model.glb" charset="hard" color="#301000,#ffb000" edges></textel-viewer>
126
126
  ```
127
127
 
@@ -141,7 +141,7 @@ background tabs and respects reduced motion.
141
141
  | `cols` | grid width in characters (default 96) |
142
142
  | `paused` | start without spinning |
143
143
  | `nodrag` | turn off dragging |
144
- | `zoomable` | wheel and `+` / `-` zoom |
144
+ | `zoomable` | wheel, pinch and `+` / `-` zoom; on touch screens it takes all touches, so the page does not scroll over it |
145
145
  | `label` | accessible name |
146
146
 
147
147
  A preset is applied first, then the attributes, then `viewer.look = { ... }` from JavaScript.
@@ -150,6 +150,46 @@ Also from JavaScript: `viewer.load(urlOrFileOrBytes)`, `viewer.mesh = mesh` (for
150
150
 
151
151
  `npm run demo` serves both demo pages with drop zones for your own files, and an output benchmark.
152
152
 
153
+ ### Animated SVG
154
+
155
+ An image that spins by itself, with no script: it plays in a GitHub README, in docs and anywhere else an
156
+ `<img>` goes. The one at the top of this page is one.
157
+
158
+ ```sh
159
+ textel render logo.glb --size 80x40 --frames 80 --fps 20 -o logo.svg
160
+ ```
161
+
162
+ ```md
163
+ ![logo](logo.svg)
164
+ ```
165
+
166
+ Every frame is stored whole, so keep it small: 80×40 with 80 frames (one turn in 4 s) is about 290 KB
167
+ (24 KB gzipped) without color; a gradient makes it several times larger. Without colors of its own it is transparent and
168
+ its text follows the reader's light or dark theme. It shows a still frame to readers who prefer reduced
169
+ motion. The playground makes one from its **Image** tab, and the library from any recording with
170
+ `recordingSVG(recording, { color, background, cell })`.
171
+
172
+ ### GIF and video
173
+
174
+ For chats and social posts, the playground's **Image** tab also makes a GIF (640×640, about 600 KB) and
175
+ an MP4 video (1280×1280, about 1.4 MB), both one turn in 4 seconds in the page colors. They are made in
176
+ your browser; the video needs a browser that can encode H.264 (current Chrome, Edge and Safari).
177
+ The same tab saves a PNG of the preview at the angle it shows.
178
+
179
+ ### One HTML file
180
+
181
+ The **Recorded loop** tab also makes a single HTML file with the player and the frames inside (about
182
+ 450 KB). It opens anywhere, from a disk, an email or a USB stick, with no internet.
183
+
184
+ ### From JavaScript
185
+
186
+ `viewer.view` tells the camera as the viewer draws it, so the same picture can be rendered at any size:
187
+
188
+ ```ts
189
+ const { yaw, pitch, zoom, fitPitch } = viewer.view;
190
+ const frame = createRenderer(mesh, { ...look, zoom, fitPitch, width: 160, height: 80 }).render({ yaw, pitch });
191
+ ```
192
+
153
193
  ## Presets
154
194
 
155
195
  A preset is the whole look in one JSON file. The same file works with the CLI and the library.
@@ -14,6 +14,16 @@ export declare class TextelViewer extends Base {
14
14
  /** the model being shown; set a Mesh to show one built in code */
15
15
  get mesh(): Mesh | null;
16
16
  set mesh(m: Mesh | null);
17
+ /**
18
+ * The camera as it is drawn right now. The same picture at any size:
19
+ * `createRenderer(mesh, { ...look, zoom, fitPitch, width, height }).render({ yaw, pitch })`.
20
+ */
21
+ get view(): {
22
+ yaw: number;
23
+ pitch: number;
24
+ zoom: number;
25
+ fitPitch: number;
26
+ };
17
27
  get paused(): boolean;
18
28
  play(): void;
19
29
  /** stops at once: the spin and any flick still coasting are dropped */
@@ -9,6 +9,7 @@ import { mergeStyles } from "../style.js";
9
9
  import { drag, gridFor, step, styleFromAttributes } from "./viewer-core.js";
10
10
  const CSS = `:host{display:block;position:relative;overflow:hidden;aspect-ratio:var(--textel-ratio,1);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;outline:none}
11
11
  :host(:not([nodrag])){cursor:grab;touch-action:pan-y}
12
+ :host([zoomable]:not([nodrag])){touch-action:none}
12
13
  :host(:not([nodrag]):active){cursor:grabbing}
13
14
  :host(:focus-visible){box-shadow:0 0 0 2px currentColor}
14
15
  pre{position:absolute;left:0;top:0;margin:0;transform-origin:0 0;font:inherit;font-size:16px;line-height:1;letter-spacing:0;white-space:pre;color:inherit;text-shadow:var(--textel-glow,none);-webkit-text-size-adjust:none;text-size-adjust:none;user-select:none;pointer-events:none}
@@ -60,6 +61,9 @@ export class TextelViewer extends Base {
60
61
  #last = -1;
61
62
  #dirty = true;
62
63
  #pointer = null;
64
+ /** every finger or pen on the element, for pinch zoom */
65
+ #touches = new Map();
66
+ #pinch = null;
63
67
  #load = null;
64
68
  #io;
65
69
  #ro;
@@ -142,6 +146,14 @@ export class TextelViewer extends Base {
142
146
  this.#layout();
143
147
  this.#kick();
144
148
  }
149
+ /**
150
+ * The camera as it is drawn right now. The same picture at any size:
151
+ * `createRenderer(mesh, { ...look, zoom, fitPitch, width, height }).render({ yaw, pitch })`.
152
+ */
153
+ get view() {
154
+ const s = this.#style(), zoom = (s.zoom ?? 1) * this.#zoom, fitPitch = this.hasAttribute('nodrag') ? 0 : MAX_PITCH;
155
+ return { yaw: this.#motion.yaw, pitch: this.#motion.pitch, zoom, fitPitch };
156
+ }
145
157
  get paused() { return !this.#playing; }
146
158
  play() { this.#playing = true; this.#kick(); }
147
159
  /** stops at once: the spin and any flick still coasting are dropped */
@@ -193,14 +205,14 @@ export class TextelViewer extends Base {
193
205
  if (!this.#renderer || !this.#grid)
194
206
  return this.#layout();
195
207
  const s = this.#style();
196
- this.#renderer.setStyle({ ...s, ...this.#gridStyle(s) });
208
+ this.#renderer.setStyle({ ...s, ...this.#gridStyle() });
197
209
  // show the change now, even while paused
198
210
  this.#draw();
199
211
  this.#kick();
200
212
  }
201
- #gridStyle(s) {
202
- const g = this.#grid;
203
- return { width: g.cols, height: g.rows, aspect: g.aspect, zoom: (s.zoom ?? 1) * this.#zoom, fitPitch: this.hasAttribute('nodrag') ? 0 : MAX_PITCH };
213
+ #gridStyle() {
214
+ const g = this.#grid, { zoom, fitPitch } = this.view;
215
+ return { width: g.cols, height: g.rows, aspect: g.aspect, zoom, fitPitch };
204
216
  }
205
217
  #layout() {
206
218
  if (!this.isConnected)
@@ -217,9 +229,9 @@ export class TextelViewer extends Base {
217
229
  const s = this.#style();
218
230
  try {
219
231
  if (!this.#renderer)
220
- this.#renderer = createRenderer(this.#mesh, { ...s, ...this.#gridStyle(s) });
232
+ this.#renderer = createRenderer(this.#mesh, { ...s, ...this.#gridStyle() });
221
233
  else
222
- this.#renderer.setStyle({ ...s, ...this.#gridStyle(s) });
234
+ this.#renderer.setStyle({ ...s, ...this.#gridStyle() });
223
235
  }
224
236
  catch (error) {
225
237
  this.dispatchEvent(new CustomEvent('error', { detail: { error } }));
@@ -280,21 +292,49 @@ export class TextelViewer extends Base {
280
292
  #onDown = (e) => {
281
293
  if (this.hasAttribute('nodrag') || !this.#renderer || e.button !== 0)
282
294
  return;
283
- this.#pointer = { x: e.clientX, y: e.clientY, t: performance.now() };
284
295
  this.setPointerCapture(e.pointerId);
296
+ this.#touches.set(e.pointerId, { x: e.clientX, y: e.clientY });
297
+ if (this.#touches.size === 2 && this.hasAttribute('zoomable')) {
298
+ // a second finger: stop turning and pinch instead
299
+ this.#pinch = { dist: this.#spread(), zoom: this.#zoom };
300
+ this.#pointer = null;
301
+ this.#motion = { ...this.#motion, vyaw: 0, vpitch: 0 };
302
+ }
303
+ else if (this.#touches.size === 1) {
304
+ this.#pointer = { id: e.pointerId, x: e.clientX, y: e.clientY, t: performance.now() };
305
+ }
285
306
  this.#kick();
286
307
  };
287
308
  #onMove = (e) => {
309
+ if (!this.#touches.has(e.pointerId))
310
+ return;
311
+ this.#touches.set(e.pointerId, { x: e.clientX, y: e.clientY });
312
+ if (this.#pinch) {
313
+ if (this.#pinch.dist > 0)
314
+ this.#setZoom(this.#pinch.zoom * (this.#spread() / this.#pinch.dist));
315
+ return;
316
+ }
288
317
  const p = this.#pointer;
289
- if (!p)
318
+ if (!p || p.id !== e.pointerId)
290
319
  return;
291
320
  const now = performance.now();
292
321
  this.#motion = drag(this.#motion, e.clientX - p.x, e.clientY - p.y, (now - p.t) / 1000, this.clientWidth, MAX_PITCH);
293
- this.#pointer = { x: e.clientX, y: e.clientY, t: now };
322
+ this.#pointer = { ...p, x: e.clientX, y: e.clientY, t: now };
294
323
  this.#kick();
295
324
  };
296
- #onUp = () => {
297
- if (!this.#pointer)
325
+ #onUp = (e) => {
326
+ if (!this.#touches.delete(e.pointerId))
327
+ return;
328
+ if (this.#pinch) {
329
+ // lifting a finger ends the pinch; a finger still down turns the model again, without a flick
330
+ if (this.#touches.size < 2)
331
+ this.#pinch = null;
332
+ const [rest] = [...this.#touches];
333
+ this.#pointer = rest && !this.#pinch ? { id: rest[0], ...rest[1], t: performance.now() } : null;
334
+ this.#kick();
335
+ return;
336
+ }
337
+ if (!this.#pointer || this.#pointer.id !== e.pointerId)
298
338
  return;
299
339
  // a pause before letting go means no flick
300
340
  if (performance.now() - this.#pointer.t > 80)
@@ -302,6 +342,11 @@ export class TextelViewer extends Base {
302
342
  this.#pointer = null;
303
343
  this.#kick();
304
344
  };
345
+ /** distance between the first two fingers, in pixels */
346
+ #spread() {
347
+ const [a, b] = [...this.#touches.values()];
348
+ return b ? Math.hypot(a.x - b.x, a.y - b.y) : 0;
349
+ }
305
350
  #onKey = (e) => {
306
351
  const m = this.#motion, step = 0.15;
307
352
  const moves = {
@@ -1,3 +1,3 @@
1
- export declare const summary = "record a spinning loop into a .textel.json file";
2
- export declare const HELP = "textel render: record a spinning loop into a .textel.json file\n\nusage\n textel render <model> [-o out.textel.json] [options]\n textel render --example <name> [options]\n\ninput\n <model> a .glb, .gltf, .obj or .stl file\n -e, --example <name> a built-in scene instead (textel list)\n\nrecording\n -o, --out <file> output file (default: <name>.textel.json here)\n --frames <n> frames in the loop (default 120)\n --fps <n> playback rate (default 30)\n --turns <n> turns over the loop; negative spins the other way (default 1)\n --force overwrite an existing file\n\nlook\n -c, --chars <ramp|name> charset name or a custom ramp, darkest first\n -p, --preset <file.json> style file; flags override it\n --color <#hex> one color; --color #301000,#ffb000 for a gradient\n --no-color plain characters\n --edges [depth] outline the silhouette; depth also marks depth steps; off\n --light <x,y,z> direction toward the light (x right, y up, z toward you)\n --ambient <0..1> brightness floor\n --shading <mode> auto | flat | smooth\n\ncamera\n --pitch <rad> resting tilt; positive shows the top\n --spin <rad/s> auto rotation speed (0 = still)\n --zoom <n> 1 fills the grid\n --distance <n> camera distance in model radii (perspective)\n -s, --size <WxH> grid size\n -a, --aspect <n> character height / width (default 2)\n\n the grid defaults to 112x56 at aspect 2 (terminals); for a web page pass the\n aspect of its font, for example --aspect 1.5\n";
1
+ export declare const summary = "record a spinning loop into a .textel.json file or an animated .svg";
2
+ export declare const HELP = "textel render: record a spinning loop into a .textel.json file or an animated .svg\n\nusage\n textel render <model> [-o out.textel.json | out.svg] [options]\n textel render --example <name> [options]\n\ninput\n <model> a .glb, .gltf, .obj or .stl file\n -e, --example <name> a built-in scene instead (textel list)\n\nrecording\n -o, --out <file> output file (default: <name>.textel.json here); a .svg file\n is an animated image that plays anywhere, a GitHub README too\n --frames <n> frames in the loop (default 120)\n --fps <n> playback rate (default 30)\n --turns <n> turns over the loop; negative spins the other way (default 1)\n --force overwrite an existing file\n\nlook\n -c, --chars <ramp|name> charset name or a custom ramp, darkest first\n -p, --preset <file.json> style file; flags override it\n --color <#hex> one color; --color #301000,#ffb000 for a gradient\n --no-color plain characters\n --edges [depth] outline the silhouette; depth also marks depth steps; off\n --light <x,y,z> direction toward the light (x right, y up, z toward you)\n --ambient <0..1> brightness floor\n --shading <mode> auto | flat | smooth\n\ncamera\n --pitch <rad> resting tilt; positive shows the top\n --spin <rad/s> auto rotation speed (0 = still)\n --zoom <n> 1 fills the grid\n --distance <n> camera distance in model radii (perspective)\n -s, --size <WxH> grid size\n -a, --aspect <n> character height / width (default 2)\n\n the grid defaults to 112x56 at aspect 2 (terminals); for a web page pass the\n aspect of its font, for example --aspect 1.5\n";
3
3
  export declare function run(argv: string[]): undefined;
@@ -2,20 +2,22 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { basename, extname, resolve } from 'node:path';
3
3
  import { gzipSync } from 'node:zlib';
4
4
  import { recordLoop, stringifyFrames } from "../../frames.js";
5
+ import { recordingSVG } from "../../svg.js";
5
6
  import { createRenderer } from "../../render.js";
6
7
  import { mergeStyles } from "../../style.js";
7
8
  import { fail, HELP_OPTIONS, INPUT_HELP, INPUT_OPTIONS, loadInput, num, parse, STYLE_HELP, STYLE_OPTIONS, styleFrom } from "../shared.js";
8
- export const summary = 'record a spinning loop into a .textel.json file';
9
- export const HELP = `textel render: record a spinning loop into a .textel.json file
9
+ export const summary = 'record a spinning loop into a .textel.json file or an animated .svg';
10
+ export const HELP = `textel render: record a spinning loop into a .textel.json file or an animated .svg
10
11
 
11
12
  usage
12
- textel render <model> [-o out.textel.json] [options]
13
+ textel render <model> [-o out.textel.json | out.svg] [options]
13
14
  textel render --example <name> [options]
14
15
 
15
16
  ${INPUT_HELP}
16
17
 
17
18
  recording
18
- -o, --out <file> output file (default: <name>.textel.json here)
19
+ -o, --out <file> output file (default: <name>.textel.json here); a .svg file
20
+ is an animated image that plays anywhere, a GitHub README too
19
21
  --frames <n> frames in the loop (default 120)
20
22
  --fps <n> playback rate (default 30)
21
23
  --turns <n> turns over the loop; negative spins the other way (default 1)
@@ -85,10 +87,10 @@ export function run(argv) {
85
87
  process.stderr.write(`\rrendering ${i}/${total} (${pct}%)`);
86
88
  },
87
89
  });
88
- const json = stringifyFrames(rec);
89
- writeFileSync(out, json);
90
+ const text = /\.svg$/i.test(out) ? recordingSVG(rec, { title: input.name }) : stringifyFrames(rec);
91
+ writeFileSync(out, text);
90
92
  if (progress)
91
93
  process.stderr.write('\r\x1b[K');
92
94
  const secs = ((performance.now() - t0) / 1000).toFixed(1);
93
- process.stderr.write(`wrote ${basename(out)}: ${frames} frames, ${rec.width}x${rec.height}, ${kb(json.length)} (${kb(gzipSync(json).length)} gzipped), ${secs}s\n`);
95
+ process.stderr.write(`wrote ${basename(out)}: ${frames} frames, ${rec.width}x${rec.height}, ${kb(text.length)} (${kb(gzipSync(text).length)} gzipped), ${secs}s\n`);
94
96
  }
package/dist/examples.js CHANGED
@@ -41,18 +41,32 @@ function* dice() {
41
41
  }
42
42
  return false;
43
43
  };
44
- // pips are shallow dimples pressed into each face
44
+ // pips are shallow dimples pressed into each face: spheres of radius r centered just outside it
45
+ const r = pip + 0.06, centers = [];
46
+ for (let f = 0; f < 6; f++) {
47
+ const [ax, sg, u, v] = SIDES[f];
48
+ const list = [];
49
+ for (const [pu, pv] of FACES[f]) {
50
+ const c = [0, 0, 0];
51
+ c[ax] = sg * (s + 0.05);
52
+ c[u] = pu;
53
+ c[v] = pv;
54
+ list.push(...c);
55
+ }
56
+ centers.push(list);
57
+ }
45
58
  const field = (x, y, z) => {
46
59
  let d = sdBox(x, y, z, s, s, s, 0.18);
47
60
  const p = [x, y, z];
48
61
  for (let f = 0; f < 6; f++) {
49
- const [ax, sg, u, v] = SIDES[f];
50
- for (const [pu, pv] of FACES[f]) {
51
- const c = [0, 0, 0];
52
- c[ax] = sg * (s + 0.05);
53
- c[u] = pu;
54
- c[v] = pv;
55
- d = Math.max(d, -(Math.hypot(p[0] - c[0], p[1] - c[1], p[2] - c[2]) - pip - 0.06));
62
+ const [ax, sg] = SIDES[f];
63
+ // a dimple only shapes the surface near its own face; farther in, the box is deeper anyway
64
+ if (p[ax] * sg < s - 0.3)
65
+ continue;
66
+ const c = centers[f];
67
+ for (let i = 0; i < c.length; i += 3) {
68
+ const dx = x - c[i], dy = y - c[i + 1], dz = z - c[i + 2];
69
+ d = Math.max(d, r - Math.sqrt(dx * dx + dy * dy + dz * dz));
56
70
  }
57
71
  }
58
72
  return d;
package/dist/frames.d.ts CHANGED
@@ -2,6 +2,7 @@ import { type ColorDepth } from './ansi.ts';
2
2
  import type { Recording } from './format.ts';
3
3
  import type { Frame, Renderer } from './render.ts';
4
4
  export * from './format.ts';
5
+ export { recordingSVG, type SVGOptions } from './svg.ts';
5
6
  export interface RecordOptions {
6
7
  /** frames in one full turn (default 120) */
7
8
  frames?: number;
package/dist/frames.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import { bg, FAINT, fg, line } from "./ansi.js";
4
4
  import { cellColor } from "./output.js";
5
5
  export * from "./format.js";
6
+ export { recordingSVG } from "./svg.js";
6
7
  const hex = (c) => '#' + c.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')).join('');
7
8
  /** Renders a seamless turntable loop: the last frame leads straight back into the first. */
8
9
  export function recordLoop(renderer, opts = {}) {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { createRenderer, type Frame, type Renderer, type View } from './render.ts';
2
2
  export { toText, toANSI, toHTML, cellColor, type ColorDepth } from './output.ts';
3
3
  export { parseModel, parseModelAsync, parseModelSteps, parseGLTF, parseOBJ, parseSTL, MODEL_EXTENSIONS } from './loaders/index.ts';
4
- export { recordLoop, RecordingBuilder, encodeStream, decodeStream, toFramesFile, fromFramesFile, stringifyFrames, parseFrames, recordingText, recordingANSI, FORMAT_VERSION, ENCODING, type Recording, type FramesFile, type RecordOptions, type CellStyle, } from './frames.ts';
4
+ export { recordLoop, RecordingBuilder, encodeStream, decodeStream, toFramesFile, fromFramesFile, stringifyFrames, parseFrames, recordingText, recordingANSI, recordingSVG, FORMAT_VERSION, ENCODING, type Recording, type FramesFile, type RecordOptions, type CellStyle, type SVGOptions, } from './frames.ts';
5
5
  export { MeshBuilder, computeSmoothNormals, vertexCount, triangleCount, type Mesh } from './mesh.ts';
6
6
  export { CHARSETS, DEFAULTS, mergeStyles, resolveStyle, type StyleInput, type Style, type MaterialInput, type ColorInput, type EdgeMode, type Vec3, type RGB, } from './style.ts';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { createRenderer } from "./render.js";
2
2
  export { toText, toANSI, toHTML, cellColor } from "./output.js";
3
3
  export { parseModel, parseModelAsync, parseModelSteps, parseGLTF, parseOBJ, parseSTL, MODEL_EXTENSIONS } from "./loaders/index.js";
4
- export { recordLoop, RecordingBuilder, encodeStream, decodeStream, toFramesFile, fromFramesFile, stringifyFrames, parseFrames, recordingText, recordingANSI, FORMAT_VERSION, ENCODING, } from "./frames.js";
4
+ export { recordLoop, RecordingBuilder, encodeStream, decodeStream, toFramesFile, fromFramesFile, stringifyFrames, parseFrames, recordingText, recordingANSI, recordingSVG, FORMAT_VERSION, ENCODING, } from "./frames.js";
5
5
  export { MeshBuilder, computeSmoothNormals, vertexCount, triangleCount } from "./mesh.js";
6
6
  export { CHARSETS, DEFAULTS, mergeStyles, resolveStyle, } from "./style.js";
package/dist/svg.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { Recording } from './format.ts';
2
+ export interface SVGOptions {
3
+ /** width of one character cell in pixels; its height is width × the recording's aspect (default 8) */
4
+ cell?: number;
5
+ /** text color; default: dark on light pages and light on dark pages, unless the recording has a background */
6
+ color?: string;
7
+ /** background color; default: the recording's own, or none (transparent) */
8
+ background?: string | null;
9
+ /** opacity of dim cells (default 0.6, as in the player) */
10
+ dim?: number;
11
+ /** font stack; the rows are stretched to the grid, so any monospace font lines up (default: system monospace) */
12
+ font?: string;
13
+ /** accessible title */
14
+ title?: string;
15
+ }
16
+ /** The whole recording as one self-contained, looping SVG document. */
17
+ export declare function recordingSVG(rec: Recording, opts?: SVGOptions): string;
package/dist/svg.js ADDED
@@ -0,0 +1,79 @@
1
+ const FONT = 'ui-monospace,SFMono-Regular,Menlo,Consolas,monospace';
2
+ const xml = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
3
+ const n = (v) => String(Math.round(v * 100) / 100);
4
+ // timing needs more digits: rounded delays would let two frames overlap for a few milliseconds
5
+ const t = (v) => String(Math.round(v * 1e4) / 1e4);
6
+ /** The whole recording as one self-contained, looping SVG document. */
7
+ export function recordingSVG(rec, opts = {}) {
8
+ const { width: W, height: H, glyphs } = rec;
9
+ const cw = opts.cell ?? 8, ch = cw * rec.aspect;
10
+ const bg = opts.background === undefined ? rec.bg : opts.background;
11
+ const dim = opts.dim ?? 0.6;
12
+ const count = rec.frames.length, dur = count / rec.fps;
13
+ const styles = rec.styles ?? [''];
14
+ // with no color and no background given, the text follows the reader's light or dark theme
15
+ const fill = opts.color ?? (bg ? (lum(bg) > 0.5 ? '#111' : '#eee') : '#24292f');
16
+ const themed = !opts.color && !bg;
17
+ const css = [
18
+ `text{font-family:${xml(opts.font ?? FONT)};font-size:${n(ch)}px;white-space:pre;fill:${xml(fill)}}`,
19
+ themed ? '@media (prefers-color-scheme:dark){text{fill:#e6edf3}}' : '',
20
+ '.d{fill-opacity:' + dim + '}',
21
+ // one short class per color, since a gradient repeats them thousands of times
22
+ ...styles.map((st, i) => (st.startsWith('#') ? `.c${i.toString(36)}{fill:${xml(st)}}` : '')),
23
+ count > 1
24
+ ? `.f{visibility:hidden;animation:t ${t(dur)}s steps(1,end) infinite}` +
25
+ `@keyframes t{0%{visibility:visible}${t(100 / count)}%{visibility:hidden}}` +
26
+ // without motion, the first frame stays
27
+ '@media (prefers-reduced-motion:reduce){.f{animation:none}#f0{visibility:visible}}'
28
+ : '',
29
+ ].join('');
30
+ const out = [];
31
+ const w = W * cw, h = H * ch;
32
+ out.push(`<svg xmlns="http://www.w3.org/2000/svg" width="${n(w)}" height="${n(h)}" viewBox="0 0 ${n(w)} ${n(h)}" role="img"${opts.title ? ` aria-label="${xml(opts.title)}"` : ''}>`);
33
+ if (opts.title)
34
+ out.push(`<title>${xml(opts.title)}</title>`);
35
+ out.push(`<style>${css}</style>`);
36
+ if (bg)
37
+ out.push(`<rect width="100%" height="100%" fill="${xml(bg)}"/>`);
38
+ // runs of one style become tspans: a color, or the dim class
39
+ const open = styles.map((st, i) => (st === '' ? '' : st === '~' ? '<tspan class="d">' : `<tspan class="c${i.toString(36)}">`));
40
+ for (let f = 0; f < count; f++) {
41
+ const { glyph, style } = rec.frames[f];
42
+ out.push(count > 1 ? `<g class="f" id="f${f}" style="animation-delay:${t((f * dur) / count)}s">` : '<g>');
43
+ for (let r = 0; r < H; r++) {
44
+ const at = r * W;
45
+ // trim blank cells at both ends; the row is placed and stretched to the grid instead
46
+ let a = 0, b = W;
47
+ while (a < b && glyphs[glyph[at + a]] === ' ')
48
+ a++;
49
+ while (b > a && glyphs[glyph[at + b - 1]] === ' ')
50
+ b--;
51
+ if (a === b)
52
+ continue;
53
+ let s = '', cur = 0;
54
+ for (let c = a; c < b; c++) {
55
+ const i = at + c, g = glyphs[glyph[i]];
56
+ // a blank keeps whatever style is open, so runs stay long
57
+ const st = style && g !== ' ' ? style[i] : cur;
58
+ if (st !== cur) {
59
+ if (cur)
60
+ s += '</tspan>';
61
+ s += open[st];
62
+ cur = st;
63
+ }
64
+ s += xml(g);
65
+ }
66
+ if (cur)
67
+ s += '</tspan>';
68
+ out.push(`<text x="${n(a * cw)}" y="${n(r * ch + ch * 0.8)}" textLength="${n((b - a) * cw)}" lengthAdjust="spacingAndGlyphs">${s}</text>`);
69
+ }
70
+ out.push('</g>');
71
+ }
72
+ out.push('</svg>');
73
+ return out.join('\n');
74
+ }
75
+ /** relative luminance of a #rrggbb color, 0..1 */
76
+ function lum(hex) {
77
+ const v = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
78
+ return 0.2126 * v[0] + 0.7152 * v[1] + 0.0722 * v[2];
79
+ }
@@ -1,10 +1,11 @@
1
- var yt=class{positions=[];normals=[];indices=[];triMaterial=[];materials=[];hasNormals=!0;matIndex=new Map;material(t){let n=this.matIndex.get(t);return n===void 0&&(n=this.materials.length,this.materials.push(t),this.matIndex.set(t,n)),n}vertex(t,n,r,s,i,o){let h=this.positions.length/3;return this.positions.push(t,n,r),s===void 0||i===void 0||o===void 0?(this.hasNormals=!1,this.normals.push(0,0,0)):this.normals.push(s,i,o),h}triangle(t,n,r,s){this.indices.push(t,n,r),this.triMaterial.push(s)}build(){return this.materials.length===0&&this.material("default"),{positions:new Float32Array(this.positions),normals:this.hasNormals&&this.positions.length?qe(new Float32Array(this.normals)):null,indices:new Uint32Array(this.indices),triMaterial:new Uint16Array(this.triMaterial),materials:this.materials}}};function qe(e){for(let t=0;t<e.length;t+=3){let n=Math.hypot(e[t],e[t+1],e[t+2])||1;e[t]/=n,e[t+1]/=n,e[t+2]/=n}return e}function xe(e){let t=e.positions,n=e.indices,r=t.length/3,s=new Uint32Array(r),i=new Map,o=[1/0,-1/0];for(let l=0;l<t.length;l++)o[0]=Math.min(o[0],t[l]),o[1]=Math.max(o[1],t[l]);let h=1e5/Math.max(o[1]-o[0],1e-9);for(let l=0;l<r;l++){let m=`${Math.round(t[l*3]*h)},${Math.round(t[l*3+1]*h)},${Math.round(t[l*3+2]*h)}`,v=i.get(m);v===void 0&&(v=i.size,i.set(m,v)),s[l]=v}let c=new Float64Array(i.size*3);for(let l=0;l<n.length;l+=3){let m=n[l]*3,v=n[l+1]*3,S=n[l+2]*3,u=t[v]-t[m],f=t[v+1]-t[m+1],a=t[v+2]-t[m+2],g=t[S]-t[m],p=t[S+1]-t[m+1],b=t[S+2]-t[m+2],A=f*b-a*p,G=a*g-u*b,O=u*p-f*g;for(let H of[n[l],n[l+1],n[l+2]]){let $=s[H]*3;c[$]+=A,c[$+1]+=G,c[$+2]+=O}}let y=new Float32Array(r*3);for(let l=0;l<r;l++){let m=s[l]*3,v=Math.hypot(c[m],c[m+1],c[m+2])||1;y[l*3]=c[m]/v,y[l*3+1]=c[m+1]/v,y[l*3+2]=c[m+2]/v}return y}var Ke={SCALAR:1,VEC2:2,VEC3:3,VEC4:4,MAT2:4,MAT3:9,MAT4:16};function ve(e,t={}){let n,r=null,s=new DataView(e.buffer,e.byteOffset,e.byteLength);if(e.length>=12&&s.getUint32(0,!0)===1179937895){let f=12;for(n={};f+8<=e.length;){let a=s.getUint32(f,!0),g=s.getUint32(f+4,!0),p=e.subarray(f+8,f+8+a);g===1313821514?n=JSON.parse(new TextDecoder().decode(p)):g===5130562&&(r=p),f+=8+a}}else n=JSON.parse(new TextDecoder().decode(e));let i=(n.extensionsRequired??[]).filter(f=>f==="KHR_draco_mesh_compression"||f==="EXT_meshopt_compression");if(i.length)throw new Error(`compressed glTF (${i.join(", ")}) is not supported yet; export it without compression`);let o=(n.buffers??[]).map((f,a)=>{if(f.uri===void 0){if(!r)throw new Error(`buffer ${a} has no data`);return r}let g=/^data:[^,]*;base64,(.*)$/.exec(f.uri);if(g)return tn(g[1]);if(!t.resolve)throw new Error(`external buffer "${f.uri}" needs a resolver`);return t.resolve(decodeURIComponent(f.uri))}),h=f=>{let a=n.accessors?.[f];if(!a)throw new Error(`missing accessor ${f}`);let g=Ke[a.type]??1,p=new Float32Array(a.count*g);if(a.bufferView===void 0)return{data:p,size:g,count:a.count};let b=n.bufferViews[a.bufferView],A=o[b.buffer],G=new DataView(A.buffer,A.byteOffset,A.byteLength),O=a.componentType,H=O===5126||O===5125?4:O===5123||O===5122?2:1,$=b.byteStride||H*g,J=(b.byteOffset??0)+(a.byteOffset??0),k=!!a.normalized;for(let d=0;d<a.count;d++)for(let et=0;et<g;et++){let Y=J+d*$+et*H,L;switch(O){case 5126:L=G.getFloat32(Y,!0);break;case 5125:L=G.getUint32(Y,!0);break;case 5123:L=G.getUint16(Y,!0),k&&(L/=65535);break;case 5122:L=G.getInt16(Y,!0),k&&(L=Math.max(L/32767,-1));break;case 5121:L=G.getUint8(Y),k&&(L/=255);break;case 5120:L=G.getInt8(Y),k&&(L=Math.max(L/127,-1));break;default:throw new Error(`unknown component type ${O}`)}p[d*g+et]=L}return{data:p,size:g,count:a.count}},c=new yt,y=f=>f===void 0?"default":n.materials?.[f]?.name||`material${f}`,l=(f,a,g)=>{if(g>64)throw new Error("node hierarchy too deep");let p=n.nodes?.[f];if(!p)return;let b=We(a,Ze(p));p.mesh!==void 0&&m(p.mesh,b);for(let A of p.children??[])l(A,b,g+1)},m=(f,a)=>{let g=Qe(a);for(let p of n.meshes?.[f]?.primitives??[]){let b=p.mode??4;if(b!==4&&b!==5&&b!==6||p.attributes.POSITION===void 0)continue;let A=h(p.attributes.POSITION),G=p.attributes.NORMAL!==void 0?h(p.attributes.NORMAL):null,O=c.material(y(p.material)),H=c.positions.length/3;for(let d=0;d<A.count;d++){let et=A.data[d*3],Y=A.data[d*3+1],L=A.data[d*3+2],w=a[0]*et+a[4]*Y+a[8]*L+a[12],I=a[1]*et+a[5]*Y+a[9]*L+a[13],C=a[2]*et+a[6]*Y+a[10]*L+a[14];if(G){let U=G.data[d*3],B=G.data[d*3+1],X=G.data[d*3+2];c.vertex(w,I,C,g[0]*U+g[3]*B+g[6]*X,g[1]*U+g[4]*B+g[7]*X,g[2]*U+g[5]*B+g[8]*X)}else c.vertex(w,I,C)}let $=p.indices!==void 0?h(p.indices).data:null,J=$?$.length:A.count,k=d=>H+($?$[d]:d);if(b===4)for(let d=0;d+2<J;d+=3)c.triangle(k(d),k(d+1),k(d+2),O);else if(b===5)for(let d=0;d+2<J;d++)d%2?c.triangle(k(d+1),k(d),k(d+2),O):c.triangle(k(d),k(d+1),k(d+2),O);else for(let d=1;d+1<J;d++)c.triangle(k(0),k(d),k(d+1),O)}},v=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],u=n.scenes?.[n.scene??0]?.nodes;if(!u){let f=new Set((n.nodes??[]).flatMap(a=>a.children??[]));u=(n.nodes??[]).map((a,g)=>g).filter(a=>!f.has(a))}for(let f of u)l(f,v,0);if(!c.indices.length)throw new Error("no triangle meshes found in the glTF");return c.build()}function We(e,t){let n=new Array(16).fill(0);for(let r=0;r<4;r++)for(let s=0;s<4;s++)for(let i=0;i<4;i++)n[r*4+s]+=e[i*4+s]*t[r*4+i];return n}function Ze(e){if(e.matrix)return e.matrix;let[t,n,r]=e.translation??[0,0,0],[s,i,o,h]=e.rotation??[0,0,0,1],[c,y,l]=e.scale??[1,1,1];return[(1-2*(i*i+o*o))*c,2*(s*i+o*h)*c,2*(s*o-i*h)*c,0,2*(s*i-o*h)*y,(1-2*(s*s+o*o))*y,2*(i*o+s*h)*y,0,2*(s*o+i*h)*l,2*(i*o-s*h)*l,(1-2*(s*s+i*i))*l,0,t,n,r,1]}function Qe(e){let t=e[0],n=e[1],r=e[2],s=e[4],i=e[5],o=e[6],h=e[8],c=e[9],y=e[10],l=i*y-o*c,m=-(s*y-o*h),v=s*c-i*h,S=t*l+n*m+r*v||1;return[l/S,m/S,v/S,-(n*y-r*c)/S,(t*y-r*h)/S,-(t*c-n*h)/S,(n*o-r*i)/S,-(t*o-r*s)/S,(t*i-n*s)/S]}function tn(e){if(typeof atob=="function")return Uint8Array.from(atob(e),t=>t.charCodeAt(0));throw new Error("base64 decoding is not available")}function*Ae(e,t=4e3){let n=[],r=[],s=new yt,i=new Map,o=-1;for(let h=0,c=0;h<e.length;c++){let y=e.indexOf(`
2
- `,h);y<0&&(y=e.length);let l=e.slice(h,y).trim();if(h=y+1,c%t===t-1&&(yield),!l||l[0]==="#")continue;let m=l.split(/\s+/),v=m[0];if(v==="v")n.push(+m[1],+m[2],+m[3]);else if(v==="vn")r.push(+m[1],+m[2],+m[3]);else if(v==="usemtl")o=s.material(m.slice(1).join(" ")||"default"),i.clear();else if(v==="f"){o<0&&(o=s.material("default"));let S=[];for(let u=1;u<m.length;u++){let f=m[u],a=i.get(f);if(a===void 0){let[g,,p]=f.split("/"),b=parseInt(g,10);if(b=b<0?n.length/3+b:b-1,!(b>=0&&b<n.length/3))throw new Error(`obj line ${c+1}: bad vertex index "${f}"`);if(p){let A=parseInt(p,10);A=A<0?r.length/3+A:A-1,a=s.vertex(n[b*3],n[b*3+1],n[b*3+2],r[A*3],r[A*3+1],r[A*3+2])}else a=s.vertex(n[b*3],n[b*3+1],n[b*3+2]);i.set(f,a)}S.push(a)}for(let u=1;u+1<S.length;u++)s.triangle(S[0],S[u],S[u+1],o)}}return s.build()}function Se(e){let t=new DataView(e.buffer,e.byteOffset,e.byteLength);if(e.length>=84){let s=t.getUint32(80,!0);if(84+s*50===e.length){let i=new Float32Array(s*9);for(let o=0;o<s;o++){let h=84+o*50+12;for(let c=0;c<9;c++)i[o*9+c]=t.getFloat32(h+c*4,!0)}return Ie(i)}}let n=new TextDecoder().decode(e),r=[];for(let s of n.matchAll(/vertex\s+(\S+)\s+(\S+)\s+(\S+)/g))r.push(+s[1],+s[2],+s[3]);if(!r.length||r.length%9)throw new Error("not a valid STL file");return Ie(new Float32Array(r))}function Ie(e){let t=e.length/3,n=new Uint32Array(t);for(let r=0;r<t;r++)n[r]=r;return{positions:e,normals:null,indices:n,triMaterial:new Uint16Array(t/3),materials:["default"]}}function en(e,t){let n=t.toLowerCase().match(/\.[a-z0-9]+$/)?.[0]??"";if(n===".glb"||n===".gltf")return"gltf";if(n===".obj")return"obj";if(n===".stl")return"stl";if(e.length>=4&&e[0]===103&&e[1]===108&&e[2]===84&&e[3]===70)return"gltf";let r=new TextDecoder().decode(e.subarray(0,512)).trimStart();return r.startsWith("{")?"gltf":/^(v|vn|vt|o|g|mtllib|#)\s/m.test(r)?"obj":"stl"}function*nn(e,t="",n={}){let r=en(e,t);return r==="obj"?yield*Ae(new TextDecoder().decode(e)):r==="gltf"?ve(e,n):Se(e)}async function Bt(e,t="",n={},r=12){let s=nn(e,t,n),i=performance.now();for(;;){let o=s.next();if(o.done)return o.value;performance.now()-i>r&&(await new Promise(h=>setTimeout(h,0)),i=performance.now())}}function Ee(e){let{width:t,height:n,chars:r}=e,s=[];for(let i=0;i<n;i++)s.push(r.slice(i*t,i*t+t).join(""));return s.join(`
3
- `)}var Fe=(e,t,n)=>[e[0]+(t[0]-e[0])*n,e[1]+(t[1]-e[1])*n,e[2]+(t[2]-e[2])*n];function rn(e,t){if(e.length===1)return e[0];let n=Math.min(1,Math.max(0,t))*(e.length-1),r=Math.min(e.length-2,Math.floor(n));return Fe(e[r],e[r+1],n-r)}function sn(e,t,n){let r=e.style.colorSteps,s=r<0?(t??e.style.chars).length:r;return s<=1?s===1?1:n:Math.min(s-1,Math.floor(n*s))/(s-1)}function on(e,t){let n=e.mat[t];if(n<0)return{rgb:null,faint:!1};let r=e.materials[n],s=e.style.color,i=r.color??(s.mode==="gradient"?rn(s.stops,sn(e,r.chars,e.lum[t])):s.mode==="mono"?s.fg:null);return i?(r.opacity<1&&(i=Fe(s.bg??[0,0,0],i,r.opacity)),{rgb:[Math.round(i[0]),Math.round(i[1]),Math.round(i[2])],faint:!1}):{rgb:null,faint:r.opacity<1}}var an=e=>e==="<"?"&lt;":e===">"?"&gt;":e==="&"?"&amp;":e,ln=e=>"#"+e.map(t=>t.toString(16).padStart(2,"0")).join("");function Re(e){let{width:t,height:n,chars:r}=e,s="";for(let i=0;i<n;i++){let o="";for(let h=0;h<t;h++){let c=i*t+h,y=r[c];if(y!==" "){let{rgb:l,faint:m}=on(e,c),v=e.mat[c],S=v>=0?e.materials[v].opacity:1,u=l?`color:${ln(l)}`:m?`opacity:${S}`:"";u!==o&&(o&&(s+="</span>"),u&&(s+=`<span style="${u}">`),o=u)}s+=an(y)}o&&(s+="</span>"),i<n-1&&(s+=`
4
- `)}return s}var cn=[..."$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. "].reverse().join(""),Te={classic:".,-~:;=!*#$@",hard:" .:-=+*#%@",detailed:cn,blocks:" \u2591\u2592\u2593\u2588",dots:" \xB7\u2022\u25CF",minimal:" .:-="},V={width:80,height:40,aspect:2,charset:"classic",pitch:.3,spin:.66,distance:5,zoom:1,light:[-.45,.6,.65],ambient:0,shading:"auto",supersample:2,coverage:.5,edgeChars:"-/|\\",edgeThreshold:.03};function Ut(...e){let t={};for(let n of e){if(!n)continue;let{materials:r,...s}=n;for(let[i,o]of Object.entries(s))o!==void 0&&(t[i]=o);r&&(t.materials={...t.materials,...r})}return t}function Ce(e,t){if(e!==void 0&&e.length)return[...e];let n=t??V.charset,r=Te[n];if(!r)throw new Error(`unknown charset "${n}" (have: ${Object.keys(Te).join(", ")})`);return[...r]}function Nt(e={}){let t=(i,o,h=-1/0,c=1/0)=>{let y=i??o;if(!Number.isFinite(y))throw new Error(`not a number: ${i}`);return Math.min(c,Math.max(h,y))},n=e.light??V.light,r=Math.hypot(n[0],n[1],n[2])||1,s=[...e.edgeChars??V.edgeChars];if(s.length!==4)throw new Error("edgeChars needs exactly 4 glyphs: horizontal, rising, vertical, falling");return{width:Math.round(t(e.width,V.width,1,2e3)),height:Math.round(t(e.height,V.height,1,2e3)),aspect:t(e.aspect,V.aspect,.25,8),chars:Ce(e.chars,e.charset),pitch:t(e.pitch,V.pitch),fitPitch:Math.abs(t(e.fitPitch,0,0,Math.PI/2)),spin:t(e.spin,V.spin),distance:t(e.distance,V.distance,1.5,1e3),zoom:t(e.zoom,V.zoom,.01,100),light:[n[0]/r,n[1]/r,n[2]/r],ambient:t(e.ambient,V.ambient,0,1),shading:e.shading??V.shading,supersample:Math.round(t(e.supersample,V.supersample,1,4)),coverage:t(e.coverage,V.coverage,0,1),edges:e.edges===!0?"outline":e.edges||!1,edgeChars:s,edgeThreshold:t(e.edgeThreshold,V.edgeThreshold,0),color:hn(e.color),colorSteps:Math.round(t(e.colorSteps,-1,-1,4096)),materials:e.materials??{}}}function ut(e){let t=/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(e.trim());if(!t)throw new Error(`not a hex color: ${e}`);let n=t[1];return n.length===3&&(n=n.replace(/./g,r=>r+r)),[parseInt(n.slice(0,2),16),parseInt(n.slice(2,4),16),parseInt(n.slice(4,6),16)]}function hn(e){let t=[255,255,255];if(e===void 0||e==="none")return{mode:"none",fg:t,stops:[],bg:null};if(typeof e=="string")return{mode:"mono",fg:ut(e),stops:[],bg:null};if(Array.isArray(e)){if(e.length===1)return{mode:"mono",fg:ut(e[0]),stops:[],bg:null};let s=e.map(ut);return{mode:"gradient",fg:s[s.length-1],stops:s,bg:null}}let n=(e.stops??[]).map(ut);return{mode:e.mode??(n.length>1?"gradient":e.fg?"mono":"none"),fg:e.fg?ut(e.fg):n[n.length-1]??t,stops:n,bg:e.bg?ut(e.bg):null}}function ze(e,t){let n={name:e,chars:null,color:null,opacity:1,carved:!1};return t===void 0||t==="solid"?n:t==="carved"?{...n,carved:!0}:t==="dim"?{...n,opacity:.6}:((t.chars!==void 0||t.charset!==void 0)&&(n.chars=Ce(t.chars,t.charset)),t.color&&(n.color=ut(t.color)),t.opacity!==void 0&&(n.opacity=Math.min(1,Math.max(0,t.opacity))),n.carved=!!t.carved,n)}var wt=-1;function ke(e,t={}){let n=e.positions.length/3,r=e.indices.length/3;if(!r)throw new Error("the model has no triangles");let s=e.indices,i=e.triMaterial,o=new Float32Array(n*3);{let w=e.positions,I=[1/0,1/0,1/0],C=[-1/0,-1/0,-1/0];for(let R=0;R<n;R++)for(let x=0;x<3;x++){let q=w[R*3+x];q<I[x]&&(I[x]=q),q>C[x]&&(C[x]=q)}let U=[0,1,2].map(R=>(I[R]+C[R])/2),B=0;for(let R=0;R<n;R++)B=Math.max(B,Math.hypot(w[R*3]-U[0],w[R*3+1]-U[1],w[R*3+2]-U[2]));let X=1/(B||1);for(let R=0;R<n;R++)for(let x=0;x<3;x++)o[R*3+x]=(w[R*3+x]-U[x])*X}let h=0,c=0;for(let w=0;w<n;w++)h=Math.max(h,Math.hypot(o[w*3],o[w*3+2])),c=Math.max(c,Math.abs(o[w*3+1]));let y=null,l=new Float32Array(n*3),m=new Float32Array(n),v=new Float32Array(n),S=new Float32Array(n),u=null,f=t,a=Nt(f),g=[],p=0,b=0,A=new Float32Array(0),G=new Float32Array(0),O=new Int16Array(0),H=new Float32Array(0),$=new Float32Array(0),J=new Int16Array(0);function k(){g=e.materials.map(C=>ze(C,a.materials[C]));let w=a.supersample;(p!==a.width*w||b!==a.height*w)&&(p=a.width*w,b=a.height*w,A=new Float32Array(p*b),G=new Float32Array(p*b),O=new Int16Array(p*b));let I=a.width*a.height;H.length!==I&&(H=new Float32Array(I),$=new Float32Array(I),J=new Int16Array(I))}k();function d(){return a.shading==="flat"?null:e.normals?e.normals:a.shading==="smooth"?y??=xe(e):null}function et(){let w=a.distance,I=Math.abs(a.pitch),C=Math.max(I,a.fitPitch),U=Math.atan2(h,c),B=nt=>c*Math.cos(nt)+h*Math.sin(nt),X=Math.max(h,.001),R=Math.max(U>I&&U<C?Math.hypot(c,h):Math.max(B(I),B(C)),.001),x=X/Math.sqrt(w*w-Math.min(X*X,w*w*.99)),q=R/Math.sqrt(w*w-1);return a.zoom*Math.min(p/2/x,b/2*a.aspect/q)*.98}function Y(w={}){let I=a.supersample,C=a.width,U=a.height,B=a.distance,X=a.aspect,R=w.yaw??0,x=w.pitch??a.pitch,q=w.roll??0,nt=Math.cos(R),ft=Math.sin(R),Mt=Math.cos(x),xt=Math.sin(x),ot=Math.cos(q),at=Math.sin(q),Tt=nt,Ct=0,E=ft,F=xt*ft,lt=Mt,mt=-xt*nt,zt=-Mt*ft,Vt=xt,kt=Mt*nt,Z=ot*Tt-at*F,vt=ot*Ct-at*lt,At=ot*E-at*mt,Ht=at*Tt+ot*F,Jt=at*Ct+ot*lt,Xt=at*E+ot*mt,_t=zt,Yt=Vt,qt=kt,Kt=et(),Ge=Kt/X,$e=p/2,Be=b/2;for(let M=0,T=0;M<n;M++,T+=3){let z=o[T],P=o[T+1],_=o[T+2],N=Z*z+vt*P+At*_,Q=Ht*z+Jt*P+Xt*_,tt=_t*z+Yt*P+qt*_;l[T]=N,l[T+1]=Q,l[T+2]=tt;let D=1/(B-tt);S[M]=D,m[M]=$e+Kt*N*D,v[M]=Be-Ge*Q*D}let ht=d();if(ht){(!u||u.length!==ht.length)&&(u=new Float32Array(ht.length));for(let M=0;M<ht.length;M+=3){let T=ht[M],z=ht[M+1],P=ht[M+2];u[M]=Z*T+vt*z+At*P,u[M+1]=Ht*T+Jt*z+Xt*P,u[M+2]=_t*T+Yt*z+qt*P}}A.fill(0),O.fill(wt);let[Wt,Zt,Qt]=a.light,Pt=a.ambient,te=1-Pt;for(let M=0;M<r;M++){let T=s[M*3],z=s[M*3+1],P=s[M*3+2],_=m[T],N=v[T],Q=m[z],tt=v[z],D=m[P],St=v[P],$t=(Q-_)*(St-N)-(tt-N)*(D-_);if($t===0||!Number.isFinite($t))continue;let ne=Math.max(0,Math.ceil(Math.min(_,Q,D)-.5)),re=Math.min(p-1,Math.floor(Math.max(_,Q,D)-.5)),se=Math.max(0,Math.ceil(Math.min(N,tt,St)-.5)),ie=Math.min(b-1,Math.floor(Math.max(N,tt,St)-.5));if(ne>re||se>ie)continue;let j=T*3,K=z*3,W=P*3,oe=l[K]-l[j],ae=l[K+1]-l[j+1],le=l[K+2]-l[j+2],ce=l[W]-l[j],he=l[W+1]-l[j+1],ue=l[W+2]-l[j+2],rt=ae*ue-le*he,st=le*ce-oe*ue,it=oe*he-ae*ce,De=(l[j]+l[K]+l[W])/3,je=(l[j+1]+l[K+1]+l[W+1])/3,Ve=(l[j+2]+l[K+2]+l[W+2])/3,fe=-De,me=-je,de=B-Ve,Ot=1,pe=0;if(u){let ct=u[j]+u[K]+u[W],dt=u[j+1]+u[K+1]+u[W+1],pt=u[j+2]+u[K+2]+u[W+2];rt*ct+st*dt+it*pt<0&&(rt=-rt,st=-st,it=-it),rt*fe+st*me+it*de<0&&(Ot=-1)}else{rt*fe+st*me+it*de<0&&(rt=-rt,st=-st,it=-it);let ct=Math.hypot(rt,st,it)||1;pe=Pt+te*Math.max(0,(rt*Wt+st*Zt+it*Qt)/ct)}let ge=1/$t,He=i[M],Je=S[T],Xe=S[z],_e=S[P];for(let ct=se;ct<=ie;ct++){let dt=ct+.5;for(let pt=ne;pt<=re;pt++){let Lt=pt+.5,gt=((Q-Lt)*(St-dt)-(tt-dt)*(D-Lt))*ge,bt=((D-Lt)*(N-dt)-(St-dt)*(_-Lt))*ge,Et=1-gt-bt;if(gt<-1e-7||bt<-1e-7||Et<-1e-7)continue;let be=gt*Je+bt*Xe+Et*_e,Ft=ct*p+pt;if(!(be<=A[Ft]))if(A[Ft]=be,O[Ft]=He,u){let ye=(gt*u[j]+bt*u[K]+Et*u[W])*Ot,we=(gt*u[j+1]+bt*u[K+1]+Et*u[W+1])*Ot,Me=(gt*u[j+2]+bt*u[K+2]+Et*u[W+2])*Ot,Ye=Math.hypot(ye,we,Me)||1;G[Ft]=Pt+te*Math.max(0,(ye*Wt+we*Zt+Me*Qt)/Ye)}else G[Ft]=pe}}}let ee=I*I,Ne=Math.max(1,Math.ceil(a.coverage*ee-1e-9)),Gt=new Int16Array(ee);for(let M=0;M<U;M++)for(let T=0;T<C;T++){let z=0,P=0,_=0;for(let Q=0;Q<I;Q++)for(let tt=0;tt<I;tt++){let D=(M*I+Q)*p+T*I+tt;O[D]!==wt&&(Gt[z++]=O[D],P+=G[D],A[D]>_&&(_=A[D]))}let N=M*C+T;if(z<Ne){J[N]=wt,$[N]=0,H[N]=0;continue}J[N]=z===1?Gt[0]:un(Gt,z),$[N]=P/z,H[N]=_}let It=new Array(C*U);for(let M=0;M<C*U;M++){let T=J[M];if(T===wt){It[M]=" ";continue}let z=g[T];if(z.carved){It[M]=" ";continue}let P=z.chars??a.chars;It[M]=P[Math.min(P.length-1,Math.floor($[M]*P.length))]}return a.edges&&L(It,C,U),{width:C,height:U,chars:It,lum:$.slice(),mat:J.slice(),materials:g,style:a}}function L(w,I,C){let U=a.edges==="depth",B=a.edgeThreshold,X=a.aspect,R=(E,F)=>E>=0&&F>=0&&E<I&&F<C&&J[F*I+E]!==wt,x=(E,F)=>R(E,F)?H[F*I+E]:0,q=Math.atan(X),nt=q/2,ft=(q+Math.PI/2)/2,Mt=Math.PI-ft,xt=Math.PI-nt,[ot,at,Tt,Ct]=a.edgeChars;for(let E=0;E<C;E++)for(let F=0;F<I;F++){let lt=E*I+F;if(J[lt]===wt||g[J[lt]].carved)continue;let mt=!1;for(let[vt,At]of[[1,0],[-1,0],[0,1],[0,-1]]){if(!R(F+vt,E+At)){mt=!0;break}if(U&&H[lt]-x(F+vt,E+At)>B*H[lt]){mt=!0;break}}if(!mt)continue;let zt=x(F+1,E-1)+2*x(F+1,E)+x(F+1,E+1)-x(F-1,E-1)-2*x(F-1,E)-x(F-1,E+1),kt=-(x(F-1,E+1)+2*x(F,E+1)+x(F+1,E+1)-x(F-1,E-1)-2*x(F,E-1)-x(F+1,E-1))/X;if(zt===0&&kt===0)continue;let Z=Math.atan2(zt,-kt);Z<0&&(Z+=Math.PI),Z>=Math.PI&&(Z-=Math.PI),w[lt]=Z<nt||Z>=xt?ot:Z<ft?at:Z<Mt?Tt:Ct}}return{mesh:e,get style(){return a},setStyle(w){f=Ut(f,w),a=Nt(f),k()},render:Y}}function un(e,t){let n=e[0],r=0;for(let s=0;s<t;s++){let i=0;for(let o=0;o<t;o++)e[o]===e[s]&&i++;i>r&&(r=i,n=e[s])}return n}function Oe(e,t,n,r,s){let i=Math.max(8,Math.round(e)),o=t>0?t:.6,h=n>0?n:1,c=r>0&&s>0?r/s:1,y=Math.max(4,Math.min(400,Math.round(i*o/(h*c)))),l=i*o,m=y*h;return{cols:i,rows:y,aspect:h/o,scale:r>0?r/l:1,ratio:l/m}}var Dt=(e,t)=>1-Math.exp(-e*t);function Le(e,t,n){let r=Math.min(Math.max(n,0),.1),s=e.vyaw+(t.spin-e.vyaw)*Dt(2,r),i=e.vpitch*(1-Dt(4,r)),o=e.pitch+i*r;return o+=(t.pitch-o)*Dt(1.5,r),o=Math.max(-t.maxPitch,Math.min(t.maxPitch,o)),{yaw:e.yaw+s*r,pitch:o,vyaw:s,vpitch:i}}function Ue(e,t,n,r,s,i){let o=s>0?Math.PI*2/s:.01,h=t*o,c=n*o,y=Math.max(-i,Math.min(i,e.pitch+c)),l=r>.001?1/r:0;return{yaw:e.yaw+h,pitch:y,vyaw:h*l,vpitch:c*l}}function Pe(e){let t={},n=h=>{let c=e(h);if(c===null||c.trim()==="")return;let y=Number(c);return Number.isFinite(y)?y:void 0},r=e("charset");r&&(t.charset=r);let s=e("chars");s&&(t.chars=s);let i=e("color");i&&(t.color=i.includes(",")?i.split(",").map(h=>h.trim()):i.trim());let o=e("edges");o!==null&&(t.edges=o==="depth"?"depth":o==="off"||o==="false"?!1:"outline");for(let h of["spin","pitch","zoom","distance","ambient"]){let c=n(h);c!==void 0&&(t[h]=c)}return t}var fn=`:host{display:block;position:relative;overflow:hidden;aspect-ratio:var(--textel-ratio,1);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;outline:none}
1
+ var yt=class{positions=[];normals=[];indices=[];triMaterial=[];materials=[];hasNormals=!0;matIndex=new Map;material(t){let n=this.matIndex.get(t);return n===void 0&&(n=this.materials.length,this.materials.push(t),this.matIndex.set(t,n)),n}vertex(t,n,r,i,s,o){let h=this.positions.length/3;return this.positions.push(t,n,r),i===void 0||s===void 0||o===void 0?(this.hasNormals=!1,this.normals.push(0,0,0)):this.normals.push(i,s,o),h}triangle(t,n,r,i){this.indices.push(t,n,r),this.triMaterial.push(i)}build(){return this.materials.length===0&&this.material("default"),{positions:new Float32Array(this.positions),normals:this.hasNormals&&this.positions.length?qe(new Float32Array(this.normals)):null,indices:new Uint32Array(this.indices),triMaterial:new Uint16Array(this.triMaterial),materials:this.materials}}};function qe(e){for(let t=0;t<e.length;t+=3){let n=Math.hypot(e[t],e[t+1],e[t+2])||1;e[t]/=n,e[t+1]/=n,e[t+2]/=n}return e}function xe(e){let t=e.positions,n=e.indices,r=t.length/3,i=new Uint32Array(r),s=new Map,o=[1/0,-1/0];for(let l=0;l<t.length;l++)o[0]=Math.min(o[0],t[l]),o[1]=Math.max(o[1],t[l]);let h=1e5/Math.max(o[1]-o[0],1e-9);for(let l=0;l<r;l++){let m=`${Math.round(t[l*3]*h)},${Math.round(t[l*3+1]*h)},${Math.round(t[l*3+2]*h)}`,v=s.get(m);v===void 0&&(v=s.size,s.set(m,v)),i[l]=v}let c=new Float64Array(s.size*3);for(let l=0;l<n.length;l+=3){let m=n[l]*3,v=n[l+1]*3,S=n[l+2]*3,u=t[v]-t[m],f=t[v+1]-t[m+1],a=t[v+2]-t[m+2],b=t[S]-t[m],p=t[S+1]-t[m+1],g=t[S+2]-t[m+2],A=f*g-a*p,G=a*b-u*g,O=u*p-f*b;for(let H of[n[l],n[l+1],n[l+2]]){let $=i[H]*3;c[$]+=A,c[$+1]+=G,c[$+2]+=O}}let y=new Float32Array(r*3);for(let l=0;l<r;l++){let m=i[l]*3,v=Math.hypot(c[m],c[m+1],c[m+2])||1;y[l*3]=c[m]/v,y[l*3+1]=c[m+1]/v,y[l*3+2]=c[m+2]/v}return y}var Ke={SCALAR:1,VEC2:2,VEC3:3,VEC4:4,MAT2:4,MAT3:9,MAT4:16};function ve(e,t={}){let n,r=null,i=new DataView(e.buffer,e.byteOffset,e.byteLength);if(e.length>=12&&i.getUint32(0,!0)===1179937895){let f=12;for(n={};f+8<=e.length;){let a=i.getUint32(f,!0),b=i.getUint32(f+4,!0),p=e.subarray(f+8,f+8+a);b===1313821514?n=JSON.parse(new TextDecoder().decode(p)):b===5130562&&(r=p),f+=8+a}}else n=JSON.parse(new TextDecoder().decode(e));let s=(n.extensionsRequired??[]).filter(f=>f==="KHR_draco_mesh_compression"||f==="EXT_meshopt_compression");if(s.length)throw new Error(`compressed glTF (${s.join(", ")}) is not supported yet; export it without compression`);let o=(n.buffers??[]).map((f,a)=>{if(f.uri===void 0){if(!r)throw new Error(`buffer ${a} has no data`);return r}let b=/^data:[^,]*;base64,(.*)$/.exec(f.uri);if(b)return tn(b[1]);if(!t.resolve)throw new Error(`external buffer "${f.uri}" needs a resolver`);return t.resolve(decodeURIComponent(f.uri))}),h=f=>{let a=n.accessors?.[f];if(!a)throw new Error(`missing accessor ${f}`);let b=Ke[a.type]??1,p=new Float32Array(a.count*b);if(a.bufferView===void 0)return{data:p,size:b,count:a.count};let g=n.bufferViews[a.bufferView],A=o[g.buffer],G=new DataView(A.buffer,A.byteOffset,A.byteLength),O=a.componentType,H=O===5126||O===5125?4:O===5123||O===5122?2:1,$=g.byteStride||H*b,X=(g.byteOffset??0)+(a.byteOffset??0),k=!!a.normalized;for(let d=0;d<a.count;d++)for(let et=0;et<b;et++){let _=X+d*$+et*H,L;switch(O){case 5126:L=G.getFloat32(_,!0);break;case 5125:L=G.getUint32(_,!0);break;case 5123:L=G.getUint16(_,!0),k&&(L/=65535);break;case 5122:L=G.getInt16(_,!0),k&&(L=Math.max(L/32767,-1));break;case 5121:L=G.getUint8(_),k&&(L/=255);break;case 5120:L=G.getInt8(_),k&&(L=Math.max(L/127,-1));break;default:throw new Error(`unknown component type ${O}`)}p[d*b+et]=L}return{data:p,size:b,count:a.count}},c=new yt,y=f=>f===void 0?"default":n.materials?.[f]?.name||`material${f}`,l=(f,a,b)=>{if(b>64)throw new Error("node hierarchy too deep");let p=n.nodes?.[f];if(!p)return;let g=We(a,Ze(p));p.mesh!==void 0&&m(p.mesh,g);for(let A of p.children??[])l(A,g,b+1)},m=(f,a)=>{let b=Qe(a);for(let p of n.meshes?.[f]?.primitives??[]){let g=p.mode??4;if(g!==4&&g!==5&&g!==6||p.attributes.POSITION===void 0)continue;let A=h(p.attributes.POSITION),G=p.attributes.NORMAL!==void 0?h(p.attributes.NORMAL):null,O=c.material(y(p.material)),H=c.positions.length/3;for(let d=0;d<A.count;d++){let et=A.data[d*3],_=A.data[d*3+1],L=A.data[d*3+2],w=a[0]*et+a[4]*_+a[8]*L+a[12],I=a[1]*et+a[5]*_+a[9]*L+a[13],T=a[2]*et+a[6]*_+a[10]*L+a[14];if(G){let P=G.data[d*3],B=G.data[d*3+1],J=G.data[d*3+2];c.vertex(w,I,T,b[0]*P+b[3]*B+b[6]*J,b[1]*P+b[4]*B+b[7]*J,b[2]*P+b[5]*B+b[8]*J)}else c.vertex(w,I,T)}let $=p.indices!==void 0?h(p.indices).data:null,X=$?$.length:A.count,k=d=>H+($?$[d]:d);if(g===4)for(let d=0;d+2<X;d+=3)c.triangle(k(d),k(d+1),k(d+2),O);else if(g===5)for(let d=0;d+2<X;d++)d%2?c.triangle(k(d+1),k(d),k(d+2),O):c.triangle(k(d),k(d+1),k(d+2),O);else for(let d=1;d+1<X;d++)c.triangle(k(0),k(d),k(d+1),O)}},v=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],u=n.scenes?.[n.scene??0]?.nodes;if(!u){let f=new Set((n.nodes??[]).flatMap(a=>a.children??[]));u=(n.nodes??[]).map((a,b)=>b).filter(a=>!f.has(a))}for(let f of u)l(f,v,0);if(!c.indices.length)throw new Error("no triangle meshes found in the glTF");return c.build()}function We(e,t){let n=new Array(16).fill(0);for(let r=0;r<4;r++)for(let i=0;i<4;i++)for(let s=0;s<4;s++)n[r*4+i]+=e[s*4+i]*t[r*4+s];return n}function Ze(e){if(e.matrix)return e.matrix;let[t,n,r]=e.translation??[0,0,0],[i,s,o,h]=e.rotation??[0,0,0,1],[c,y,l]=e.scale??[1,1,1];return[(1-2*(s*s+o*o))*c,2*(i*s+o*h)*c,2*(i*o-s*h)*c,0,2*(i*s-o*h)*y,(1-2*(i*i+o*o))*y,2*(s*o+i*h)*y,0,2*(i*o+s*h)*l,2*(s*o-i*h)*l,(1-2*(i*i+s*s))*l,0,t,n,r,1]}function Qe(e){let t=e[0],n=e[1],r=e[2],i=e[4],s=e[5],o=e[6],h=e[8],c=e[9],y=e[10],l=s*y-o*c,m=-(i*y-o*h),v=i*c-s*h,S=t*l+n*m+r*v||1;return[l/S,m/S,v/S,-(n*y-r*c)/S,(t*y-r*h)/S,-(t*c-n*h)/S,(n*o-r*s)/S,-(t*o-r*i)/S,(t*s-n*i)/S]}function tn(e){if(typeof atob=="function")return Uint8Array.from(atob(e),t=>t.charCodeAt(0));throw new Error("base64 decoding is not available")}function*Ae(e,t=4e3){let n=[],r=[],i=new yt,s=new Map,o=-1;for(let h=0,c=0;h<e.length;c++){let y=e.indexOf(`
2
+ `,h);y<0&&(y=e.length);let l=e.slice(h,y).trim();if(h=y+1,c%t===t-1&&(yield),!l||l[0]==="#")continue;let m=l.split(/\s+/),v=m[0];if(v==="v")n.push(+m[1],+m[2],+m[3]);else if(v==="vn")r.push(+m[1],+m[2],+m[3]);else if(v==="usemtl")o=i.material(m.slice(1).join(" ")||"default"),s.clear();else if(v==="f"){o<0&&(o=i.material("default"));let S=[];for(let u=1;u<m.length;u++){let f=m[u],a=s.get(f);if(a===void 0){let[b,,p]=f.split("/"),g=parseInt(b,10);if(g=g<0?n.length/3+g:g-1,!(g>=0&&g<n.length/3))throw new Error(`obj line ${c+1}: bad vertex index "${f}"`);if(p){let A=parseInt(p,10);A=A<0?r.length/3+A:A-1,a=i.vertex(n[g*3],n[g*3+1],n[g*3+2],r[A*3],r[A*3+1],r[A*3+2])}else a=i.vertex(n[g*3],n[g*3+1],n[g*3+2]);s.set(f,a)}S.push(a)}for(let u=1;u+1<S.length;u++)i.triangle(S[0],S[u],S[u+1],o)}}return i.build()}function Se(e){let t=new DataView(e.buffer,e.byteOffset,e.byteLength);if(e.length>=84){let i=t.getUint32(80,!0);if(84+i*50===e.length){let s=new Float32Array(i*9);for(let o=0;o<i;o++){let h=84+o*50+12;for(let c=0;c<9;c++)s[o*9+c]=t.getFloat32(h+c*4,!0)}return Ie(s)}}let n=new TextDecoder().decode(e),r=[];for(let i of n.matchAll(/vertex\s+(\S+)\s+(\S+)\s+(\S+)/g))r.push(+i[1],+i[2],+i[3]);if(!r.length||r.length%9)throw new Error("not a valid STL file");return Ie(new Float32Array(r))}function Ie(e){let t=e.length/3,n=new Uint32Array(t);for(let r=0;r<t;r++)n[r]=r;return{positions:e,normals:null,indices:n,triMaterial:new Uint16Array(t/3),materials:["default"]}}function en(e,t){let n=t.toLowerCase().match(/\.[a-z0-9]+$/)?.[0]??"";if(n===".glb"||n===".gltf")return"gltf";if(n===".obj")return"obj";if(n===".stl")return"stl";if(e.length>=4&&e[0]===103&&e[1]===108&&e[2]===84&&e[3]===70)return"gltf";let r=new TextDecoder().decode(e.subarray(0,512)).trimStart();return r.startsWith("{")?"gltf":/^(v|vn|vt|o|g|mtllib|#)\s/m.test(r)?"obj":"stl"}function*nn(e,t="",n={}){let r=en(e,t);return r==="obj"?yield*Ae(new TextDecoder().decode(e)):r==="gltf"?ve(e,n):Se(e)}async function Bt(e,t="",n={},r=12){let i=nn(e,t,n),s=performance.now();for(;;){let o=i.next();if(o.done)return o.value;performance.now()-s>r&&(await new Promise(h=>setTimeout(h,0)),s=performance.now())}}function Ee(e){let{width:t,height:n,chars:r}=e,i=[];for(let s=0;s<n;s++)i.push(r.slice(s*t,s*t+t).join(""));return i.join(`
3
+ `)}var ze=(e,t,n)=>[e[0]+(t[0]-e[0])*n,e[1]+(t[1]-e[1])*n,e[2]+(t[2]-e[2])*n];function rn(e,t){if(e.length===1)return e[0];let n=Math.min(1,Math.max(0,t))*(e.length-1),r=Math.min(e.length-2,Math.floor(n));return ze(e[r],e[r+1],n-r)}function sn(e,t,n){let r=e.style.colorSteps,i=r<0?(t??e.style.chars).length:r;return i<=1?i===1?1:n:Math.min(i-1,Math.floor(n*i))/(i-1)}function on(e,t){let n=e.mat[t];if(n<0)return{rgb:null,faint:!1};let r=e.materials[n],i=e.style.color,s=r.color??(i.mode==="gradient"?rn(i.stops,sn(e,r.chars,e.lum[t])):i.mode==="mono"?i.fg:null);return s?(r.opacity<1&&(s=ze(i.bg??[0,0,0],s,r.opacity)),{rgb:[Math.round(s[0]),Math.round(s[1]),Math.round(s[2])],faint:!1}):{rgb:null,faint:r.opacity<1}}var an=e=>e==="<"?"&lt;":e===">"?"&gt;":e==="&"?"&amp;":e,ln=e=>"#"+e.map(t=>t.toString(16).padStart(2,"0")).join("");function Fe(e){let{width:t,height:n,chars:r}=e,i="";for(let s=0;s<n;s++){let o="";for(let h=0;h<t;h++){let c=s*t+h,y=r[c];if(y!==" "){let{rgb:l,faint:m}=on(e,c),v=e.mat[c],S=v>=0?e.materials[v].opacity:1,u=l?`color:${ln(l)}`:m?`opacity:${S}`:"";u!==o&&(o&&(i+="</span>"),u&&(i+=`<span style="${u}">`),o=u)}i+=an(y)}o&&(i+="</span>"),s<n-1&&(i+=`
4
+ `)}return i}var cn=[..."$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. "].reverse().join(""),Re={classic:".,-~:;=!*#$@",hard:" .:-=+*#%@",detailed:cn,blocks:" \u2591\u2592\u2593\u2588",dots:" \xB7\u2022\u25CF",minimal:" .:-="},V={width:80,height:40,aspect:2,charset:"classic",pitch:.3,spin:.66,distance:5,zoom:1,light:[-.45,.6,.65],ambient:0,shading:"auto",supersample:2,coverage:.5,edgeChars:"-/|\\",edgeThreshold:.03};function Pt(...e){let t={};for(let n of e){if(!n)continue;let{materials:r,...i}=n;for(let[s,o]of Object.entries(i))o!==void 0&&(t[s]=o);r&&(t.materials={...t.materials,...r})}return t}function Te(e,t){if(e!==void 0&&e.length)return[...e];let n=t??V.charset,r=Re[n];if(!r)throw new Error(`unknown charset "${n}" (have: ${Object.keys(Re).join(", ")})`);return[...r]}function Nt(e={}){let t=(s,o,h=-1/0,c=1/0)=>{let y=s??o;if(!Number.isFinite(y))throw new Error(`not a number: ${s}`);return Math.min(c,Math.max(h,y))},n=e.light??V.light,r=Math.hypot(n[0],n[1],n[2])||1,i=[...e.edgeChars??V.edgeChars];if(i.length!==4)throw new Error("edgeChars needs exactly 4 glyphs: horizontal, rising, vertical, falling");return{width:Math.round(t(e.width,V.width,1,2e3)),height:Math.round(t(e.height,V.height,1,2e3)),aspect:t(e.aspect,V.aspect,.25,8),chars:Te(e.chars,e.charset),pitch:t(e.pitch,V.pitch),fitPitch:Math.abs(t(e.fitPitch,0,0,Math.PI/2)),spin:t(e.spin,V.spin),distance:t(e.distance,V.distance,1.5,1e3),zoom:t(e.zoom,V.zoom,.01,100),light:[n[0]/r,n[1]/r,n[2]/r],ambient:t(e.ambient,V.ambient,0,1),shading:e.shading??V.shading,supersample:Math.round(t(e.supersample,V.supersample,1,4)),coverage:t(e.coverage,V.coverage,0,1),edges:e.edges===!0?"outline":e.edges||!1,edgeChars:i,edgeThreshold:t(e.edgeThreshold,V.edgeThreshold,0),color:hn(e.color),colorSteps:Math.round(t(e.colorSteps,-1,-1,4096)),materials:e.materials??{}}}function ut(e){let t=/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(e.trim());if(!t)throw new Error(`not a hex color: ${e}`);let n=t[1];return n.length===3&&(n=n.replace(/./g,r=>r+r)),[parseInt(n.slice(0,2),16),parseInt(n.slice(2,4),16),parseInt(n.slice(4,6),16)]}function hn(e){let t=[255,255,255];if(e===void 0||e==="none")return{mode:"none",fg:t,stops:[],bg:null};if(typeof e=="string")return{mode:"mono",fg:ut(e),stops:[],bg:null};if(Array.isArray(e)){if(e.length===1)return{mode:"mono",fg:ut(e[0]),stops:[],bg:null};let i=e.map(ut);return{mode:"gradient",fg:i[i.length-1],stops:i,bg:null}}let n=(e.stops??[]).map(ut);return{mode:e.mode??(n.length>1?"gradient":e.fg?"mono":"none"),fg:e.fg?ut(e.fg):n[n.length-1]??t,stops:n,bg:e.bg?ut(e.bg):null}}function Ce(e,t){let n={name:e,chars:null,color:null,opacity:1,carved:!1};return t===void 0||t==="solid"?n:t==="carved"?{...n,carved:!0}:t==="dim"?{...n,opacity:.6}:((t.chars!==void 0||t.charset!==void 0)&&(n.chars=Te(t.chars,t.charset)),t.color&&(n.color=ut(t.color)),t.opacity!==void 0&&(n.opacity=Math.min(1,Math.max(0,t.opacity))),n.carved=!!t.carved,n)}var wt=-1;function ke(e,t={}){let n=e.positions.length/3,r=e.indices.length/3;if(!r)throw new Error("the model has no triangles");let i=e.indices,s=e.triMaterial,o=new Float32Array(n*3);{let w=e.positions,I=[1/0,1/0,1/0],T=[-1/0,-1/0,-1/0];for(let F=0;F<n;F++)for(let x=0;x<3;x++){let q=w[F*3+x];q<I[x]&&(I[x]=q),q>T[x]&&(T[x]=q)}let P=[0,1,2].map(F=>(I[F]+T[F])/2),B=0;for(let F=0;F<n;F++)B=Math.max(B,Math.hypot(w[F*3]-P[0],w[F*3+1]-P[1],w[F*3+2]-P[2]));let J=1/(B||1);for(let F=0;F<n;F++)for(let x=0;x<3;x++)o[F*3+x]=(w[F*3+x]-P[x])*J}let h=0,c=0;for(let w=0;w<n;w++)h=Math.max(h,Math.hypot(o[w*3],o[w*3+2])),c=Math.max(c,Math.abs(o[w*3+1]));let y=null,l=new Float32Array(n*3),m=new Float32Array(n),v=new Float32Array(n),S=new Float32Array(n),u=null,f=t,a=Nt(f),b=[],p=0,g=0,A=new Float32Array(0),G=new Float32Array(0),O=new Int16Array(0),H=new Float32Array(0),$=new Float32Array(0),X=new Int16Array(0);function k(){b=e.materials.map(T=>Ce(T,a.materials[T]));let w=a.supersample;(p!==a.width*w||g!==a.height*w)&&(p=a.width*w,g=a.height*w,A=new Float32Array(p*g),G=new Float32Array(p*g),O=new Int16Array(p*g));let I=a.width*a.height;H.length!==I&&(H=new Float32Array(I),$=new Float32Array(I),X=new Int16Array(I))}k();function d(){return a.shading==="flat"?null:e.normals?e.normals:a.shading==="smooth"?y??=xe(e):null}function et(){let w=a.distance,I=Math.abs(a.pitch),T=Math.max(I,a.fitPitch),P=Math.atan2(h,c),B=nt=>c*Math.cos(nt)+h*Math.sin(nt),J=Math.max(h,.001),F=Math.max(P>I&&P<T?Math.hypot(c,h):Math.max(B(I),B(T)),.001),x=J/Math.sqrt(w*w-Math.min(J*J,w*w*.99)),q=F/Math.sqrt(w*w-1);return a.zoom*Math.min(p/2/x,g/2*a.aspect/q)*.98}function _(w={}){let I=a.supersample,T=a.width,P=a.height,B=a.distance,J=a.aspect,F=w.yaw??0,x=w.pitch??a.pitch,q=w.roll??0,nt=Math.cos(F),ft=Math.sin(F),Mt=Math.cos(x),xt=Math.sin(x),ot=Math.cos(q),at=Math.sin(q),Rt=nt,Tt=0,E=ft,z=xt*ft,lt=Mt,mt=-xt*nt,Ct=-Mt*ft,Vt=xt,kt=Mt*nt,Z=ot*Rt-at*z,vt=ot*Tt-at*lt,At=ot*E-at*mt,Ht=at*Rt+ot*z,Xt=at*Tt+ot*lt,Jt=at*E+ot*mt,Yt=Ct,_t=Vt,qt=kt,Kt=et(),Ge=Kt/J,$e=p/2,Be=g/2;for(let M=0,R=0;M<n;M++,R+=3){let C=o[R],U=o[R+1],Y=o[R+2],N=Z*C+vt*U+At*Y,Q=Ht*C+Xt*U+Jt*Y,tt=Yt*C+_t*U+qt*Y;l[R]=N,l[R+1]=Q,l[R+2]=tt;let D=1/(B-tt);S[M]=D,m[M]=$e+Kt*N*D,v[M]=Be-Ge*Q*D}let ht=d();if(ht){(!u||u.length!==ht.length)&&(u=new Float32Array(ht.length));for(let M=0;M<ht.length;M+=3){let R=ht[M],C=ht[M+1],U=ht[M+2];u[M]=Z*R+vt*C+At*U,u[M+1]=Ht*R+Xt*C+Jt*U,u[M+2]=Yt*R+_t*C+qt*U}}A.fill(0),O.fill(wt);let[Wt,Zt,Qt]=a.light,Ut=a.ambient,te=1-Ut;for(let M=0;M<r;M++){let R=i[M*3],C=i[M*3+1],U=i[M*3+2],Y=m[R],N=v[R],Q=m[C],tt=v[C],D=m[U],St=v[U],$t=(Q-Y)*(St-N)-(tt-N)*(D-Y);if($t===0||!Number.isFinite($t))continue;let ne=Math.max(0,Math.ceil(Math.min(Y,Q,D)-.5)),re=Math.min(p-1,Math.floor(Math.max(Y,Q,D)-.5)),ie=Math.max(0,Math.ceil(Math.min(N,tt,St)-.5)),se=Math.min(g-1,Math.floor(Math.max(N,tt,St)-.5));if(ne>re||ie>se)continue;let j=R*3,K=C*3,W=U*3,oe=l[K]-l[j],ae=l[K+1]-l[j+1],le=l[K+2]-l[j+2],ce=l[W]-l[j],he=l[W+1]-l[j+1],ue=l[W+2]-l[j+2],rt=ae*ue-le*he,it=le*ce-oe*ue,st=oe*he-ae*ce,De=(l[j]+l[K]+l[W])/3,je=(l[j+1]+l[K+1]+l[W+1])/3,Ve=(l[j+2]+l[K+2]+l[W+2])/3,fe=-De,me=-je,de=B-Ve,Ot=1,pe=0;if(u){let ct=u[j]+u[K]+u[W],dt=u[j+1]+u[K+1]+u[W+1],pt=u[j+2]+u[K+2]+u[W+2];rt*ct+it*dt+st*pt<0&&(rt=-rt,it=-it,st=-st),rt*fe+it*me+st*de<0&&(Ot=-1)}else{rt*fe+it*me+st*de<0&&(rt=-rt,it=-it,st=-st);let ct=Math.hypot(rt,it,st)||1;pe=Ut+te*Math.max(0,(rt*Wt+it*Zt+st*Qt)/ct)}let be=1/$t,He=s[M],Xe=S[R],Je=S[C],Ye=S[U];for(let ct=ie;ct<=se;ct++){let dt=ct+.5;for(let pt=ne;pt<=re;pt++){let Lt=pt+.5,bt=((Q-Lt)*(St-dt)-(tt-dt)*(D-Lt))*be,gt=((D-Lt)*(N-dt)-(St-dt)*(Y-Lt))*be,Et=1-bt-gt;if(bt<-1e-7||gt<-1e-7||Et<-1e-7)continue;let ge=bt*Xe+gt*Je+Et*Ye,zt=ct*p+pt;if(!(ge<=A[zt]))if(A[zt]=ge,O[zt]=He,u){let ye=(bt*u[j]+gt*u[K]+Et*u[W])*Ot,we=(bt*u[j+1]+gt*u[K+1]+Et*u[W+1])*Ot,Me=(bt*u[j+2]+gt*u[K+2]+Et*u[W+2])*Ot,_e=Math.hypot(ye,we,Me)||1;G[zt]=Ut+te*Math.max(0,(ye*Wt+we*Zt+Me*Qt)/_e)}else G[zt]=pe}}}let ee=I*I,Ne=Math.max(1,Math.ceil(a.coverage*ee-1e-9)),Gt=new Int16Array(ee);for(let M=0;M<P;M++)for(let R=0;R<T;R++){let C=0,U=0,Y=0;for(let Q=0;Q<I;Q++)for(let tt=0;tt<I;tt++){let D=(M*I+Q)*p+R*I+tt;O[D]!==wt&&(Gt[C++]=O[D],U+=G[D],A[D]>Y&&(Y=A[D]))}let N=M*T+R;if(C<Ne){X[N]=wt,$[N]=0,H[N]=0;continue}X[N]=C===1?Gt[0]:un(Gt,C),$[N]=U/C,H[N]=Y}let It=new Array(T*P);for(let M=0;M<T*P;M++){let R=X[M];if(R===wt){It[M]=" ";continue}let C=b[R];if(C.carved){It[M]=" ";continue}let U=C.chars??a.chars;It[M]=U[Math.min(U.length-1,Math.floor($[M]*U.length))]}return a.edges&&L(It,T,P),{width:T,height:P,chars:It,lum:$.slice(),mat:X.slice(),materials:b,style:a}}function L(w,I,T){let P=a.edges==="depth",B=a.edgeThreshold,J=a.aspect,F=(E,z)=>E>=0&&z>=0&&E<I&&z<T&&X[z*I+E]!==wt,x=(E,z)=>F(E,z)?H[z*I+E]:0,q=Math.atan(J),nt=q/2,ft=(q+Math.PI/2)/2,Mt=Math.PI-ft,xt=Math.PI-nt,[ot,at,Rt,Tt]=a.edgeChars;for(let E=0;E<T;E++)for(let z=0;z<I;z++){let lt=E*I+z;if(X[lt]===wt||b[X[lt]].carved)continue;let mt=!1;for(let[vt,At]of[[1,0],[-1,0],[0,1],[0,-1]]){if(!F(z+vt,E+At)){mt=!0;break}if(P&&H[lt]-x(z+vt,E+At)>B*H[lt]){mt=!0;break}}if(!mt)continue;let Ct=x(z+1,E-1)+2*x(z+1,E)+x(z+1,E+1)-x(z-1,E-1)-2*x(z-1,E)-x(z-1,E+1),kt=-(x(z-1,E+1)+2*x(z,E+1)+x(z+1,E+1)-x(z-1,E-1)-2*x(z,E-1)-x(z+1,E-1))/J;if(Ct===0&&kt===0)continue;let Z=Math.atan2(Ct,-kt);Z<0&&(Z+=Math.PI),Z>=Math.PI&&(Z-=Math.PI),w[lt]=Z<nt||Z>=xt?ot:Z<ft?at:Z<Mt?Rt:Tt}}return{mesh:e,get style(){return a},setStyle(w){f=Pt(f,w),a=Nt(f),k()},render:_}}function un(e,t){let n=e[0],r=0;for(let i=0;i<t;i++){let s=0;for(let o=0;o<t;o++)e[o]===e[i]&&s++;s>r&&(r=s,n=e[i])}return n}function Oe(e,t,n,r,i){let s=Math.max(8,Math.round(e)),o=t>0?t:.6,h=n>0?n:1,c=r>0&&i>0?r/i:1,y=Math.max(4,Math.min(400,Math.round(s*o/(h*c)))),l=s*o,m=y*h;return{cols:s,rows:y,aspect:h/o,scale:r>0?r/l:1,ratio:l/m}}var Dt=(e,t)=>1-Math.exp(-e*t);function Le(e,t,n){let r=Math.min(Math.max(n,0),.1),i=e.vyaw+(t.spin-e.vyaw)*Dt(2,r),s=e.vpitch*(1-Dt(4,r)),o=e.pitch+s*r;return o+=(t.pitch-o)*Dt(1.5,r),o=Math.max(-t.maxPitch,Math.min(t.maxPitch,o)),{yaw:e.yaw+i*r,pitch:o,vyaw:i,vpitch:s}}function Pe(e,t,n,r,i,s){let o=i>0?Math.PI*2/i:.01,h=t*o,c=n*o,y=Math.max(-s,Math.min(s,e.pitch+c)),l=r>.001?1/r:0;return{yaw:e.yaw+h,pitch:y,vyaw:h*l,vpitch:c*l}}function Ue(e){let t={},n=h=>{let c=e(h);if(c===null||c.trim()==="")return;let y=Number(c);return Number.isFinite(y)?y:void 0},r=e("charset");r&&(t.charset=r);let i=e("chars");i&&(t.chars=i);let s=e("color");s&&(t.color=s.includes(",")?s.split(",").map(h=>h.trim()):s.trim());let o=e("edges");o!==null&&(t.edges=o==="depth"?"depth":o==="off"||o==="false"?!1:"outline");for(let h of["spin","pitch","zoom","distance","ambient"]){let c=n(h);c!==void 0&&(t[h]=c)}return t}var fn=`:host{display:block;position:relative;overflow:hidden;aspect-ratio:var(--textel-ratio,1);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;outline:none}
5
5
  :host(:not([nodrag])){cursor:grab;touch-action:pan-y}
6
+ :host([zoomable]:not([nodrag])){touch-action:none}
6
7
  :host(:not([nodrag]):active){cursor:grabbing}
7
8
  :host(:focus-visible){box-shadow:0 0 0 2px currentColor}
8
9
  pre{position:absolute;left:0;top:0;margin:0;transform-origin:0 0;font:inherit;font-size:16px;line-height:1;letter-spacing:0;white-space:pre;color:inherit;text-shadow:var(--textel-glow,none);-webkit-text-size-adjust:none;text-size-adjust:none;user-select:none;pointer-events:none}
9
- .probe{visibility:hidden}`,Rt=1.2,mn=["charset","chars","color","edges","spin","pitch","zoom","distance","ambient"],dn=typeof HTMLElement>"u"?class{}:HTMLElement;async function pn(e,t){let n=async i=>{let o=await fetch(i,{signal:t});if(!o.ok)throw new Error(`${o.status} ${o.statusText} for ${i}`);return new Uint8Array(await o.arrayBuffer())},r=await n(e),s=new Map;if(/\.gltf(\?|#|$)/i.test(e)){let i=JSON.parse(new TextDecoder().decode(r));for(let o of i.buffers??[])o.uri&&!o.uri.startsWith("data:")&&s.set(decodeURIComponent(o.uri),await n(new URL(o.uri,new URL(e,location.href)).href))}return Bt(r,e.split(/[?#]/)[0],{resolve:i=>{let o=s.get(i);if(!o)throw new Error(`missing buffer ${i}`);return o}})}var jt=class extends dn{static observedAttributes=["src","preset","cols","paused","label",...mn];#i;#l;#f=null;#n=null;#A={};#g={};#c=null;#t={yaw:0,pitch:0,vyaw:0,vpitch:0};#h=1;#o=!0;#I=!0;#r=0;#m=-1;#d=!0;#s=null;#S=null;#b;#y;#T=typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches;constructor(){super();let t=this.attachShadow({mode:"open"}),n=document.createElement("style");n.textContent=fn,this.#i=document.createElement("pre"),this.#i.setAttribute("part","screen"),this.#l=document.createElement("pre"),this.#l.className="probe",this.#l.textContent=Array(4).fill("M".repeat(40)).join(`
10
- `),t.append(n,this.#l,this.#i),this.addEventListener("pointerdown",this.#z),this.addEventListener("pointermove",this.#k),this.addEventListener("pointerup",this.#R),this.addEventListener("pointercancel",this.#R),this.addEventListener("keydown",this.#O),this.addEventListener("wheel",this.#L,{passive:!1})}connectedCallback(){this.setAttribute("role","img"),this.hasAttribute("tabindex")||(this.tabIndex=0),this.#E(),this.#o=!this.hasAttribute("paused"),typeof IntersectionObserver=="function"&&(this.#b=new IntersectionObserver(([t])=>{this.#I=t.isIntersecting,this.#e()}),this.#b.observe(this)),typeof ResizeObserver=="function"&&(this.#y=new ResizeObserver(()=>this.#a()),this.#y.observe(this)),this.ownerDocument.addEventListener("visibilitychange",this.#e),this.ownerDocument.fonts?.ready.then(()=>this.#a()),this.#a(),this.#e()}disconnectedCallback(){this.#b?.disconnect(),this.#y?.disconnect(),this.ownerDocument.removeEventListener("visibilitychange",this.#e),cancelAnimationFrame(this.#r),this.#r=0}attributeChangedCallback(t,n,r){n!==r&&(t==="src"?r&&this.load(r):t==="preset"?r&&this.#C(r):t==="paused"?r===null?this.play():this.pause():t==="label"?this.#E():t==="cols"?this.#a():this.#p())}get look(){return this.#g}set look(t){this.#g=t??{},this.#p()}get mesh(){return this.#f}set mesh(t){this.#f=t,this.#n=null,this.#t={yaw:0,pitch:this.#u().pitch??.3,vyaw:0,vpitch:0},this.#a(),this.#e()}get paused(){return!this.#o}play(){this.#o=!0,this.#e()}pause(){this.#o=!1,this.#t={...this.#t,vyaw:0,vpitch:0},this.#e()}async load(t,n=""){this.#S?.abort();let r=this.#S=new AbortController;try{let s;if(typeof t=="string")s=await pn(t,r.signal);else{let i=t instanceof Uint8Array?t:new Uint8Array(t instanceof Blob?await t.arrayBuffer():t);s=await Bt(i,n||(t instanceof File?t.name:""))}if(r.signal.aborted)return;this.mesh=s,this.dispatchEvent(new CustomEvent("load",{detail:{triangles:s.indices.length/3,materials:s.materials}}))}catch(s){if(r.signal.aborted)return;this.dispatchEvent(new CustomEvent("error",{detail:{error:s}}))}}async#C(t){try{let n=await fetch(t);if(!n.ok)throw new Error(`${n.status} ${n.statusText} for ${t}`);this.#A=await n.json(),this.#p()}catch(n){this.dispatchEvent(new CustomEvent("error",{detail:{error:n}}))}}#u(){return Ut(this.#A,Pe(t=>this.getAttribute(t)),this.#g)}#p(){if(!this.#n||!this.#c)return this.#a();let t=this.#u();this.#n.setStyle({...t,...this.#w(t)}),this.#M(),this.#e()}#w(t){let n=this.#c;return{width:n.cols,height:n.rows,aspect:n.aspect,zoom:(t.zoom??1)*this.#h,fitPitch:this.hasAttribute("nodrag")?0:Rt}}#a(){if(!this.isConnected)return;let t=this.#l,n=t.offsetWidth/40,r=t.offsetHeight/4,s=Number(this.getAttribute("cols"))||96;if(this.#c=Oe(s,n,r,this.clientWidth,this.clientHeight),this.style.setProperty("--textel-ratio",String(this.#c.ratio)),this.#i.style.transform=`scale(${this.#c.scale})`,!this.#f)return;let i=this.#u();try{this.#n?this.#n.setStyle({...i,...this.#w(i)}):this.#n=ke(this.#f,{...i,...this.#w(i)})}catch(o){this.dispatchEvent(new CustomEvent("error",{detail:{error:o}}));return}this.#d=!0,this.#M()}#M(){if(!this.#n)return;let t=this.#n.render({yaw:this.#t.yaw,pitch:this.#t.pitch});t.style.color.mode==="none"&&t.materials.every(r=>!r.color&&r.opacity===1)?this.#i.textContent=Ee(t):this.#i.innerHTML=Re(t),this.#d=!1}#E(){this.isConnected&&this.setAttribute("aria-label",this.getAttribute("label")||"3D model drawn in ASCII")}#x(){return this.#o&&!this.#T?this.#u().spin??.66:0}#e=()=>{let t=this.#t,n=this.#x()!==0||Math.abs(t.vyaw)>.001||Math.abs(t.vpitch)>.001||!!this.#s,r=!!this.#n&&this.#I&&!this.ownerDocument.hidden&&this.isConnected&&(n||this.#d);r&&!this.#r?(this.#m=-1,this.#r=requestAnimationFrame(this.#F)):!r&&this.#r&&(cancelAnimationFrame(this.#r),this.#r=0)};#F=t=>{this.#r=0;let n=this.#m<0?0:(t-this.#m)/1e3;this.#m=t,this.#s||(this.#t=Le(this.#t,{spin:this.#x(),pitch:this.#u().pitch??.3,maxPitch:Rt},n)),this.#M(),this.#r=requestAnimationFrame(this.#F);let r=this.#t;this.#x()===0&&!this.#s&&Math.abs(r.vyaw)<.001&&Math.abs(r.vpitch)<.001&&(cancelAnimationFrame(this.#r),this.#r=0)};#z=t=>{this.hasAttribute("nodrag")||!this.#n||t.button!==0||(this.#s={x:t.clientX,y:t.clientY,t:performance.now()},this.setPointerCapture(t.pointerId),this.#e())};#k=t=>{let n=this.#s;if(!n)return;let r=performance.now();this.#t=Ue(this.#t,t.clientX-n.x,t.clientY-n.y,(r-n.t)/1e3,this.clientWidth,Rt),this.#s={x:t.clientX,y:t.clientY,t:r},this.#e()};#R=()=>{this.#s&&(performance.now()-this.#s.t>80&&(this.#t={...this.#t,vyaw:0,vpitch:0}),this.#s=null,this.#e())};#O=t=>{let n=this.#t,r=.15,s={ArrowLeft:()=>n.yaw-=r,ArrowRight:()=>n.yaw+=r,ArrowUp:()=>n.pitch=Math.max(-Rt,n.pitch-r),ArrowDown:()=>n.pitch=Math.min(Rt,n.pitch+r)," ":()=>this.#o?this.pause():this.play()};this.hasAttribute("zoomable")&&(s["+"]=s["="]=()=>this.#v(this.#h*1.1),s["-"]=()=>this.#v(this.#h/1.1));let i=s[t.key];i&&(t.preventDefault(),i(),this.#d=!0,this.#e())};#L=t=>{!this.hasAttribute("zoomable")||!this.#n||(t.preventDefault(),this.#v(this.#h*Math.exp(-t.deltaY*.001)))};#v(t){this.#h=Math.max(.3,Math.min(4,t)),this.#p()}};typeof customElements<"u"&&!customElements.get("textel-viewer")&&customElements.define("textel-viewer",jt);export{jt as TextelViewer};
10
+ .probe{visibility:hidden}`,Ft=1.2,mn=["charset","chars","color","edges","spin","pitch","zoom","distance","ambient"],dn=typeof HTMLElement>"u"?class{}:HTMLElement;async function pn(e,t){let n=async s=>{let o=await fetch(s,{signal:t});if(!o.ok)throw new Error(`${o.status} ${o.statusText} for ${s}`);return new Uint8Array(await o.arrayBuffer())},r=await n(e),i=new Map;if(/\.gltf(\?|#|$)/i.test(e)){let s=JSON.parse(new TextDecoder().decode(r));for(let o of s.buffers??[])o.uri&&!o.uri.startsWith("data:")&&i.set(decodeURIComponent(o.uri),await n(new URL(o.uri,new URL(e,location.href)).href))}return Bt(r,e.split(/[?#]/)[0],{resolve:s=>{let o=i.get(s);if(!o)throw new Error(`missing buffer ${s}`);return o}})}var jt=class extends dn{static observedAttributes=["src","preset","cols","paused","label",...mn];#a;#f;#d=null;#r=null;#S={};#w={};#m=null;#t={yaw:0,pitch:0,vyaw:0,vpitch:0};#l=1;#c=!0;#E=!0;#i=0;#p=-1;#b=!0;#n=null;#s=new Map;#o=null;#z=null;#M;#x;#k=typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches;constructor(){super();let t=this.attachShadow({mode:"open"}),n=document.createElement("style");n.textContent=fn,this.#a=document.createElement("pre"),this.#a.setAttribute("part","screen"),this.#f=document.createElement("pre"),this.#f.className="probe",this.#f.textContent=Array(4).fill("M".repeat(40)).join(`
11
+ `),t.append(n,this.#f,this.#a),this.addEventListener("pointerdown",this.#L),this.addEventListener("pointermove",this.#P),this.addEventListener("pointerup",this.#T),this.addEventListener("pointercancel",this.#T),this.addEventListener("keydown",this.#U),this.addEventListener("wheel",this.#G,{passive:!1})}connectedCallback(){this.setAttribute("role","img"),this.hasAttribute("tabindex")||(this.tabIndex=0),this.#F(),this.#c=!this.hasAttribute("paused"),typeof IntersectionObserver=="function"&&(this.#M=new IntersectionObserver(([t])=>{this.#E=t.isIntersecting,this.#e()}),this.#M.observe(this)),typeof ResizeObserver=="function"&&(this.#x=new ResizeObserver(()=>this.#u()),this.#x.observe(this)),this.ownerDocument.addEventListener("visibilitychange",this.#e),this.ownerDocument.fonts?.ready.then(()=>this.#u()),this.#u(),this.#e()}disconnectedCallback(){this.#M?.disconnect(),this.#x?.disconnect(),this.ownerDocument.removeEventListener("visibilitychange",this.#e),cancelAnimationFrame(this.#i),this.#i=0}attributeChangedCallback(t,n,r){n!==r&&(t==="src"?r&&this.load(r):t==="preset"?r&&this.#O(r):t==="paused"?r===null?this.play():this.pause():t==="label"?this.#F():t==="cols"?this.#u():this.#g())}get look(){return this.#w}set look(t){this.#w=t??{},this.#g()}get mesh(){return this.#d}set mesh(t){this.#d=t,this.#r=null,this.#t={yaw:0,pitch:this.#h().pitch??.3,vyaw:0,vpitch:0},this.#u(),this.#e()}get view(){let t=this.#h(),n=(t.zoom??1)*this.#l,r=this.hasAttribute("nodrag")?0:Ft;return{yaw:this.#t.yaw,pitch:this.#t.pitch,zoom:n,fitPitch:r}}get paused(){return!this.#c}play(){this.#c=!0,this.#e()}pause(){this.#c=!1,this.#t={...this.#t,vyaw:0,vpitch:0},this.#e()}async load(t,n=""){this.#z?.abort();let r=this.#z=new AbortController;try{let i;if(typeof t=="string")i=await pn(t,r.signal);else{let s=t instanceof Uint8Array?t:new Uint8Array(t instanceof Blob?await t.arrayBuffer():t);i=await Bt(s,n||(t instanceof File?t.name:""))}if(r.signal.aborted)return;this.mesh=i,this.dispatchEvent(new CustomEvent("load",{detail:{triangles:i.indices.length/3,materials:i.materials}}))}catch(i){if(r.signal.aborted)return;this.dispatchEvent(new CustomEvent("error",{detail:{error:i}}))}}async#O(t){try{let n=await fetch(t);if(!n.ok)throw new Error(`${n.status} ${n.statusText} for ${t}`);this.#S=await n.json(),this.#g()}catch(n){this.dispatchEvent(new CustomEvent("error",{detail:{error:n}}))}}#h(){return Pt(this.#S,Ue(t=>this.getAttribute(t)),this.#w)}#g(){if(!this.#r||!this.#m)return this.#u();let t=this.#h();this.#r.setStyle({...t,...this.#v()}),this.#A(),this.#e()}#v(){let t=this.#m,{zoom:n,fitPitch:r}=this.view;return{width:t.cols,height:t.rows,aspect:t.aspect,zoom:n,fitPitch:r}}#u(){if(!this.isConnected)return;let t=this.#f,n=t.offsetWidth/40,r=t.offsetHeight/4,i=Number(this.getAttribute("cols"))||96;if(this.#m=Oe(i,n,r,this.clientWidth,this.clientHeight),this.style.setProperty("--textel-ratio",String(this.#m.ratio)),this.#a.style.transform=`scale(${this.#m.scale})`,!this.#d)return;let s=this.#h();try{this.#r?this.#r.setStyle({...s,...this.#v()}):this.#r=ke(this.#d,{...s,...this.#v()})}catch(o){this.dispatchEvent(new CustomEvent("error",{detail:{error:o}}));return}this.#b=!0,this.#A()}#A(){if(!this.#r)return;let t=this.#r.render({yaw:this.#t.yaw,pitch:this.#t.pitch});t.style.color.mode==="none"&&t.materials.every(r=>!r.color&&r.opacity===1)?this.#a.textContent=Ee(t):this.#a.innerHTML=Fe(t),this.#b=!1}#F(){this.isConnected&&this.setAttribute("aria-label",this.getAttribute("label")||"3D model drawn in ASCII")}#I(){return this.#c&&!this.#k?this.#h().spin??.66:0}#e=()=>{let t=this.#t,n=this.#I()!==0||Math.abs(t.vyaw)>.001||Math.abs(t.vpitch)>.001||!!this.#n,r=!!this.#r&&this.#E&&!this.ownerDocument.hidden&&this.isConnected&&(n||this.#b);r&&!this.#i?(this.#p=-1,this.#i=requestAnimationFrame(this.#R)):!r&&this.#i&&(cancelAnimationFrame(this.#i),this.#i=0)};#R=t=>{this.#i=0;let n=this.#p<0?0:(t-this.#p)/1e3;this.#p=t,this.#n||(this.#t=Le(this.#t,{spin:this.#I(),pitch:this.#h().pitch??.3,maxPitch:Ft},n)),this.#A(),this.#i=requestAnimationFrame(this.#R);let r=this.#t;this.#I()===0&&!this.#n&&Math.abs(r.vyaw)<.001&&Math.abs(r.vpitch)<.001&&(cancelAnimationFrame(this.#i),this.#i=0)};#L=t=>{this.hasAttribute("nodrag")||!this.#r||t.button!==0||(this.setPointerCapture(t.pointerId),this.#s.set(t.pointerId,{x:t.clientX,y:t.clientY}),this.#s.size===2&&this.hasAttribute("zoomable")?(this.#o={dist:this.#C(),zoom:this.#l},this.#n=null,this.#t={...this.#t,vyaw:0,vpitch:0}):this.#s.size===1&&(this.#n={id:t.pointerId,x:t.clientX,y:t.clientY,t:performance.now()}),this.#e())};#P=t=>{if(!this.#s.has(t.pointerId))return;if(this.#s.set(t.pointerId,{x:t.clientX,y:t.clientY}),this.#o){this.#o.dist>0&&this.#y(this.#o.zoom*(this.#C()/this.#o.dist));return}let n=this.#n;if(!n||n.id!==t.pointerId)return;let r=performance.now();this.#t=Pe(this.#t,t.clientX-n.x,t.clientY-n.y,(r-n.t)/1e3,this.clientWidth,Ft),this.#n={...n,x:t.clientX,y:t.clientY,t:r},this.#e()};#T=t=>{if(this.#s.delete(t.pointerId)){if(this.#o){this.#s.size<2&&(this.#o=null);let[n]=[...this.#s];this.#n=n&&!this.#o?{id:n[0],...n[1],t:performance.now()}:null,this.#e();return}!this.#n||this.#n.id!==t.pointerId||(performance.now()-this.#n.t>80&&(this.#t={...this.#t,vyaw:0,vpitch:0}),this.#n=null,this.#e())}};#C(){let[t,n]=[...this.#s.values()];return n?Math.hypot(t.x-n.x,t.y-n.y):0}#U=t=>{let n=this.#t,r=.15,i={ArrowLeft:()=>n.yaw-=r,ArrowRight:()=>n.yaw+=r,ArrowUp:()=>n.pitch=Math.max(-Ft,n.pitch-r),ArrowDown:()=>n.pitch=Math.min(Ft,n.pitch+r)," ":()=>this.#c?this.pause():this.play()};this.hasAttribute("zoomable")&&(i["+"]=i["="]=()=>this.#y(this.#l*1.1),i["-"]=()=>this.#y(this.#l/1.1));let s=i[t.key];s&&(t.preventDefault(),s(),this.#b=!0,this.#e())};#G=t=>{!this.hasAttribute("zoomable")||!this.#r||(t.preventDefault(),this.#y(this.#l*Math.exp(-t.deltaY*.001)))};#y(t){this.#l=Math.max(.3,Math.min(4,t)),this.#g()}};typeof customElements<"u"&&!customElements.get("textel-viewer")&&customElements.define("textel-viewer",jt);export{jt as TextelViewer};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feelgxxd/textel",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Turn 3D models into animated ASCII art for the terminal and the web.",
5
5
  "keywords": [
6
6
  "ascii",
@@ -61,7 +61,9 @@
61
61
  "./dist/browser/player.js",
62
62
  "./dist/textel-player.js",
63
63
  "./dist/browser/viewer.js",
64
- "./dist/textel-viewer.js"
64
+ "./dist/textel-viewer.js",
65
+ "./src/browser/player.ts",
66
+ "./src/browser/viewer.ts"
65
67
  ],
66
68
  "files": [
67
69
  "dist"
@@ -77,14 +79,17 @@
77
79
  "snapshots": "node test/update-snapshots.ts",
78
80
  "size": "node scripts/build-browser.ts --check",
79
81
  "demo": "node scripts/build-browser.ts && node scripts/demo.ts",
80
- "prepublishOnly": "npm run typecheck && npm test && npm run build"
82
+ "prepublishOnly": "npm run typecheck && npm test && npm run build",
83
+ "site": "vite --config site/vite.config.ts",
84
+ "site:build": "vite build --config site/vite.config.ts"
81
85
  },
82
86
  "devDependencies": {
83
87
  "@happy-dom/global-registrator": "^21.0.4",
84
88
  "@types/node": "^24.0.0",
85
89
  "esbuild": "^0.28.2",
86
90
  "happy-dom": "^21.0.4",
87
- "typescript": "^5.9.0"
91
+ "typescript": "^5.9.0",
92
+ "vite": "^8.3.6"
88
93
  },
89
94
  "publishConfig": {
90
95
  "access": "public"