import { useEffect, useId, useRef, useState } from "preact/hooks"; import V2 from "./V2"; const range = (n: number) => [...new Array(n)].map((_, i) => i); type Nyp = 0 | 1 | 2 | 3; type Edge = [V2, V2]; const INITIAL_DIRECTIONS: Record = { 0b00: new V2(+1, -1), 0b01: new V2(-1, -1), 0b10: new V2(-1, +1), 0b11: new V2(+1, +1), }; const edgesForPath = (path: V2[]) => { const edges = new Array(Math.max(0, path.length - 1)); for (let i = 0; i < path.length - 1; i++) edges[i] = [path[i], path[i + 1]]; return edges; }; function redraw(ctx: CanvasRenderingContext2D, str: string) { function computePath(stream: Nyp[]): { path: V2[]; consumed: number } { let pos = V2.zero; const path: V2[] = [pos]; let consumed = 0; let edgesTouched = new Set(); const edgeToSetValue = (edge: Edge) => String([...edge].sort()); const tryMove = (displacement: V2, distance: number = 1): boolean => { const intermediateEdges = range(distance).map( (offset): Edge => [ pos.add(displacement.scale(offset)), pos.add(displacement.scale(offset + 1)), ], ); if ( intermediateEdges .map(edgeToSetValue) .some(edgeValue => edgesTouched.has(edgeValue)) ) return false; intermediateEdges.forEach(edge => edgesTouched.add(edgeToSetValue(edge))); pos = pos.add(displacement.scale(distance)); path.push(pos); consumed++; return true; }; if (!tryMove(INITIAL_DIRECTIONS[stream[0]])) { return { path, consumed }; } let dir = pos; for (const nyp of stream.slice(1)) { const moveRight = nyp % 2 === 1; const moveBy = nyp >> 1 ? 2 : 1; dir = moveRight ? new V2(-dir.y, dir.x) : new V2(dir.y, -dir.x); if (!tryMove(dir, moveBy)) break; } return { path, consumed }; } const utf8Bytes = [...new TextEncoder().encode(str)]; const nyps = utf8Bytes.flatMap(byte => [6, 4, 2, 0].map(power => (0b11 & (byte >> power)) as Nyp), ); let consumedSum = 0; const paths = []; while (consumedSum < nyps.length) { const { path, consumed } = computePath(nyps.slice(consumedSum)); paths.push(path); consumedSum += consumed; } const adjustPath = (path: V2[]) => { const minPoint = path.reduce( ([a, b], [x, y]) => new V2(Math.min(a, x), Math.min(b, y)), V2.zero, ); path = path.map(vertex => vertex.sub(minPoint).scale(10)); return { path, width: Math.max(...path.map(vertex => vertex.x)), }; }; ctx.reset(); let offsetX = 0; for (const { path, width } of paths.map(adjustPath)) { const totalPathLength = edgesForPath(path) .map(([from, to]) => Math.abs(to.sub(from).x)) .reduce((a, b) => a + b, 0) / 10; const totalSamples = totalPathLength * 100; ctx.strokeStyle = "#f00"; ctx.beginPath(); for (let i = 0; i <= totalSamples; i++) { const t = i / totalSamples; let pathRedux = path; while (pathRedux.length > 1) { pathRedux = edgesForPath(pathRedux).map(([from, to]) => from.add(to.sub(from).scale(t)), ); } const solePoint = pathRedux[0]; ctx.lineTo(solePoint.x + offsetX, solePoint.y); } ctx.stroke(); ctx.strokeStyle = "#00f7"; ctx.beginPath(); path.forEach(([x, y]) => ctx.lineTo(x + offsetX, y)); ctx.stroke(); offsetX += width + 10; } } export default function App({}: {}) { const inputId = useId(); const canvasRef = useRef(null); const [input, setInput] = useState(""); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d")!; redraw(ctx, input); }, [canvasRef, input]); return (
setInput((e.target as HTMLInputElement).value)} />
); }