// 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 };