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}