arthur/js/raster.js

84 lines
3.1 KiB
JavaScript
Raw Normal View History

// Indexed flat-fill rasteriser.
//
// Canvas2D antialiases path fills, and antialiasing is exactly what the target
// idiom does not have: Animator Pro fills polygons into a 256-colour indexed
// raster with hard edges (csd_render_poly). A preview that antialiases would
// misrepresent the look it exists to judge, so this writes palette indices into
// a byte buffer with an even-odd scanline fill and expands to RGBA only at the
// very end.
export class IndexedRaster {
constructor(w, h) {
this.w = w; this.h = h;
this.buf = new Uint8Array(w * h);
}
clear(index) { this.buf.fill(index); }
// Even-odd scanline fill. Samples at pixel centres (y + 0.5), so a polygon
// edge landing exactly on a pixel boundary resolves consistently.
fillPoly(pts, index) {
const n = pts.length;
if (n < 3) return;
let minY = Infinity, maxY = -Infinity;
for (const p of pts) { if (p.y < minY) minY = p.y; if (p.y > maxY) maxY = p.y; }
const y0 = Math.max(0, Math.ceil(minY - 0.5));
const y1 = Math.min(this.h - 1, Math.floor(maxY - 0.5) + 1);
const xs = [];
for (let y = y0; y <= y1; y++) {
const sy = y + 0.5;
xs.length = 0;
for (let i = 0; i < n; i++) {
const a = pts[i], b = pts[(i + 1) % n];
if (a.y === b.y) continue;
const lo = Math.min(a.y, b.y), hi = Math.max(a.y, b.y);
if (sy < lo || sy >= hi) continue;
xs.push(a.x + ((sy - a.y) / (b.y - a.y)) * (b.x - a.x));
}
if (xs.length < 2) continue;
xs.sort((p, q) => p - q);
for (let k = 0; k + 1 < xs.length; k += 2) {
const xa = Math.max(0, Math.ceil(xs[k] - 0.5));
const xb = Math.min(this.w - 1, Math.floor(xs[k + 1] - 0.5));
const row = y * this.w;
for (let x = xa; x <= xb; x++) this.buf[row + x] = index;
}
}
}
fillDisc(cx, cy, r, index) {
const rr = r * r;
const y0 = Math.max(0, Math.floor(cy - r)), y1 = Math.min(this.h - 1, Math.ceil(cy + r));
const x0 = Math.max(0, Math.floor(cx - r)), x1 = Math.min(this.w - 1, Math.ceil(cx + r));
for (let y = y0; y <= y1; y++) {
for (let x = x0; x <= x1; x++) {
const dx = x + 0.5 - cx, dy = y + 0.5 - cy;
if (dx * dx + dy * dy <= rr) this.buf[y * this.w + x] = index;
}
}
}
// Expand indices through the palette into an ImageData at integer zoom.
// Nearest-neighbour by construction, so no filtering softens the result.
toImageData(palette, zoom = 1) {
const W = this.w * zoom, H = this.h * zoom;
const img = new ImageData(W, H);
const d = img.data;
const rgb = palette.map(hexToRgb);
for (let y = 0; y < H; y++) {
const srow = Math.floor(y / zoom) * this.w;
for (let x = 0; x < W; x++) {
const c = rgb[this.buf[srow + Math.floor(x / zoom)]] || [255, 0, 255];
const o = (y * W + x) * 4;
d[o] = c[0]; d[o + 1] = c[1]; d[o + 2] = c[2]; d[o + 3] = 255;
}
}
return img;
}
}
export function hexToRgb(hex) {
const s = hex.replace('#', '');
return [parseInt(s.slice(0, 2), 16), parseInt(s.slice(2, 4), 16), parseInt(s.slice(4, 6), 16)];
}