@codincod/ornament 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,164 @@
1
+ /**
2
+ * A drawing, walked.
3
+ *
4
+ * The ornament writes its animals and its wreckage as paths, because that is
5
+ * what a browser and Qt both take: a string of moves and curves. This renderer
6
+ * takes triangles, so somewhere between the two the curves have to become
7
+ * points, and here is the cheapest place for it to happen. The alternative is a
8
+ * path parser on the other side of the wire and a string squeezed through an
9
+ * array of floats to reach it.
10
+ *
11
+ * Absolute commands only, and only the four the ornament writes: a move, a
12
+ * line, a cubic and a quadratic, with a close. Nothing here guesses at the rest
13
+ * of SVG. If a drawing ever grows an arc, this will draw it as the chord and
14
+ * somebody will see it.
15
+ */
16
+ import type { Point } from "./pen.ts"
17
+
18
+ /**
19
+ * How fine a curve is cut, as a share of how big the whole drawing is.
20
+ *
21
+ * A share rather than a length, and that is the whole of the change this file
22
+ * once needed. The drawings in this ornament are not written at one scale: a
23
+ * fish is about forty units nose to tail and an octopus is written about the
24
+ * origin at less than one, and both are stood at whatever size they are
25
+ * actually drawn. Cut to a fixed length, the fish came out smooth and the
26
+ * octopus came out as a heptagon with legs, and nothing said so until one was
27
+ * magnified on a plate.
28
+ *
29
+ * So a curve is cut against the drawing it belongs to. The steps are still
30
+ * counted against how far the curve actually runs, because a long sweep down a
31
+ * whale's back and the notch in a tail fin are the same command and want a
32
+ * different number of pieces.
33
+ */
34
+ const STEP = 0.035
35
+ const MOST = 24
36
+
37
+ /** What one path came to, kept, since a fish is the same drawing as the fish
38
+ * beating beside it and there are only so many steps in a beat. */
39
+ const walked = new Map<string, Point[][]>()
40
+
41
+ export function trace(d: string): Point[][] {
42
+ const held = walked.get(d)
43
+ if (held) return held
44
+
45
+ const cut = walk(d)
46
+ walked.set(d, cut)
47
+ return cut
48
+ }
49
+
50
+ /**
51
+ * How big the drawing is, off the numbers in it.
52
+ *
53
+ * The furthest any coordinate gets from the origin, which is a bound on the
54
+ * drawing rather than its size. It is read off the string because it has to be
55
+ * known before the walk it decides the fineness of, and because every drawing
56
+ * in this ornament is written about its own middle, where the two are close
57
+ * enough.
58
+ */
59
+ function span(d: string): number {
60
+ let widest = 0
61
+ const numbers = d.match(/-?\d*\.?\d+(?:e-?\d+)?/g)
62
+ if (!numbers) return 1
63
+ for (const one of numbers) widest = Math.max(widest, Math.abs(Number(one)))
64
+ return Math.max(widest, 0.001)
65
+ }
66
+
67
+ function walk(d: string): Point[][] {
68
+ const step = span(d) * STEP
69
+ const parts: Point[][] = []
70
+ let here: Point[] = []
71
+ let at = { x: 0, y: 0 }
72
+ let start = { x: 0, y: 0 }
73
+
74
+ const tokens = d.match(/[MLCQZmlcqz]|-?\d*\.?\d+(?:e-?\d+)?/g) ?? []
75
+ let read = 0
76
+ const number = () => Number(tokens[read++])
77
+
78
+ while (read < tokens.length) {
79
+ const command = tokens[read++]
80
+
81
+ if (command === "M" || command === "m") {
82
+ if (here.length > 1) parts.push(here)
83
+ here = []
84
+ at = { x: number(), y: number() }
85
+ start = at
86
+ here.push(at)
87
+ continue
88
+ }
89
+
90
+ if (command === "L" || command === "l") {
91
+ at = { x: number(), y: number() }
92
+ here.push(at)
93
+ continue
94
+ }
95
+
96
+ if (command === "C" || command === "c") {
97
+ const one = { x: number(), y: number() }
98
+ const two = { x: number(), y: number() }
99
+ const end = { x: number(), y: number() }
100
+ cubic(here, at, one, two, end, step)
101
+ at = end
102
+ continue
103
+ }
104
+
105
+ if (command === "Q" || command === "q") {
106
+ const hold = { x: number(), y: number() }
107
+ const end = { x: number(), y: number() }
108
+ quadratic(here, at, hold, end, step)
109
+ at = end
110
+ continue
111
+ }
112
+
113
+ if (command === "Z" || command === "z") {
114
+ if (here.length > 1) parts.push(here)
115
+ here = []
116
+ at = start
117
+ continue
118
+ }
119
+
120
+ // A number where a command was expected is a repeat of the last one, which
121
+ // the ornament does not write. Skipping it is better than reading the rest
122
+ // of the drawing off by one.
123
+ }
124
+
125
+ if (here.length > 1) parts.push(here)
126
+ return parts
127
+ }
128
+
129
+ /** How many pieces a curve of this reach is worth cutting into. */
130
+ function pieces(reach: number, step: number): number {
131
+ return Math.max(2, Math.min(MOST, Math.ceil(reach / step)))
132
+ }
133
+
134
+ function cubic(into: Point[], from: Point, one: Point, two: Point, end: Point, step: number): void {
135
+ const reach =
136
+ Math.hypot(one.x - from.x, one.y - from.y) +
137
+ Math.hypot(two.x - one.x, two.y - one.y) +
138
+ Math.hypot(end.x - two.x, end.y - two.y)
139
+ const steps = pieces(reach, step)
140
+
141
+ for (let i = 1; i <= steps; i++) {
142
+ const t = i / steps
143
+ const u = 1 - t
144
+ into.push({
145
+ x: u * u * u * from.x + 3 * u * u * t * one.x + 3 * u * t * t * two.x + t * t * t * end.x,
146
+ y: u * u * u * from.y + 3 * u * u * t * one.y + 3 * u * t * t * two.y + t * t * t * end.y,
147
+ })
148
+ }
149
+ }
150
+
151
+ function quadratic(into: Point[], from: Point, hold: Point, end: Point, step: number): void {
152
+ const reach =
153
+ Math.hypot(hold.x - from.x, hold.y - from.y) + Math.hypot(end.x - hold.x, end.y - hold.y)
154
+ const steps = pieces(reach, step)
155
+
156
+ for (let i = 1; i <= steps; i++) {
157
+ const t = i / steps
158
+ const u = 1 - t
159
+ into.push({
160
+ x: u * u * from.x + 2 * u * t * hold.x + t * t * end.x,
161
+ y: u * u * from.y + 2 * u * t * hold.y + t * t * end.y,
162
+ })
163
+ }
164
+ }