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}