feat: update for browser run

pull/1/head
flop 4 weeks ago
parent 27deb4cbf8
commit 4d9bb7d26a
  1. 222
      ts/index.html
  2. 6
      ts/package.json
  3. 428
      ts/public/bundle.js
  4. 9
      ts/src/browser.ts
  5. 43
      ts/src/library.ts

@ -0,0 +1,222 @@
<!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>

@ -1,7 +1,11 @@
{
"name": "libmonoformat",
"module": "index.ts",
"module": "src/index.ts",
"type": "module",
"scripts": {
"build": "bun build src/browser.ts --target browser --outfile public/mono-display.js",
"test": "bun test"
},
"devDependencies": {
"@types/bun": "latest"
},

@ -0,0 +1,428 @@
// src/library.ts
class BinaryReader {
view;
pos = 0;
constructor(buffer) {
this.view = new DataView(buffer);
}
get offset() {
return this.pos;
}
get byteLength() {
return this.view.byteLength;
}
get remaining() {
return this.view.byteLength - this.pos;
}
readByte() {
this.#assertRemaining(1);
return this.view.getUint8(this.pos++);
}
readUint16() {
this.#assertRemaining(2);
const v = this.view.getUint16(this.pos, true);
this.pos += 2;
return v;
}
readShort() {
return this.readUint16();
}
readUint32() {
this.#assertRemaining(4);
const v = this.view.getUint32(this.pos, true);
this.pos += 4;
return v;
}
readUint64() {
this.#assertRemaining(8);
const lo = BigInt(this.view.getUint32(this.pos, true));
const hi = BigInt(this.view.getUint32(this.pos + 4, true));
this.pos += 8;
return hi << 32n | lo;
}
readBytes(n) {
this.#assertRemaining(n);
const slice = new Uint8Array(this.view.buffer, this.pos, n);
this.pos += n;
return slice;
}
readUtf8(n) {
return new TextDecoder().decode(this.readBytes(n));
}
seek(pos) {
if (pos < 0 || pos > this.view.byteLength) {
throw new RangeError(`seek(${pos}) out of bounds [0, ${this.view.byteLength}]`);
}
this.pos = pos;
}
#assertRemaining(n) {
if (this.remaining < n) {
throw new RangeError(`BinaryReader: need ${n} byte(s) at offset ${this.pos}, only ${this.remaining} remain`);
}
}
}
var MAGIC = 1330532173;
var ContentTypeByte = {
0: "static",
1: "animation",
2: "text",
3: "scrolltext"
};
class MonoDisplayParser {
parse(buffer) {
const r = new BinaryReader(buffer);
const magic = r.readUint32();
if (magic !== MAGIC) {
throw new Error(`Invalid magic 0x${magic.toString(16).toUpperCase()} — expected 0x4F4E4F4D ("MONO")`);
}
const version = r.readByte();
if (version !== 1) {
throw new Error(`Unsupported format version ${version}`);
}
const contentTypeByte = r.readByte();
const contentType = ContentTypeByte[contentTypeByte];
if (!contentType) {
throw new Error(`Unknown content type byte 0x${contentTypeByte.toString(16)}`);
}
const width = r.readUint16();
const height = r.readUint16();
switch (contentType) {
case "static":
return this.#parseStatic(r, width, height);
case "animation":
return this.#parseAnimation(r, width, height);
case "text":
return this.#parseText(r);
case "scrolltext":
return this.#parseScrollText(r);
}
}
#parseStatic(r, width, height) {
const raw = r.readBytes(width * height);
return { type: "static", width, height, pixels: new Uint8Array(raw) };
}
#parseAnimation(r, width, height) {
const frameCount = r.readUint16();
const frames = [];
for (let i = 0;i < frameCount; i++) {
const durationMs = r.readUint32();
const raw = r.readBytes(width * height);
frames.push({ durationMs, pixels: new Uint8Array(raw) });
}
return { type: "animation", width, height, frames };
}
#parseText(r) {
const textLen = r.readUint16();
const text = r.readUtf8(textLen);
const fontId = r.readByte();
const alignByte = r.readByte();
const align = alignByte === 1 ? 1 : alignByte === 2 ? 2 : 0;
return { type: "text", text, fontId, align };
}
#parseScrollText(r) {
const textLen = r.readUint16();
const text = r.readUtf8(textLen);
const speedPps = r.readUint16();
const dirByte = r.readByte();
const direction = dirByte === 1 ? 1 : 0;
return { type: "scrolltext", text, speedPps, direction };
}
}
class MonoDisplayRenderer {
ctx;
opts;
animFrame = 0;
animTimer = null;
scrollX = 0;
scrollRaf = null;
scrollLastTs = null;
constructor(canvas, opts) {
const ctx = canvas.getContext("2d");
if (!ctx)
throw new Error("Cannot get 2D canvas context");
this.ctx = ctx;
this.opts = opts;
}
stop() {
if (this.animTimer !== null) {
clearTimeout(this.animTimer);
this.animTimer = null;
}
if (this.scrollRaf !== null) {
cancelAnimationFrame(this.scrollRaf);
this.scrollRaf = null;
}
}
render(content) {
this.stop();
switch (content.type) {
case "static":
this.#renderStatic(content);
break;
case "animation":
this.#startAnimation(content);
break;
case "text":
this.#renderText(content);
break;
case "scrolltext":
this.#startScrollText(content);
break;
}
}
#resizeCanvas(w, h) {
const s = this.opts.scale;
this.ctx.canvas.width = w * s;
this.ctx.canvas.height = h * s;
}
#renderPixels(pixels, width, height) {
const { onColor, offColor, scale: s } = this.opts;
const ctx = this.ctx;
ctx.fillStyle = offColor;
ctx.fillRect(0, 0, width * s, height * s);
ctx.fillStyle = onColor;
for (let y = 0;y < height; y++) {
for (let x = 0;x < width; x++) {
if (pixels[y * width + x]) {
ctx.fillRect(x * s, y * s, s, s);
}
}
}
}
#renderStatic(content) {
this.#resizeCanvas(content.width, content.height);
this.#renderPixels(content.pixels, content.width, content.height);
}
#startAnimation(content) {
this.#resizeCanvas(content.width, content.height);
this.animFrame = 0;
const tick = () => {
if (content.frames.length === 0)
return;
const frame = content.frames[this.animFrame];
this.#renderPixels(frame.pixels, content.width, content.height);
this.animFrame++;
if (this.animFrame >= content.frames.length) {
if (!this.opts.loop)
return;
this.animFrame = 0;
}
this.animTimer = setTimeout(tick, frame.durationMs);
};
tick();
}
#renderText(content) {
const ctx = this.ctx;
const { displayWidth: dw, displayHeight: dh, scale: s, offColor, onColor } = this.opts;
ctx.canvas.width = dw * s;
ctx.canvas.height = dh * s;
ctx.fillStyle = offColor;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = onColor;
const fontSize = Math.max(8, Math.floor(dh * s * 0.6));
ctx.font = `${fontSize}px monospace`;
ctx.textBaseline = "middle";
const padding = 4 * s;
ctx.fillText(content.text, padding, ctx.canvas.height / 2);
}
#startScrollText(content) {
const ctx = this.ctx;
const { displayWidth: dw, displayHeight: dh, scale: s } = this.opts;
ctx.canvas.width = dw * s;
ctx.canvas.height = dh * s;
this.scrollX = content.direction === 1 ? -(content.text.length * 8 * s) : ctx.canvas.width;
const tick = (ts) => {
const dt = this.scrollLastTs !== null ? (ts - this.scrollLastTs) / 1000 : 0;
this.scrollLastTs = ts;
const { displayWidth: dw2, displayHeight: dh2, scale: s2, offColor, onColor } = this.opts;
const delta = content.speedPps * s2 * dt * (content.direction === 1 ? 1 : -1);
this.scrollX += delta;
const cpCount = [...content.text].length;
const textWidth = cpCount * 8 * s2;
if (content.direction === 0 && this.scrollX < -textWidth) {
this.scrollX = ctx.canvas.width;
} else if (content.direction === 1 && this.scrollX > ctx.canvas.width) {
this.scrollX = -textWidth;
}
ctx.fillStyle = offColor;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = onColor;
const fontSize = Math.max(8, Math.floor(dh2 * s2 * 0.6));
ctx.font = `${fontSize}px monospace`;
ctx.textBaseline = "middle";
ctx.fillText(content.text, this.scrollX, ctx.canvas.height / 2);
this.scrollRaf = requestAnimationFrame(tick);
};
this.scrollRaf = requestAnimationFrame(tick);
}
}
class MonoDisplayDriver {
canvas;
opts;
parser;
renderer = null;
constructor(canvasId, options = {}) {
const el = document.getElementById(canvasId);
if (!el || el.tagName !== "CANVAS") {
throw new Error(`MonoDisplayDriver: element #${canvasId} is not a <canvas>`);
}
this.canvas = el;
this.opts = {
onColor: options.onColor ?? "#ffffff",
offColor: options.offColor ?? "#000000",
scale: options.scale ?? 1,
displayWidth: options.displayWidth ?? 160,
displayHeight: options.displayHeight ?? 80,
loop: options.loop ?? true,
onError: options.onError ?? ((e) => {
throw e;
})
};
this.parser = new MonoDisplayParser;
}
async load(loader) {
try {
const buffer = await loader();
const content = this.parser.parse(buffer);
if (!this.renderer) {
this.renderer = new MonoDisplayRenderer(this.canvas, this.opts);
}
this.renderer.render(content);
} catch (err) {
this.opts.onError(err instanceof Error ? err : new Error(String(err)));
}
}
stop() {
this.renderer?.stop();
}
}
function buildBinBuffer(content) {
if (content.type !== "static") {
throw new Error(`buildBinBuffer: encoding "${content.type}" not yet implemented`);
}
const headerSize = 4 + 1 + 1 + 2 + 2;
const pixelSize = content.width * content.height;
const buf = new ArrayBuffer(headerSize + pixelSize);
const view = new DataView(buf);
view.setUint32(0, MAGIC, true);
view.setUint8(4, 1);
view.setUint8(5, 0);
view.setUint16(6, content.width, true);
view.setUint16(8, content.height, true);
new Uint8Array(buf, headerSize).set(content.pixels);
return buf;
}
// demo.ts
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 buildBinBuffer({ type: "static", width: W, height: H, pixels });
}
function makeAnimation() {
const W = 160, H = 80;
const on = new Uint8Array(W * H).fill(1);
const off = new Uint8Array(W * H).fill(0);
const MAGIC2 = new Uint8Array([77, 79, 78, 79]);
const header = new Uint8Array(10);
header.set(MAGIC2, 0);
header[4] = 1;
header[5] = 1;
new DataView(header.buffer).setUint16(6, W, true);
new DataView(header.buffer).setUint16(8, H, true);
const frameCount = new Uint8Array(2);
new DataView(frameCount.buffer).setUint16(0, 2, true);
function frame(pixels, durationMs) {
const dur = new Uint8Array(4);
new DataView(dur.buffer).setUint32(0, durationMs, true);
const f = new Uint8Array(4 + pixels.byteLength);
f.set(dur, 0);
f.set(pixels, 4);
return f;
}
const parts = [header, frameCount, frame(on, 500), frame(off, 500)];
const total = parts.reduce((s, p) => s + p.byteLength, 0);
const buf = new Uint8Array(total);
let off2 = 0;
for (const p of parts) {
buf.set(p, off2);
off2 += p.byteLength;
}
return buf.buffer;
}
function makeTextBin(text, align = 1) {
const enc = new TextEncoder().encode(text);
const MAGIC2 = new Uint8Array([77, 79, 78, 79]);
const header = new Uint8Array(10);
header.set(MAGIC2, 0);
header[4] = 1;
header[5] = 2;
const textLen = new Uint8Array(2);
new DataView(textLen.buffer).setUint16(0, enc.byteLength, true);
const tail = new Uint8Array([0, align]);
const total = 10 + 2 + enc.byteLength + 2;
const out = new Uint8Array(total);
let o = 0;
out.set(header, o);
o += 10;
out.set(textLen, o);
o += 2;
out.set(enc, o);
o += enc.byteLength;
out.set(tail, o);
return out.buffer;
}
function makeScrollTextBin(text, speedPps = 60, direction = 0) {
const enc = new TextEncoder().encode(text);
const MAGIC2 = new Uint8Array([77, 79, 78, 79]);
const header = new Uint8Array(10);
header.set(MAGIC2, 0);
header[4] = 1;
header[5] = 3;
const textLen = new Uint8Array(2);
new DataView(textLen.buffer).setUint16(0, enc.byteLength, true);
const speed = new Uint8Array(2);
new DataView(speed.buffer).setUint16(0, speedPps, true);
const total = 10 + 2 + enc.byteLength + 2 + 1;
const out = new Uint8Array(total);
let o = 0;
out.set(header, o);
o += 10;
out.set(textLen, o);
o += 2;
out.set(enc, o);
o += enc.byteLength;
out.set(speed, o);
o += 2;
out[o] = direction;
return out.buffer;
}
var driver = new MonoDisplayDriver("canvas_root", {
onColor: "#33ff66",
offColor: "#0a0a0a"
});
var demos = {
"Checkerboard (static)": makeCheckerboard,
"Blink (animation)": makeAnimation,
"Text (centered)": () => makeTextBin("Hello, World! \uD83D\uDFE2", 1),
"Scrolltext (left)": () => makeScrollTextBin("MONO DISPLAY — scrolling ticker test — 日本語テスト \uD83D\uDE80 ", 80, 0)
};
var controls = document.getElementById("controls");
var active = null;
for (const [label, factory] of Object.entries(demos)) {
const btn = document.createElement("button");
btn.textContent = label;
btn.onclick = () => {
active?.classList.remove("active");
btn.classList.add("active");
active = btn;
driver.load(() => Promise.resolve(factory()));
};
controls.appendChild(btn);
}
controls.firstElementChild?.click();

@ -0,0 +1,9 @@
// browser.ts — IIFE-style browser entry point
// Built by: bun build src/browser.ts --target browser --outfile public/mono-display.js
// Attaches everything to window.MonoDisplay so inline <script> tags can use it
// without ES module syntax.
import * as MonoDisplay from "./index";
// Expose on globalThis (= window in browser, globalThis elsewhere)
(globalThis as typeof globalThis & { MonoDisplay: typeof MonoDisplay }).MonoDisplay = MonoDisplay;

@ -114,6 +114,15 @@ export interface MonoDisplayDriverOptions {
* Set false to stop after one pass.
*/
loop?: boolean;
/**
* Target render rate in frames per second for continuous content
* (scrolltext, animation). Default 25. The browser rAF / setTimeout loop
* skips drawing if the frame would land earlier than 1000/fps ms after the
* previous draw, keeping CPU/GPU load close to the target rate.
* Note: animation frame *duration* from the file takes precedence when it
* is longer than the render interval (slow animations stay slow).
*/
fps?: number;
/**
* Callback fired whenever the driver encounters a parse or render error.
* If omitted, errors are thrown.
@ -338,7 +347,10 @@ export class MonoDisplayRenderer {
// ScrollText state
private scrollX: number = 0;
private scrollRaf: number | null = null;
// Tracks last rAF timestamp for motion integration (runs every rAF)
private scrollLastTs: number | null = null;
// Tracks last actual draw timestamp for fps throttle
private scrollLastDrawTs: number | null = null;
constructor(canvas: HTMLCanvasElement, opts: Required<MonoDisplayDriverOptions>) {
const ctx = canvas.getContext("2d");
@ -357,6 +369,8 @@ export class MonoDisplayRenderer {
cancelAnimationFrame(this.scrollRaf);
this.scrollRaf = null;
}
this.scrollLastTs = null;
this.scrollLastDrawTs = null;
}
render(content: DisplayContent): void {
@ -409,7 +423,9 @@ export class MonoDisplayRenderer {
if (!this.opts.loop) return;
this.animFrame = 0;
}
this.animTimer = setTimeout(tick, frame.durationMs) as unknown as number;
// Clamp to fps budget: never draw faster than 1000/fps ms per frame
const minInterval = 1000 / this.opts.fps;
this.animTimer = setTimeout(tick, Math.max(frame.durationMs, minInterval)) as unknown as number;
};
tick();
}
@ -441,10 +457,13 @@ export class MonoDisplayRenderer {
this.scrollX = content.direction === 1 ? -(content.text.length * 8 * s) : ctx.canvas.width;
const tick = (ts: number) => {
// --- motion integration runs every rAF for smooth position accumulation ---
const dt = this.scrollLastTs !== null ? (ts - this.scrollLastTs) / 1000 : 0;
this.scrollLastTs = ts;
const { displayWidth: dw, displayHeight: dh, scale: s, offColor, onColor } = this.opts;
const { displayWidth: dw, displayHeight: dh, scale: s, offColor, onColor, fps } = this.opts;
const frameInterval = 1000 / fps; // e.g. 40ms at 25fps
// speedPps is in logical pixels/s; scale to canvas pixels
const delta = content.speedPps * s * dt * (content.direction === 1 ? 1 : -1);
this.scrollX += delta;
@ -458,13 +477,18 @@ export class MonoDisplayRenderer {
this.scrollX = -textWidth;
}
ctx.fillStyle = offColor;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = onColor;
const fontSize = Math.max(8, Math.floor(dh * s * 0.6));
ctx.font = `${fontSize}px monospace`;
ctx.textBaseline = "middle";
ctx.fillText(content.text, this.scrollX, ctx.canvas.height / 2);
// --- draw only when fps budget allows (~25Hz) ---
const sinceLastDraw = this.scrollLastDrawTs !== null ? ts - this.scrollLastDrawTs : Infinity;
if (sinceLastDraw >= frameInterval) {
this.scrollLastDrawTs = ts;
ctx.fillStyle = offColor;
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = onColor;
const fontSize = Math.max(8, Math.floor(dh * s * 0.6));
ctx.font = `${fontSize}px monospace`;
ctx.textBaseline = "middle";
ctx.fillText(content.text, this.scrollX, ctx.canvas.height / 2);
}
this.scrollRaf = requestAnimationFrame(tick);
};
@ -510,6 +534,7 @@ export class MonoDisplayDriver {
scale: options.scale ?? 1,
displayWidth: options.displayWidth ?? 160,
displayHeight: options.displayHeight ?? 80,
fps: options.fps ?? 25,
loop: options.loop ?? true,
onError: options.onError ?? ((e: Error) => { throw e; }),
};

Loading…
Cancel
Save