@micahdb/tscon 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Micah Baker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,45 @@
1
- # Temporary Holding Version
1
+ # @micahdb/tscon
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ WebGL 2.0 text-mode graphics in TypeScript.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ bun add @micahdb/tscon
9
+ # or
10
+ npm install @micahdb/tscon
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { Canvas, Terminal, textGlyphs, type Rect } from "@micahdb/tscon";
17
+
18
+ const canvas = new Canvas(document.querySelector("canvas")!);
19
+ const terminal = new Terminal(canvas);
20
+
21
+ const region: Rect = { row: 0, col: 0, rows: canvas.rows, cols: canvas.cols };
22
+ terminal.clear();
23
+ terminal.blit(textGlyphs("Hello, tscon!"), region);
24
+ terminal.draw();
25
+ ```
26
+
27
+ ## Development
28
+
29
+ This project uses [Bun](https://bun.sh) as its primary toolchain
30
+ (installs, scripts, and publishing) with the TypeScript compiler for
31
+ type-checking and `dist/` emission.
32
+
33
+ ```sh
34
+ bun install # install dev dependencies
35
+ bun run shaders # regenerate src/shaders.ts from shaders/
36
+ bun run build # type-check + emit dist/ (JS + .d.ts)
37
+ ```
38
+
39
+ Source files import each other with explicit `.ts` extensions so they run
40
+ natively under Bun; the build rewrites those specifiers to `.js` in the
41
+ published output via `rewriteRelativeImportExtensions`.
42
+
43
+ ## License
44
+
45
+ MIT
@@ -0,0 +1,147 @@
1
+ /**
2
+ * A WebGL2 canvas presented as a grid of character cells.
3
+ *
4
+ * `Canvas` wraps an `HTMLCanvasElement` with a WebGL2 context, exposing it
5
+ * as a grid of fixed-size cells (`rows` x `cols`) rather than a pixel
6
+ * buffer. It owns the cell grid dimensions, the colour {@link palette}, the
7
+ * built-in bitmap font texture ({@link bitmap_font}), an optional
8
+ * application-provided font ({@link user_font}), and pointer/wheel/keyboard
9
+ * input.
10
+ *
11
+ * It is an `EventTarget`: it dispatches `"resize"` when the grid
12
+ * dimensions change and `"wheel"` (a `CustomEvent` with `detail.rows`) on
13
+ * scroll. {@link Terminal} and {@link Renderer} are constructed with a
14
+ * `Canvas` and read its state every frame.
15
+ */
16
+ export declare class Canvas extends EventTarget {
17
+ /**
18
+ * Width of a single cell, in CSS pixels at 100% zoom and DPR 1.
19
+ *
20
+ * Used as the base unit from which {@link actual_cell_width} is derived
21
+ * after accounting for device pixel ratio.
22
+ */
23
+ static readonly CELL_WIDTH = 8;
24
+ /**
25
+ * Height of a single cell, in CSS pixels at 100% zoom and DPR 1.
26
+ *
27
+ * Used as the base unit from which {@link actual_cell_height} is
28
+ * derived after accounting for device pixel ratio.
29
+ */
30
+ static readonly CELL_HEIGHT = 16;
31
+ private mouse_wheel_px;
32
+ private scroll_keys_held;
33
+ private scroll_delay;
34
+ private scroll_timer;
35
+ /** The underlying `<canvas>` DOM element this `Canvas` manages. */
36
+ element: HTMLCanvasElement;
37
+ /** The WebGL2 rendering context obtained from {@link element}. */
38
+ gl: WebGL2RenderingContext;
39
+ /** Width of the canvas in device pixels (CSS pixels x DPR). */
40
+ height: number;
41
+ /** Height of the canvas in device pixels (CSS pixels x DPR). */
42
+ width: number;
43
+ /** Number of cell rows currently visible on the canvas. */
44
+ rows: number;
45
+ /** Number of cell columns currently visible on the canvas. */
46
+ cols: number;
47
+ /** Height of a single cell in device pixels. */
48
+ actual_cell_height: number;
49
+ /** Width of a single cell in device pixels. */
50
+ actual_cell_width: number;
51
+ /**
52
+ * Name of the component that currently "owns" the mouse, or `""` when
53
+ * unclaimed. Components doing drag-style interaction set this to a
54
+ * unique name when a press begins inside their region and clear it
55
+ * when the press ends, so other components can avoid stealing the
56
+ * gesture mid-drag.
57
+ */
58
+ mouse_owner: string;
59
+ /** Cell row the pointer is over, or `undefined` if off-canvas. */
60
+ mouse_row: number | undefined;
61
+ /** Cell column the pointer is over, or `undefined` if off-canvas. */
62
+ mouse_col: number | undefined;
63
+ /** `true` while a pointer button is held down on the canvas. */
64
+ mouse_down: boolean;
65
+ /**
66
+ * `true` for one frame after a pointer press is released (a click).
67
+ * Intended to be consumed (reset to `false`) by the draw loop after
68
+ * components have processed it.
69
+ */
70
+ mouse_click: boolean;
71
+ /** Cell row where the current press started, or `undefined`. */
72
+ mouse_down_row: number | undefined;
73
+ /** Cell column where the current press started, or `undefined`. */
74
+ mouse_down_col: number | undefined;
75
+ /**
76
+ * The 16-colour palette as normalised `[R, G, B]` floats (divided by
77
+ * `0xff`), derived from {@link PALETTE}.
78
+ */
79
+ palette: Float32Array;
80
+ /**
81
+ * The built-in 8x16 bitmap font as a WebGL2 integer texture.
82
+ * This is the Modern DOS CP437 font baked into tscon.
83
+ */
84
+ bitmap_font: WebGLTexture;
85
+ /**
86
+ * Optional application-provided font texture, or `null`. When set,
87
+ * {@link Terminal}/{@link Renderer} use it for non-box-drawing glyphs
88
+ * (those with char codes outside 176..223). Generate one with
89
+ * {@link renderCp437} and {@link loadTexture}.
90
+ */
91
+ user_font: WebGLTexture | null;
92
+ /**
93
+ * CSS class name for the body cursor, set by components during a frame
94
+ * (e.g. `"pointer"` when hovering a link) and reset to `""` on
95
+ * {@link clear}.
96
+ */
97
+ class_name: string;
98
+ /**
99
+ * Create a `Canvas` managing the given element.
100
+ *
101
+ * Acquires a WebGL2 context from `element` (throwing if unavailable),
102
+ * loads the built-in bitmap font, and attaches listeners for pointer,
103
+ * wheel, keyboard, and window-resize input.
104
+ *
105
+ * @param element The `<canvas>` element to render into.
106
+ */
107
+ constructor(element: HTMLCanvasElement);
108
+ private resize;
109
+ private mouseMove;
110
+ private mouseScroll;
111
+ private scrollDeltaForKey;
112
+ private startKeyScroll;
113
+ private stopKeyScroll;
114
+ /**
115
+ * Whether the pointer is currently inside the given cell rectangle.
116
+ *
117
+ * @param row Top row of the rectangle.
118
+ * @param col Left column of the rectangle.
119
+ * @param rows Height of the rectangle in cells.
120
+ * @param cols Width of the rectangle in cells.
121
+ * @returns `true` if {@link mouse_row}/{@link mouse_col} fall
122
+ * inside.
123
+ */
124
+ mouseAt(row: number, col: number, rows: number, cols: number): boolean;
125
+ /**
126
+ * Whether the current press started inside the given cell rectangle.
127
+ *
128
+ * Use this for click hit-testing: it checks where the press began
129
+ * ({@link mouse_down_row}/{@link mouse_down_col}), not where the
130
+ * pointer is now, so a drag that leaves the rectangle still counts as
131
+ * a click on it.
132
+ *
133
+ * @param row Top row of the rectangle.
134
+ * @param col Left column of the rectangle.
135
+ * @param rows Height of the rectangle in cells.
136
+ * @param cols Width of the rectangle in cells.
137
+ * @returns `true` if the press started inside the rectangle.
138
+ */
139
+ mouseDownAt(row: number, col: number, rows: number, cols: number): boolean;
140
+ /**
141
+ * Clears the framebuffer with the palette background colour
142
+ * ({@link palette}[0..2]) and resets {@link class_name} to `""`.
143
+ *
144
+ * Call at the start of every frame, before rendering content.
145
+ */
146
+ clear(): void;
147
+ }
package/dist/canvas.js ADDED
@@ -0,0 +1,369 @@
1
+ import { loadModernDosTexture } from "./moderndos.js";
2
+ import { PALETTE } from "./colour.js";
3
+ var DeltaMode;
4
+ (function (DeltaMode) {
5
+ DeltaMode[DeltaMode["PX"] = 0] = "PX";
6
+ DeltaMode[DeltaMode["ROW"] = 1] = "ROW";
7
+ DeltaMode[DeltaMode["PAGE"] = 2] = "PAGE";
8
+ })(DeltaMode || (DeltaMode = {}));
9
+ /**
10
+ * A WebGL2 canvas presented as a grid of character cells.
11
+ *
12
+ * `Canvas` wraps an `HTMLCanvasElement` with a WebGL2 context, exposing it
13
+ * as a grid of fixed-size cells (`rows` x `cols`) rather than a pixel
14
+ * buffer. It owns the cell grid dimensions, the colour {@link palette}, the
15
+ * built-in bitmap font texture ({@link bitmap_font}), an optional
16
+ * application-provided font ({@link user_font}), and pointer/wheel/keyboard
17
+ * input.
18
+ *
19
+ * It is an `EventTarget`: it dispatches `"resize"` when the grid
20
+ * dimensions change and `"wheel"` (a `CustomEvent` with `detail.rows`) on
21
+ * scroll. {@link Terminal} and {@link Renderer} are constructed with a
22
+ * `Canvas` and read its state every frame.
23
+ */
24
+ export class Canvas extends EventTarget {
25
+ /**
26
+ * Width of a single cell, in CSS pixels at 100% zoom and DPR 1.
27
+ *
28
+ * Used as the base unit from which {@link actual_cell_width} is derived
29
+ * after accounting for device pixel ratio.
30
+ */
31
+ static CELL_WIDTH = 8;
32
+ /**
33
+ * Height of a single cell, in CSS pixels at 100% zoom and DPR 1.
34
+ *
35
+ * Used as the base unit from which {@link actual_cell_height} is
36
+ * derived after accounting for device pixel ratio.
37
+ */
38
+ static CELL_HEIGHT = 16;
39
+ // mouse wheel tracking fields
40
+ mouse_wheel_px;
41
+ // keyboard scroll tracking fields (for i/j, dn/up)
42
+ scroll_keys_held;
43
+ scroll_delay;
44
+ scroll_timer;
45
+ /** The underlying `<canvas>` DOM element this `Canvas` manages. */
46
+ element;
47
+ /** The WebGL2 rendering context obtained from {@link element}. */
48
+ gl;
49
+ /** Width of the canvas in device pixels (CSS pixels x DPR). */
50
+ height;
51
+ /** Height of the canvas in device pixels (CSS pixels x DPR). */
52
+ width;
53
+ /** Number of cell rows currently visible on the canvas. */
54
+ rows;
55
+ /** Number of cell columns currently visible on the canvas. */
56
+ cols;
57
+ /** Height of a single cell in device pixels. */
58
+ actual_cell_height;
59
+ /** Width of a single cell in device pixels. */
60
+ actual_cell_width;
61
+ /**
62
+ * Name of the component that currently "owns" the mouse, or `""` when
63
+ * unclaimed. Components doing drag-style interaction set this to a
64
+ * unique name when a press begins inside their region and clear it
65
+ * when the press ends, so other components can avoid stealing the
66
+ * gesture mid-drag.
67
+ */
68
+ mouse_owner;
69
+ /** Cell row the pointer is over, or `undefined` if off-canvas. */
70
+ mouse_row;
71
+ /** Cell column the pointer is over, or `undefined` if off-canvas. */
72
+ mouse_col;
73
+ /** `true` while a pointer button is held down on the canvas. */
74
+ mouse_down;
75
+ /**
76
+ * `true` for one frame after a pointer press is released (a click).
77
+ * Intended to be consumed (reset to `false`) by the draw loop after
78
+ * components have processed it.
79
+ */
80
+ mouse_click;
81
+ /** Cell row where the current press started, or `undefined`. */
82
+ mouse_down_row;
83
+ /** Cell column where the current press started, or `undefined`. */
84
+ mouse_down_col;
85
+ /**
86
+ * The 16-colour palette as normalised `[R, G, B]` floats (divided by
87
+ * `0xff`), derived from {@link PALETTE}.
88
+ */
89
+ palette;
90
+ /**
91
+ * The built-in 8x16 bitmap font as a WebGL2 integer texture.
92
+ * This is the Modern DOS CP437 font baked into tscon.
93
+ */
94
+ bitmap_font;
95
+ /**
96
+ * Optional application-provided font texture, or `null`. When set,
97
+ * {@link Terminal}/{@link Renderer} use it for non-box-drawing glyphs
98
+ * (those with char codes outside 176..223). Generate one with
99
+ * {@link renderCp437} and {@link loadTexture}.
100
+ */
101
+ user_font;
102
+ /**
103
+ * CSS class name for the body cursor, set by components during a frame
104
+ * (e.g. `"pointer"` when hovering a link) and reset to `""` on
105
+ * {@link clear}.
106
+ */
107
+ class_name;
108
+ /**
109
+ * Create a `Canvas` managing the given element.
110
+ *
111
+ * Acquires a WebGL2 context from `element` (throwing if unavailable),
112
+ * loads the built-in bitmap font, and attaches listeners for pointer,
113
+ * wheel, keyboard, and window-resize input.
114
+ *
115
+ * @param element The `<canvas>` element to render into.
116
+ */
117
+ constructor(element) {
118
+ super();
119
+ this.element = element;
120
+ const gl = element.getContext("webgl2");
121
+ if (!gl) {
122
+ throw new Error("No WebGL context");
123
+ }
124
+ this.gl = gl;
125
+ this.mouse_wheel_px = 0;
126
+ this.scroll_keys_held = new Set();
127
+ this.scroll_delay = undefined;
128
+ this.scroll_timer = undefined;
129
+ this.mouse_owner = "";
130
+ this.mouse_down = false;
131
+ this.mouse_click = false;
132
+ this.palette = new Float32Array(PALETTE.map((byte) => byte / 0xff));
133
+ this.bitmap_font = loadModernDosTexture(gl);
134
+ this.user_font = null;
135
+ this.class_name = "";
136
+ this.resize();
137
+ // add event listeners
138
+ window.addEventListener("resize", () => {
139
+ this.resize();
140
+ });
141
+ window.addEventListener("pointermove", (e) => {
142
+ this.mouseMove(e.clientX, e.clientY);
143
+ });
144
+ this.element.addEventListener("pointerdown", (e) => {
145
+ e.preventDefault();
146
+ this.mouseMove(e.clientX, e.clientY);
147
+ this.mouse_down = true;
148
+ this.mouse_down_row = this.mouse_row;
149
+ this.mouse_down_col = this.mouse_col;
150
+ e.currentTarget
151
+ .setPointerCapture(e.pointerId);
152
+ });
153
+ this.element.addEventListener("pointerup", () => {
154
+ this.mouse_down = false;
155
+ this.mouse_click = true;
156
+ });
157
+ this.element.addEventListener("pointercancel", () => {
158
+ this.mouse_down = false;
159
+ });
160
+ this.element.addEventListener("pointerleave", () => {
161
+ this.mouse_down = false;
162
+ });
163
+ this.element.addEventListener("wheel", (e) => {
164
+ e.preventDefault();
165
+ this.mouseScroll(e.deltaMode, e.deltaY);
166
+ }, { passive: false });
167
+ this.element.addEventListener("contextmenu", (e) => {
168
+ e.preventDefault();
169
+ });
170
+ window.addEventListener("keydown", (e) => {
171
+ const target = e.target;
172
+ if (target &&
173
+ (target.tagName === "INPUT" ||
174
+ target.tagName === "TEXTAREA" ||
175
+ target.isContentEditable)) {
176
+ return;
177
+ }
178
+ if (this.scrollDeltaForKey(e.key) === 0) {
179
+ return;
180
+ }
181
+ // stop the page from scrolling when using arrow keys
182
+ e.preventDefault();
183
+ if (e.repeat) {
184
+ return;
185
+ }
186
+ this.startKeyScroll(e.key);
187
+ });
188
+ window.addEventListener("keyup", (e) => {
189
+ if (this.scrollDeltaForKey(e.key) === 0) {
190
+ return;
191
+ }
192
+ this.stopKeyScroll(e.key);
193
+ });
194
+ window.addEventListener("blur", () => {
195
+ this.scroll_keys_held.clear();
196
+ if (this.scroll_delay !== undefined) {
197
+ clearTimeout(this.scroll_delay);
198
+ this.scroll_delay = undefined;
199
+ }
200
+ if (this.scroll_timer !== undefined) {
201
+ clearInterval(this.scroll_timer);
202
+ this.scroll_timer = undefined;
203
+ }
204
+ this.mouse_down = false;
205
+ this.mouse_click = false;
206
+ });
207
+ }
208
+ resize() {
209
+ const dpr = window.devicePixelRatio || 1;
210
+ this.height = this.element.clientHeight * dpr;
211
+ this.width = this.element.clientWidth * dpr;
212
+ this.element.height = this.height;
213
+ this.element.width = this.width;
214
+ // update viewport
215
+ this.gl.viewport(0, 0, this.width, this.height);
216
+ const target_cell_height = Math.max(Canvas.CELL_HEIGHT / 2, Canvas.CELL_HEIGHT * dpr);
217
+ const target_cell_width = Math.max(Canvas.CELL_WIDTH / 2, Canvas.CELL_WIDTH * dpr);
218
+ const rows = Math.round(Math.min(Math.max(1, this.height / target_cell_height), 256));
219
+ const cols = Math.round(Math.min(Math.max(1, this.width / target_cell_width), 1024));
220
+ this.actual_cell_height = this.height / rows;
221
+ this.actual_cell_width = this.width / cols;
222
+ // reset mouse related fields
223
+ this.mouse_owner = "";
224
+ this.mouse_row = undefined;
225
+ this.mouse_col = undefined;
226
+ this.mouse_down = false;
227
+ this.mouse_click = false;
228
+ this.mouse_down_row = undefined;
229
+ this.mouse_down_col = undefined;
230
+ if (rows !== this.rows || cols !== this.cols) {
231
+ this.rows = rows;
232
+ this.cols = cols;
233
+ this.dispatchEvent(new Event("resize"));
234
+ }
235
+ }
236
+ mouseMove(client_x, client_y) {
237
+ const dpr = window.devicePixelRatio || 1;
238
+ const actual_client_y = client_y * dpr;
239
+ const actual_client_x = client_x * dpr;
240
+ this.mouse_row = Math.floor(actual_client_y / this.actual_cell_height);
241
+ this.mouse_col = Math.floor(actual_client_x / this.actual_cell_width);
242
+ }
243
+ mouseScroll(delta_mode, delta) {
244
+ let rows = 0;
245
+ switch (delta_mode) {
246
+ case DeltaMode.PX:
247
+ this.mouse_wheel_px += delta;
248
+ if (Math.abs(this.mouse_wheel_px) >=
249
+ this.actual_cell_height) {
250
+ rows = Math.floor(this.mouse_wheel_px /
251
+ this.actual_cell_height);
252
+ this.mouse_wheel_px %=
253
+ this.actual_cell_height;
254
+ }
255
+ break;
256
+ case DeltaMode.ROW:
257
+ rows = delta;
258
+ break;
259
+ case DeltaMode.PAGE:
260
+ rows = delta * this.rows;
261
+ break;
262
+ }
263
+ this.dispatchEvent(new CustomEvent("wheel", { detail: { rows } }));
264
+ }
265
+ scrollDeltaForKey(key) {
266
+ switch (key) {
267
+ case "j":
268
+ case "ArrowDown":
269
+ return 1;
270
+ case "k":
271
+ case "ArrowUp":
272
+ return -1;
273
+ case "0":
274
+ return -999999; // full 'scend to top
275
+ default:
276
+ return 0;
277
+ }
278
+ }
279
+ startKeyScroll(key) {
280
+ if (this.scroll_keys_held.has(key)) {
281
+ return;
282
+ }
283
+ this.scroll_keys_held.add(key);
284
+ // immediate first scroll for responsiveness
285
+ this.mouseScroll(DeltaMode.ROW, this.scrollDeltaForKey(key));
286
+ // key repeats after held for a full second
287
+ if (this.scroll_delay === undefined &&
288
+ this.scroll_timer === undefined) {
289
+ this.scroll_delay = setTimeout(() => {
290
+ this.scroll_delay = undefined;
291
+ this.scroll_timer = setInterval(() => {
292
+ let delta = 0;
293
+ for (const k of this.scroll_keys_held) {
294
+ delta += this.scrollDeltaForKey(k);
295
+ }
296
+ if (delta !== 0) {
297
+ this.mouseScroll(DeltaMode.ROW, delta);
298
+ }
299
+ }, 50); // repeat every 50ms
300
+ }, 333); // wait 333ms
301
+ }
302
+ }
303
+ stopKeyScroll(key) {
304
+ this.scroll_keys_held.delete(key);
305
+ if (this.scroll_keys_held.size === 0) {
306
+ if (this.scroll_delay !== undefined) {
307
+ clearTimeout(this.scroll_delay);
308
+ this.scroll_delay = undefined;
309
+ }
310
+ if (this.scroll_timer !== undefined) {
311
+ clearInterval(this.scroll_timer);
312
+ this.scroll_timer = undefined;
313
+ }
314
+ }
315
+ }
316
+ /**
317
+ * Whether the pointer is currently inside the given cell rectangle.
318
+ *
319
+ * @param row Top row of the rectangle.
320
+ * @param col Left column of the rectangle.
321
+ * @param rows Height of the rectangle in cells.
322
+ * @param cols Width of the rectangle in cells.
323
+ * @returns `true` if {@link mouse_row}/{@link mouse_col} fall
324
+ * inside.
325
+ */
326
+ mouseAt(row, col, rows, cols) {
327
+ return (this.mouse_row !== undefined &&
328
+ this.mouse_col !== undefined &&
329
+ this.mouse_row >= row &&
330
+ this.mouse_row < row + rows &&
331
+ this.mouse_col >= col &&
332
+ this.mouse_col < col + cols);
333
+ }
334
+ /**
335
+ * Whether the current press started inside the given cell rectangle.
336
+ *
337
+ * Use this for click hit-testing: it checks where the press began
338
+ * ({@link mouse_down_row}/{@link mouse_down_col}), not where the
339
+ * pointer is now, so a drag that leaves the rectangle still counts as
340
+ * a click on it.
341
+ *
342
+ * @param row Top row of the rectangle.
343
+ * @param col Left column of the rectangle.
344
+ * @param rows Height of the rectangle in cells.
345
+ * @param cols Width of the rectangle in cells.
346
+ * @returns `true` if the press started inside the rectangle.
347
+ */
348
+ mouseDownAt(row, col, rows, cols) {
349
+ return (this.mouse_down_row !== undefined &&
350
+ this.mouse_down_col !== undefined &&
351
+ this.mouse_down_row >= row &&
352
+ this.mouse_down_row < row + rows &&
353
+ this.mouse_down_col >= col &&
354
+ this.mouse_down_col < col + cols);
355
+ }
356
+ /**
357
+ * Clears the framebuffer with the palette background colour
358
+ * ({@link palette}[0..2]) and resets {@link class_name} to `""`.
359
+ *
360
+ * Call at the start of every frame, before rendering content.
361
+ */
362
+ clear() {
363
+ const gl = this.gl;
364
+ // clear the canvas
365
+ gl.clearColor(this.palette[0], this.palette[1], this.palette[2], 1.0);
366
+ gl.clear(gl.COLOR_BUFFER_BIT);
367
+ this.class_name = "";
368
+ }
369
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Indices into the 16-entry terminal {@link PALETTE}.
3
+ *
4
+ * The palette is split into eight "dark" colours (0..7) and eight "bright"
5
+ * colours (8..15). Glyph text encodes a foreground and background colour as
6
+ * 4-bit values, the high bit of which selects the bright half of the palette.
7
+ *
8
+ * Use these members with the `Colour` escape sequences understood by
9
+ * {@link textGlyphs} (`\fX`, `\FX`, `\bX`, `\BX`) and as the `fg`/`bg`
10
+ * indices when building glyphs programmatically.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { Colour } from "@micahdb/tscon";
15
+ *
16
+ * // bright-blue foreground on black background
17
+ * const text = "\\F4\\b0Link";
18
+ * ```
19
+ */
20
+ export declare enum Colour {
21
+ BLACK = 0,
22
+ RED = 1,
23
+ GREEN = 2,
24
+ ORANGE = 3,
25
+ BLUE = 4,
26
+ MAGENTA = 5,
27
+ CYAN = 6,
28
+ GREY = 7,
29
+ BRIGHT_BLACK = 8,
30
+ BRIGHT_RED = 9,
31
+ BRIGHT_GREEN = 10,
32
+ YELLOW = 11,
33
+ BRIGHT_BLUE = 12,
34
+ BRIGHT_MAGENTA = 13,
35
+ BRIGHT_CYAN = 14,
36
+ WHITE = 15
37
+ }
38
+ /**
39
+ * The 16-colour terminal palette as flat 8-bit-per-channel RGB bytes.
40
+ *
41
+ * The array contains 48 entries: 16 colours of three bytes each, ordered to
42
+ * match the {@link Colour} enum. Each triplet is an `[R, G, B]` value in the
43
+ * range `0x00`-`0xff`.
44
+ *
45
+ * The first eight entries are the dark colours ({@link Colour.BLACK} ..
46
+ * {@link Colour.GREY}) and the last eight are the bright colours
47
+ * ({@link Colour.BRIGHT_BLACK} .. {@link Colour.WHITE}).
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * import { Colour, PALETTE } from "@micahdb/tscon";
52
+ *
53
+ * const [r, g, b] = PALETTE.slice(Colour.BLUE * 3, Colour.BLUE * 3 + 3);
54
+ * ```
55
+ */
56
+ export declare const PALETTE: number[];