hex.js

3.9 kB · javascript · 105 lines

1// GEOMETRY23const RATIO = 0.8660254037844386;45const north = (x, y) => [[x, 2 * y + 2], [x + 1, 2 * y], [x + 2, 2 * y + 2]];6const south = (x, y) => [[x, 2 * y], [x + 1, 2 * y + 2], [x + 2, 2 * y]];7const east = (x, y) => [[2 * x, y], [2 * x, y + 2], [2 * x + 2, y + 1]];8const west = (x, y) => [[2 * x + 2, y], [2 * x + 2, y + 2], [2 * x, y + 1]];910function shrink(pts, gap) {11  const cx = (pts[0][0] + pts[1][0] + pts[2][0]) / 3;12  const cy = (pts[0][1] + pts[1][1] + pts[2][1]) / 3;13  const side = (a, b) => Math.sqrt((b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]));14  const perimeter = side(pts[0], pts[1]) + side(pts[1], pts[2]) + side(pts[2], pts[0]);15  const area = Math.abs((pts[1][0] - pts[0][0]) * (pts[2][1] - pts[0][1]) - (pts[2][0] - pts[0][0]) * (pts[1][1] - pts[0][1])) / 2;16  const inradius = (2 * area) / perimeter;17  const k = inradius > 0 ? Math.max((inradius - gap) / inradius, 0) : 0;18  return pts.map((p) => [cx + (p[0] - cx) * k, cy + (p[1] - cy) * k]);19}2021function fit(mesh, frame) {22  let [lx, ly, hx, hy] = [Number.MAX_VALUE, Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE];23  for (const tri of mesh) {24    for (const p of tri) {25      if (p[0] < lx) lx = p[0];26      if (p[1] < ly) ly = p[1];27      if (p[0] > hx) hx = p[0];28      if (p[1] > hy) hy = p[1];29    }30  }31  const sx = Math.max(hx - lx, 1e-9);32  const sy = Math.max(hy - ly, 1e-9);33  const scale = Math.min(frame.w / sx, frame.h / sy);34  const ox = frame.x + (frame.w - sx * scale) / 2;35  const oy = frame.y + (frame.h - sy * scale) / 2;36  return (p) => [ox + (p[0] - lx) * scale, oy + (p[1] - ly) * scale];37}3839// DRAWING4041export function draw(pen, frame, cell, gap, ink) {42  const [height, width] = cell.cell.shape;43  if (width === height) throw new Error("Cell must be a hexagon.");44  const across = width > height;45  const types = cell.cell.types;46  const mesh = [];47  const paint = [];48  for (let y = 0; y < height; y++) {49    for (let x = 0; x < width; x++) {50      const color = ink(types[y * width + x] & 255);51      if (!color) continue;52      const flip = (x + y + cell.start) % 2;53      const points = across ? (flip === 0 ? north(x, y) : south(x, y)) : flip === 0 ? east(x, y) : west(x, y);54      mesh.push(points.map((p) => (across ? [p[0], p[1] * RATIO] : [p[0] * RATIO, p[1]])));55      paint.push(color);56    }57  }58  if (mesh.length === 0) return;59  const place = fit(mesh, frame);60  mesh.forEach((tri, i) => {61    const small = shrink(tri.map(place), gap);62    pen.triangle(small[0], small[1], small[2], paint[i]);63  });64}6566export function count(n) {67  return 6 * n * n;68}6970export function row_len(n, row) {71  const reach = row < n ? row : 2 * n - 1 - row;72  return 2 * (n + reach) + 1;73}7475export function hexagon(pen, frame, n, gap, ink) {76  if (n === 0) return;77  const side = Math.min(frame.w / (2 * n), frame.h / (n * 2 * RATIO));78  const rise = side * RATIO;79  const [cx, cy] = frame.center();80  const left = cx - side * n;81  const top = cy - rise * n;82  for (let row = 0; row < 2 * n; row++) {83    const reach = row < n ? row : 2 * n - 1 - row;84    const up = row < n;85    const long = n + reach + 1;86    const short = n + reach;87    const [topLen, botLen] = up ? [short, long] : [long, short];88    const tx = left + ((2 * n - topLen) * side) / 2;89    const bx = left + ((2 * n - botLen) * side) / 2;90    const y0 = top + row * rise;91    const y1 = top + (row + 1) * rise;92    for (let col = 0; col < row_len(n, row); col++) {93      const pointing = (col % 2 === 0) === up;94      const j = Math.floor(col / 2);95      const points = pointing96        ? [[bx + j * side, y1], [bx + (j + 1) * side, y1], [bx + (j + 0.5) * side, y0]]97        : [[tx + j * side, y0], [tx + (j + 1) * side, y0], [tx + (j + 0.5) * side, y1]];98      const color = ink(row, col, pointing ? 1 : 0);99      if (color) {100        const small = shrink(points, gap);101        pen.triangle(small[0], small[1], small[2], color);102      }103    }104  }105}