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