@teletext/react 0.1.1

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 ADDED
@@ -0,0 +1,175 @@
1
+ # @teletext/react
2
+
3
+ Embed ASCII art animations in any React app. One component, zero config.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @teletext/react
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ Export a `.teletext` file from [teletext.com](https://teletext.com), drop it in your `public/` directory, and point at it:
14
+
15
+ ```tsx
16
+ import { TeletextEmbed } from '@teletext/react'
17
+
18
+ <TeletextEmbed data="/animation.teletext" autoPlay loop />
19
+ ```
20
+
21
+ Or pass raw bytes:
22
+
23
+ ```tsx
24
+ const bytes = new Uint8Array(await fetch('/animation.teletext').then(r => r.arrayBuffer()))
25
+ <TeletextEmbed data={bytes} autoPlay loop />
26
+ ```
27
+
28
+ ## Examples
29
+
30
+ **Full-screen background**
31
+
32
+ ```tsx
33
+ <TeletextEmbed
34
+ data="/bg.teletext"
35
+ autoPlay
36
+ loop
37
+ backgroundColor="#000"
38
+ style={{ position: 'fixed', inset: 0, opacity: 0.3, zIndex: -1 }}
39
+ />
40
+ ```
41
+
42
+ **Hero section**
43
+
44
+ ```tsx
45
+ <TeletextEmbed
46
+ data="/animation.teletext"
47
+ autoPlay
48
+ loop
49
+ fontFamily="'Fira Code', monospace"
50
+ className="w-full h-[400px]"
51
+ />
52
+ ```
53
+
54
+ **SSR-safe rendering**
55
+
56
+ The `dom` renderer outputs `<pre>` and `<span>` elements — no canvas, works with SSR, accessible to screen readers.
57
+
58
+ ```tsx
59
+ <TeletextEmbed data="/animation.teletext" renderer="dom" autoPlay loop />
60
+ ```
61
+
62
+ **Pause / resume**
63
+
64
+ ```tsx
65
+ const [paused, setPaused] = useState(false)
66
+
67
+ <TeletextEmbed data="/animation.teletext" autoPlay loop paused={paused} />
68
+ <button onClick={() => setPaused(p => !p)}>
69
+ {paused ? 'Play' : 'Pause'}
70
+ </button>
71
+ ```
72
+
73
+ **Single frame (low-level)**
74
+
75
+ ```tsx
76
+ import { TeletextCanvas, TeletextDom, loadTeletext } from '@teletext/react'
77
+
78
+ const data = await loadTeletext('/animation.teletext')
79
+ <TeletextCanvas frame={data.frames[0]} cellSize={12} />
80
+ <TeletextDom frame={data.frames[0]} />
81
+ ```
82
+
83
+ ## API
84
+
85
+ ### `<TeletextEmbed>`
86
+
87
+ | Prop | Type | Default | |
88
+ |------|------|---------|---|
89
+ | `data` | `Uint8Array \| string` | *required* | Binary bytes or URL to `.teletext` file |
90
+ | `renderer` | `"canvas" \| "dom"` | `"canvas"` | Canvas = fast. DOM = SSR + accessible. |
91
+ | `autoPlay` | `boolean` | `false` | Play on mount |
92
+ | `loop` | `boolean` | `false` | Loop the animation |
93
+ | `paused` | `boolean` | `false` | External pause control |
94
+ | `fps` | `number` | from export | Override frame rate |
95
+ | `fontFamily` | `string` | `"monospace"` | Any monospace font |
96
+ | `backgroundColor` | `string` | `"#111"` | Background color |
97
+ | `className` | `string` | — | Wrapper CSS class |
98
+ | `style` | `CSSProperties` | — | Wrapper inline styles |
99
+ | `aria-label` | `string` | `"ASCII art animation"` | Accessibility label |
100
+
101
+ The canvas scales to fill its container. Animations automatically pause when off-screen via `IntersectionObserver`.
102
+
103
+ ### `<TeletextCanvas>`
104
+
105
+ Renders a single frame to `<canvas>`. Scales to fill its container.
106
+
107
+ | Prop | Type | Default | |
108
+ |------|------|---------|---|
109
+ | `frame` | `TeletextFrame` | *required* | Single frame to render |
110
+ | `cellSize` | `number` | `10` | Character size in px |
111
+ | `fontFamily` | `string` | `"monospace"` | Font |
112
+ | `backgroundColor` | `string` | `"#111"` | Background |
113
+
114
+ Ref: `getCanvas()` returns the `<canvas>` element.
115
+
116
+ ### `<TeletextDom>`
117
+
118
+ Renders a single frame as `<pre>` + colored `<span>` elements.
119
+
120
+ | Prop | Type | Default | |
121
+ |------|------|---------|---|
122
+ | `frame` | `TeletextFrame` | *required* | Single frame to render |
123
+ | `fontFamily` | `string` | `"monospace"` | Font |
124
+ | `backgroundColor` | `string` | `"#111"` | Background |
125
+
126
+ ### Codec
127
+
128
+ Encode and decode `.teletext` binary files directly:
129
+
130
+ ```ts
131
+ import { encode, decode, loadTeletext } from '@teletext/react'
132
+
133
+ // Decode from URL
134
+ const data = await loadTeletext('/animation.teletext')
135
+
136
+ // Decode from bytes
137
+ const data = await decode(bytes)
138
+
139
+ // Encode TeletextData to compressed binary
140
+ const bytes = await encode(data)
141
+ ```
142
+
143
+ ### Utilities
144
+
145
+ For custom renderers or playback logic:
146
+
147
+ ```ts
148
+ import { renderCanvasFrame, renderDomFrame, getFrameIndex } from '@teletext/react'
149
+
150
+ // Draw to any canvas context
151
+ renderCanvasFrame(ctx, frame, { cellSize: 10 })
152
+
153
+ // Get React elements
154
+ const elements = renderDomFrame(frame)
155
+
156
+ // Frame timing math
157
+ const index = getFrameIndex(elapsedMs, frameCount, fps, loop)
158
+ ```
159
+
160
+ ## `.teletext` binary format
161
+
162
+ Compressed binary — each cell is 1 byte (space) or 4 bytes (char index + RGB), gzipped with browser-native `CompressionStream`. Typical compression results:
163
+
164
+ | Content | JSON | `.teletext` | Reduction |
165
+ |---------|------|-------------|-----------|
166
+ | 51x91, 85 frames | 13 MB | 201 KB | 98.5% |
167
+ | 90x160, 85 frames | 43 MB | 1.6 MB | 96.3% |
168
+
169
+ ## Requirements
170
+
171
+ React 18+. No other dependencies.
172
+
173
+ ## License
174
+
175
+ MIT
@@ -0,0 +1,45 @@
1
+ interface TeletextCell {
2
+ char: string;
3
+ r: number;
4
+ g: number;
5
+ b: number;
6
+ }
7
+ type TeletextFrame = TeletextCell[][];
8
+ interface TeletextData {
9
+ version: number;
10
+ settings: {
11
+ palette: string;
12
+ density: string;
13
+ colorMode: string;
14
+ invert: boolean;
15
+ cellSize: number;
16
+ };
17
+ fps: number;
18
+ frames: TeletextFrame[];
19
+ }
20
+
21
+ /**
22
+ * Encode TeletextData into the compressed .teletext binary format.
23
+ *
24
+ * Binary layout (before gzip):
25
+ * HEADER
26
+ * [0..3] Magic "TLTX"
27
+ * [4] Version 0x02
28
+ * [5..6] Rows (uint16 BE)
29
+ * [7..8] Cols (uint16 BE)
30
+ * [9] FPS (uint8)
31
+ * [10..11] Frame count (uint16 BE)
32
+ * [12] Char count N (uint8)
33
+ * [13..] Char table: N × [len:uint8, ...utf8]
34
+ * [..] Settings JSON length (uint16 BE) + UTF-8 bytes
35
+ * PAYLOAD (per frame, per row, per col)
36
+ * char_index: uint8
37
+ * if char_index != space_index: r g b (uint8 each)
38
+ */
39
+ declare function encode(data: TeletextData): Promise<Uint8Array>;
40
+ /**
41
+ * Decode a compressed .teletext binary into TeletextData.
42
+ */
43
+ declare function decode(bytes: Uint8Array): Promise<TeletextData>;
44
+
45
+ export { type TeletextFrame as T, type TeletextData as a, type TeletextCell as b, decode as d, encode as e };
@@ -0,0 +1,45 @@
1
+ interface TeletextCell {
2
+ char: string;
3
+ r: number;
4
+ g: number;
5
+ b: number;
6
+ }
7
+ type TeletextFrame = TeletextCell[][];
8
+ interface TeletextData {
9
+ version: number;
10
+ settings: {
11
+ palette: string;
12
+ density: string;
13
+ colorMode: string;
14
+ invert: boolean;
15
+ cellSize: number;
16
+ };
17
+ fps: number;
18
+ frames: TeletextFrame[];
19
+ }
20
+
21
+ /**
22
+ * Encode TeletextData into the compressed .teletext binary format.
23
+ *
24
+ * Binary layout (before gzip):
25
+ * HEADER
26
+ * [0..3] Magic "TLTX"
27
+ * [4] Version 0x02
28
+ * [5..6] Rows (uint16 BE)
29
+ * [7..8] Cols (uint16 BE)
30
+ * [9] FPS (uint8)
31
+ * [10..11] Frame count (uint16 BE)
32
+ * [12] Char count N (uint8)
33
+ * [13..] Char table: N × [len:uint8, ...utf8]
34
+ * [..] Settings JSON length (uint16 BE) + UTF-8 bytes
35
+ * PAYLOAD (per frame, per row, per col)
36
+ * char_index: uint8
37
+ * if char_index != space_index: r g b (uint8 each)
38
+ */
39
+ declare function encode(data: TeletextData): Promise<Uint8Array>;
40
+ /**
41
+ * Decode a compressed .teletext binary into TeletextData.
42
+ */
43
+ declare function decode(bytes: Uint8Array): Promise<TeletextData>;
44
+
45
+ export { type TeletextFrame as T, type TeletextData as a, type TeletextCell as b, decode as d, encode as e };
package/dist/codec.cjs ADDED
@@ -0,0 +1,141 @@
1
+ 'use strict';
2
+
3
+ // src/codec.ts
4
+ var MAGIC = new Uint8Array([84, 76, 84, 88]);
5
+ var FORMAT_VERSION = 2;
6
+ async function compress(bytes) {
7
+ const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
8
+ return new Uint8Array(await new Response(stream).arrayBuffer());
9
+ }
10
+ async function decompress(bytes) {
11
+ const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"));
12
+ return new Uint8Array(await new Response(stream).arrayBuffer());
13
+ }
14
+ async function encode(data) {
15
+ const { frames, fps, settings } = data;
16
+ if (frames.length === 0) throw new Error("No frames to encode");
17
+ const rows = frames[0].length;
18
+ const cols = rows > 0 ? frames[0][0].length : 0;
19
+ const charSet = /* @__PURE__ */ new Set();
20
+ charSet.add(" ");
21
+ for (const frame of frames) {
22
+ for (const row of frame) {
23
+ for (const cell of row) {
24
+ charSet.add(cell.char);
25
+ }
26
+ }
27
+ }
28
+ const chars = Array.from(charSet);
29
+ if (chars.length > 255) throw new Error("Too many unique characters (max 255)");
30
+ const charToIndex = /* @__PURE__ */ new Map();
31
+ for (let i = 0; i < chars.length; i++) {
32
+ charToIndex.set(chars[i], i);
33
+ }
34
+ const spaceIndex = charToIndex.get(" ");
35
+ const encoder = new TextEncoder();
36
+ const charEntries = chars.map((c) => encoder.encode(c));
37
+ const settingsBytes = encoder.encode(JSON.stringify(settings));
38
+ let headerSize = 13;
39
+ for (const entry of charEntries) {
40
+ headerSize += 1 + entry.length;
41
+ }
42
+ headerSize += 2 + settingsBytes.length;
43
+ const buf = new Uint8Array(headerSize + frames.length * rows * cols * 4);
44
+ const view = new DataView(buf.buffer);
45
+ let off = 0;
46
+ buf.set(MAGIC, off);
47
+ off += 4;
48
+ buf[off++] = FORMAT_VERSION;
49
+ view.setUint16(off, rows);
50
+ off += 2;
51
+ view.setUint16(off, cols);
52
+ off += 2;
53
+ buf[off++] = fps;
54
+ view.setUint16(off, frames.length);
55
+ off += 2;
56
+ buf[off++] = chars.length;
57
+ for (const entry of charEntries) {
58
+ buf[off++] = entry.length;
59
+ buf.set(entry, off);
60
+ off += entry.length;
61
+ }
62
+ view.setUint16(off, settingsBytes.length);
63
+ off += 2;
64
+ buf.set(settingsBytes, off);
65
+ off += settingsBytes.length;
66
+ for (const frame of frames) {
67
+ for (const row of frame) {
68
+ for (const cell of row) {
69
+ const idx = charToIndex.get(cell.char);
70
+ buf[off++] = idx;
71
+ if (idx !== spaceIndex) {
72
+ buf[off++] = cell.r;
73
+ buf[off++] = cell.g;
74
+ buf[off++] = cell.b;
75
+ }
76
+ }
77
+ }
78
+ }
79
+ return compress(buf.subarray(0, off));
80
+ }
81
+ async function decode(bytes) {
82
+ const buf = await decompress(bytes);
83
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
84
+ let off = 0;
85
+ if (buf[0] !== 84 || buf[1] !== 76 || buf[2] !== 84 || buf[3] !== 88) {
86
+ throw new Error("Invalid .teletext file: bad magic bytes");
87
+ }
88
+ off += 4;
89
+ const version = buf[off++];
90
+ if (version !== FORMAT_VERSION) {
91
+ throw new Error(`Unsupported .teletext version: ${version}`);
92
+ }
93
+ const rows = view.getUint16(off);
94
+ off += 2;
95
+ const cols = view.getUint16(off);
96
+ off += 2;
97
+ const fps = buf[off++];
98
+ const frameCount = view.getUint16(off);
99
+ off += 2;
100
+ const charCount = buf[off++];
101
+ const chars = [];
102
+ const td = new TextDecoder();
103
+ for (let i = 0; i < charCount; i++) {
104
+ const len = buf[off++];
105
+ chars.push(td.decode(buf.subarray(off, off + len)));
106
+ off += len;
107
+ }
108
+ const spaceIndex = chars.indexOf(" ");
109
+ const settingsLen = view.getUint16(off);
110
+ off += 2;
111
+ const settings = JSON.parse(td.decode(buf.subarray(off, off + settingsLen)));
112
+ off += settingsLen;
113
+ const frames = [];
114
+ for (let f = 0; f < frameCount; f++) {
115
+ const frame = [];
116
+ for (let r = 0; r < rows; r++) {
117
+ const row = [];
118
+ for (let c = 0; c < cols; c++) {
119
+ const ci = buf[off++];
120
+ if (ci === spaceIndex) {
121
+ row.push({ char: " ", r: 0, g: 0, b: 0 });
122
+ } else {
123
+ row.push({
124
+ char: chars[ci],
125
+ r: buf[off++],
126
+ g: buf[off++],
127
+ b: buf[off++]
128
+ });
129
+ }
130
+ }
131
+ frame.push(row);
132
+ }
133
+ frames.push(frame);
134
+ }
135
+ return { version: 1, settings, fps, frames };
136
+ }
137
+
138
+ exports.decode = decode;
139
+ exports.encode = encode;
140
+ //# sourceMappingURL=codec.cjs.map
141
+ //# sourceMappingURL=codec.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/codec.ts"],"names":[],"mappings":"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","file":"codec.cjs","sourcesContent":["import type { TeletextData, TeletextCell, TeletextFrame } from \"./types\";\n\nconst MAGIC = new Uint8Array([0x54, 0x4c, 0x54, 0x58]); // \"TLTX\"\nconst FORMAT_VERSION = 0x02;\n\nasync function compress(bytes: Uint8Array): Promise<Uint8Array> {\n const stream = new Blob([bytes as BlobPart])\n .stream()\n .pipeThrough(new CompressionStream(\"gzip\"));\n return new Uint8Array(await new Response(stream).arrayBuffer());\n}\n\nasync function decompress(bytes: Uint8Array): Promise<Uint8Array> {\n const stream = new Blob([bytes as BlobPart])\n .stream()\n .pipeThrough(new DecompressionStream(\"gzip\"));\n return new Uint8Array(await new Response(stream).arrayBuffer());\n}\n\n/**\n * Encode TeletextData into the compressed .teletext binary format.\n *\n * Binary layout (before gzip):\n * HEADER\n * [0..3] Magic \"TLTX\"\n * [4] Version 0x02\n * [5..6] Rows (uint16 BE)\n * [7..8] Cols (uint16 BE)\n * [9] FPS (uint8)\n * [10..11] Frame count (uint16 BE)\n * [12] Char count N (uint8)\n * [13..] Char table: N × [len:uint8, ...utf8]\n * [..] Settings JSON length (uint16 BE) + UTF-8 bytes\n * PAYLOAD (per frame, per row, per col)\n * char_index: uint8\n * if char_index != space_index: r g b (uint8 each)\n */\nexport async function encode(data: TeletextData): Promise<Uint8Array> {\n const { frames, fps, settings } = data;\n if (frames.length === 0) throw new Error(\"No frames to encode\");\n\n const rows = frames[0].length;\n const cols = rows > 0 ? frames[0][0].length : 0;\n\n // Build char table from all frames\n const charSet = new Set<string>();\n charSet.add(\" \");\n for (const frame of frames) {\n for (const row of frame) {\n for (const cell of row) {\n charSet.add(cell.char);\n }\n }\n }\n\n const chars = Array.from(charSet);\n if (chars.length > 255) throw new Error(\"Too many unique characters (max 255)\");\n\n const charToIndex = new Map<string, number>();\n for (let i = 0; i < chars.length; i++) {\n charToIndex.set(chars[i], i);\n }\n const spaceIndex = charToIndex.get(\" \")!;\n\n const encoder = new TextEncoder();\n const charEntries = chars.map((c) => encoder.encode(c));\n const settingsBytes = encoder.encode(JSON.stringify(settings));\n\n // Calculate header size\n let headerSize = 13; // magic(4)+ver(1)+rows(2)+cols(2)+fps(1)+frames(2)+charCount(1)\n for (const entry of charEntries) {\n headerSize += 1 + entry.length;\n }\n headerSize += 2 + settingsBytes.length;\n\n // Allocate max possible size (4 bytes per cell)\n const buf = new Uint8Array(headerSize + frames.length * rows * cols * 4);\n const view = new DataView(buf.buffer);\n let off = 0;\n\n // Header\n buf.set(MAGIC, off);\n off += 4;\n buf[off++] = FORMAT_VERSION;\n view.setUint16(off, rows);\n off += 2;\n view.setUint16(off, cols);\n off += 2;\n buf[off++] = fps;\n view.setUint16(off, frames.length);\n off += 2;\n buf[off++] = chars.length;\n\n // Char table\n for (const entry of charEntries) {\n buf[off++] = entry.length;\n buf.set(entry, off);\n off += entry.length;\n }\n\n // Settings JSON\n view.setUint16(off, settingsBytes.length);\n off += 2;\n buf.set(settingsBytes, off);\n off += settingsBytes.length;\n\n // Payload\n for (const frame of frames) {\n for (const row of frame) {\n for (const cell of row) {\n const idx = charToIndex.get(cell.char)!;\n buf[off++] = idx;\n if (idx !== spaceIndex) {\n buf[off++] = cell.r;\n buf[off++] = cell.g;\n buf[off++] = cell.b;\n }\n }\n }\n }\n\n return compress(buf.subarray(0, off));\n}\n\n/**\n * Decode a compressed .teletext binary into TeletextData.\n */\nexport async function decode(bytes: Uint8Array): Promise<TeletextData> {\n const buf = await decompress(bytes);\n const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);\n let off = 0;\n\n // Magic\n if (\n buf[0] !== 0x54 ||\n buf[1] !== 0x4c ||\n buf[2] !== 0x54 ||\n buf[3] !== 0x58\n ) {\n throw new Error(\"Invalid .teletext file: bad magic bytes\");\n }\n off += 4;\n\n const version = buf[off++];\n if (version !== FORMAT_VERSION) {\n throw new Error(`Unsupported .teletext version: ${version}`);\n }\n\n const rows = view.getUint16(off);\n off += 2;\n const cols = view.getUint16(off);\n off += 2;\n const fps = buf[off++];\n const frameCount = view.getUint16(off);\n off += 2;\n\n // Char table\n const charCount = buf[off++];\n const chars: string[] = [];\n const td = new TextDecoder();\n for (let i = 0; i < charCount; i++) {\n const len = buf[off++];\n chars.push(td.decode(buf.subarray(off, off + len)));\n off += len;\n }\n const spaceIndex = chars.indexOf(\" \");\n\n // Settings\n const settingsLen = view.getUint16(off);\n off += 2;\n const settings = JSON.parse(td.decode(buf.subarray(off, off + settingsLen)));\n off += settingsLen;\n\n // Payload\n const frames: TeletextFrame[] = [];\n for (let f = 0; f < frameCount; f++) {\n const frame: TeletextCell[][] = [];\n for (let r = 0; r < rows; r++) {\n const row: TeletextCell[] = [];\n for (let c = 0; c < cols; c++) {\n const ci = buf[off++];\n if (ci === spaceIndex) {\n row.push({ char: \" \", r: 0, g: 0, b: 0 });\n } else {\n row.push({\n char: chars[ci],\n r: buf[off++],\n g: buf[off++],\n b: buf[off++],\n });\n }\n }\n frame.push(row);\n }\n frames.push(frame);\n }\n\n return { version: 1, settings, fps, frames };\n}\n"]}
@@ -0,0 +1 @@
1
+ export { d as decode, e as encode } from './codec-CwTlBC1h.cjs';
@@ -0,0 +1 @@
1
+ export { d as decode, e as encode } from './codec-CwTlBC1h.js';
package/dist/codec.js ADDED
@@ -0,0 +1,138 @@
1
+ // src/codec.ts
2
+ var MAGIC = new Uint8Array([84, 76, 84, 88]);
3
+ var FORMAT_VERSION = 2;
4
+ async function compress(bytes) {
5
+ const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"));
6
+ return new Uint8Array(await new Response(stream).arrayBuffer());
7
+ }
8
+ async function decompress(bytes) {
9
+ const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"));
10
+ return new Uint8Array(await new Response(stream).arrayBuffer());
11
+ }
12
+ async function encode(data) {
13
+ const { frames, fps, settings } = data;
14
+ if (frames.length === 0) throw new Error("No frames to encode");
15
+ const rows = frames[0].length;
16
+ const cols = rows > 0 ? frames[0][0].length : 0;
17
+ const charSet = /* @__PURE__ */ new Set();
18
+ charSet.add(" ");
19
+ for (const frame of frames) {
20
+ for (const row of frame) {
21
+ for (const cell of row) {
22
+ charSet.add(cell.char);
23
+ }
24
+ }
25
+ }
26
+ const chars = Array.from(charSet);
27
+ if (chars.length > 255) throw new Error("Too many unique characters (max 255)");
28
+ const charToIndex = /* @__PURE__ */ new Map();
29
+ for (let i = 0; i < chars.length; i++) {
30
+ charToIndex.set(chars[i], i);
31
+ }
32
+ const spaceIndex = charToIndex.get(" ");
33
+ const encoder = new TextEncoder();
34
+ const charEntries = chars.map((c) => encoder.encode(c));
35
+ const settingsBytes = encoder.encode(JSON.stringify(settings));
36
+ let headerSize = 13;
37
+ for (const entry of charEntries) {
38
+ headerSize += 1 + entry.length;
39
+ }
40
+ headerSize += 2 + settingsBytes.length;
41
+ const buf = new Uint8Array(headerSize + frames.length * rows * cols * 4);
42
+ const view = new DataView(buf.buffer);
43
+ let off = 0;
44
+ buf.set(MAGIC, off);
45
+ off += 4;
46
+ buf[off++] = FORMAT_VERSION;
47
+ view.setUint16(off, rows);
48
+ off += 2;
49
+ view.setUint16(off, cols);
50
+ off += 2;
51
+ buf[off++] = fps;
52
+ view.setUint16(off, frames.length);
53
+ off += 2;
54
+ buf[off++] = chars.length;
55
+ for (const entry of charEntries) {
56
+ buf[off++] = entry.length;
57
+ buf.set(entry, off);
58
+ off += entry.length;
59
+ }
60
+ view.setUint16(off, settingsBytes.length);
61
+ off += 2;
62
+ buf.set(settingsBytes, off);
63
+ off += settingsBytes.length;
64
+ for (const frame of frames) {
65
+ for (const row of frame) {
66
+ for (const cell of row) {
67
+ const idx = charToIndex.get(cell.char);
68
+ buf[off++] = idx;
69
+ if (idx !== spaceIndex) {
70
+ buf[off++] = cell.r;
71
+ buf[off++] = cell.g;
72
+ buf[off++] = cell.b;
73
+ }
74
+ }
75
+ }
76
+ }
77
+ return compress(buf.subarray(0, off));
78
+ }
79
+ async function decode(bytes) {
80
+ const buf = await decompress(bytes);
81
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
82
+ let off = 0;
83
+ if (buf[0] !== 84 || buf[1] !== 76 || buf[2] !== 84 || buf[3] !== 88) {
84
+ throw new Error("Invalid .teletext file: bad magic bytes");
85
+ }
86
+ off += 4;
87
+ const version = buf[off++];
88
+ if (version !== FORMAT_VERSION) {
89
+ throw new Error(`Unsupported .teletext version: ${version}`);
90
+ }
91
+ const rows = view.getUint16(off);
92
+ off += 2;
93
+ const cols = view.getUint16(off);
94
+ off += 2;
95
+ const fps = buf[off++];
96
+ const frameCount = view.getUint16(off);
97
+ off += 2;
98
+ const charCount = buf[off++];
99
+ const chars = [];
100
+ const td = new TextDecoder();
101
+ for (let i = 0; i < charCount; i++) {
102
+ const len = buf[off++];
103
+ chars.push(td.decode(buf.subarray(off, off + len)));
104
+ off += len;
105
+ }
106
+ const spaceIndex = chars.indexOf(" ");
107
+ const settingsLen = view.getUint16(off);
108
+ off += 2;
109
+ const settings = JSON.parse(td.decode(buf.subarray(off, off + settingsLen)));
110
+ off += settingsLen;
111
+ const frames = [];
112
+ for (let f = 0; f < frameCount; f++) {
113
+ const frame = [];
114
+ for (let r = 0; r < rows; r++) {
115
+ const row = [];
116
+ for (let c = 0; c < cols; c++) {
117
+ const ci = buf[off++];
118
+ if (ci === spaceIndex) {
119
+ row.push({ char: " ", r: 0, g: 0, b: 0 });
120
+ } else {
121
+ row.push({
122
+ char: chars[ci],
123
+ r: buf[off++],
124
+ g: buf[off++],
125
+ b: buf[off++]
126
+ });
127
+ }
128
+ }
129
+ frame.push(row);
130
+ }
131
+ frames.push(frame);
132
+ }
133
+ return { version: 1, settings, fps, frames };
134
+ }
135
+
136
+ export { decode, encode };
137
+ //# sourceMappingURL=codec.js.map
138
+ //# sourceMappingURL=codec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/codec.ts"],"names":[],"mappings":"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","file":"codec.js","sourcesContent":["import type { TeletextData, TeletextCell, TeletextFrame } from \"./types\";\n\nconst MAGIC = new Uint8Array([0x54, 0x4c, 0x54, 0x58]); // \"TLTX\"\nconst FORMAT_VERSION = 0x02;\n\nasync function compress(bytes: Uint8Array): Promise<Uint8Array> {\n const stream = new Blob([bytes as BlobPart])\n .stream()\n .pipeThrough(new CompressionStream(\"gzip\"));\n return new Uint8Array(await new Response(stream).arrayBuffer());\n}\n\nasync function decompress(bytes: Uint8Array): Promise<Uint8Array> {\n const stream = new Blob([bytes as BlobPart])\n .stream()\n .pipeThrough(new DecompressionStream(\"gzip\"));\n return new Uint8Array(await new Response(stream).arrayBuffer());\n}\n\n/**\n * Encode TeletextData into the compressed .teletext binary format.\n *\n * Binary layout (before gzip):\n * HEADER\n * [0..3] Magic \"TLTX\"\n * [4] Version 0x02\n * [5..6] Rows (uint16 BE)\n * [7..8] Cols (uint16 BE)\n * [9] FPS (uint8)\n * [10..11] Frame count (uint16 BE)\n * [12] Char count N (uint8)\n * [13..] Char table: N × [len:uint8, ...utf8]\n * [..] Settings JSON length (uint16 BE) + UTF-8 bytes\n * PAYLOAD (per frame, per row, per col)\n * char_index: uint8\n * if char_index != space_index: r g b (uint8 each)\n */\nexport async function encode(data: TeletextData): Promise<Uint8Array> {\n const { frames, fps, settings } = data;\n if (frames.length === 0) throw new Error(\"No frames to encode\");\n\n const rows = frames[0].length;\n const cols = rows > 0 ? frames[0][0].length : 0;\n\n // Build char table from all frames\n const charSet = new Set<string>();\n charSet.add(\" \");\n for (const frame of frames) {\n for (const row of frame) {\n for (const cell of row) {\n charSet.add(cell.char);\n }\n }\n }\n\n const chars = Array.from(charSet);\n if (chars.length > 255) throw new Error(\"Too many unique characters (max 255)\");\n\n const charToIndex = new Map<string, number>();\n for (let i = 0; i < chars.length; i++) {\n charToIndex.set(chars[i], i);\n }\n const spaceIndex = charToIndex.get(\" \")!;\n\n const encoder = new TextEncoder();\n const charEntries = chars.map((c) => encoder.encode(c));\n const settingsBytes = encoder.encode(JSON.stringify(settings));\n\n // Calculate header size\n let headerSize = 13; // magic(4)+ver(1)+rows(2)+cols(2)+fps(1)+frames(2)+charCount(1)\n for (const entry of charEntries) {\n headerSize += 1 + entry.length;\n }\n headerSize += 2 + settingsBytes.length;\n\n // Allocate max possible size (4 bytes per cell)\n const buf = new Uint8Array(headerSize + frames.length * rows * cols * 4);\n const view = new DataView(buf.buffer);\n let off = 0;\n\n // Header\n buf.set(MAGIC, off);\n off += 4;\n buf[off++] = FORMAT_VERSION;\n view.setUint16(off, rows);\n off += 2;\n view.setUint16(off, cols);\n off += 2;\n buf[off++] = fps;\n view.setUint16(off, frames.length);\n off += 2;\n buf[off++] = chars.length;\n\n // Char table\n for (const entry of charEntries) {\n buf[off++] = entry.length;\n buf.set(entry, off);\n off += entry.length;\n }\n\n // Settings JSON\n view.setUint16(off, settingsBytes.length);\n off += 2;\n buf.set(settingsBytes, off);\n off += settingsBytes.length;\n\n // Payload\n for (const frame of frames) {\n for (const row of frame) {\n for (const cell of row) {\n const idx = charToIndex.get(cell.char)!;\n buf[off++] = idx;\n if (idx !== spaceIndex) {\n buf[off++] = cell.r;\n buf[off++] = cell.g;\n buf[off++] = cell.b;\n }\n }\n }\n }\n\n return compress(buf.subarray(0, off));\n}\n\n/**\n * Decode a compressed .teletext binary into TeletextData.\n */\nexport async function decode(bytes: Uint8Array): Promise<TeletextData> {\n const buf = await decompress(bytes);\n const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);\n let off = 0;\n\n // Magic\n if (\n buf[0] !== 0x54 ||\n buf[1] !== 0x4c ||\n buf[2] !== 0x54 ||\n buf[3] !== 0x58\n ) {\n throw new Error(\"Invalid .teletext file: bad magic bytes\");\n }\n off += 4;\n\n const version = buf[off++];\n if (version !== FORMAT_VERSION) {\n throw new Error(`Unsupported .teletext version: ${version}`);\n }\n\n const rows = view.getUint16(off);\n off += 2;\n const cols = view.getUint16(off);\n off += 2;\n const fps = buf[off++];\n const frameCount = view.getUint16(off);\n off += 2;\n\n // Char table\n const charCount = buf[off++];\n const chars: string[] = [];\n const td = new TextDecoder();\n for (let i = 0; i < charCount; i++) {\n const len = buf[off++];\n chars.push(td.decode(buf.subarray(off, off + len)));\n off += len;\n }\n const spaceIndex = chars.indexOf(\" \");\n\n // Settings\n const settingsLen = view.getUint16(off);\n off += 2;\n const settings = JSON.parse(td.decode(buf.subarray(off, off + settingsLen)));\n off += settingsLen;\n\n // Payload\n const frames: TeletextFrame[] = [];\n for (let f = 0; f < frameCount; f++) {\n const frame: TeletextCell[][] = [];\n for (let r = 0; r < rows; r++) {\n const row: TeletextCell[] = [];\n for (let c = 0; c < cols; c++) {\n const ci = buf[off++];\n if (ci === spaceIndex) {\n row.push({ char: \" \", r: 0, g: 0, b: 0 });\n } else {\n row.push({\n char: chars[ci],\n r: buf[off++],\n g: buf[off++],\n b: buf[off++],\n });\n }\n }\n frame.push(row);\n }\n frames.push(frame);\n }\n\n return { version: 1, settings, fps, frames };\n}\n"]}