summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/App.tsx154
-rw-r--r--src/V2.ts38
-rw-r--r--src/index.tsx4
3 files changed, 196 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>
+ );
+}
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(<App />, document.body);