svg.js
3.6 kB · javascript · 91 lines
1import { CLEAR, board } from "./frame.js";23const TAU = 2 * Math.PI;45// PAINT67const hex = (c) => "#" + [c[0], c[1], c[2]].map((v) => v.toString(16).padStart(2, "0")).join("");8const alpha = (key, c) => (c[3] < 255 ? ` ${key}-opacity="${c[3] / 255}"` : "");9const fill = (c) => ` fill="${hex(c)}"${alpha("fill", c)}`;10const stroke = (c, thick) => ` fill="none" stroke="${hex(c)}"${alpha("stroke", c)} stroke-width="${thick}"`;11const points = (pts) => pts.map((p) => `${p[0]},${p[1]}`).join(" ");12const ROUND = ` stroke-linecap="round" stroke-linejoin="round"`;1314function base64(bytes) {15 let text = "";16 for (let i = 0; i < bytes.length; i += 0x8000) text += String.fromCharCode(...bytes.subarray(i, i + 0x8000));17 return btoa(text);18}1920async function href(pixels, png) {21 if (!png) throw new Error("svg: an image needs a png encoder, text({ png })");22 const out = await png(pixels);23 if (typeof out === "string") return out;24 return `data:image/png;base64,${base64(out instanceof Uint8Array ? out : new Uint8Array(out))}`;25}2627// SVG2829export function svg(width, height, ground = CLEAR) {30 const parts = [];31 if (ground[3] > 0) parts.push(`<rect width="${width}" height="${height}"${fill(ground)}/>`);3233 function disc(cx, cy, r, c) {34 if (r > 0) parts.push(`<circle cx="${cx}" cy="${cy}" r="${r}"${fill(c)}/>`);35 }3637 function polygon(pts, c) {38 if (pts.length >= 3) parts.push(`<polygon points="${points(pts)}"${fill(c)} fill-rule="evenodd"/>`);39 }4041 function ring(cx, cy, r, thick, c) {42 if (r <= 0) return disc(cx, cy, r + thick / 2, c);43 parts.push(`<circle cx="${cx}" cy="${cy}" r="${r}"${stroke(c, thick)}/>`);44 }4546 return {47 ...board(width, height),48 async text({ png } = {}) {49 const body = [];50 for (const part of parts) {51 if (typeof part === "string") body.push(part);52 else body.push(`<image x="${part.x}" y="${part.y}" width="${part.w}" height="${part.h}" preserveAspectRatio="none" image-rendering="pixelated" href="${await href(part.pixels, png)}"/>`);53 }54 return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">\n${body.join("\n")}\n</svg>\n`;55 },56 rect(x, y, w, h, c) {57 parts.push(`<rect x="${x}" y="${y}" width="${w}" height="${h}"${fill(c)}/>`);58 },59 round_rect(x, y, w, h, r, c) {60 const rr = Math.max(Math.min(r, w / 2, h / 2), 0);61 parts.push(`<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${rr}" ry="${rr}"${fill(c)}/>`);62 },63 disc,64 ring,65 segment(a, b, thick, c) {66 if (a[0] === b[0] && a[1] === b[1]) return disc(a[0], a[1], thick / 2, c);67 parts.push(`<line x1="${a[0]}" y1="${a[1]}" x2="${b[0]}" y2="${b[1]}"${stroke(c, thick)}${ROUND}/>`);68 },69 polyline(pts, thick, c) {70 if (pts.length >= 2) parts.push(`<polyline points="${points(pts)}"${stroke(c, thick)}${ROUND}/>`);71 },72 triangle(a, b, c, color) {73 polygon([a, b, c], color);74 },75 polygon,76 arc(center, r, angles, thick, c) {77 const [cx, cy] = center;78 const lo = Math.min(angles[0], angles[1]);79 const hi = Math.max(angles[0], angles[1]);80 const span = hi - lo;81 const at = (t) => [cx + r * Math.cos(t), cy + r * Math.sin(t)];82 if (span >= TAU) return ring(cx, cy, r, thick, c);83 if (span === 0) return disc(...at(lo), thick / 2, c);84 const arm = `A${r},${r} 0 0 1`;85 parts.push(`<path d="M${at(lo)} ${arm} ${at(lo + span / 2)} ${arm} ${at(hi)}"${stroke(c, thick)}${ROUND}/>`);86 },87 image(x, y, w, h, pixels) {88 parts.push({ x, y, w, h, pixels: { shape: [...pixels.shape], colors: Uint8Array.from(pixels.colors) } });89 },90 };91}