Skip to content

Commit ea8fce3

Browse files
committed
Add size selector, colour, and original-project credits to the demo
Size selector: a few width/height presets (40x12 up to 160x50) so the ASCII resolution is adjustable. worker.js now reads width/height from its own URL (query params app.js sets when it spawns it) instead of a hardcoded 80x25, and font-size is computed in JS from the actual viewport and grid dimensions rather than a flat CSS clamp(), so larger grids scale down to fit instead of overflowing. Colour: aalib's stdout driver only ever advertised AA_NORMAL_MASK, so aa_render() never picked any other attribute class -- attrbuffer was always 0, no matter what a consumer did with it. web/patches/aastdout.c also advertises DIM/BOLD/REVERSE (not BOLDFONT: that's a font-switch request meaningful only to curses/X11's alternate font resources, and not EXTENDED: see the existing note on why that mode is unsafe here) and emits a second attribute-class plane alongside the character plane. worker.js run-length-encodes each row into (attribute, text) segments; app.js renders them as styled spans -- a green phosphor-CRT palette in place of the original's VGA text attributes. Credits: links to the original aa-lib and bb project pages, the system-requirements blurb from bb's original page, its last-modified date, and the original bb.jpg logo (fetched from aa-project.sourceforge.net, last-modified 1997, matching the date in the blurb). Claude-Session: https://claude.ai/code/session_01F6QKUwwiAxLNkYn4dv5TfW
1 parent b1f7a8c commit ea8fce3

6 files changed

Lines changed: 289 additions & 33 deletions

File tree

web/Dockerfile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,14 @@ RUN for f in src/aaslnkbd.c src/aaslang.c src/aacurkbd.c src/aacurmou.c src/aacu
5959
# setjmp/longjmp aren't supported on wasm32 without the (non-standard)
6060
# exception-handling proposal, so drop the resize-interrupt trick entirely --
6161
# it's meaningless here regardless.
62+
# aastdout.c's driver only advertises AA_NORMAL_MASK, so aa_render() never
63+
# picks any other attribute class -- attrbuffer is always 0, no color/dim/
64+
# bold/reverse variation is possible no matter what the web page does with
65+
# it. Advertise DIM/BOLD/REVERSE too, and emit an attribute plane alongside
66+
# the character plane (see the file for the exact format web/worker.js
67+
# expects).
6268
COPY web/patches/aastdin.c src/aastdin.c
69+
COPY web/patches/aastdout.c src/aastdout.c
6370
RUN cp /usr/share/misc/config.guess /usr/share/misc/config.sub . \
6471
&& ./configure --host=wasm32-wasi --disable-shared --enable-static \
6572
&& make && make install
@@ -81,4 +88,4 @@ RUN cp /usr/share/misc/config.guess /usr/share/misc/config.sub . \
8188

8289
FROM scratch AS export
8390
COPY --from=build /bb-1.3.0/bb /bb.wasm
84-
COPY web/index.html web/wasi-shim.js web/worker.js web/app.js /
91+
COPY web/index.html web/wasi-shim.js web/worker.js web/app.js web/bb-logo.jpg /

web/app.js

Lines changed: 84 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,95 @@
11
const screen = document.getElementById("screen");
22
const status = document.getElementById("status");
3+
const sizesEl = document.getElementById("sizes");
4+
5+
const SIZES = [
6+
{ label: "40×12", w: 40, h: 12 },
7+
{ label: "80×25", w: 80, h: 25 },
8+
{ label: "120×37", w: 120, h: 37 },
9+
{ label: "160×50", w: 160, h: 50 },
10+
];
11+
const DEFAULT_SIZE = SIZES[1];
312

413
const FPS = 20;
514
const MAX_QUEUE = 4; // drop backlog rather than let display lag behind
615

7-
const queue = [];
8-
const worker = new Worker("worker.js", { type: "module" });
16+
const ESCAPE = { "&": "&amp;", "<": "&lt;", ">": "&gt;" };
17+
const escapeHtml = (s) => s.replace(/[&<>]/g, (c) => ESCAPE[c]);
18+
19+
// Attribute classes from aa_render(), per aalib's aamktabl.c: 0 normal,
20+
// 1 dim, 2 bold, 3 boldfont (unused here -- font-switching only makes
21+
// sense for curses/X11), 4 reverse. Anything else is a fill-table miss
22+
// (aarender.c falls back to an unused sentinel table slot); style it as
23+
// normal rather than give it special meaning.
24+
function rowsToHtml(rows) {
25+
let html = "";
26+
for (const row of rows) {
27+
for (const [attr, text] of row) {
28+
const cls = attr >= 0 && attr <= 4 ? attr : 0;
29+
html += `<span class="a${cls}">${escapeHtml(text)}</span>`;
30+
}
31+
html += "\n";
32+
}
33+
return html;
34+
}
35+
36+
let worker = null;
37+
let queue = [];
38+
let cols = DEFAULT_SIZE.w;
39+
let rows = DEFAULT_SIZE.h;
40+
41+
function startWorker(size) {
42+
if (worker) worker.terminate();
43+
queue = [];
44+
status.textContent = "Loading bb.wasm…";
45+
screen.after(status);
46+
worker = new Worker(`worker.js?w=${size.w}&h=${size.h}`, { type: "module" });
47+
worker.onmessage = (event) => {
48+
const msg = event.data;
49+
if (msg.type === "meta") {
50+
cols = msg.width;
51+
rows = msg.height;
52+
fitFont();
53+
return;
54+
}
55+
status.remove();
56+
queue.push(msg.rows);
57+
while (queue.length > MAX_QUEUE) queue.shift();
58+
};
59+
worker.onerror = (event) => {
60+
status.textContent = `Failed to run bb.wasm: ${event.message}`;
61+
screen.after(status);
62+
};
63+
}
64+
65+
function fitFont() {
66+
const reserved = ["logo", "sizes", "status"]
67+
.map((id) => document.getElementById(id))
68+
.reduce((sum, el) => sum + (el && el.isConnected ? el.getBoundingClientRect().height + 12 : 0), 0);
69+
const footer = document.querySelector("footer").getBoundingClientRect().height;
70+
const availWidth = window.innerWidth - 32;
71+
const availHeight = window.innerHeight - reserved - footer - 48;
72+
const fontByWidth = availWidth / cols / 0.6; // ~character-width:font-size ratio for monospace
73+
const fontByHeight = availHeight / rows / 1.2; // ~line-height:font-size ratio
74+
const fontSize = Math.max(3, Math.min(fontByWidth, fontByHeight, 22));
75+
screen.style.fontSize = `${fontSize}px`;
76+
}
977

10-
worker.onmessage = (event) => {
11-
status.remove();
12-
queue.push(event.data);
13-
while (queue.length > MAX_QUEUE) queue.shift();
14-
};
78+
for (const size of SIZES) {
79+
const button = document.createElement("button");
80+
button.textContent = size.label;
81+
button.setAttribute("aria-pressed", size === DEFAULT_SIZE);
82+
button.onclick = () => {
83+
for (const b of sizesEl.children) b.setAttribute("aria-pressed", b === button);
84+
startWorker(size);
85+
};
86+
sizesEl.appendChild(button);
87+
}
1588

16-
worker.onerror = (event) => {
17-
status.textContent = `Failed to run bb.wasm: ${event.message}`;
18-
};
89+
window.addEventListener("resize", fitFont);
90+
startWorker(DEFAULT_SIZE);
1991

2092
setInterval(() => {
21-
const frame = queue.shift();
22-
if (frame !== undefined) screen.textContent = frame;
93+
const rows = queue.shift();
94+
if (rows !== undefined) screen.innerHTML = rowsToHtml(rows);
2395
}, 1000 / FPS);

web/bb-logo.jpg

5.15 KB
Loading

web/index.html

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,84 @@
1717
flex-direction: column;
1818
align-items: center;
1919
justify-content: center;
20-
gap: 1rem;
20+
gap: 0.75rem;
21+
padding: 1rem 0;
22+
box-sizing: border-box;
23+
}
24+
#logo {
25+
image-rendering: pixelated;
26+
width: 119px;
27+
height: 87px;
2128
}
2229
pre#screen {
2330
margin: 0;
2431
line-height: 1.15;
25-
font-size: clamp(6px, 1.1vw, 14px);
2632
white-space: pre;
2733
}
34+
/* aalib attribute classes (see web/patches/aastdout.c): a green
35+
phosphor-CRT palette, in place of the original's VGA text
36+
attributes (normal/dim/bold/reverse). */
37+
.a0 { color: #4da34d; }
38+
.a1 { color: #235923; }
39+
.a2 { color: #aef7ae; }
40+
.a3 { color: #4da34d; }
41+
.a4 { color: #000; background: #4da34d; }
2842
#status { color: #666; }
43+
#sizes {
44+
display: flex;
45+
gap: 0.4rem;
46+
}
47+
#sizes button {
48+
background: #111;
49+
color: #999;
50+
border: 1px solid #333;
51+
font: inherit;
52+
font-size: 0.8rem;
53+
padding: 0.2rem 0.6rem;
54+
cursor: pointer;
55+
}
56+
#sizes button:hover { border-color: #666; color: #ccc; }
57+
#sizes button[aria-pressed="true"] { color: #0f0; border-color: #0f0; }
2958
footer {
3059
color: #444;
31-
font-size: 0.8rem;
60+
font-size: 0.75rem;
61+
text-align: center;
62+
max-width: 40rem;
63+
line-height: 1.4;
3264
}
3365
footer a { color: #666; }
66+
footer .blurb {
67+
color: #333;
68+
font-style: italic;
69+
margin-top: 0.4rem;
70+
}
3471
</style>
3572
</head>
3673
<body>
74+
<img id="logo" src="bb-logo.jpg" alt="bb logo">
75+
<div id="sizes"></div>
3776
<pre id="screen"></pre>
3877
<div id="status">Loading bb.wasm&hellip;</div>
39-
<footer><a href="https://github.com/chrisns/docker-bb">chrisns/docker-bb</a> — bb compiled to wasm32-wasi with zig cc, run client-side</footer>
78+
<footer>
79+
<div>
80+
<a href="https://github.com/chrisns/docker-bb">chrisns/docker-bb</a>
81+
— bb compiled to wasm32-wasi with zig cc, run client-side.
82+
Original: <a href="https://aa-project.sourceforge.net/aalib/">aa-lib</a>
83+
&middot;
84+
<a href="https://aa-project.sourceforge.net/bb/">bb</a>
85+
(last modified Wed Mar 26 1997)
86+
</div>
87+
<div class="blurb">
88+
&ldquo;This demo requires computer at least as fast as 486/33 with coprocesor.
89+
But speed of 486/66 or pentium is highly recomended (especially for hight
90+
resolution SVGA modes). This demo does not require real operating system -
91+
works even under MS-DOS. For dual monitor modes you need secondary
92+
hercules / MDA compatible card. To compile demo you need 32 or 64 bit ANSI C
93+
compiler (no it does not compile under Borland one) and libraries listed
94+
bellow. PC speaker driver eats lots of CPU so running it at computers
95+
slower than pentium is really not good idea.&rdquo;
96+
</div>
97+
</footer>
4098
<script type="module" src="app.js"></script>
4199
</body>
42100
</html>

web/patches/aastdout.c

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#include "config.h"
2+
#include <stdio.h>
3+
#include "aalib.h"
4+
#include "aaint.h"
5+
6+
/* Advertise DIM/BOLD/REVERSE in addition to NORMAL so aa_render() actually
7+
* varies c->attrbuffer per cell (aamktabl.c's ALOWED() only ever picks
8+
* attribute classes the driver's supported mask includes -- with just
9+
* AA_NORMAL_MASK, as upstream shipped, attrbuffer is always 0). Not
10+
* AA_BOLDFONT_MASK: that's a font-switch request for drivers with actual
11+
* alternate font resources (curses/X11), meaningless for plain text output.
12+
* Not AA_EXTENDED: see the -extended note in web/worker.js.
13+
*/
14+
static int stdout_init(__AA_CONST struct aa_hardware_params *p,__AA_CONST void *none, struct aa_hardware_params *dest, void **n)
15+
{
16+
__AA_CONST static struct aa_hardware_params def={NULL, AA_NORMAL_MASK | AA_DIM_MASK | AA_BOLD_MASK | AA_REVERSE_MASK};
17+
*dest=def;
18+
return 1;
19+
}
20+
static void stdout_uninit(aa_context * c)
21+
{
22+
}
23+
static void stdout_getsize(aa_context * c, int *width, int *height)
24+
{
25+
}
26+
27+
/* Frame format consumed by web/worker.js: a WIDTH*HEIGHT+HEIGHT-byte
28+
* character plane (as upstream), immediately followed by a same-sized
29+
* attribute plane (each cell's class index 0-4 as a single ASCII digit),
30+
* then the usual form-feed + newline trailer.
31+
*/
32+
static void stdout_flush(aa_context * c)
33+
{
34+
int x, y;
35+
for (y = 0; y < aa_scrheight(c); y++) {
36+
for (x = 0; x < aa_scrwidth(c); x++) {
37+
putc(c->textbuffer[x + y * aa_scrwidth(c)], stdout);
38+
}
39+
putc('\n', stdout);
40+
}
41+
for (y = 0; y < aa_scrheight(c); y++) {
42+
for (x = 0; x < aa_scrwidth(c); x++) {
43+
putc('0' + (c->attrbuffer[x + y * aa_scrwidth(c)] & 7), stdout);
44+
}
45+
putc('\n', stdout);
46+
}
47+
putc('\f', stdout);
48+
putc('\n', stdout);
49+
fflush(stdout);
50+
}
51+
static void stdout_gotoxy(aa_context * c, int x, int y)
52+
{
53+
}
54+
__AA_CONST struct aa_driver stdout_d =
55+
{
56+
"stdout", "Standard output driver",
57+
stdout_init,
58+
stdout_uninit,
59+
stdout_getsize,
60+
NULL,
61+
NULL,
62+
stdout_gotoxy,
63+
stdout_flush,
64+
NULL
65+
};
66+
67+
68+
static void stderr_flush(aa_context * c)
69+
{
70+
int x, y;
71+
for (y = 0; y < aa_scrheight(c); y++) {
72+
for (x = 0; x < aa_scrwidth(c); x++) {
73+
putc(c->textbuffer[x + y * aa_scrwidth(c)], stderr);
74+
}
75+
putc('\n', stderr);
76+
}
77+
putc('\f', stderr);
78+
putc('\n', stderr);
79+
fflush(stderr);
80+
}
81+
__AA_CONST struct aa_driver stderr_d =
82+
{
83+
"stderr", "Standard error driver",
84+
stdout_init,
85+
stdout_uninit,
86+
stdout_getsize,
87+
NULL,
88+
NULL,
89+
stdout_gotoxy,
90+
stderr_flush,
91+
NULL
92+
};

web/worker.js

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,48 @@
33
import { WASI } from "./wasi-shim.js";
44

55
// -width/-height pin aalib's stdout driver to an exact, known frame size.
6-
// aalib's stdout driver (aastdout.c) writes each frame as WIDTH*HEIGHT
7-
// content bytes (HEIGHT newline-terminated rows) followed by a form-feed
8-
// and a newline. No -extended: that mode uses aalib's full 256-value
9-
// palette as raw byte codes meant for the linux/curses drivers' custom
10-
// font remapping, not as printable text -- fed into textContent, those
11-
// bytes produce invalid-UTF-8 replacement glyphs and, worse, spurious
12-
// embedded newlines wherever a "pixel" happens to equal 0x0A or 0x0C.
13-
// The plain character set is safe, printable ASCII. Even so, count
14-
// bytes for framing rather than scanning for the trailer -- more robust
15-
// regardless of what's in the content.
16-
const WIDTH = 80;
17-
const HEIGHT = 25;
18-
const FRAME_BYTES = WIDTH * HEIGHT + HEIGHT;
19-
const FRAME_STRIDE = FRAME_BYTES + 2; // + trailing "\f\n"
6+
// The patched web/patches/aastdout.c (see that file) writes each frame as
7+
// two WIDTH*HEIGHT+HEIGHT-byte planes -- a character plane, then an
8+
// attribute-class plane (each cell's class 0-4 as an ASCII digit) -- then
9+
// a form-feed and a newline. No -extended: that mode uses aalib's full
10+
// 256-value character palette as raw byte codes meant for the linux/
11+
// curses drivers' custom font remapping, not as printable text -- fed
12+
// into the page, those bytes produce invalid-UTF-8 replacement glyphs
13+
// and, worse, spurious embedded newlines wherever a "pixel" happens to
14+
// equal 0x0A or 0x0C. The plain character set is safe, printable ASCII.
15+
// Even so, count bytes for framing rather than scanning for the
16+
// trailer -- more robust regardless of what's in the content.
17+
const params = new URL(self.location.href).searchParams;
18+
const WIDTH = Number(params.get("w")) || 80;
19+
const HEIGHT = Number(params.get("h")) || 25;
20+
const ROW_BYTES = WIDTH + 1; // + row newline
21+
const PLANE_BYTES = ROW_BYTES * HEIGHT;
22+
const FRAME_STRIDE = 2 * PLANE_BYTES + 2; // char plane + attr plane + "\f\n"
23+
24+
postMessage({ type: "meta", width: WIDTH, height: HEIGHT });
25+
26+
function toRows(frame) {
27+
const chars = frame.subarray(0, PLANE_BYTES);
28+
const attrs = frame.subarray(PLANE_BYTES, 2 * PLANE_BYTES);
29+
const decoder = new TextDecoder();
30+
const rows = [];
31+
for (let y = 0; y < HEIGHT; y++) {
32+
const rowOffset = y * ROW_BYTES;
33+
const segments = [];
34+
let start = 0;
35+
let currentAttr = attrs[rowOffset] - 0x30;
36+
for (let x = 1; x <= WIDTH; x++) {
37+
const attr = x < WIDTH ? attrs[rowOffset + x] - 0x30 : -1;
38+
if (attr !== currentAttr) {
39+
segments.push([currentAttr, decoder.decode(chars.subarray(rowOffset + start, rowOffset + x))]);
40+
start = x;
41+
currentAttr = attr;
42+
}
43+
}
44+
rows.push(segments);
45+
}
46+
return rows;
47+
}
2048

2149
let pending = new Uint8Array(0);
2250

@@ -28,8 +56,7 @@ function append(bytes) {
2856

2957
let offset = 0;
3058
while (offset + FRAME_STRIDE <= pending.length) {
31-
const frame = pending.subarray(offset, offset + FRAME_BYTES);
32-
postMessage(new TextDecoder().decode(frame));
59+
postMessage({ type: "frame", rows: toRows(pending.subarray(offset, offset + FRAME_STRIDE)) });
3360
offset += FRAME_STRIDE;
3461
}
3562
pending = pending.subarray(offset);

0 commit comments

Comments
 (0)