@micahdb/tscon 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +44 -2
- package/dist/canvas.d.ts +147 -0
- package/dist/canvas.js +369 -0
- package/dist/colour.d.ts +56 -0
- package/dist/colour.js +77 -0
- package/dist/cp437.d.ts +48 -0
- package/dist/cp437.js +124 -0
- package/dist/glyphs.d.ts +222 -0
- package/dist/glyphs.js +449 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +9 -0
- package/dist/math.d.ts +214 -0
- package/dist/math.js +499 -0
- package/dist/moderndos.d.ts +1 -0
- package/dist/moderndos.js +322 -0
- package/dist/program.d.ts +72 -0
- package/dist/program.js +127 -0
- package/dist/rect.d.ts +26 -0
- package/dist/rect.js +1 -0
- package/dist/renderer.d.ts +59 -0
- package/dist/renderer.js +172 -0
- package/dist/shaders.d.ts +5 -0
- package/dist/shaders.js +470 -0
- package/dist/terminal.d.ts +92 -0
- package/dist/terminal.js +275 -0
- package/dist/texture.d.ts +66 -0
- package/dist/texture.js +119 -0
- package/package.json +44 -5
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
|
-
#
|
|
1
|
+
# @micahdb/tscon
|
|
2
2
|
|
|
3
|
-
|
|
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
|
package/dist/canvas.d.ts
ADDED
|
@@ -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.flat().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
|
+
}
|
package/dist/colour.d.ts
ADDED
|
@@ -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[Colour.BLUE];
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export declare const PALETTE: readonly [number, number, number][];
|