@8bitscript/web 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 8BitScript contributors
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/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@8bitscript/web",
3
+ "version": "0.1.0",
4
+ "description": "Browser target support for 8BitScript: the hardware underneath the portable APIs, where the hardware is a canvas.",
5
+ "license": "MIT",
6
+ "8bitscript": {
7
+ "entry": "./src/index.8bs",
8
+ "exports": {
9
+ "./screen": "./src/screen.8bs",
10
+ "./text": "./src/text.8bs",
11
+ "./input": "./src/input.8bs",
12
+ "./pointer": "./src/pointer.8bs"
13
+ },
14
+ "hardware": {
15
+ "facts": {
16
+ "video.columns": 40,
17
+ "video.rows": 25,
18
+ "video.cellWidth": 8,
19
+ "video.cellHeight": 8,
20
+ "video.palette": 16,
21
+ "video.cellColors": 2,
22
+ "video.colorPerCell": true,
23
+ "video.glyphs": 0,
24
+ "video.blockWidth": 0,
25
+ "video.blockHeight": 0,
26
+ "video.bitmap": false,
27
+ "video.layers": 1,
28
+ "video.scroll": false,
29
+ "video.sprites": 0,
30
+ "video.spritesPerLine": 0,
31
+ "video.spriteWidth": 0,
32
+ "video.spriteHeight": 0,
33
+ "video.spriteColors": 0,
34
+ "video.frameRate": 60,
35
+ "audio.voices": 0,
36
+ "audio.noise": false,
37
+ "audio.envelope": false,
38
+ "audio.filter": false,
39
+ "audio.pcm": false,
40
+ "audio.volume": false,
41
+ "audio.entropy": false,
42
+ "input.keyboard": true,
43
+ "input.joysticks": 0,
44
+ "input.pads": 0,
45
+ "input.mouse": false,
46
+ "input.paddles": false,
47
+ "storage.save": false,
48
+ "memory.ram": 57344,
49
+ "memory.banked": false,
50
+ "memory.bankedKib": 0,
51
+ "storage.kib": 0
52
+ },
53
+ "options": {},
54
+ "presets": {}
55
+ }
56
+ },
57
+ "files": [
58
+ "src"
59
+ ],
60
+ "publishConfig": {
61
+ "access": "public"
62
+ }
63
+ }
package/src/index.8bs ADDED
@@ -0,0 +1,42 @@
1
+ // @8bitscript/web — browser target support for 8BitScript: the "hardware"
2
+ // underneath.
3
+ //
4
+ // The module entry point named by the "8bitscript".entry field in this
5
+ // package's package.json, resolved and linked for:
6
+ //
7
+ // import { WebRegisters } from "@8bitscript/web";
8
+ //
9
+ // What a program usually wants sits one layer up, in this package's own
10
+ // implementations of the portable capability packages — `./src/screen.8bs`
11
+ // behind @8bitscript/screen and `./src/text.8bs` behind @8bitscript/text,
12
+ // named by the "8bitscript".exports map in package.json — which are built
13
+ // on what this file exports and on nothing else.
14
+ //
15
+ // There is no $900F or $D020 here: the web target has no fixed hardware, so
16
+ // "the screen" is a canvas the host paints from wasm's own linear memory
17
+ // (the `--initialMemory 1` page every web build reserves, per
18
+ // @8bitscript/backend-web). This package's hardware surface is therefore an
19
+ // AGREEMENT rather than a register map: a handful of fixed byte offsets in
20
+ // that memory — two for the border and background colours, a virtual
21
+ // character screen for everything `text` pokes, and one byte of input the
22
+ // page writes and `@8bitscript/web/input` reads — which the host
23
+ // (packages/cli's browser runtime, packages/cli/src/web-runtime.mjs) paints
24
+ // from once per display refresh. It is the same deferred-apply contract a
25
+ // VIC-II register has, aimed at a buffer instead.
26
+ //
27
+ // The character screen is 1000 cells, 40 wide — the C64's own screen shape
28
+ // (see @8bitscript/c64) — picked as a safe superset of the VIC-20's smaller
29
+ // 506-cell one so the same program's cell numbers never run off the end of
30
+ // this buffer either. Two side-by-side regions, not interleaved, so a
31
+ // caller that only ever touches a handful of cells leaves most of both
32
+ // untouched.
33
+ export namespace WebRegisters {
34
+ const BORDER_OFFSET: usmallint = 0;
35
+ const BACKGROUND_OFFSET: usmallint = 1;
36
+ const CHAR_BASE: usmallint = 2;
37
+ const COLOR_BASE: usmallint = 1002;
38
+ // Directions, confirm and cancel as the same Edge bits every input
39
+ // layer uses. The page writes this; poll() in ./input.8bs reads it.
40
+ // First byte after the 1000 colour cells at COLOR_BASE.
41
+ const INPUT_OFFSET: usmallint = 2002;
42
+ }
package/src/input.8bs ADDED
@@ -0,0 +1,100 @@
1
+ // @8bitscript/web/input — the browser runtime behind @8bitscript/input.
2
+ //
3
+ // Named by "8bitscript".exports["./input"] in this package's package.json,
4
+ // and by "8bitscript".entry.web in @8bitscript/input's, so a portable
5
+ // program writes
6
+ //
7
+ // import { input } from "@8bitscript/input";
8
+ //
9
+ // and gets this file in a browser.
10
+ //
11
+ // ---- the page writes, the program reads ----------------------------------
12
+ //
13
+ // The program runs in a worker and cannot see DOM events. The page listens
14
+ // for keys and stores a snapshot at WebRegisters.INPUT_OFFSET in the
15
+ // shared wasm memory; poll() reads that byte once a frame, the same shape
16
+ // every other machine's layer uses — a held mask, last frame's mask, and
17
+ // the bits that began this frame. No new wasm import: the agreement page
18
+ // is the port.
19
+ //
20
+ // - **left / right / up / down** — ArrowLeft / ArrowRight / ArrowUp /
21
+ // ArrowDown.
22
+ // - **confirm** — Enter.
23
+ // - **cancel** — Escape.
24
+ //
25
+ // There is still no pointer. `pointer()` is a constant false; a mouse
26
+ // belongs in the same snapshot when someone writes it, and
27
+ // `input.mouse` stays false until then.
28
+ //
29
+ // ---- edges, not levels ---------------------------------------------------
30
+ //
31
+ // Every answer is **edge-triggered**: true on the one frame the press
32
+ // begins, false while it is held. `poll()` must be called exactly once a
33
+ // frame, right after waitFrame().
34
+ import { WebRegisters } from "./index.8bs";
35
+
36
+ namespace Edge {
37
+ const LEFT: utinyint = 1;
38
+ const RIGHT: utinyint = 2;
39
+ const UP: utinyint = 4;
40
+ const DOWN: utinyint = 8;
41
+ const CONFIRM: utinyint = 16;
42
+ const CANCEL: utinyint = 32;
43
+ }
44
+
45
+ let held: utinyint = 0;
46
+ let before: utinyint = 0;
47
+ let began: utinyint = 0;
48
+
49
+ export namespace input {
50
+
51
+ // Nothing to set up: the page owns the snapshot byte. Here so a
52
+ // program written for nine machines calls the same thing on all of
53
+ // them.
54
+ function begin(): void {
55
+ }
56
+
57
+ // Read the page's snapshot and work out what began this frame. Once
58
+ // a frame, right after waitFrame().
59
+ function poll(): void {
60
+ before = held;
61
+ held = memory.read(WebRegisters.INPUT_OFFSET);
62
+ began = held & (before ^ 0xFF);
63
+ }
64
+
65
+ function left(): bool {
66
+ return (began & Edge.LEFT) != 0;
67
+ }
68
+
69
+ function right(): bool {
70
+ return (began & Edge.RIGHT) != 0;
71
+ }
72
+
73
+ function up(): bool {
74
+ return (began & Edge.UP) != 0;
75
+ }
76
+
77
+ function down(): bool {
78
+ return (began & Edge.DOWN) != 0;
79
+ }
80
+
81
+ function confirm(): bool {
82
+ return (began & Edge.CONFIRM) != 0;
83
+ }
84
+
85
+ function cancel(): bool {
86
+ return (began & Edge.CANCEL) != 0;
87
+ }
88
+
89
+ function pointer(): bool {
90
+ return false;
91
+ }
92
+
93
+ function pointerCell(): usmallint {
94
+ return 0;
95
+ }
96
+
97
+ function pointerButton(): bool {
98
+ return false;
99
+ }
100
+ }
@@ -0,0 +1,42 @@
1
+ // @8bitscript/web/pointer — the web runtime behind @8bitscript/pointer,
2
+ // and the one target where the pointer the user already has is the
3
+ // problem rather than the solution.
4
+ //
5
+ // Every call here does nothing and `DRAWS` is false: the runtime now
6
+ // delivers arrow keys through @8bitscript/web/input, but still no pointer.
7
+ // A mouse belongs in the same shared snapshot as those keys, and until
8
+ // that lands there is nothing to draw.
9
+ //
10
+ // This target has a decision the other eight do not, and it is worth
11
+ // writing down before anyone implements it: **the user is already looking
12
+ // at a real mouse cursor** — the browser's — sitting over the canvas. So
13
+ // "draw a pointer" here means one of two quite different things. Either
14
+ // the runtime hides the browser's cursor over the canvas and draws the
15
+ // machine's own arrow, which is what makes a web build look like the
16
+ // machine it is emulating and what every other target does; or it leaves
17
+ // the browser's cursor alone and `DRAWS` stays false forever, with
18
+ // `input.pointerCell()` reporting where that cursor is. The second is less
19
+ // work and the first is more honest to what this target is for, and every
20
+ // "hardware" fact of this machine is a decision recorded in the runtime's
21
+ // source (see packages/web/AGENTS.md), so this one belongs written down
22
+ // there when it is made.
23
+ //
24
+ // It costs the target nothing today: `DRAWS` is a const and every function
25
+ // is empty, so a program's `if (pointer.DRAWS)` folds away and the backend
26
+ // drops the calls.
27
+ export namespace pointer {
28
+
29
+ const DRAWS: bool = false;
30
+
31
+ function begin(): void {
32
+ }
33
+
34
+ function setColor(color: utinyint): void {
35
+ }
36
+
37
+ function update(): void {
38
+ }
39
+
40
+ function hide(): void {
41
+ }
42
+ }
package/src/screen.8bs ADDED
@@ -0,0 +1,107 @@
1
+ // @8bitscript/web/screen — the browser target's implementation of
2
+ // @8bitscript/screen.
3
+ //
4
+ // Named by "8bitscript".exports["./screen"] in this package's package.json,
5
+ // and what
6
+ //
7
+ // import { screen, BorderColor, BackgroundColor } from "@8bitscript/screen";
8
+ //
9
+ // resolves to when the build is for the web: @8bitscript/screen's entry is
10
+ // keyed by machine and delegates here. Built on the memory-layout agreement
11
+ // @8bitscript/web exports, one layer up from it. Every machine's screen.8bs
12
+ // exports this same surface, which is what lets a program import it from
13
+ // @8bitscript/screen and never name the hardware — or, here, the host.
14
+ import { WebRegisters } from "./index.8bs";
15
+
16
+ // ---- the screen: border and background --------------------------------------
17
+ //
18
+ // Sixteen colours, masked the way the C64's registers are: the web target
19
+ // has no palette size of its own to be faithful to, so it borrows the C64's
20
+ // (see COLORS in packages/cli/src/web-runtime.mjs), which keeps colour
21
+ // numbers meaning the same thing across every 8BitScript example. The host
22
+ // reads the two bytes once per painted frame, so the picture changes at
23
+ // the next paint.
24
+ export namespace screen {
25
+ function setColors(border: u8, background: u8): void {
26
+ memory.write(WebRegisters.BORDER_OFFSET, border & 15);
27
+ memory.write(WebRegisters.BACKGROUND_OFFSET, background & 15);
28
+ }
29
+
30
+ // `blank(border, background)`: every cell blank and both colours set —
31
+ // black when left off, or `BorderColor.KEEP` / `BackgroundColor.KEEP`
32
+ // to leave one as it is. `setBorder` and `setBackground` are one
33
+ // colour each, `setColors` the pair; `text.setColor` owns what the
34
+ // next print looks like.
35
+ function blank(border: utinyint = BorderColor.BLACK, background: utinyint = BackgroundColor.BLACK): void {
36
+ if (border != BorderColor.KEEP) {
37
+ screen.setBorder(border);
38
+ }
39
+ if (background != BackgroundColor.KEEP) {
40
+ screen.setBackground(background);
41
+ }
42
+ for (let cell: usmallint = 0; cell < 1000; cell++) {
43
+ memory.write(WebRegisters.CHAR_BASE + cell, 32);
44
+ }
45
+ }
46
+
47
+ function setBackground(background: u8): void {
48
+ memory.write(WebRegisters.BACKGROUND_OFFSET, background & 15);
49
+ }
50
+
51
+ function setBorder(border: u8): void {
52
+ memory.write(WebRegisters.BORDER_OFFSET, border & 15);
53
+ }
54
+ }
55
+
56
+ // ---- colour names ----------------------------------------------------------
57
+ //
58
+ // The same eight names every machine's screen.8bs exports — Black, White,
59
+ // Red, Cyan, Purple, Green, Blue, Yellow — so a program can write
60
+ // `screen.setColors(BorderColor.BLUE, BackgroundColor.BLACK)` through
61
+ // @8bitscript/screen and get blue on every target. The values are the C64's
62
+ // sixteen colours in the C64's numbering, the palette this target borrows
63
+ // (see setColors above).
64
+
65
+ export namespace BorderColor {
66
+ const BLACK: utinyint = 0;
67
+ const WHITE: utinyint = 1;
68
+ const RED: utinyint = 2;
69
+ const CYAN: utinyint = 3;
70
+ const PURPLE: utinyint = 4;
71
+ const GREEN: utinyint = 5;
72
+ const BLUE: utinyint = 6;
73
+ const YELLOW: utinyint = 7;
74
+ const ORANGE: utinyint = 8;
75
+ const BROWN: utinyint = 9;
76
+ const LIGHT_RED: utinyint = 10;
77
+ const DARK_GREY: utinyint = 11;
78
+ const GREY: utinyint = 12;
79
+ const LIGHT_GREEN: utinyint = 13;
80
+ const LIGHT_BLUE: utinyint = 14;
81
+ const LIGHT_GREY: utinyint = 15;
82
+ // Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
83
+ // a value no register here takes.
84
+ const KEEP: utinyint = 255;
85
+ }
86
+
87
+ export namespace BackgroundColor {
88
+ const BLACK: utinyint = 0;
89
+ const WHITE: utinyint = 1;
90
+ const RED: utinyint = 2;
91
+ const CYAN: utinyint = 3;
92
+ const PURPLE: utinyint = 4;
93
+ const GREEN: utinyint = 5;
94
+ const BLUE: utinyint = 6;
95
+ const YELLOW: utinyint = 7;
96
+ const ORANGE: utinyint = 8;
97
+ const BROWN: utinyint = 9;
98
+ const LIGHT_RED: utinyint = 10;
99
+ const DARK_GREY: utinyint = 11;
100
+ const GREY: utinyint = 12;
101
+ const LIGHT_GREEN: utinyint = 13;
102
+ const LIGHT_BLUE: utinyint = 14;
103
+ const LIGHT_GREY: utinyint = 15;
104
+ // Not a colour: `blank(KEEP, ...)` leaves this one as it is. 255 is
105
+ // a value no register here takes.
106
+ const KEEP: utinyint = 255;
107
+ }
package/src/text.8bs ADDED
@@ -0,0 +1,105 @@
1
+ // @8bitscript/web/text — the browser target's implementation of
2
+ // @8bitscript/text.
3
+ //
4
+ // Named by "8bitscript".exports["./text"] in this package's package.json,
5
+ // and what
6
+ //
7
+ // import { text } from "@8bitscript/text";
8
+ //
9
+ // resolves to when the build is for the web. Built on the memory-layout
10
+ // agreement @8bitscript/web exports. Every machine's text.8bs exports this
11
+ // same namespace — the same names, the same shapes, ASCII codes, and a cell
12
+ // 0 at the top-left corner inside the border — so a program written against
13
+ // it doesn't know or care that "screen memory" is a real 6502 address on
14
+ // most targets and a wasm-memory offset on this one.
15
+ import { WebRegisters } from "./index.8bs";
16
+
17
+ // Codes are ASCII, as they are everywhere (space, '0'-'9', 'A'-'Z' and a
18
+ // little punctuation, upper case only); this buffer holds them as they
19
+ // come, and the host (packages/cli/src/web-runtime.mjs) draws whatever
20
+ // isn't blank as the text it is, once per painted frame — there is no
21
+ // character ROM here to need screen codes for.
22
+ // ---- print: strings and number fields ------------------------------------
23
+ //
24
+ // `text.print(cell, s)` writes a string's characters into consecutive
25
+ // cells from `cell`, and `text.printNumber(cell, value, width)` writes
26
+ // `value` as exactly `width` decimal digits, zero-padded and right-aligned,
27
+ // so a field on a HUD never shifts columns. Both draw in the current colour —
28
+ // white until `text.setColor(TextColor.CYAN)` changes it, and that one
29
+ // call then colours everything printed after it, on the machines that have
30
+ // per-cell colour. They are also the two
31
+ // functions the compiler's template layout targets: `text.print(0,
32
+ // \`TICK ${ticks:1}\`)` is laid out at compile time into these same calls
33
+ // (see packages/compiler/src/ir).
34
+ //
35
+ // Under them: `place()` puts one character at one cell in the current
36
+ // colour, and is all a run of text costs per character.
37
+ // `text.putChar` and `text.putColor` stay the one-cell pokes a caller can
38
+ // build anything from.
39
+ let currentColor: utinyint = 1; // white, until text.setColor() says otherwise
40
+ let currentReverse: bool = false; // until text.setReverse() says otherwise
41
+
42
+ // Reverse rides in bit 7 of the colour byte: the host fills the cell with
43
+ // the foreground colour and punches the glyph out in the screen background.
44
+ // A reverse space is a solid block. The low four bits stay the colour.
45
+ function colorByte(): utinyint {
46
+ if (currentReverse) {
47
+ return currentColor + 128;
48
+ }
49
+ return currentColor;
50
+ }
51
+
52
+ function place(cell: usmallint, code: utinyint): void {
53
+ memory.write(WebRegisters.CHAR_BASE + cell, code);
54
+ memory.write(WebRegisters.COLOR_BASE + cell, colorByte());
55
+ }
56
+
57
+ export namespace text {
58
+ const CELL_COUNT: usmallint = 1000; // 40 columns x 25 rows
59
+ const COLUMNS: utinyint = 40; // cells per row, so cell = y * text.COLUMNS + x
60
+
61
+ function putChar(cell: usmallint, code: utinyint): void {
62
+ memory.write(WebRegisters.CHAR_BASE + cell, code);
63
+ }
64
+
65
+ function putColor(cell: usmallint, color: utinyint): void {
66
+ memory.write(WebRegisters.COLOR_BASE + cell, color);
67
+ }
68
+
69
+ function setColor(color: utinyint): void {
70
+ currentColor = color;
71
+ }
72
+
73
+ function setReverse(on: bool): void {
74
+ currentReverse = on;
75
+ }
76
+
77
+ function print(cell: usmallint, s: string): void {
78
+ for (let i: utinyint = 0; i < s.length; i++) {
79
+ place(cell + i, s[i]);
80
+ }
81
+ }
82
+
83
+ // Least significant digit first, from the right-hand end of the field.
84
+ // Division, unlike on the 6502 machines: wasm has a divide instruction,
85
+ // and the subtraction they use to avoid one is more code here.
86
+ function printNumber(cell: usmallint, value: usmallint, width: utinyint): void {
87
+ for (let i: utinyint = width; i > 0; i--) {
88
+ place(cell + i - 1, 48 + value % 10); // 48 = '0'
89
+ value = value / 10;
90
+ }
91
+ }
92
+ }
93
+
94
+ // The colours `text.setColor()` takes: the eight names every machine
95
+ // shares, with this machine's colour-RAM values.
96
+ export namespace TextColor {
97
+ const BLACK: utinyint = 0;
98
+ const WHITE: utinyint = 1;
99
+ const RED: utinyint = 2;
100
+ const CYAN: utinyint = 3;
101
+ const PURPLE: utinyint = 4;
102
+ const GREEN: utinyint = 5;
103
+ const BLUE: utinyint = 6;
104
+ const YELLOW: utinyint = 7;
105
+ }