diff options
Diffstat (limited to 'src/App.tsx')
| -rw-r--r-- | src/App.tsx | 154 |
1 files changed, 154 insertions, 0 deletions
diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..5239d20 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,154 @@ +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<Nyp, V2> = { + 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<Edge>(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<string>(); + 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<HTMLCanvasElement>(null); + + const [input, setInput] = useState(""); + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d")!; + redraw(ctx, input); + }, [canvasRef, input]); + + return ( + <div style={{ display: "flex", flexDirection: "column", gap: "1em" }}> + <div style={{ display: "flex", gap: "0.5em" }}> + <label htmlFor={inputId}>Input: </label> + <input + id={inputId} + style={{ flexGrow: 1 }} + type="text" + value={input} + onInput={e => setInput((e.target as HTMLInputElement).value)} + /> + </div> + <div style={{ overflowX: "scroll" }}> + <canvas ref={canvasRef} width={4000} height={200} /> + </div> + </div> + ); +} |
