From 7d14ee03e9fdad3736f14748115ad5a55c0dcd53 Mon Sep 17 00:00:00 2001 From: uakci Date: Sat, 13 Apr 2024 13:48:55 +0000 Subject: init --- src/App.tsx | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/V2.ts | 38 +++++++++++++++ src/index.tsx | 4 ++ 3 files changed, 196 insertions(+) create mode 100644 src/App.tsx create mode 100644 src/V2.ts create mode 100644 src/index.tsx (limited to 'src') 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 = { + 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)} + /> +
+
+ +
+
+ ); +} diff --git a/src/V2.ts b/src/V2.ts new file mode 100644 index 0000000..5ef60b3 --- /dev/null +++ b/src/V2.ts @@ -0,0 +1,38 @@ +export default class V2 { + readonly x: number; + readonly y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + + static get zero() { + return new V2(0, 0); + } + + toString() { + return `(${this.x}, ${this.y})`; + } + + *[Symbol.iterator]() { + yield this.x; + yield this.y; + } + + scale(factor: number) { + return new V2(this.x * factor, this.y * factor); + } + + get neg() { + return this.scale(-1); + } + + add(other: V2) { + return new V2(this.x + other.x, this.y + other.y); + } + + sub(other: V2) { + return this.add(other.neg); + } +} diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..4998105 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,4 @@ +import { render } from "preact"; +import App from "./App"; + +render(, document.body); -- cgit v1.2.3