@feelgxxd/textel 0.2.0 → 0.3.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 +46 -2
- package/dist/browser/player.js +4 -4
- package/dist/browser/viewer-core.d.ts +42 -0
- package/dist/browser/viewer-core.js +67 -0
- package/dist/browser/viewer.d.ts +29 -0
- package/dist/browser/viewer.js +338 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/loaders/index.d.ts +12 -2
- package/dist/loaders/index.js +46 -12
- package/dist/loaders/obj.d.ts +5 -0
- package/dist/loaders/obj.js +20 -3
- package/dist/output.js +13 -1
- package/dist/render.js +6 -2
- package/dist/style.d.ts +13 -0
- package/dist/style.js +2 -0
- package/dist/textel-player.js +1 -1
- package/dist/textel-viewer.js +10 -0
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -77,7 +77,18 @@ frame when the output is not a terminal.
|
|
|
77
77
|
|
|
78
78
|
## On a web page
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
Two elements, both without dependencies and both loaded with one script tag:
|
|
81
|
+
|
|
82
|
+
| | `<textel-player>` | `<textel-viewer>` |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| shows | a recorded loop (`.textel.json`) | the model itself, rendered live |
|
|
85
|
+
| size | about 3 KB | about 11 KB |
|
|
86
|
+
| dragging | scrubs through the loop | turns the model any way, with a flick |
|
|
87
|
+
| best for | a spinning logo | something people play with |
|
|
88
|
+
|
|
89
|
+
### `<textel-player>`
|
|
90
|
+
|
|
91
|
+
Record once, then play the file: no 3D code in the page.
|
|
81
92
|
|
|
82
93
|
```sh
|
|
83
94
|
textel render logo.glb --aspect 1.5 -o logo.textel.json
|
|
@@ -107,7 +118,37 @@ Style it with CSS: `font-family`, `color`, `--textel-glow` (a `text-shadow`), `-
|
|
|
107
118
|
or `::part(screen)`. From JavaScript: `player.play()`, `player.pause()`, `player.frame`,
|
|
108
119
|
`player.recording = json`, and the `load` and `error` events.
|
|
109
120
|
|
|
110
|
-
|
|
121
|
+
### `<textel-viewer>`
|
|
122
|
+
|
|
123
|
+
```html
|
|
124
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/@feelgxxd/textel@0.3/dist/textel-viewer.js"></script>
|
|
125
|
+
<textel-viewer src="model.glb" charset="hard" color="#301000,#ffb000" edges></textel-viewer>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Or `import '@feelgxxd/textel/viewer'` in a bundled app.
|
|
129
|
+
|
|
130
|
+
It measures the page font and renders for its real cell shape, so the model is never squashed. The box
|
|
131
|
+
is square unless you give it a height. Drag in any direction and let go to flick it; it eases back to
|
|
132
|
+
its resting spin. When focused, the arrow keys turn it and space pauses. Large `.obj` files are parsed
|
|
133
|
+
in slices so the page keeps responding while they load. Like the player, it pauses off screen and in
|
|
134
|
+
background tabs and respects reduced motion.
|
|
135
|
+
|
|
136
|
+
| attribute | |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `src` | a `.glb`, `.gltf` (external buffers are fetched too), `.obj` or `.stl` |
|
|
139
|
+
| `preset` | a preset JSON file (see [Presets](#presets)) |
|
|
140
|
+
| `charset`, `chars`, `color`, `edges`, `spin`, `pitch`, `zoom`, `distance`, `ambient` | the same as the preset fields; `color` takes a comma separated gradient |
|
|
141
|
+
| `cols` | grid width in characters (default 96) |
|
|
142
|
+
| `paused` | start without spinning |
|
|
143
|
+
| `nodrag` | turn off dragging |
|
|
144
|
+
| `zoomable` | wheel and `+` / `-` zoom |
|
|
145
|
+
| `label` | accessible name |
|
|
146
|
+
|
|
147
|
+
A preset is applied first, then the attributes, then `viewer.look = { ... }` from JavaScript.
|
|
148
|
+
Also from JavaScript: `viewer.load(urlOrFileOrBytes)`, `viewer.mesh = mesh` (for example from
|
|
149
|
+
`@feelgxxd/textel/sdf`), `play()`, `pause()`, and the `load` and `error` events.
|
|
150
|
+
|
|
151
|
+
`npm run demo` serves both demo pages with drop zones for your own files, and an output benchmark.
|
|
111
152
|
|
|
112
153
|
## Presets
|
|
113
154
|
|
|
@@ -136,6 +177,9 @@ Materials are matched by the names in the model file:
|
|
|
136
177
|
|
|
137
178
|
Charsets: `classic`, `hard`, `detailed`, `blocks`, `dots`, `minimal`, or any string of your own.
|
|
138
179
|
|
|
180
|
+
A gradient gives each glyph of the ramp one color, so colors change only where glyphs do; that keeps
|
|
181
|
+
terminal output and web pages light. `"colorSteps": 0` makes it smooth instead, or set a number of steps.
|
|
182
|
+
|
|
139
183
|
## Library
|
|
140
184
|
|
|
141
185
|
```ts
|
package/dist/browser/player.js
CHANGED
|
@@ -54,14 +54,14 @@ export class TextelPlayer extends Base {
|
|
|
54
54
|
this.#ro = new ResizeObserver(() => this.#fit());
|
|
55
55
|
this.#ro.observe(this);
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
this.ownerDocument.addEventListener('visibilitychange', this.#kick);
|
|
58
|
+
this.ownerDocument.fonts?.ready.then(() => this.#fit());
|
|
59
59
|
this.#kick();
|
|
60
60
|
}
|
|
61
61
|
disconnectedCallback() {
|
|
62
62
|
this.#io?.disconnect();
|
|
63
63
|
this.#ro?.disconnect();
|
|
64
|
-
|
|
64
|
+
this.ownerDocument.removeEventListener('visibilitychange', this.#kick);
|
|
65
65
|
cancelAnimationFrame(this.#raf);
|
|
66
66
|
this.#raf = 0;
|
|
67
67
|
}
|
|
@@ -154,7 +154,7 @@ export class TextelPlayer extends Base {
|
|
|
154
154
|
}
|
|
155
155
|
/** runs the animation only while it is playing, on screen and in a visible tab */
|
|
156
156
|
#kick = () => {
|
|
157
|
-
const run = this.#playing && this.#visible && !
|
|
157
|
+
const run = this.#playing && this.#visible && !this.ownerDocument.hidden && !!this.#rec && !this.#drag && this.isConnected;
|
|
158
158
|
if (run && !this.#raf) {
|
|
159
159
|
// the first tick only sets the clock: time is measured between frame timestamps alone,
|
|
160
160
|
// never mixed with performance.now(), whose reading can be later than the next frame's
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface Grid {
|
|
2
|
+
cols: number;
|
|
3
|
+
rows: number;
|
|
4
|
+
/** cell height / width of the page font, handed to the renderer so nothing has to be stretched */
|
|
5
|
+
aspect: number;
|
|
6
|
+
/** transform scale that makes the grid fill the box width */
|
|
7
|
+
scale: number;
|
|
8
|
+
/** width / height of the drawn grid, for an auto-height box */
|
|
9
|
+
ratio: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The grid for a box: `cols` is fixed, rows follow from the box shape in this font.
|
|
13
|
+
* A box without a height gets a square picture.
|
|
14
|
+
*/
|
|
15
|
+
export declare function gridFor(cols: number, cellW: number, cellH: number, boxW: number, boxH: number): Grid;
|
|
16
|
+
export interface Motion {
|
|
17
|
+
yaw: number;
|
|
18
|
+
pitch: number;
|
|
19
|
+
/** radians per second */
|
|
20
|
+
vyaw: number;
|
|
21
|
+
vpitch: number;
|
|
22
|
+
}
|
|
23
|
+
export interface MotionTarget {
|
|
24
|
+
/** resting spin, radians per second (0 when paused or reduced motion) */
|
|
25
|
+
spin: number;
|
|
26
|
+
/** resting pitch */
|
|
27
|
+
pitch: number;
|
|
28
|
+
/** how far people may tilt it either way */
|
|
29
|
+
maxPitch: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Advances the turntable by `dt` seconds. After a drag the flick keeps going and eases back to the
|
|
33
|
+
* resting spin, and the tilt drifts back to the resting pitch.
|
|
34
|
+
*/
|
|
35
|
+
export declare function step(m: Motion, t: MotionTarget, dt: number): Motion;
|
|
36
|
+
/**
|
|
37
|
+
* A pointer moved by (dx, dy) pixels over `dt` seconds on a box `width` wide: a drag across the
|
|
38
|
+
* whole width turns the model once. The speed is kept so a release turns into a flick.
|
|
39
|
+
*/
|
|
40
|
+
export declare function drag(m: Motion, dx: number, dy: number, dt: number, width: number, maxPitch: number): Motion;
|
|
41
|
+
/** attribute values to style fields; unknown or empty values are left out */
|
|
42
|
+
export declare function styleFromAttributes(get: (name: string) => string | null): Record<string, unknown>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// The parts of <textel-viewer> that do not touch the DOM, so they can be tested anywhere.
|
|
2
|
+
/**
|
|
3
|
+
* The grid for a box: `cols` is fixed, rows follow from the box shape in this font.
|
|
4
|
+
* A box without a height gets a square picture.
|
|
5
|
+
*/
|
|
6
|
+
export function gridFor(cols, cellW, cellH, boxW, boxH) {
|
|
7
|
+
const c = Math.max(8, Math.round(cols));
|
|
8
|
+
const cw = cellW > 0 ? cellW : 0.6, ch = cellH > 0 ? cellH : 1;
|
|
9
|
+
const wantRatio = boxW > 0 && boxH > 0 ? boxW / boxH : 1;
|
|
10
|
+
const rows = Math.max(4, Math.min(400, Math.round((c * cw) / (ch * wantRatio))));
|
|
11
|
+
const w = c * cw, h = rows * ch;
|
|
12
|
+
return { cols: c, rows, aspect: ch / cw, scale: boxW > 0 ? boxW / w : 1, ratio: w / h };
|
|
13
|
+
}
|
|
14
|
+
const ease = (rate, dt) => 1 - Math.exp(-rate * dt);
|
|
15
|
+
/**
|
|
16
|
+
* Advances the turntable by `dt` seconds. After a drag the flick keeps going and eases back to the
|
|
17
|
+
* resting spin, and the tilt drifts back to the resting pitch.
|
|
18
|
+
*/
|
|
19
|
+
export function step(m, t, dt) {
|
|
20
|
+
const d = Math.min(Math.max(dt, 0), 0.1);
|
|
21
|
+
const vyaw = m.vyaw + (t.spin - m.vyaw) * ease(2, d);
|
|
22
|
+
const vpitch = m.vpitch * (1 - ease(4, d));
|
|
23
|
+
let pitch = m.pitch + vpitch * d;
|
|
24
|
+
pitch += (t.pitch - pitch) * ease(1.5, d);
|
|
25
|
+
pitch = Math.max(-t.maxPitch, Math.min(t.maxPitch, pitch));
|
|
26
|
+
return { yaw: m.yaw + vyaw * d, pitch, vyaw, vpitch };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A pointer moved by (dx, dy) pixels over `dt` seconds on a box `width` wide: a drag across the
|
|
30
|
+
* whole width turns the model once. The speed is kept so a release turns into a flick.
|
|
31
|
+
*/
|
|
32
|
+
export function drag(m, dx, dy, dt, width, maxPitch) {
|
|
33
|
+
const k = width > 0 ? (Math.PI * 2) / width : 0.01;
|
|
34
|
+
const dyaw = dx * k, dpitch = dy * k;
|
|
35
|
+
const pitch = Math.max(-maxPitch, Math.min(maxPitch, m.pitch + dpitch));
|
|
36
|
+
const inv = dt > 0.001 ? 1 / dt : 0;
|
|
37
|
+
return { yaw: m.yaw + dyaw, pitch, vyaw: dyaw * inv, vpitch: dpitch * inv };
|
|
38
|
+
}
|
|
39
|
+
/** attribute values to style fields; unknown or empty values are left out */
|
|
40
|
+
export function styleFromAttributes(get) {
|
|
41
|
+
const out = {};
|
|
42
|
+
const n = (name) => {
|
|
43
|
+
const v = get(name);
|
|
44
|
+
if (v === null || v.trim() === '')
|
|
45
|
+
return undefined;
|
|
46
|
+
const x = Number(v);
|
|
47
|
+
return Number.isFinite(x) ? x : undefined;
|
|
48
|
+
};
|
|
49
|
+
const charset = get('charset');
|
|
50
|
+
if (charset)
|
|
51
|
+
out.charset = charset;
|
|
52
|
+
const chars = get('chars');
|
|
53
|
+
if (chars)
|
|
54
|
+
out.chars = chars;
|
|
55
|
+
const color = get('color');
|
|
56
|
+
if (color)
|
|
57
|
+
out.color = color.includes(',') ? color.split(',').map((s) => s.trim()) : color.trim();
|
|
58
|
+
const edges = get('edges');
|
|
59
|
+
if (edges !== null)
|
|
60
|
+
out.edges = edges === 'depth' ? 'depth' : edges === 'off' || edges === 'false' ? false : 'outline';
|
|
61
|
+
for (const k of ['spin', 'pitch', 'zoom', 'distance', 'ambient']) {
|
|
62
|
+
const v = n(k);
|
|
63
|
+
if (v !== undefined)
|
|
64
|
+
out[k] = v;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Mesh } from '../mesh.ts';
|
|
2
|
+
import { type StyleInput } from '../style.ts';
|
|
3
|
+
declare const Base: typeof HTMLElement;
|
|
4
|
+
export declare class TextelViewer extends Base {
|
|
5
|
+
#private;
|
|
6
|
+
static observedAttributes: string[];
|
|
7
|
+
constructor();
|
|
8
|
+
connectedCallback(): void;
|
|
9
|
+
disconnectedCallback(): void;
|
|
10
|
+
attributeChangedCallback(name: string, old: string | null, value: string | null): void;
|
|
11
|
+
/** extra style from JavaScript, on top of the preset and the attributes */
|
|
12
|
+
get look(): StyleInput;
|
|
13
|
+
set look(s: StyleInput);
|
|
14
|
+
/** the model being shown; set a Mesh to show one built in code */
|
|
15
|
+
get mesh(): Mesh | null;
|
|
16
|
+
set mesh(m: Mesh | null);
|
|
17
|
+
get paused(): boolean;
|
|
18
|
+
play(): void;
|
|
19
|
+
/** stops at once: the spin and any flick still coasting are dropped */
|
|
20
|
+
pause(): void;
|
|
21
|
+
/** loads a model from a URL, a File, or bytes */
|
|
22
|
+
load(source: string | Blob | ArrayBuffer | Uint8Array, name?: string): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
declare global {
|
|
25
|
+
interface HTMLElementTagNameMap {
|
|
26
|
+
'textel-viewer': TextelViewer;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// <textel-viewer>: renders a 3D model live in the page; drag to turn it.
|
|
2
|
+
//
|
|
3
|
+
// <script type="module" src="https://cdn.jsdelivr.net/npm/@feelgxxd/textel@0.3/dist/textel-viewer.js"></script>
|
|
4
|
+
// <textel-viewer src="model.glb" charset="hard" color="#301000,#ffb000"></textel-viewer>
|
|
5
|
+
import { parseModelAsync } from "../loaders/index.js";
|
|
6
|
+
import { toHTML, toText } from "../output.js";
|
|
7
|
+
import { createRenderer } from "../render.js";
|
|
8
|
+
import { mergeStyles } from "../style.js";
|
|
9
|
+
import { drag, gridFor, step, styleFromAttributes } from "./viewer-core.js";
|
|
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
|
+
:host(:not([nodrag])){cursor:grab;touch-action:pan-y}
|
|
12
|
+
:host(:not([nodrag]):active){cursor:grabbing}
|
|
13
|
+
:host(:focus-visible){box-shadow:0 0 0 2px currentColor}
|
|
14
|
+
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}
|
|
15
|
+
.probe{visibility:hidden}`;
|
|
16
|
+
const MAX_PITCH = 1.2;
|
|
17
|
+
const STYLE_ATTRS = ['charset', 'chars', 'color', 'edges', 'spin', 'pitch', 'zoom', 'distance', 'ambient'];
|
|
18
|
+
const Base = (typeof HTMLElement === 'undefined' ? class {
|
|
19
|
+
} : HTMLElement);
|
|
20
|
+
/** fetches a model; a .gltf brings its external buffers along */
|
|
21
|
+
async function fetchModel(url, signal) {
|
|
22
|
+
const get = async (u) => {
|
|
23
|
+
const res = await fetch(u, { signal });
|
|
24
|
+
if (!res.ok)
|
|
25
|
+
throw new Error(`${res.status} ${res.statusText} for ${u}`);
|
|
26
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
27
|
+
};
|
|
28
|
+
const bytes = await get(url);
|
|
29
|
+
const buffers = new Map();
|
|
30
|
+
if (/\.gltf(\?|#|$)/i.test(url)) {
|
|
31
|
+
const json = JSON.parse(new TextDecoder().decode(bytes));
|
|
32
|
+
for (const b of json.buffers ?? []) {
|
|
33
|
+
if (b.uri && !b.uri.startsWith('data:'))
|
|
34
|
+
buffers.set(decodeURIComponent(b.uri), await get(new URL(b.uri, new URL(url, location.href)).href));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return parseModelAsync(bytes, url.split(/[?#]/)[0], {
|
|
38
|
+
resolve: (uri) => {
|
|
39
|
+
const b = buffers.get(uri);
|
|
40
|
+
if (!b)
|
|
41
|
+
throw new Error(`missing buffer ${uri}`);
|
|
42
|
+
return b;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export class TextelViewer extends Base {
|
|
47
|
+
static observedAttributes = ['src', 'preset', 'cols', 'paused', 'label', ...STYLE_ATTRS];
|
|
48
|
+
#pre;
|
|
49
|
+
#probe;
|
|
50
|
+
#mesh = null;
|
|
51
|
+
#renderer = null;
|
|
52
|
+
#preset = {};
|
|
53
|
+
#look = {};
|
|
54
|
+
#grid = null;
|
|
55
|
+
#motion = { yaw: 0, pitch: 0, vyaw: 0, vpitch: 0 };
|
|
56
|
+
#zoom = 1;
|
|
57
|
+
#playing = true;
|
|
58
|
+
#visible = true;
|
|
59
|
+
#raf = 0;
|
|
60
|
+
#last = -1;
|
|
61
|
+
#dirty = true;
|
|
62
|
+
#pointer = null;
|
|
63
|
+
#load = null;
|
|
64
|
+
#io;
|
|
65
|
+
#ro;
|
|
66
|
+
#reduced = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
67
|
+
constructor() {
|
|
68
|
+
super();
|
|
69
|
+
const root = this.attachShadow({ mode: 'open' });
|
|
70
|
+
const style = document.createElement('style');
|
|
71
|
+
style.textContent = CSS;
|
|
72
|
+
this.#pre = document.createElement('pre');
|
|
73
|
+
this.#pre.setAttribute('part', 'screen');
|
|
74
|
+
this.#probe = document.createElement('pre');
|
|
75
|
+
this.#probe.className = 'probe';
|
|
76
|
+
// 40 glyphs on each of 4 lines: one glyph's width and one line's height, averaged
|
|
77
|
+
this.#probe.textContent = Array(4).fill('M'.repeat(40)).join('\n');
|
|
78
|
+
root.append(style, this.#probe, this.#pre);
|
|
79
|
+
this.addEventListener('pointerdown', this.#onDown);
|
|
80
|
+
this.addEventListener('pointermove', this.#onMove);
|
|
81
|
+
this.addEventListener('pointerup', this.#onUp);
|
|
82
|
+
this.addEventListener('pointercancel', this.#onUp);
|
|
83
|
+
this.addEventListener('keydown', this.#onKey);
|
|
84
|
+
this.addEventListener('wheel', this.#onWheel, { passive: false });
|
|
85
|
+
}
|
|
86
|
+
connectedCallback() {
|
|
87
|
+
this.setAttribute('role', 'img');
|
|
88
|
+
if (!this.hasAttribute('tabindex'))
|
|
89
|
+
this.tabIndex = 0;
|
|
90
|
+
this.#updateLabel();
|
|
91
|
+
this.#playing = !this.hasAttribute('paused');
|
|
92
|
+
if (typeof IntersectionObserver === 'function') {
|
|
93
|
+
this.#io = new IntersectionObserver(([e]) => { this.#visible = e.isIntersecting; this.#kick(); });
|
|
94
|
+
this.#io.observe(this);
|
|
95
|
+
}
|
|
96
|
+
if (typeof ResizeObserver === 'function') {
|
|
97
|
+
this.#ro = new ResizeObserver(() => this.#layout());
|
|
98
|
+
this.#ro.observe(this);
|
|
99
|
+
}
|
|
100
|
+
this.ownerDocument.addEventListener('visibilitychange', this.#kick);
|
|
101
|
+
this.ownerDocument.fonts?.ready.then(() => this.#layout());
|
|
102
|
+
this.#layout();
|
|
103
|
+
this.#kick();
|
|
104
|
+
}
|
|
105
|
+
disconnectedCallback() {
|
|
106
|
+
this.#io?.disconnect();
|
|
107
|
+
this.#ro?.disconnect();
|
|
108
|
+
this.ownerDocument.removeEventListener('visibilitychange', this.#kick);
|
|
109
|
+
cancelAnimationFrame(this.#raf);
|
|
110
|
+
this.#raf = 0;
|
|
111
|
+
}
|
|
112
|
+
attributeChangedCallback(name, old, value) {
|
|
113
|
+
if (old === value)
|
|
114
|
+
return;
|
|
115
|
+
if (name === 'src') {
|
|
116
|
+
if (value)
|
|
117
|
+
this.load(value);
|
|
118
|
+
}
|
|
119
|
+
else if (name === 'preset') {
|
|
120
|
+
if (value)
|
|
121
|
+
this.#fetchPreset(value);
|
|
122
|
+
}
|
|
123
|
+
else if (name === 'paused') {
|
|
124
|
+
value === null ? this.play() : this.pause();
|
|
125
|
+
}
|
|
126
|
+
else if (name === 'label')
|
|
127
|
+
this.#updateLabel();
|
|
128
|
+
else if (name === 'cols')
|
|
129
|
+
this.#layout();
|
|
130
|
+
else
|
|
131
|
+
this.#restyle();
|
|
132
|
+
}
|
|
133
|
+
/** extra style from JavaScript, on top of the preset and the attributes */
|
|
134
|
+
get look() { return this.#look; }
|
|
135
|
+
set look(s) { this.#look = s ?? {}; this.#restyle(); }
|
|
136
|
+
/** the model being shown; set a Mesh to show one built in code */
|
|
137
|
+
get mesh() { return this.#mesh; }
|
|
138
|
+
set mesh(m) {
|
|
139
|
+
this.#mesh = m;
|
|
140
|
+
this.#renderer = null;
|
|
141
|
+
this.#motion = { yaw: 0, pitch: this.#style().pitch ?? 0.3, vyaw: 0, vpitch: 0 };
|
|
142
|
+
this.#layout();
|
|
143
|
+
this.#kick();
|
|
144
|
+
}
|
|
145
|
+
get paused() { return !this.#playing; }
|
|
146
|
+
play() { this.#playing = true; this.#kick(); }
|
|
147
|
+
/** stops at once: the spin and any flick still coasting are dropped */
|
|
148
|
+
pause() {
|
|
149
|
+
this.#playing = false;
|
|
150
|
+
this.#motion = { ...this.#motion, vyaw: 0, vpitch: 0 };
|
|
151
|
+
this.#kick();
|
|
152
|
+
}
|
|
153
|
+
/** loads a model from a URL, a File, or bytes */
|
|
154
|
+
async load(source, name = '') {
|
|
155
|
+
this.#load?.abort();
|
|
156
|
+
const ac = (this.#load = new AbortController());
|
|
157
|
+
try {
|
|
158
|
+
let mesh;
|
|
159
|
+
if (typeof source === 'string')
|
|
160
|
+
mesh = await fetchModel(source, ac.signal);
|
|
161
|
+
else {
|
|
162
|
+
const bytes = source instanceof Uint8Array ? source : new Uint8Array(source instanceof Blob ? await source.arrayBuffer() : source);
|
|
163
|
+
mesh = await parseModelAsync(bytes, name || (source instanceof File ? source.name : ''));
|
|
164
|
+
}
|
|
165
|
+
if (ac.signal.aborted)
|
|
166
|
+
return;
|
|
167
|
+
this.mesh = mesh;
|
|
168
|
+
this.dispatchEvent(new CustomEvent('load', { detail: { triangles: mesh.indices.length / 3, materials: mesh.materials } }));
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
if (ac.signal.aborted)
|
|
172
|
+
return;
|
|
173
|
+
this.dispatchEvent(new CustomEvent('error', { detail: { error } }));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async #fetchPreset(url) {
|
|
177
|
+
try {
|
|
178
|
+
const res = await fetch(url);
|
|
179
|
+
if (!res.ok)
|
|
180
|
+
throw new Error(`${res.status} ${res.statusText} for ${url}`);
|
|
181
|
+
this.#preset = await res.json();
|
|
182
|
+
this.#restyle();
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
this.dispatchEvent(new CustomEvent('error', { detail: { error } }));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** preset < attributes < .look, then the parts the viewer owns */
|
|
189
|
+
#style() {
|
|
190
|
+
return mergeStyles(this.#preset, styleFromAttributes((n) => this.getAttribute(n)), this.#look);
|
|
191
|
+
}
|
|
192
|
+
#restyle() {
|
|
193
|
+
if (!this.#renderer || !this.#grid)
|
|
194
|
+
return this.#layout();
|
|
195
|
+
const s = this.#style();
|
|
196
|
+
this.#renderer.setStyle({ ...s, ...this.#gridStyle(s) });
|
|
197
|
+
// show the change now, even while paused
|
|
198
|
+
this.#draw();
|
|
199
|
+
this.#kick();
|
|
200
|
+
}
|
|
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 };
|
|
204
|
+
}
|
|
205
|
+
#layout() {
|
|
206
|
+
if (!this.isConnected)
|
|
207
|
+
return;
|
|
208
|
+
const p = this.#probe, cellW = p.offsetWidth / 40, cellH = p.offsetHeight / 4;
|
|
209
|
+
const cols = Number(this.getAttribute('cols')) || 96;
|
|
210
|
+
// A page-set height shapes the grid. Without one the box follows --textel-ratio (square at
|
|
211
|
+
// first), so its height already matches the grid and the layout settles on the same rows.
|
|
212
|
+
this.#grid = gridFor(cols, cellW, cellH, this.clientWidth, this.clientHeight);
|
|
213
|
+
this.style.setProperty('--textel-ratio', String(this.#grid.ratio));
|
|
214
|
+
this.#pre.style.transform = `scale(${this.#grid.scale})`;
|
|
215
|
+
if (!this.#mesh)
|
|
216
|
+
return;
|
|
217
|
+
const s = this.#style();
|
|
218
|
+
try {
|
|
219
|
+
if (!this.#renderer)
|
|
220
|
+
this.#renderer = createRenderer(this.#mesh, { ...s, ...this.#gridStyle(s) });
|
|
221
|
+
else
|
|
222
|
+
this.#renderer.setStyle({ ...s, ...this.#gridStyle(s) });
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
this.dispatchEvent(new CustomEvent('error', { detail: { error } }));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
this.#dirty = true;
|
|
229
|
+
this.#draw();
|
|
230
|
+
}
|
|
231
|
+
#draw() {
|
|
232
|
+
if (!this.#renderer)
|
|
233
|
+
return;
|
|
234
|
+
const frame = this.#renderer.render({ yaw: this.#motion.yaw, pitch: this.#motion.pitch });
|
|
235
|
+
// without any color or dimming the frame is plain text, which is the cheapest thing to draw
|
|
236
|
+
const plain = frame.style.color.mode === 'none' && frame.materials.every((m) => !m.color && m.opacity === 1);
|
|
237
|
+
if (plain)
|
|
238
|
+
this.#pre.textContent = toText(frame);
|
|
239
|
+
else
|
|
240
|
+
this.#pre.innerHTML = toHTML(frame);
|
|
241
|
+
this.#dirty = false;
|
|
242
|
+
}
|
|
243
|
+
#updateLabel() {
|
|
244
|
+
if (this.isConnected)
|
|
245
|
+
this.setAttribute('aria-label', this.getAttribute('label') || '3D model drawn in ASCII');
|
|
246
|
+
}
|
|
247
|
+
#spin() {
|
|
248
|
+
return this.#playing && !this.#reduced ? this.#style().spin ?? 0.66 : 0;
|
|
249
|
+
}
|
|
250
|
+
/** animates while it spins, coasts after a flick, or is being dragged; otherwise it stays still */
|
|
251
|
+
#kick = () => {
|
|
252
|
+
const m = this.#motion;
|
|
253
|
+
const moving = this.#spin() !== 0 || Math.abs(m.vyaw) > 1e-3 || Math.abs(m.vpitch) > 1e-3 || !!this.#pointer;
|
|
254
|
+
const run = !!this.#renderer && this.#visible && !this.ownerDocument.hidden && this.isConnected && (moving || this.#dirty);
|
|
255
|
+
if (run && !this.#raf) {
|
|
256
|
+
this.#last = -1;
|
|
257
|
+
this.#raf = requestAnimationFrame(this.#tick);
|
|
258
|
+
}
|
|
259
|
+
else if (!run && this.#raf) {
|
|
260
|
+
cancelAnimationFrame(this.#raf);
|
|
261
|
+
this.#raf = 0;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
#tick = (now) => {
|
|
265
|
+
this.#raf = 0;
|
|
266
|
+
const dt = this.#last < 0 ? 0 : (now - this.#last) / 1000;
|
|
267
|
+
this.#last = now;
|
|
268
|
+
if (!this.#pointer) {
|
|
269
|
+
this.#motion = step(this.#motion, { spin: this.#spin(), pitch: this.#style().pitch ?? 0.3, maxPitch: MAX_PITCH }, dt);
|
|
270
|
+
}
|
|
271
|
+
this.#draw();
|
|
272
|
+
this.#raf = requestAnimationFrame(this.#tick);
|
|
273
|
+
// stop once everything has settled
|
|
274
|
+
const m = this.#motion;
|
|
275
|
+
if (this.#spin() === 0 && !this.#pointer && Math.abs(m.vyaw) < 1e-3 && Math.abs(m.vpitch) < 1e-3) {
|
|
276
|
+
cancelAnimationFrame(this.#raf);
|
|
277
|
+
this.#raf = 0;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
#onDown = (e) => {
|
|
281
|
+
if (this.hasAttribute('nodrag') || !this.#renderer || e.button !== 0)
|
|
282
|
+
return;
|
|
283
|
+
this.#pointer = { x: e.clientX, y: e.clientY, t: performance.now() };
|
|
284
|
+
this.setPointerCapture(e.pointerId);
|
|
285
|
+
this.#kick();
|
|
286
|
+
};
|
|
287
|
+
#onMove = (e) => {
|
|
288
|
+
const p = this.#pointer;
|
|
289
|
+
if (!p)
|
|
290
|
+
return;
|
|
291
|
+
const now = performance.now();
|
|
292
|
+
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 };
|
|
294
|
+
this.#kick();
|
|
295
|
+
};
|
|
296
|
+
#onUp = () => {
|
|
297
|
+
if (!this.#pointer)
|
|
298
|
+
return;
|
|
299
|
+
// a pause before letting go means no flick
|
|
300
|
+
if (performance.now() - this.#pointer.t > 80)
|
|
301
|
+
this.#motion = { ...this.#motion, vyaw: 0, vpitch: 0 };
|
|
302
|
+
this.#pointer = null;
|
|
303
|
+
this.#kick();
|
|
304
|
+
};
|
|
305
|
+
#onKey = (e) => {
|
|
306
|
+
const m = this.#motion, step = 0.15;
|
|
307
|
+
const moves = {
|
|
308
|
+
ArrowLeft: () => (m.yaw -= step),
|
|
309
|
+
ArrowRight: () => (m.yaw += step),
|
|
310
|
+
ArrowUp: () => (m.pitch = Math.max(-MAX_PITCH, m.pitch - step)),
|
|
311
|
+
ArrowDown: () => (m.pitch = Math.min(MAX_PITCH, m.pitch + step)),
|
|
312
|
+
' ': () => (this.#playing ? this.pause() : this.play()),
|
|
313
|
+
};
|
|
314
|
+
if (this.hasAttribute('zoomable')) {
|
|
315
|
+
moves['+'] = moves['='] = () => this.#setZoom(this.#zoom * 1.1);
|
|
316
|
+
moves['-'] = () => this.#setZoom(this.#zoom / 1.1);
|
|
317
|
+
}
|
|
318
|
+
const f = moves[e.key];
|
|
319
|
+
if (!f)
|
|
320
|
+
return;
|
|
321
|
+
e.preventDefault();
|
|
322
|
+
f();
|
|
323
|
+
this.#dirty = true;
|
|
324
|
+
this.#kick();
|
|
325
|
+
};
|
|
326
|
+
#onWheel = (e) => {
|
|
327
|
+
if (!this.hasAttribute('zoomable') || !this.#renderer)
|
|
328
|
+
return;
|
|
329
|
+
e.preventDefault();
|
|
330
|
+
this.#setZoom(this.#zoom * Math.exp(-e.deltaY * 0.001));
|
|
331
|
+
};
|
|
332
|
+
#setZoom(z) {
|
|
333
|
+
this.#zoom = Math.max(0.3, Math.min(4, z));
|
|
334
|
+
this.#restyle();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (typeof customElements !== 'undefined' && !customElements.get('textel-viewer'))
|
|
338
|
+
customElements.define('textel-viewer', TextelViewer);
|
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
|
-
export { parseModel, parseGLTF, parseOBJ, parseSTL, MODEL_EXTENSIONS } from './loaders/index.ts';
|
|
3
|
+
export { parseModel, parseModelAsync, parseModelSteps, parseGLTF, parseOBJ, parseSTL, MODEL_EXTENSIONS } from './loaders/index.ts';
|
|
4
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';
|
|
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
|
-
export { parseModel, parseGLTF, parseOBJ, parseSTL, MODEL_EXTENSIONS } from "./loaders/index.js";
|
|
3
|
+
export { parseModel, parseModelAsync, parseModelSteps, parseGLTF, parseOBJ, parseSTL, MODEL_EXTENSIONS } from "./loaders/index.js";
|
|
4
4
|
export { recordLoop, RecordingBuilder, encodeStream, decodeStream, toFramesFile, fromFramesFile, stringifyFrames, parseFrames, recordingText, recordingANSI, 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/loaders/index.d.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import type { Mesh } from '../mesh.ts';
|
|
2
2
|
import { parseGLTF, type GltfOptions } from './gltf.ts';
|
|
3
|
-
import { parseOBJ } from './obj.ts';
|
|
3
|
+
import { parseOBJ, parseOBJSteps } from './obj.ts';
|
|
4
4
|
import { parseSTL } from './stl.ts';
|
|
5
|
-
export { parseGLTF, parseOBJ, parseSTL };
|
|
5
|
+
export { parseGLTF, parseOBJ, parseOBJSteps, parseSTL };
|
|
6
6
|
export declare const MODEL_EXTENSIONS: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Parsing as a generator that yields now and then. Only text formats yield: binary glTF and STL
|
|
9
|
+
* are fast enough to parse in one go.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseModelSteps(bytes: Uint8Array, filename?: string, opts?: GltfOptions): Generator<void, Mesh>;
|
|
7
12
|
/** Picks a parser from the file name, falling back to sniffing the bytes. */
|
|
8
13
|
export declare function parseModel(bytes: Uint8Array, filename?: string, opts?: GltfOptions): Mesh;
|
|
14
|
+
/**
|
|
15
|
+
* parseModel without freezing a page: works in slices of about `sliceMs` and lets the event loop
|
|
16
|
+
* run between them, so input and animation keep going while a large file loads.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseModelAsync(bytes: Uint8Array, filename?: string, opts?: GltfOptions, sliceMs?: number): Promise<Mesh>;
|
package/dist/loaders/index.js
CHANGED
|
@@ -1,24 +1,58 @@
|
|
|
1
1
|
import { parseGLTF } from "./gltf.js";
|
|
2
|
-
import { parseOBJ } from "./obj.js";
|
|
2
|
+
import { parseOBJ, parseOBJSteps } from "./obj.js";
|
|
3
3
|
import { parseSTL } from "./stl.js";
|
|
4
|
-
export { parseGLTF, parseOBJ, parseSTL };
|
|
4
|
+
export { parseGLTF, parseOBJ, parseOBJSteps, parseSTL };
|
|
5
5
|
export const MODEL_EXTENSIONS = ['.glb', '.gltf', '.obj', '.stl'];
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/** from the file name, falling back to sniffing the bytes */
|
|
7
|
+
function kindOf(bytes, filename) {
|
|
8
8
|
const ext = filename.toLowerCase().match(/\.[a-z0-9]+$/)?.[0] ?? '';
|
|
9
9
|
if (ext === '.glb' || ext === '.gltf')
|
|
10
|
-
return
|
|
10
|
+
return 'gltf';
|
|
11
11
|
if (ext === '.obj')
|
|
12
|
-
return
|
|
12
|
+
return 'obj';
|
|
13
13
|
if (ext === '.stl')
|
|
14
|
-
return
|
|
15
|
-
// no or unknown extension
|
|
14
|
+
return 'stl';
|
|
16
15
|
if (bytes.length >= 4 && bytes[0] === 0x67 && bytes[1] === 0x6c && bytes[2] === 0x54 && bytes[3] === 0x46)
|
|
17
|
-
return
|
|
16
|
+
return 'gltf';
|
|
18
17
|
const head = new TextDecoder().decode(bytes.subarray(0, 512)).trimStart();
|
|
19
18
|
if (head.startsWith('{'))
|
|
20
|
-
return
|
|
19
|
+
return 'gltf';
|
|
21
20
|
if (/^(v|vn|vt|o|g|mtllib|#)\s/m.test(head))
|
|
22
|
-
return
|
|
23
|
-
return
|
|
21
|
+
return 'obj';
|
|
22
|
+
return 'stl';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parsing as a generator that yields now and then. Only text formats yield: binary glTF and STL
|
|
26
|
+
* are fast enough to parse in one go.
|
|
27
|
+
*/
|
|
28
|
+
export function* parseModelSteps(bytes, filename = '', opts = {}) {
|
|
29
|
+
const kind = kindOf(bytes, filename);
|
|
30
|
+
if (kind === 'obj')
|
|
31
|
+
return yield* parseOBJSteps(new TextDecoder().decode(bytes));
|
|
32
|
+
return kind === 'gltf' ? parseGLTF(bytes, opts) : parseSTL(bytes);
|
|
33
|
+
}
|
|
34
|
+
/** Picks a parser from the file name, falling back to sniffing the bytes. */
|
|
35
|
+
export function parseModel(bytes, filename = '', opts = {}) {
|
|
36
|
+
const g = parseModelSteps(bytes, filename, opts);
|
|
37
|
+
let r = g.next();
|
|
38
|
+
while (!r.done)
|
|
39
|
+
r = g.next();
|
|
40
|
+
return r.value;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* parseModel without freezing a page: works in slices of about `sliceMs` and lets the event loop
|
|
44
|
+
* run between them, so input and animation keep going while a large file loads.
|
|
45
|
+
*/
|
|
46
|
+
export async function parseModelAsync(bytes, filename = '', opts = {}, sliceMs = 12) {
|
|
47
|
+
const g = parseModelSteps(bytes, filename, opts);
|
|
48
|
+
let t = performance.now();
|
|
49
|
+
for (;;) {
|
|
50
|
+
const r = g.next();
|
|
51
|
+
if (r.done)
|
|
52
|
+
return r.value;
|
|
53
|
+
if (performance.now() - t > sliceMs) {
|
|
54
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
55
|
+
t = performance.now();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
24
58
|
}
|
package/dist/loaders/obj.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
import { type Mesh } from '../mesh.ts';
|
|
2
2
|
/** Wavefront OBJ: v, vn, f (any polygon, fan-triangulated), usemtl. Texture coordinates are ignored. */
|
|
3
3
|
export declare function parseOBJ(text: string): Mesh;
|
|
4
|
+
/**
|
|
5
|
+
* The same parser as a generator that yields every `slice` lines, so a page can parse a large file
|
|
6
|
+
* in small pieces and stay responsive (see parseModelAsync).
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseOBJSteps(text: string, slice?: number): Generator<void, Mesh>;
|
package/dist/loaders/obj.js
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import { MeshBuilder } from "../mesh.js";
|
|
2
2
|
/** Wavefront OBJ: v, vn, f (any polygon, fan-triangulated), usemtl. Texture coordinates are ignored. */
|
|
3
3
|
export function parseOBJ(text) {
|
|
4
|
+
const g = parseOBJSteps(text);
|
|
5
|
+
let r = g.next();
|
|
6
|
+
while (!r.done)
|
|
7
|
+
r = g.next();
|
|
8
|
+
return r.value;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The same parser as a generator that yields every `slice` lines, so a page can parse a large file
|
|
12
|
+
* in small pieces and stay responsive (see parseModelAsync).
|
|
13
|
+
*/
|
|
14
|
+
export function* parseOBJSteps(text, slice = 4000) {
|
|
4
15
|
const v = [], vn = [];
|
|
5
16
|
const b = new MeshBuilder();
|
|
6
17
|
const seen = new Map();
|
|
7
18
|
let mat = -1;
|
|
8
|
-
|
|
9
|
-
for (let ln = 0;
|
|
10
|
-
|
|
19
|
+
// walk the text line by line instead of splitting it, so a big file is not copied into one huge array
|
|
20
|
+
for (let start = 0, ln = 0; start < text.length; ln++) {
|
|
21
|
+
let end = text.indexOf('\n', start);
|
|
22
|
+
if (end < 0)
|
|
23
|
+
end = text.length;
|
|
24
|
+
const line = text.slice(start, end).trim();
|
|
25
|
+
start = end + 1;
|
|
26
|
+
if (ln % slice === slice - 1)
|
|
27
|
+
yield;
|
|
11
28
|
if (!line || line[0] === '#')
|
|
12
29
|
continue;
|
|
13
30
|
const parts = line.split(/\s+/), tag = parts[0];
|
package/dist/output.js
CHANGED
|
@@ -14,6 +14,18 @@ function gradient(stops, t) {
|
|
|
14
14
|
const x = Math.min(1, Math.max(0, t)) * (stops.length - 1), i = Math.min(stops.length - 2, Math.floor(x));
|
|
15
15
|
return lerp(stops[i], stops[i + 1], x - i);
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Where a cell sits on the gradient. By default each glyph level of the ramp gets one color, from
|
|
19
|
+
* the first stop for the darkest glyph to the last for the brightest; colorSteps sets another count,
|
|
20
|
+
* and 0 keeps it smooth.
|
|
21
|
+
*/
|
|
22
|
+
function gradientPosition(frame, ramp, lum) {
|
|
23
|
+
const set = frame.style.colorSteps;
|
|
24
|
+
const steps = set < 0 ? (ramp ?? frame.style.chars).length : set;
|
|
25
|
+
if (steps <= 1)
|
|
26
|
+
return steps === 1 ? 1 : lum;
|
|
27
|
+
return Math.min(steps - 1, Math.floor(lum * steps)) / (steps - 1);
|
|
28
|
+
}
|
|
17
29
|
/**
|
|
18
30
|
* Color of one cell, or null when the style has no color for it (use the default color).
|
|
19
31
|
* `faint` asks for a dimmed default color, for dim materials in colorless styles.
|
|
@@ -23,7 +35,7 @@ export function cellColor(frame, i) {
|
|
|
23
35
|
if (m < 0)
|
|
24
36
|
return { rgb: null, faint: false };
|
|
25
37
|
const ms = frame.materials[m], cs = frame.style.color;
|
|
26
|
-
let rgb = ms.color ?? (cs.mode === 'gradient' ? gradient(cs.stops, frame.lum[i]) : cs.mode === 'mono' ? cs.fg : null);
|
|
38
|
+
let rgb = ms.color ?? (cs.mode === 'gradient' ? gradient(cs.stops, gradientPosition(frame, ms.chars, frame.lum[i])) : cs.mode === 'mono' ? cs.fg : null);
|
|
27
39
|
if (!rgb)
|
|
28
40
|
return { rgb: null, faint: ms.opacity < 1 };
|
|
29
41
|
if (ms.opacity < 1)
|
package/dist/render.js
CHANGED
|
@@ -78,8 +78,12 @@ export function createRenderer(mesh, input = {}) {
|
|
|
78
78
|
}
|
|
79
79
|
/** focal length in samples: the model fits the grid at any yaw, at the resting pitch */
|
|
80
80
|
function focal() {
|
|
81
|
-
const D = style.distance
|
|
82
|
-
|
|
81
|
+
const D = style.distance;
|
|
82
|
+
// vertical half extent ry·cos p + rxz·sin p, at its largest over the pitches that must fit;
|
|
83
|
+
// it peaks at p = atan(rxz / ry), so check that point when it is inside the range
|
|
84
|
+
const lo = Math.abs(style.pitch), hi = Math.max(lo, style.fitPitch), peak = Math.atan2(rxz, ry);
|
|
85
|
+
const extent = (p) => ry * Math.cos(p) + rxz * Math.sin(p);
|
|
86
|
+
const hx = Math.max(rxz, 1e-3), hy = Math.max(peak > lo && peak < hi ? Math.hypot(ry, rxz) : Math.max(extent(lo), extent(hi)), 1e-3);
|
|
83
87
|
// largest x/z a circle of radius r at distance D reaches, with perspective
|
|
84
88
|
const tx = hx / Math.sqrt(D * D - Math.min(hx * hx, D * D * 0.99));
|
|
85
89
|
const ty = hy / Math.sqrt(D * D - 1);
|
package/dist/style.d.ts
CHANGED
|
@@ -53,6 +53,11 @@ export interface StyleInput {
|
|
|
53
53
|
chars?: string;
|
|
54
54
|
/** resting tilt toward the viewer, radians. Positive shows the top. */
|
|
55
55
|
pitch?: number;
|
|
56
|
+
/**
|
|
57
|
+
* the model is fitted so it stays inside the grid for any pitch up to this (radians, either way);
|
|
58
|
+
* default: just the resting pitch. Viewers that let people tilt the model set it higher.
|
|
59
|
+
*/
|
|
60
|
+
fitPitch?: number;
|
|
56
61
|
/** auto-rotation speed around the vertical axis, radians per second (players use it, the renderer does not) */
|
|
57
62
|
spin?: number;
|
|
58
63
|
/** camera distance in model radii; smaller is more perspective */
|
|
@@ -76,6 +81,11 @@ export interface StyleInput {
|
|
|
76
81
|
/** depth jump (relative) that counts as an edge in `depth` mode */
|
|
77
82
|
edgeThreshold?: number;
|
|
78
83
|
color?: ColorInput;
|
|
84
|
+
/**
|
|
85
|
+
* how many distinct colors a gradient uses. Default: one per glyph of the ramp, so a color
|
|
86
|
+
* changes only where the glyph does (far fewer color changes to draw). 0 = smooth.
|
|
87
|
+
*/
|
|
88
|
+
colorSteps?: number;
|
|
79
89
|
/** per material name */
|
|
80
90
|
materials?: Record<string, MaterialInput>;
|
|
81
91
|
}
|
|
@@ -85,6 +95,7 @@ export interface Style {
|
|
|
85
95
|
aspect: number;
|
|
86
96
|
chars: string[];
|
|
87
97
|
pitch: number;
|
|
98
|
+
fitPitch: number;
|
|
88
99
|
spin: number;
|
|
89
100
|
distance: number;
|
|
90
101
|
zoom: number;
|
|
@@ -97,6 +108,8 @@ export interface Style {
|
|
|
97
108
|
edgeChars: string[];
|
|
98
109
|
edgeThreshold: number;
|
|
99
110
|
color: ResolvedColor;
|
|
111
|
+
/** 0 = smooth; otherwise the number of gradient colors (default: the ramp length) */
|
|
112
|
+
colorSteps: number;
|
|
100
113
|
materials: Record<string, MaterialInput>;
|
|
101
114
|
}
|
|
102
115
|
export declare const DEFAULTS: {
|
package/dist/style.js
CHANGED
|
@@ -71,6 +71,7 @@ export function resolveStyle(input = {}) {
|
|
|
71
71
|
aspect: num(input.aspect, DEFAULTS.aspect, 0.25, 8),
|
|
72
72
|
chars: resolveRamp(input.chars, input.charset),
|
|
73
73
|
pitch: num(input.pitch, DEFAULTS.pitch),
|
|
74
|
+
fitPitch: Math.abs(num(input.fitPitch, 0, 0, Math.PI / 2)),
|
|
74
75
|
spin: num(input.spin, DEFAULTS.spin),
|
|
75
76
|
distance: num(input.distance, DEFAULTS.distance, 1.5, 1000),
|
|
76
77
|
zoom: num(input.zoom, DEFAULTS.zoom, 0.01, 100),
|
|
@@ -83,6 +84,7 @@ export function resolveStyle(input = {}) {
|
|
|
83
84
|
edgeChars,
|
|
84
85
|
edgeThreshold: num(input.edgeThreshold, DEFAULTS.edgeThreshold, 0),
|
|
85
86
|
color: resolveColor(input.color),
|
|
87
|
+
colorSteps: Math.round(num(input.colorSteps, -1, -1, 4096)),
|
|
86
88
|
materials: input.materials ?? {},
|
|
87
89
|
};
|
|
88
90
|
}
|
package/dist/textel-player.js
CHANGED
|
@@ -4,4 +4,4 @@ var L="rle-delta-1";var y=class{pos=0;buf;constructor(t){this.buf=t}byte(){if(th
|
|
|
4
4
|
:host(:not([nodrag])){cursor:grab;touch-action:pan-y}
|
|
5
5
|
:host(:not([nodrag]):active){cursor:grabbing}
|
|
6
6
|
pre{position:absolute;left:50%;top:50%;margin:0;transform-origin:50% 50%;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}
|
|
7
|
-
.d{opacity:var(--textel-dim-opacity,.6)}`,H=typeof HTMLElement>"u"?class{}:HTMLElement,x=class extends H{static observedAttributes=["src","fps","paused","color","label"];#t=null;#p=[];#s;#r=0;#h=-1;#o=!0;#d=!0;#n=0;#a=0;#i=null;#g=null;#u;#c;#w=typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches;constructor(){super();let t=this.attachShadow({mode:"open"}),e=document.createElement("style");e.textContent=j,this.#s=document.createElement("pre"),this.#s.setAttribute("part","screen"),t.append(e,this.#s),this.addEventListener("pointerdown",this.#v),this.addEventListener("pointermove",this.#E),this.addEventListener("pointerup",this.#y),this.addEventListener("pointercancel",this.#y)}connectedCallback(){this.setAttribute("role","img"),this.#f(),this.#o=!this.hasAttribute("paused")&&!this.#w,typeof IntersectionObserver=="function"&&(this.#u=new IntersectionObserver(([t])=>{this.#d=t.isIntersecting,this.#e()}),this.#u.observe(this)),typeof ResizeObserver=="function"&&(this.#c=new ResizeObserver(()=>this.#m()),this.#c.observe(this)),
|
|
7
|
+
.d{opacity:var(--textel-dim-opacity,.6)}`,H=typeof HTMLElement>"u"?class{}:HTMLElement,x=class extends H{static observedAttributes=["src","fps","paused","color","label"];#t=null;#p=[];#s;#r=0;#h=-1;#o=!0;#d=!0;#n=0;#a=0;#i=null;#g=null;#u;#c;#w=typeof matchMedia=="function"&&matchMedia("(prefers-reduced-motion: reduce)").matches;constructor(){super();let t=this.attachShadow({mode:"open"}),e=document.createElement("style");e.textContent=j,this.#s=document.createElement("pre"),this.#s.setAttribute("part","screen"),t.append(e,this.#s),this.addEventListener("pointerdown",this.#v),this.addEventListener("pointermove",this.#E),this.addEventListener("pointerup",this.#y),this.addEventListener("pointercancel",this.#y)}connectedCallback(){this.setAttribute("role","img"),this.#f(),this.#o=!this.hasAttribute("paused")&&!this.#w,typeof IntersectionObserver=="function"&&(this.#u=new IntersectionObserver(([t])=>{this.#d=t.isIntersecting,this.#e()}),this.#u.observe(this)),typeof ResizeObserver=="function"&&(this.#c=new ResizeObserver(()=>this.#m()),this.#c.observe(this)),this.ownerDocument.addEventListener("visibilitychange",this.#e),this.ownerDocument.fonts?.ready.then(()=>this.#m()),this.#e()}disconnectedCallback(){this.#u?.disconnect(),this.#c?.disconnect(),this.ownerDocument.removeEventListener("visibilitychange",this.#e),cancelAnimationFrame(this.#n),this.#n=0}attributeChangedCallback(t,e,r){e!==r&&(t==="src"?r&&this.#x(r):t==="paused"?r===null?this.play():this.pause():t==="color"?this.#s.style.color=r??"":t==="label"&&this.#f())}get recording(){return this.#t}set recording(t){this.#t=t&&"data"in t?w(t):t,this.#p=[],this.#r=0,this.#h=-1,this.#f(),this.#l(),this.#m(),this.#e()}get frame(){return Math.floor(this.#r)}set frame(t){let e=this.#t?.frames.length??1;this.#r=(Math.floor(t)%e+e)%e,this.#l()}get paused(){return!this.#o}play(){this.#o=!0,this.#e()}pause(){this.#o=!1,this.#e()}async#x(t){this.#g?.abort();let e=this.#g=new AbortController;try{let r=await fetch(t,{signal:e.signal});if(!r.ok)throw new Error(`${r.status} ${r.statusText} for ${t}`);let h=await r.json();if(e.signal.aborted)return;this.recording=w(h),this.dispatchEvent(new CustomEvent("load",{detail:{frames:this.#t.frames.length}}))}catch(r){if(e.signal.aborted)return;this.dispatchEvent(new CustomEvent("error",{detail:{error:r}}))}}#f(){this.isConnected&&this.setAttribute("aria-label",F(this.#t,this.getAttribute("label")))}#A(){let t=Number(this.getAttribute("fps"));return t>0?t:this.#t?.fps??30}#l(){let t=this.#t;if(!t)return;let e=Math.floor(this.#r)%t.frames.length;if(e===this.#h)return;this.#h=e;let r=this.#p[e]??=R(t,e);r.html?this.#s.innerHTML=r.content:this.#s.textContent=r.content}#m(){let t=this.#t;if(!t)return;let e=this.#s,r=e.offsetWidth/t.width,h=e.offsetHeight/t.height,s=S(t.width,t.height,t.aspect,r,h,this.clientWidth,this.clientHeight);this.style.setProperty("--textel-ratio",String(s.ratio)),e.style.transform=`translate(-50%,-50%) scale(${s.scale},${s.scale*s.stretchY})`}#e=()=>{let t=this.#o&&this.#d&&!this.ownerDocument.hidden&&!!this.#t&&!this.#i&&this.isConnected;t&&!this.#n?(this.#a=-1,this.#n=requestAnimationFrame(this.#b)):!t&&this.#n&&(cancelAnimationFrame(this.#n),this.#n=0)};#b=t=>{let e=this.#t;this.#a>=0&&(this.#r=M(this.#r,(t-this.#a)/1e3,this.#A(),e.frames.length)),this.#a=t,this.#l(),this.#n=requestAnimationFrame(this.#b)};#v=t=>{this.hasAttribute("nodrag")||!this.#t||t.button!==0||(this.#i={x:t.clientX,pos:this.#r},this.setPointerCapture(t.pointerId),this.#e())};#E=t=>{!this.#i||!this.#t||(this.#r=k(this.#i.pos,t.clientX-this.#i.x,this.clientWidth,this.#t.frames.length),this.#l())};#y=()=>{this.#i&&(this.#i=null,this.#e())}};typeof customElements<"u"&&!customElements.get("textel-player")&&customElements.define("textel-player",x);export{x as TextelPlayer};
|
|
@@ -0,0 +1,10 @@
|
|
|
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==="<"?"<":e===">"?">":e==="&"?"&":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}
|
|
5
|
+
:host(:not([nodrag])){cursor:grab;touch-action:pan-y}
|
|
6
|
+
:host(:not([nodrag]):active){cursor:grabbing}
|
|
7
|
+
:host(:focus-visible){box-shadow:0 0 0 2px currentColor}
|
|
8
|
+
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};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feelgxxd/textel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Turn 3D models into animated ASCII art for the terminal and the web.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ascii",
|
|
@@ -51,11 +51,17 @@
|
|
|
51
51
|
"./player": {
|
|
52
52
|
"types": "./dist/browser/player.d.ts",
|
|
53
53
|
"default": "./dist/textel-player.js"
|
|
54
|
+
},
|
|
55
|
+
"./viewer": {
|
|
56
|
+
"types": "./dist/browser/viewer.d.ts",
|
|
57
|
+
"default": "./dist/textel-viewer.js"
|
|
54
58
|
}
|
|
55
59
|
},
|
|
56
60
|
"sideEffects": [
|
|
57
61
|
"./dist/browser/player.js",
|
|
58
|
-
"./dist/textel-player.js"
|
|
62
|
+
"./dist/textel-player.js",
|
|
63
|
+
"./dist/browser/viewer.js",
|
|
64
|
+
"./dist/textel-viewer.js"
|
|
59
65
|
],
|
|
60
66
|
"files": [
|
|
61
67
|
"dist"
|