103 lines
3.0 KiB
JavaScript
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 };
|