raster.js
7.4 kB · javascript · 223 lines
1import { CLEAR, board } from "./frame.js";23const TAU = 2 * Math.PI;4const MAX = Number.MAX_VALUE;56// GEOMETRY78const fmin = (a, b) => (b < a || a !== a ? b : a);9const fmax = (a, b) => (b > a || a !== a ? b : a);10const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);1112function boxSdf(px, py, cx, cy, hw, hh) {13 const qx = Math.abs(px - cx) - hw;14 const qy = Math.abs(py - cy) - hh;15 const ox = fmax(qx, 0);16 const oy = fmax(qy, 0);17 return Math.sqrt(ox * ox + oy * oy) + fmin(fmax(qx, qy), 0);18}1920function segmentSdf(px, py, a, b) {21 const vx = b[0] - a[0];22 const vy = b[1] - a[1];23 const wx = px - a[0];24 const wy = py - a[1];25 const len = vx * vx + vy * vy;26 const t = len <= Number.EPSILON ? 0 : clamp((wx * vx + wy * vy) / len, 0, 1);27 const dx = wx - t * vx;28 const dy = wy - t * vy;29 return Math.sqrt(dx * dx + dy * dy);30}3132function polygonSdf(px, py, pts) {33 let dist = MAX;34 let inside = false;35 for (let i = 0; i < pts.length; i++) {36 const a = pts[i];37 const b = pts[(i + 1) % pts.length];38 dist = fmin(dist, segmentSdf(px, py, a, b));39 if (a[1] > py !== b[1] > py && px < a[0] + ((py - a[1]) / (b[1] - a[1])) * (b[0] - a[0])) inside = !inside;40 }41 return inside ? -dist : dist;42}4344function bounds(pts) {45 let x0 = MAX;46 let y0 = MAX;47 let x1 = -MAX;48 let y1 = -MAX;49 for (const p of pts) {50 x0 = fmin(x0, p[0]);51 y0 = fmin(y0, p[1]);52 x1 = fmax(x1, p[0]);53 y1 = fmax(y1, p[1]);54 }55 return [x0, y0, x1, y1];56}5758// RASTER5960export function raster(width, height, ground = CLEAR) {61 const colors = new Uint8ClampedArray(width * height * 4);62 new Uint32Array(colors.buffer).fill(new Uint32Array(Uint8ClampedArray.from(ground.slice(0, 4)).buffer)[0]);63 const swatch = [0, 0, 0, 0];6465 function blend(x, y, c, cover) {66 if (x >= width || y >= height) return;67 const a = (c[3] / 255) * clamp(cover, 0, 1);68 if (a <= 0) return;69 const i = (y * width + x) * 4;70 const keep = 1 - a;71 colors[i] = Math.round(c[0] * a + colors[i] * keep);72 colors[i + 1] = Math.round(c[1] * a + colors[i + 1] * keep);73 colors[i + 2] = Math.round(c[2] * a + colors[i + 2] * keep);74 colors[i + 3] = Math.round((a + (colors[i + 3] / 255) * keep) * 255);75 }7677 function shade(ax0, ay0, ax1, ay1, c, sdf) {78 const x0 = fmax(Math.floor(ax0 - 1), 0);79 const y0 = fmax(Math.floor(ay0 - 1), 0);80 const x1 = Math.min(fmax(Math.ceil(ax1 + 1), 0), width);81 const y1 = Math.min(fmax(Math.ceil(ay1 + 1), 0), height);82 for (let py = y0; py < y1; py++) {83 for (let px = x0; px < x1; px++) blend(px, py, c, 0.5 - sdf(px + 0.5, py + 0.5));84 }85 }8687 function polygon(pts, c) {88 if (pts.length < 3) return;89 const [x0, y0, x1, y1] = bounds(pts);90 shade(x0, y0, x1, y1, c, (px, py) => polygonSdf(px, py, pts));91 }9293 return {94 ...board(width, height),95 pixels: () => ({ shape: [height, width], colors }),96 rect(x, y, w, h, c) {97 const cx = x + w / 2;98 const cy = y + h / 2;99 const hw = w / 2;100 const hh = h / 2;101 shade(x, y, x + w, y + h, c, (px, py) => boxSdf(px, py, cx, cy, hw, hh));102 },103 round_rect(x, y, w, h, r, c) {104 const cx = x + w / 2;105 const cy = y + h / 2;106 const rr = fmax(fmin(fmin(r, w / 2), h / 2), 0);107 const hw = w / 2 - rr;108 const hh = h / 2 - rr;109 shade(x, y, x + w, y + h, c, (px, py) => boxSdf(px, py, cx, cy, hw, hh) - rr);110 },111 disc(cx, cy, r, c) {112 shade(cx - r, cy - r, cx + r, cy + r, c, (px, py) => {113 const dx = px - cx;114 const dy = py - cy;115 return Math.sqrt(dx * dx + dy * dy) - r;116 });117 },118 ring(cx, cy, r, thick, c) {119 const outer = r + thick / 2;120 shade(cx - outer, cy - outer, cx + outer, cy + outer, c, (px, py) => {121 const dx = px - cx;122 const dy = py - cy;123 return Math.abs(Math.sqrt(dx * dx + dy * dy) - r) - thick / 2;124 });125 },126 segment(a, b, thick, c) {127 const half = thick / 2;128 const [x0, y0, x1, y1] = bounds([a, b]);129 shade(x0 - half, y0 - half, x1 + half, y1 + half, c, (px, py) => segmentSdf(px, py, a, b) - half);130 },131 polyline(pts, thick, c) {132 if (pts.length < 2) return;133 const half = thick / 2;134 const pad = half + 1;135 const [bx0, by0, bx1, by1] = bounds(pts);136 const x0 = fmax(Math.floor(bx0 - pad), 0);137 const y0 = fmax(Math.floor(by0 - pad), 0);138 const x1 = Math.min(fmax(Math.ceil(bx1 + pad), 0), width);139 const y1 = Math.min(fmax(Math.ceil(by1 + pad), 0), height);140 if (x1 <= x0 || y1 <= y0) return;141 const span = x1 - x0;142 const mask = new Float64Array(span * (y1 - y0));143 for (let k = 0; k + 1 < pts.length; k++) {144 const a = pts[k];145 const b = pts[k + 1];146 const [ax0, ay0, ax1, ay1] = bounds([a, b]);147 const sx0 = fmax(Math.floor(ax0 - pad), x0);148 const sy0 = fmax(Math.floor(ay0 - pad), y0);149 const sx1 = Math.min(fmax(Math.ceil(ax1 + pad), 0), x1);150 const sy1 = Math.min(fmax(Math.ceil(ay1 + pad), 0), y1);151 for (let py = sy0; py < sy1; py++) {152 const row = (py - y0) * span;153 for (let px = sx0; px < sx1; px++) {154 const cover = clamp(0.5 - (segmentSdf(px + 0.5, py + 0.5, a, b) - half), 0, 1);155 if (cover > mask[row + px - x0]) mask[row + px - x0] = cover;156 }157 }158 }159 for (let py = y0; py < y1; py++) {160 const row = (py - y0) * span;161 for (let px = x0; px < x1; px++) {162 const cover = mask[row + px - x0];163 if (cover > 0) blend(px, py, c, cover);164 }165 }166 },167 triangle(a, b, c, color) {168 polygon([a, b, c], color);169 },170 polygon,171 arc(center, r, angles, thick, c) {172 const [cx, cy] = center;173 const [from, to] = angles;174 const half = thick / 2;175 const outer = r + half;176 const span = Math.abs(to - from);177 const lo = to >= from ? from : to;178 const hi = to >= from ? to : from;179 const ends = [180 [cx + r * Math.cos(lo), cy + r * Math.sin(lo)],181 [cx + r * Math.cos(hi), cy + r * Math.sin(hi)],182 ];183 const reach = fmin(span, TAU);184 shade(cx - outer, cy - outer, cx + outer, cy + outer, c, (px, py) => {185 let turn = Math.atan2(py - cy, px - cx) - lo;186 while (turn < 0) turn += TAU;187 if (turn <= reach) {188 const dx = px - cx;189 const dy = py - cy;190 return Math.abs(Math.sqrt(dx * dx + dy * dy) - r) - half;191 }192 let d = MAX;193 for (const e of ends) {194 const dx = px - e[0];195 const dy = py - e[1];196 d = fmin(d, Math.sqrt(dx * dx + dy * dy));197 }198 return d - half;199 });200 },201 image(x, y, w, h, pixels) {202 const [sh, sw] = pixels.shape;203 const src = pixels.colors;204 if (!(sw > 0 && sh > 0) || src.length < sw * sh * 4) return;205 const x0 = fmax(Math.ceil(x), 0);206 const y0 = fmax(Math.ceil(y), 0);207 const x1 = Math.min(fmax(Math.floor(x + w), 0), width);208 const y1 = Math.min(fmax(Math.floor(y + h), 0), height);209 for (let py = y0; py < y1; py++) {210 const row = Math.min(Math.floor(((py + 0.5 - y) / h) * sh), sh - 1);211 for (let px = x0; px < x1; px++) {212 const col = Math.min(Math.floor(((px + 0.5 - x) / w) * sw), sw - 1);213 const j = (row * sw + col) * 4;214 swatch[0] = src[j];215 swatch[1] = src[j + 1];216 swatch[2] = src[j + 2];217 swatch[3] = src[j + 3];218 blend(px, py, swatch, 1);219 }220 }221 },222 };223}