You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
222 lines
6.7 KiB
222 lines
6.7 KiB
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>MonoDisplay Test</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
background: #111;
|
|
color: #ccc;
|
|
font-family: monospace;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 100vh;
|
|
gap: 20px;
|
|
padding: 20px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 14px;
|
|
letter-spacing: 0.15em;
|
|
color: #555;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
/*
|
|
* Display container: holds the canvas at a fixed 2:1 aspect ratio (160:80).
|
|
* Canvas internal resolution stays at 160×80 logical pixels.
|
|
* CSS stretches it to fill this box — image-rendering: pixelated keeps crisp edges.
|
|
* To resize the on-screen display: change max-width here. Driver is unaffected.
|
|
*/
|
|
#display-box {
|
|
width: 100%;
|
|
max-width: 800px;
|
|
aspect-ratio: 2 / 1;
|
|
background: #000;
|
|
border: 2px solid #333;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
#canvas_root {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
image-rendering: pixelated;
|
|
image-rendering: crisp-edges; /* Firefox */
|
|
}
|
|
|
|
#resolution {
|
|
position: absolute;
|
|
bottom: 6px;
|
|
right: 8px;
|
|
font-size: 10px;
|
|
color: #333;
|
|
pointer-events: none;
|
|
}
|
|
|
|
#controls {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
justify-content: center;
|
|
}
|
|
|
|
#controls button {
|
|
background: #1a1a1a;
|
|
color: #888;
|
|
border: 1px solid #333;
|
|
border-radius: 3px;
|
|
padding: 6px 14px;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
transition: background 0.1s, color 0.1s;
|
|
}
|
|
|
|
#controls button:hover { background: #222; color: #bbb; }
|
|
#controls button.active { background: #1e3a1e; color: #33ff66; border-color: #33ff66; }
|
|
|
|
#status { font-size: 11px; color: #444; min-height: 1em; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<h1>MonoDisplay — 160 × 80</h1>
|
|
|
|
<div id="display-box">
|
|
<!-- Internal res = displayWidth × displayHeight (160×80). CSS does the upscaling. -->
|
|
<canvas id="canvas_root" width="160" height="80"></canvas>
|
|
<div id="resolution">160 × 80 @ 25fps</div>
|
|
</div>
|
|
|
|
<div id="controls"></div>
|
|
<div id="status">select a demo</div>
|
|
|
|
<!--
|
|
Build the library first: bun run build
|
|
Produces public/mono-display.js — exposes all types on window.MonoDisplay.
|
|
Then serve index.html from any static file server (or file:// directly).
|
|
-->
|
|
<script src="./public/mono-display.js"></script>
|
|
|
|
<script>
|
|
// -------------------------------------------------------------------------
|
|
// Demo helpers — build synthetic .bin ArrayBuffers for all 4 content types
|
|
// -------------------------------------------------------------------------
|
|
|
|
const MAGIC = new Uint8Array([0x4d, 0x4f, 0x4e, 0x4f]); // "MONO"
|
|
|
|
function makeHeader(contentType, width, height) {
|
|
const h = new Uint8Array(10);
|
|
h.set(MAGIC, 0);
|
|
h[4] = 1; // version
|
|
h[5] = contentType;
|
|
new DataView(h.buffer).setUint16(6, width, true);
|
|
new DataView(h.buffer).setUint16(8, height, true);
|
|
return h;
|
|
}
|
|
|
|
function concat(...parts) {
|
|
const total = parts.reduce((s, p) => s + p.byteLength, 0);
|
|
const out = new Uint8Array(total);
|
|
let off = 0;
|
|
for (const p of parts) { out.set(new Uint8Array(p), off); off += p.byteLength; }
|
|
return out.buffer;
|
|
}
|
|
|
|
function u16le(n) { const b = new Uint8Array(2); new DataView(b.buffer).setUint16(0, n, true); return b; }
|
|
function u32le(n) { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, n, true); return b; }
|
|
|
|
// 160×80 checkerboard — static
|
|
function makeCheckerboard() {
|
|
const W = 160, H = 80;
|
|
const pixels = new Uint8Array(W * H);
|
|
for (let y = 0; y < H; y++)
|
|
for (let x = 0; x < W; x++)
|
|
pixels[y * W + x] = (x + y) % 2;
|
|
return concat(makeHeader(0, W, H), pixels);
|
|
}
|
|
|
|
// 160×80 two-frame blink — animation
|
|
function makeAnimation() {
|
|
const W = 160, H = 80;
|
|
function frame(fill, durationMs) {
|
|
return concat(u32le(durationMs), new Uint8Array(W * H).fill(fill));
|
|
}
|
|
return concat(makeHeader(1, W, H), u16le(2), frame(1, 200), frame(0, 200));
|
|
}
|
|
|
|
// Text content type
|
|
function makeTextBin(text, align /* 0=left 1=center 2=right */) {
|
|
const enc = new TextEncoder().encode(text);
|
|
return concat(
|
|
makeHeader(2, 0, 0),
|
|
u16le(enc.byteLength), enc,
|
|
new Uint8Array([0, align ?? 1]) // fontId=0, align
|
|
);
|
|
}
|
|
|
|
// Scrolltext content type
|
|
function makeScrollTextBin(text, speedPps, direction /* 0=left 1=right */) {
|
|
const enc = new TextEncoder().encode(text);
|
|
return concat(
|
|
makeHeader(3, 0, 0),
|
|
u16le(enc.byteLength), enc,
|
|
u16le(speedPps ?? 60),
|
|
new Uint8Array([direction ?? 0])
|
|
);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Driver — 160×80, 25fps, green-on-dark
|
|
// -------------------------------------------------------------------------
|
|
|
|
const { MonoDisplayDriver } = window.MonoDisplay;
|
|
|
|
const driver = new MonoDisplayDriver("canvas_root", {
|
|
onColor: "#33ff66",
|
|
offColor: "#0a0a0a",
|
|
fps: 25,
|
|
// displayWidth/Height default to 160×80
|
|
// scale stays 1 — CSS handles visual upscaling
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Demo catalogue
|
|
// -------------------------------------------------------------------------
|
|
|
|
const demos = [
|
|
{ label: "Checkerboard (static)", make: makeCheckerboard },
|
|
{ label: "Blink (animation)", make: makeAnimation },
|
|
{ label: "Text (centered)", make: () => makeTextBin("Hello, World!", 1) },
|
|
{ label: "Scrolltext (left)", make: () => makeScrollTextBin("MONO DISPLAY — scrolling ticker — 日本語テスト 🚀 ", 80, 0) },
|
|
];
|
|
|
|
const controls = document.getElementById("controls");
|
|
let activeBtn = null;
|
|
|
|
for (const demo of demos) {
|
|
const btn = document.createElement("button");
|
|
btn.textContent = demo.label;
|
|
btn.onclick = () => {
|
|
if (activeBtn) activeBtn.classList.remove("active");
|
|
btn.classList.add("active");
|
|
activeBtn = btn;
|
|
driver.load(() => Promise.resolve(demo.make()));
|
|
};
|
|
controls.appendChild(btn);
|
|
}
|
|
|
|
// Auto-load first demo
|
|
controls.firstElementChild.click();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|
|
|