84 lines
3.1 KiB
JavaScript
84 lines
3.1 KiB
JavaScript
|
|
// 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)];
|
||
|
|
}
|