fertig-classic-games/tools/lib/canvasStub.js

103 lines
3.0 KiB
JavaScript

// A canvas just real enough for ExcitebikeRaster.js to run under Node.
//
// The raster stage is the one Excitebike module that touches the DOM, and
// without this it could only ever be checked in a browser. It uses a very small
// slice of the API — createImageData, putImageData, getImageData — so stubbing
// it lets tools/verifyExcitebike.js assert on the actual pixels the game paints:
// that every colour on the track comes out of the NES master palette, and that
// a rasterised track is the size the model says it is.
//
// Importing this module installs the stub as a side effect. Import it before
// anything that reaches for `document`.
class StubImageData {
constructor(width, height) {
this.width = width;
this.height = height;
this.data = new Uint8ClampedArray(width * height * 4);
}
}
class StubContext2D {
constructor(canvas) {
this.canvas = canvas;
this.pixels = new Uint8ClampedArray(canvas.width * canvas.height * 4);
}
createImageData(w, h) {
return new StubImageData(w, h);
}
putImageData(img, dx = 0, dy = 0) {
const { width: cw, height: ch } = this.canvas;
for (let y = 0; y < img.height; y += 1) {
const ty = dy + y;
if (ty < 0 || ty >= ch) continue;
for (let x = 0; x < img.width; x += 1) {
const tx = dx + x;
if (tx < 0 || tx >= cw) continue;
const s = (y * img.width + x) * 4;
const d = (ty * cw + tx) * 4;
this.pixels[d] = img.data[s];
this.pixels[d + 1] = img.data[s + 1];
this.pixels[d + 2] = img.data[s + 2];
this.pixels[d + 3] = img.data[s + 3];
}
}
}
getImageData(sx = 0, sy = 0, sw = this.canvas.width, sh = this.canvas.height) {
const out = new StubImageData(sw, sh);
const { width: cw } = this.canvas;
for (let y = 0; y < sh; y += 1) {
for (let x = 0; x < sw; x += 1) {
const s = ((sy + y) * cw + (sx + x)) * 4;
const d = (y * sw + x) * 4;
out.data[d] = this.pixels[s];
out.data[d + 1] = this.pixels[s + 1];
out.data[d + 2] = this.pixels[s + 2];
out.data[d + 3] = this.pixels[s + 3];
}
}
return out;
}
}
class StubCanvas {
constructor() {
this._w = 0;
this._h = 0;
this._ctx = null;
}
get width() { return this._w; }
set width(v) { this._w = v | 0; this._ctx = null; }
get height() { return this._h; }
set height(v) { this._h = v | 0; this._ctx = null; }
getContext(kind) {
if (kind !== '2d') return null;
if (!this._ctx) this._ctx = new StubContext2D(this);
return this._ctx;
}
}
if (typeof globalThis.document === 'undefined') {
globalThis.document = {
createElement(tag) {
if (tag !== 'canvas') throw new Error(`canvasStub only makes canvases, not <${tag}>`);
return new StubCanvas();
},
};
}
/** Read a canvas back as flat RGBA — the verifier's window onto the pixels. */
export function canvasPixels(canvas) {
return canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data;
}
export { StubCanvas, StubContext2D, StubImageData };