@feelgxxd/textel 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +224 -2
- package/dist/ansi.d.ts +13 -0
- package/dist/ansi.js +46 -0
- package/dist/browser/player-core.d.ts +23 -0
- package/dist/browser/player-core.js +68 -0
- package/dist/browser/player.d.ts +25 -0
- package/dist/browser/player.js +198 -0
- 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/cli/commands/list.d.ts +3 -0
- package/dist/cli/commands/list.js +21 -0
- package/dist/cli/commands/play.d.ts +3 -0
- package/dist/cli/commands/play.js +55 -0
- package/dist/cli/commands/preview.d.ts +3 -0
- package/dist/cli/commands/preview.js +57 -0
- package/dist/cli/commands/render.d.ts +3 -0
- package/dist/cli/commands/render.js +94 -0
- package/dist/cli/live.d.ts +13 -0
- package/dist/cli/live.js +116 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +53 -0
- package/dist/cli/shared.d.ts +103 -0
- package/dist/cli/shared.js +178 -0
- package/dist/examples.d.ts +13 -0
- package/dist/examples.js +164 -0
- package/dist/format.d.ts +57 -0
- package/dist/format.js +262 -0
- package/dist/frames.d.ts +34 -0
- package/dist/frames.js +88 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/loaders/gltf.d.ts +7 -0
- package/dist/loaders/gltf.js +187 -0
- package/dist/loaders/index.d.ts +18 -0
- package/dist/loaders/index.js +58 -0
- package/dist/loaders/obj.d.ts +8 -0
- package/dist/loaders/obj.js +69 -0
- package/dist/loaders/stl.d.ts +3 -0
- package/dist/loaders/stl.js +29 -0
- package/dist/mesh.d.ts +35 -0
- package/dist/mesh.js +106 -0
- package/dist/output.d.ts +18 -0
- package/dist/output.js +88 -0
- package/dist/render.d.ts +36 -0
- package/dist/render.js +309 -0
- package/dist/sdf.d.ts +36 -0
- package/dist/sdf.js +151 -0
- package/dist/style.d.ts +138 -0
- package/dist/style.js +132 -0
- package/dist/terminal.d.ts +89 -0
- package/dist/terminal.js +197 -0
- package/dist/textel-player.js +7 -0
- package/dist/textel-viewer.js +10 -0
- package/package.json +90 -4
package/dist/style.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Paul Bourke's 70-level ramp, flipped so it runs dark to bright like the others
|
|
2
|
+
const DETAILED = [...'$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,"^`\'. '].reverse().join('');
|
|
3
|
+
/**
|
|
4
|
+
* Character ramps, darkest first. A ramp that starts with a space lets shadows go empty
|
|
5
|
+
* (crisp on flat faces); one that does not keeps the whole silhouette visible.
|
|
6
|
+
*/
|
|
7
|
+
export const CHARSETS = {
|
|
8
|
+
classic: '.,-~:;=!*#$@',
|
|
9
|
+
hard: ' .:-=+*#%@',
|
|
10
|
+
detailed: DETAILED,
|
|
11
|
+
blocks: ' ░▒▓█',
|
|
12
|
+
dots: ' ·•●',
|
|
13
|
+
minimal: ' .:-=',
|
|
14
|
+
};
|
|
15
|
+
export const DEFAULTS = {
|
|
16
|
+
width: 80,
|
|
17
|
+
height: 40,
|
|
18
|
+
aspect: 2,
|
|
19
|
+
charset: 'classic',
|
|
20
|
+
pitch: 0.3,
|
|
21
|
+
spin: 0.66,
|
|
22
|
+
distance: 5,
|
|
23
|
+
zoom: 1,
|
|
24
|
+
light: [-0.45, 0.6, 0.65],
|
|
25
|
+
ambient: 0,
|
|
26
|
+
shading: 'auto',
|
|
27
|
+
supersample: 2,
|
|
28
|
+
coverage: 0.5,
|
|
29
|
+
edgeChars: '-/|\\',
|
|
30
|
+
edgeThreshold: 0.03,
|
|
31
|
+
};
|
|
32
|
+
/** Later inputs win; materials are merged by name. */
|
|
33
|
+
export function mergeStyles(...inputs) {
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const s of inputs) {
|
|
36
|
+
if (!s)
|
|
37
|
+
continue;
|
|
38
|
+
const { materials, ...rest } = s;
|
|
39
|
+
for (const [k, v] of Object.entries(rest))
|
|
40
|
+
if (v !== undefined)
|
|
41
|
+
out[k] = v;
|
|
42
|
+
if (materials)
|
|
43
|
+
out.materials = { ...out.materials, ...materials };
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
export function resolveRamp(chars, charset) {
|
|
48
|
+
if (chars !== undefined && chars.length)
|
|
49
|
+
return [...chars];
|
|
50
|
+
const name = charset ?? DEFAULTS.charset;
|
|
51
|
+
const r = CHARSETS[name];
|
|
52
|
+
if (!r)
|
|
53
|
+
throw new Error(`unknown charset "${name}" (have: ${Object.keys(CHARSETS).join(', ')})`);
|
|
54
|
+
return [...r];
|
|
55
|
+
}
|
|
56
|
+
export function resolveStyle(input = {}) {
|
|
57
|
+
const num = (v, d, min = -Infinity, max = Infinity) => {
|
|
58
|
+
const n = v ?? d;
|
|
59
|
+
if (!Number.isFinite(n))
|
|
60
|
+
throw new Error(`not a number: ${v}`);
|
|
61
|
+
return Math.min(max, Math.max(min, n));
|
|
62
|
+
};
|
|
63
|
+
const l = input.light ?? DEFAULTS.light;
|
|
64
|
+
const ll = Math.hypot(l[0], l[1], l[2]) || 1;
|
|
65
|
+
const edgeChars = [...(input.edgeChars ?? DEFAULTS.edgeChars)];
|
|
66
|
+
if (edgeChars.length !== 4)
|
|
67
|
+
throw new Error('edgeChars needs exactly 4 glyphs: horizontal, rising, vertical, falling');
|
|
68
|
+
return {
|
|
69
|
+
width: Math.round(num(input.width, DEFAULTS.width, 1, 2000)),
|
|
70
|
+
height: Math.round(num(input.height, DEFAULTS.height, 1, 2000)),
|
|
71
|
+
aspect: num(input.aspect, DEFAULTS.aspect, 0.25, 8),
|
|
72
|
+
chars: resolveRamp(input.chars, input.charset),
|
|
73
|
+
pitch: num(input.pitch, DEFAULTS.pitch),
|
|
74
|
+
fitPitch: Math.abs(num(input.fitPitch, 0, 0, Math.PI / 2)),
|
|
75
|
+
spin: num(input.spin, DEFAULTS.spin),
|
|
76
|
+
distance: num(input.distance, DEFAULTS.distance, 1.5, 1000),
|
|
77
|
+
zoom: num(input.zoom, DEFAULTS.zoom, 0.01, 100),
|
|
78
|
+
light: [l[0] / ll, l[1] / ll, l[2] / ll],
|
|
79
|
+
ambient: num(input.ambient, DEFAULTS.ambient, 0, 1),
|
|
80
|
+
shading: input.shading ?? DEFAULTS.shading,
|
|
81
|
+
supersample: Math.round(num(input.supersample, DEFAULTS.supersample, 1, 4)),
|
|
82
|
+
coverage: num(input.coverage, DEFAULTS.coverage, 0, 1),
|
|
83
|
+
edges: input.edges === true ? 'outline' : input.edges || false,
|
|
84
|
+
edgeChars,
|
|
85
|
+
edgeThreshold: num(input.edgeThreshold, DEFAULTS.edgeThreshold, 0),
|
|
86
|
+
color: resolveColor(input.color),
|
|
87
|
+
colorSteps: Math.round(num(input.colorSteps, -1, -1, 4096)),
|
|
88
|
+
materials: input.materials ?? {},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export function parseHex(hex) {
|
|
92
|
+
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
|
|
93
|
+
if (!m)
|
|
94
|
+
throw new Error(`not a hex color: ${hex}`);
|
|
95
|
+
let h = m[1];
|
|
96
|
+
if (h.length === 3)
|
|
97
|
+
h = h.replace(/./g, (c) => c + c);
|
|
98
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
99
|
+
}
|
|
100
|
+
export function resolveColor(c) {
|
|
101
|
+
const white = [255, 255, 255];
|
|
102
|
+
if (c === undefined || c === 'none')
|
|
103
|
+
return { mode: 'none', fg: white, stops: [], bg: null };
|
|
104
|
+
if (typeof c === 'string')
|
|
105
|
+
return { mode: 'mono', fg: parseHex(c), stops: [], bg: null };
|
|
106
|
+
if (Array.isArray(c)) {
|
|
107
|
+
if (c.length === 1)
|
|
108
|
+
return { mode: 'mono', fg: parseHex(c[0]), stops: [], bg: null };
|
|
109
|
+
const stops = c.map(parseHex);
|
|
110
|
+
return { mode: 'gradient', fg: stops[stops.length - 1], stops, bg: null };
|
|
111
|
+
}
|
|
112
|
+
const stops = (c.stops ?? []).map(parseHex);
|
|
113
|
+
const mode = c.mode ?? (stops.length > 1 ? 'gradient' : c.fg ? 'mono' : 'none');
|
|
114
|
+
return { mode, fg: c.fg ? parseHex(c.fg) : stops[stops.length - 1] ?? white, stops, bg: c.bg ? parseHex(c.bg) : null };
|
|
115
|
+
}
|
|
116
|
+
export function resolveMaterial(name, input) {
|
|
117
|
+
const m = { name, chars: null, color: null, opacity: 1, carved: false };
|
|
118
|
+
if (input === undefined || input === 'solid')
|
|
119
|
+
return m;
|
|
120
|
+
if (input === 'carved')
|
|
121
|
+
return { ...m, carved: true };
|
|
122
|
+
if (input === 'dim')
|
|
123
|
+
return { ...m, opacity: 0.6 };
|
|
124
|
+
if (input.chars !== undefined || input.charset !== undefined)
|
|
125
|
+
m.chars = resolveRamp(input.chars, input.charset);
|
|
126
|
+
if (input.color)
|
|
127
|
+
m.color = parseHex(input.color);
|
|
128
|
+
if (input.opacity !== undefined)
|
|
129
|
+
m.opacity = Math.min(1, Math.max(0, input.opacity));
|
|
130
|
+
m.carved = !!input.carved;
|
|
131
|
+
return m;
|
|
132
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type ColorDepth } from './ansi.ts';
|
|
2
|
+
import { type Recording } from './frames.ts';
|
|
3
|
+
/** What the players need from an output stream; `process.stdout` is one. */
|
|
4
|
+
export interface TerminalOutput {
|
|
5
|
+
write(s: string): unknown;
|
|
6
|
+
on(event: 'resize', listener: () => void): unknown;
|
|
7
|
+
off(event: 'resize', listener: () => void): unknown;
|
|
8
|
+
isTTY?: boolean;
|
|
9
|
+
columns?: number;
|
|
10
|
+
rows?: number;
|
|
11
|
+
}
|
|
12
|
+
/** What the players need from an input stream; `process.stdin` is one. */
|
|
13
|
+
export interface TerminalInput {
|
|
14
|
+
on(event: 'data', listener: (key: string) => void): unknown;
|
|
15
|
+
off(event: 'data', listener: (key: string) => void): unknown;
|
|
16
|
+
resume(): unknown;
|
|
17
|
+
pause(): unknown;
|
|
18
|
+
setEncoding(encoding: 'utf8'): unknown;
|
|
19
|
+
isTTY?: boolean;
|
|
20
|
+
setRawMode?(on: boolean): unknown;
|
|
21
|
+
}
|
|
22
|
+
type Out = TerminalOutput;
|
|
23
|
+
type In = TerminalInput;
|
|
24
|
+
/** What a stream can show, honoring NO_COLOR, FORCE_COLOR and COLORTERM. */
|
|
25
|
+
export declare function detectColorDepth(stream?: Out, env?: NodeJS.ProcessEnv): ColorDepth;
|
|
26
|
+
export interface ScreenOptions {
|
|
27
|
+
output?: Out;
|
|
28
|
+
/** keys arrive here when given; the input is put in raw mode */
|
|
29
|
+
input?: In | null;
|
|
30
|
+
onKey?: (key: string) => void;
|
|
31
|
+
onResize?: () => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A full-screen session: alternate screen, hidden cursor, raw keys. `close()` puts everything back,
|
|
35
|
+
* and so does process exit, so a crash never leaves the terminal broken.
|
|
36
|
+
*/
|
|
37
|
+
export declare class Screen {
|
|
38
|
+
readonly output: Out;
|
|
39
|
+
private input;
|
|
40
|
+
private closed;
|
|
41
|
+
private keyHandler?;
|
|
42
|
+
private resizeHandler?;
|
|
43
|
+
private exitHandler;
|
|
44
|
+
constructor(opts?: ScreenOptions);
|
|
45
|
+
get columns(): number;
|
|
46
|
+
get rows(): number;
|
|
47
|
+
/** draws lines from the top left; `status` goes on the last row, dimmed */
|
|
48
|
+
draw(lines: string[], status?: string): void;
|
|
49
|
+
private restore;
|
|
50
|
+
close(): void;
|
|
51
|
+
}
|
|
52
|
+
export type PlayResult = 'end' | 'quit' | 'interrupt' | 'abort';
|
|
53
|
+
export interface PlayOptions {
|
|
54
|
+
output?: Out;
|
|
55
|
+
input?: In | null;
|
|
56
|
+
/** keys: space pause, ←/→ step a frame, +/- speed, q quit (default false) */
|
|
57
|
+
interactive?: boolean;
|
|
58
|
+
/** keep looping until stopped (default true) */
|
|
59
|
+
loop?: boolean;
|
|
60
|
+
/** stop after this long */
|
|
61
|
+
seconds?: number;
|
|
62
|
+
signal?: AbortSignal;
|
|
63
|
+
/** default: detected from the output */
|
|
64
|
+
depth?: ColorDepth;
|
|
65
|
+
/** playback rate; default the file's */
|
|
66
|
+
fps?: number;
|
|
67
|
+
/** a status line at the bottom (default: on when interactive) */
|
|
68
|
+
status?: boolean;
|
|
69
|
+
/** shown in the status line */
|
|
70
|
+
title?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Plays a recording full screen. Resolves when it ends, the user quits, Ctrl+C is pressed or the
|
|
74
|
+
* signal aborts; the terminal is restored in every case. On output that is not a terminal it prints
|
|
75
|
+
* the first frame and returns.
|
|
76
|
+
*/
|
|
77
|
+
export declare function play(rec: Recording, opts?: PlayOptions): Promise<PlayResult>;
|
|
78
|
+
/** Reads a `.textel.json` file. */
|
|
79
|
+
export declare function loadFrames(file: string): Recording;
|
|
80
|
+
/**
|
|
81
|
+
* A spinning logo for a few seconds, then back to the normal screen: for the start of another CLI.
|
|
82
|
+
* Any key skips it. Does nothing visible when output is not a terminal.
|
|
83
|
+
*/
|
|
84
|
+
export declare function playSplash(source: string | Recording, opts?: {
|
|
85
|
+
seconds?: number;
|
|
86
|
+
output?: Out;
|
|
87
|
+
input?: In | null;
|
|
88
|
+
}): Promise<PlayResult>;
|
|
89
|
+
export {};
|
package/dist/terminal.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Terminal playback for Node: a full-screen session with clean restore, and players for frames files.
|
|
2
|
+
// `@feelgxxd/textel/terminal` in the package.
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { RESET } from "./ansi.js";
|
|
5
|
+
import { parseFrames, recordingANSI, recordingText } from "./frames.js";
|
|
6
|
+
const CSI = '\x1b[';
|
|
7
|
+
/** What a stream can show, honoring NO_COLOR, FORCE_COLOR and COLORTERM. */
|
|
8
|
+
export function detectColorDepth(stream = process.stdout, env = process.env) {
|
|
9
|
+
if (env.NO_COLOR)
|
|
10
|
+
return 'none';
|
|
11
|
+
if (!stream.isTTY && !env.FORCE_COLOR)
|
|
12
|
+
return 'none';
|
|
13
|
+
if (env.COLORTERM === 'truecolor' || env.COLORTERM === '24bit' || env.WT_SESSION || env.TERM_PROGRAM === 'vscode' || env.TERM_PROGRAM === 'iTerm.app')
|
|
14
|
+
return 'truecolor';
|
|
15
|
+
return '256';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A full-screen session: alternate screen, hidden cursor, raw keys. `close()` puts everything back,
|
|
19
|
+
* and so does process exit, so a crash never leaves the terminal broken.
|
|
20
|
+
*/
|
|
21
|
+
export class Screen {
|
|
22
|
+
output;
|
|
23
|
+
input;
|
|
24
|
+
closed = false;
|
|
25
|
+
keyHandler;
|
|
26
|
+
resizeHandler;
|
|
27
|
+
exitHandler = () => this.restore();
|
|
28
|
+
constructor(opts = {}) {
|
|
29
|
+
this.output = opts.output ?? process.stdout;
|
|
30
|
+
this.input = opts.onKey ? opts.input ?? process.stdin : null;
|
|
31
|
+
this.output.write(`${CSI}?1049h${CSI}?25l${CSI}2J`);
|
|
32
|
+
process.on('exit', this.exitHandler);
|
|
33
|
+
if (opts.onResize) {
|
|
34
|
+
this.resizeHandler = () => { this.output.write(`${CSI}2J`); opts.onResize(); };
|
|
35
|
+
this.output.on('resize', this.resizeHandler);
|
|
36
|
+
}
|
|
37
|
+
if (this.input && opts.onKey) {
|
|
38
|
+
this.keyHandler = opts.onKey;
|
|
39
|
+
this.input.setRawMode?.(true);
|
|
40
|
+
this.input.setEncoding('utf8');
|
|
41
|
+
this.input.on('data', this.keyHandler);
|
|
42
|
+
this.input.resume();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
get columns() { return this.output.columns ?? 80; }
|
|
46
|
+
get rows() { return this.output.rows ?? 24; }
|
|
47
|
+
/** draws lines from the top left; `status` goes on the last row, dimmed */
|
|
48
|
+
draw(lines, status) {
|
|
49
|
+
let s = `${CSI}H${lines.join(`${CSI}K\n`)}${CSI}K`;
|
|
50
|
+
if (status !== undefined) {
|
|
51
|
+
const w = this.columns;
|
|
52
|
+
s += `${CSI}${this.rows};1H${CSI}2m${status.slice(0, w).padEnd(w)}${RESET}`;
|
|
53
|
+
}
|
|
54
|
+
this.output.write(s);
|
|
55
|
+
}
|
|
56
|
+
restore() {
|
|
57
|
+
if (this.closed)
|
|
58
|
+
return;
|
|
59
|
+
this.closed = true;
|
|
60
|
+
this.output.write(`${RESET}${CSI}?25h${CSI}?1049l`);
|
|
61
|
+
if (this.input) {
|
|
62
|
+
if (this.keyHandler)
|
|
63
|
+
this.input.off('data', this.keyHandler);
|
|
64
|
+
this.input.setRawMode?.(false);
|
|
65
|
+
this.input.pause();
|
|
66
|
+
}
|
|
67
|
+
if (this.resizeHandler)
|
|
68
|
+
this.output.off('resize', this.resizeHandler);
|
|
69
|
+
}
|
|
70
|
+
close() {
|
|
71
|
+
this.restore();
|
|
72
|
+
process.off('exit', this.exitHandler);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Plays a recording full screen. Resolves when it ends, the user quits, Ctrl+C is pressed or the
|
|
77
|
+
* signal aborts; the terminal is restored in every case. On output that is not a terminal it prints
|
|
78
|
+
* the first frame and returns.
|
|
79
|
+
*/
|
|
80
|
+
export function play(rec, opts = {}) {
|
|
81
|
+
const output = opts.output ?? process.stdout;
|
|
82
|
+
const depth = opts.depth ?? detectColorDepth(output);
|
|
83
|
+
if (!output.isTTY) {
|
|
84
|
+
output.write((depth === 'none' ? recordingText(rec, 0) : recordingANSI(rec, 0, depth).join('\n')) + '\n');
|
|
85
|
+
return Promise.resolve('end');
|
|
86
|
+
}
|
|
87
|
+
const n = rec.frames.length, baseFps = opts.fps ?? rec.fps, loop = opts.loop ?? true;
|
|
88
|
+
const interactive = !!opts.interactive, showStatus = opts.status ?? interactive;
|
|
89
|
+
return new Promise((resolvePlay) => {
|
|
90
|
+
let pos = 0, speed = 1, paused = false, last = performance.now(), elapsed = 0, done = false;
|
|
91
|
+
let timer;
|
|
92
|
+
const finish = (r) => {
|
|
93
|
+
if (done)
|
|
94
|
+
return;
|
|
95
|
+
done = true;
|
|
96
|
+
clearInterval(timer);
|
|
97
|
+
opts.signal?.removeEventListener('abort', onAbort);
|
|
98
|
+
process.off('SIGINT', onSigint);
|
|
99
|
+
screen.close();
|
|
100
|
+
resolvePlay(r);
|
|
101
|
+
};
|
|
102
|
+
const onAbort = () => finish('abort');
|
|
103
|
+
const onSigint = () => finish('interrupt');
|
|
104
|
+
const screen = new Screen({
|
|
105
|
+
output,
|
|
106
|
+
input: opts.input,
|
|
107
|
+
onResize: () => draw(),
|
|
108
|
+
onKey: interactive || opts.input ? (key) => {
|
|
109
|
+
// without controls, any key ends playback (a splash screen can be skipped)
|
|
110
|
+
if (!interactive)
|
|
111
|
+
return finish(key === '\x03' ? 'interrupt' : 'quit');
|
|
112
|
+
switch (key) {
|
|
113
|
+
case '\x03':
|
|
114
|
+
finish('interrupt');
|
|
115
|
+
break;
|
|
116
|
+
case 'q':
|
|
117
|
+
case 'Q':
|
|
118
|
+
case '\x1b':
|
|
119
|
+
finish('quit');
|
|
120
|
+
break;
|
|
121
|
+
case ' ':
|
|
122
|
+
paused = !paused;
|
|
123
|
+
break;
|
|
124
|
+
case '\x1b[C':
|
|
125
|
+
paused = true;
|
|
126
|
+
pos = Math.floor(pos) + 1;
|
|
127
|
+
draw();
|
|
128
|
+
break;
|
|
129
|
+
case '\x1b[D':
|
|
130
|
+
paused = true;
|
|
131
|
+
pos = Math.floor(pos) - 1 + n;
|
|
132
|
+
draw();
|
|
133
|
+
break;
|
|
134
|
+
case '+':
|
|
135
|
+
case '=':
|
|
136
|
+
speed = Math.min(8, speed * 1.25);
|
|
137
|
+
break;
|
|
138
|
+
case '-':
|
|
139
|
+
case '_':
|
|
140
|
+
speed = Math.max(0.125, speed / 1.25);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
} : undefined,
|
|
144
|
+
});
|
|
145
|
+
if (opts.signal?.aborted)
|
|
146
|
+
return finish('abort');
|
|
147
|
+
opts.signal?.addEventListener('abort', onAbort);
|
|
148
|
+
process.on('SIGINT', onSigint);
|
|
149
|
+
function draw() {
|
|
150
|
+
const i = Math.floor(pos) % n;
|
|
151
|
+
const lines = depth === 'none' ? recordingText(rec, i).split('\n') : recordingANSI(rec, i, depth);
|
|
152
|
+
// center the grid; crop when the terminal is smaller
|
|
153
|
+
const rows = screen.rows - (showStatus ? 1 : 0), cols = screen.columns;
|
|
154
|
+
const top = Math.max(0, Math.floor((rows - rec.height) / 2)), left = Math.max(0, Math.floor((cols - rec.width) / 2));
|
|
155
|
+
const pad = ' '.repeat(left);
|
|
156
|
+
const shown = lines.slice(0, Math.max(0, rows - top)).map((l) => pad + l);
|
|
157
|
+
const out = [...new Array(top).fill(''), ...shown];
|
|
158
|
+
let status;
|
|
159
|
+
if (showStatus) {
|
|
160
|
+
const small = rec.width > cols || rec.height > rows ? ' · terminal too small, cropped' : '';
|
|
161
|
+
status = ` ${opts.title ?? 'textel'} · frame ${i + 1}/${n} · ${(speed * baseFps).toFixed(0)}fps${paused ? ' · paused' : ''}${small} space pause ←→ step +/- speed q quit`;
|
|
162
|
+
}
|
|
163
|
+
screen.draw(out, status);
|
|
164
|
+
}
|
|
165
|
+
const tick = () => {
|
|
166
|
+
const now = performance.now(), dt = Math.min(now - last, 100) / 1000;
|
|
167
|
+
last = now;
|
|
168
|
+
elapsed += dt;
|
|
169
|
+
if (opts.seconds !== undefined && elapsed >= opts.seconds)
|
|
170
|
+
return finish('end');
|
|
171
|
+
if (!paused) {
|
|
172
|
+
pos += dt * baseFps * speed;
|
|
173
|
+
if (!loop && pos >= n)
|
|
174
|
+
return finish('end');
|
|
175
|
+
}
|
|
176
|
+
draw();
|
|
177
|
+
};
|
|
178
|
+
draw();
|
|
179
|
+
timer = setInterval(tick, 1000 / Math.max(1, Math.min(120, baseFps)));
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/** Reads a `.textel.json` file. */
|
|
183
|
+
export function loadFrames(file) {
|
|
184
|
+
return parseFrames(readFileSync(file, 'utf8'));
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* A spinning logo for a few seconds, then back to the normal screen: for the start of another CLI.
|
|
188
|
+
* Any key skips it. Does nothing visible when output is not a terminal.
|
|
189
|
+
*/
|
|
190
|
+
export async function playSplash(source, opts = {}) {
|
|
191
|
+
const output = opts.output ?? process.stdout;
|
|
192
|
+
if (!output.isTTY)
|
|
193
|
+
return 'end';
|
|
194
|
+
const rec = typeof source === 'string' ? loadFrames(source) : source;
|
|
195
|
+
const input = opts.input === undefined ? (process.stdin.isTTY ? process.stdin : null) : opts.input;
|
|
196
|
+
return play(rec, { output, input, seconds: opts.seconds ?? 2, interactive: false, status: false });
|
|
197
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
var L="rle-delta-1";var y=class{pos=0;buf;constructor(t){this.buf=t}byte(){if(this.pos>=this.buf.length)throw new Error("frames data ends early");return this.buf[this.pos++]}varint(){let t=0,e=0,r;do if(r=this.byte(),t+=(r&127)*2**e,e+=7,e>35)throw new Error("bad varint in frames data");while(r&128);return t}get done(){return this.pos>=this.buf.length}},U=0,I=1;function C(n,t,e,r,h){let s=r,u=r+h;if(u>t.length)throw new Error("frames data runs past the grid");for(;s<u;){let a=n.varint(),o=n.varint(),i=e?n.varint():0;if(a===0||s+a>u)throw new Error("bad run in frames data");t.fill(o,s,s+a),e&&e.fill(i,s,s+a),s+=a}}function T(n,t,e,r,h,s=65536,u=65536){let a=s<=256?Uint8Array:Uint16Array,o=u<=256?Uint8Array:Uint16Array,i=t*e,l=new y(n),c=[];for(let m=0;m<r;m++){let p=l.byte(),f=c[m-1],g=new a(i),d=h?new o(i):null;if(p===U)C(l,g,d,0,i);else if(p===I){if(!f)throw new Error("first frame cannot be a delta");g.set(f.glyph),d&&d.set(f.style);let O=l.varint(),A=0;for(let v=0;v<O;v++){let E=l.varint(),b=l.varint();if(b===0)throw new Error("empty span in frames data");C(l,g,d,A+E,b),A+=E+b}}else throw new Error(`unknown frame type ${p}`);c.push({glyph:g,style:d})}if(!l.done)throw new Error("extra bytes after the last frame");return c}function w(n){if(!n||typeof n!="object")throw new Error("not a textel frames file");if(n.textel!==1)throw new Error(`frames format ${n.textel} is not supported (this version reads 1)`);if(n.encoding!==L)throw new Error(`unknown frames encoding "${n.encoding}"`);let{width:t,height:e,frameCount:r}=n;if(!(t>0&&e>0&&r>0))throw new Error("bad frames header");if(!Array.isArray(n.glyphs)||!n.glyphs.length)throw new Error("bad frames header");let h=n.glyphs.length,s=n.styles?.length??1,u=T(N(n.data),t,e,r,!!n.styles,h,s);for(let a of u)for(let o=0;o<a.glyph.length;o++)if(a.glyph[o]>=h||a.style&&a.style[o]>=s)throw new Error("frames data points outside its palette");return{width:t,height:e,aspect:n.aspect,fps:n.fps,glyphs:n.glyphs,styles:n.styles??null,bg:n.bg??null,frames:u,meta:n.meta??{}}}function N(n){let t=atob(n),e=new Uint8Array(t.length);for(let r=0;r<t.length;r++)e[r]=t.charCodeAt(r);return e}var $=n=>n==="<"?"<":n===">"?">":n==="&"?"&":n;function R(n,t){let{width:e,height:r,glyphs:h,styles:s}=n,u=n.frames[t];if(!s||!u.style){let i="";for(let l=0;l<r;l++){for(let c=0;c<e;c++)i+=h[u.glyph[l*e+c]];l<r-1&&(i+=`
|
|
2
|
+
`)}return{html:!1,content:i}}let a=s.map(i=>i===""?"":i==="~"?'<span class="d">':`<span style="color:${i}">`),o="";for(let i=0;i<r;i++){let l=0;for(let c=0;c<e;c++){let m=i*e+c,p=h[u.glyph[m]];if(p!==" "){let f=u.style[m];f!==l&&(l&&(o+="</span>"),o+=a[f],l=f)}o+=$(p)}l&&(o+="</span>"),i<r-1&&(o+=`
|
|
3
|
+
`)}return{html:!0,content:o}}function F(n,t){if(t)return t;let e=n?.meta.title;return typeof e=="string"&&e?`${e}, ASCII animation`:"ASCII animation"}function M(n,t,e,r){return(n+Math.min(Math.max(t,0),.1)*e)%r}function k(n,t,e,r){return e>0?((n+t/e*r)%r+r)%r:n}function S(n,t,e,r,h,s,u){let a=r>0&&h>0?e*r/h:1,o=n*r,i=t*h*a;return o>0&&i>0?{scale:u>0?Math.min(s/o,u/i):s/o,stretchY:a,ratio:o/i}:{scale:1,stretchY:a,ratio:1}}var j=`:host{display:block;position:relative;overflow:hidden;aspect-ratio:var(--textel-ratio,auto);min-height:1px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
4
|
+
:host(:not([nodrag])){cursor:grab;touch-action:pan-y}
|
|
5
|
+
:host(:not([nodrag]):active){cursor:grabbing}
|
|
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)),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,92 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feelgxxd/textel",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Turn 3D models into animated ASCII art for the terminal and the web.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ascii",
|
|
7
|
+
"ascii-art",
|
|
8
|
+
"3d",
|
|
9
|
+
"terminal",
|
|
10
|
+
"renderer",
|
|
11
|
+
"gltf",
|
|
12
|
+
"obj",
|
|
13
|
+
"stl",
|
|
14
|
+
"sdf"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/feelgxxd/textel#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/feelgxxd/textel/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/feelgxxd/textel.git"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "Berk Beken",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"bin": {
|
|
28
|
+
"textel": "dist/cli/main.js"
|
|
29
|
+
},
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./sdf": {
|
|
36
|
+
"types": "./dist/sdf.d.ts",
|
|
37
|
+
"default": "./dist/sdf.js"
|
|
38
|
+
},
|
|
39
|
+
"./examples": {
|
|
40
|
+
"types": "./dist/examples.d.ts",
|
|
41
|
+
"default": "./dist/examples.js"
|
|
42
|
+
},
|
|
43
|
+
"./terminal": {
|
|
44
|
+
"types": "./dist/terminal.d.ts",
|
|
45
|
+
"default": "./dist/terminal.js"
|
|
46
|
+
},
|
|
47
|
+
"./frames": {
|
|
48
|
+
"types": "./dist/frames.d.ts",
|
|
49
|
+
"default": "./dist/frames.js"
|
|
50
|
+
},
|
|
51
|
+
"./player": {
|
|
52
|
+
"types": "./dist/browser/player.d.ts",
|
|
53
|
+
"default": "./dist/textel-player.js"
|
|
54
|
+
},
|
|
55
|
+
"./viewer": {
|
|
56
|
+
"types": "./dist/browser/viewer.d.ts",
|
|
57
|
+
"default": "./dist/textel-viewer.js"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"sideEffects": [
|
|
61
|
+
"./dist/browser/player.js",
|
|
62
|
+
"./dist/textel-player.js",
|
|
63
|
+
"./dist/browser/viewer.js",
|
|
64
|
+
"./dist/textel-viewer.js"
|
|
65
|
+
],
|
|
66
|
+
"files": [
|
|
67
|
+
"dist"
|
|
68
|
+
],
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=22"
|
|
71
|
+
},
|
|
72
|
+
"scripts": {
|
|
73
|
+
"dev": "node src/cli/main.ts",
|
|
74
|
+
"build": "tsc -p tsconfig.build.json && node scripts/build-browser.ts",
|
|
75
|
+
"typecheck": "tsc",
|
|
76
|
+
"test": "node --test \"test/*.test.ts\"",
|
|
77
|
+
"snapshots": "node test/update-snapshots.ts",
|
|
78
|
+
"size": "node scripts/build-browser.ts --check",
|
|
79
|
+
"demo": "node scripts/build-browser.ts && node scripts/demo.ts",
|
|
80
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@happy-dom/global-registrator": "^21.0.4",
|
|
84
|
+
"@types/node": "^24.0.0",
|
|
85
|
+
"esbuild": "^0.28.2",
|
|
86
|
+
"happy-dom": "^21.0.4",
|
|
87
|
+
"typescript": "^5.9.0"
|
|
88
|
+
},
|
|
89
|
+
"publishConfig": {
|
|
90
|
+
"access": "public"
|
|
91
|
+
}
|
|
92
|
+
}
|