1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
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>
);
}
|