@codincod/ornament 0.0.0-stage → 0.1.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,300 @@
1
+ /**
2
+ * One animal, alone, with the water held close enough to see it.
3
+ *
4
+ * A sea is drawn to be a sea: an animal in it is a few percent of the box
5
+ * across, because that is what a fish at eight metres actually looks like and
6
+ * `sizes.ts` is not going to lie about it for the sake of a nice picture. That
7
+ * is right for a background and useless for a plate, and a plate is what finds
8
+ * drawing faults. At the weight ornament ships at, a wreck that is upside down
9
+ * and a wreck that is working look identical.
10
+ *
11
+ * So a specimen is not a bigger animal. It is the same animal in the same
12
+ * water, with the camera brought in until it fills the frame, and `look` is
13
+ * where to point it. Nothing about the drawing changes, which is the whole
14
+ * value: a fault seen here is a fault the sea has.
15
+ *
16
+ * ## Why the water is bigger than the box it is shown in
17
+ *
18
+ * Because the animal has to have somewhere to be. A fish given a box the size
19
+ * of the view spends its life against an edge it can feel; given room, it swims
20
+ * about its business and the camera follows. What is shown is a window on a
21
+ * water that carries on past it.
22
+ */
23
+ import type { Presence } from "../biome.ts"
24
+ import { createCephalopods } from "../cephalopods.ts"
25
+ import { createJellies, type JellyKind } from "../jellies.ts"
26
+ import { createPuffers, type Puffers } from "../puffers.ts"
27
+ import { createSeabed } from "../seabed.ts"
28
+ import { createShoal, type Kind as FishKind } from "../shoal.ts"
29
+ import { createWalkers } from "../walkers.ts"
30
+ import { paintInklings, paintJellies, paintPuffers, paintShoal, paintWalkers } from "./life.ts"
31
+ import { Pen } from "./pen.ts"
32
+ import type { Box } from "./sky.ts"
33
+
34
+ /**
35
+ * The ones the design system shows, one from each family that stays in frame.
36
+ *
37
+ * The animals that cross rather than live here are deliberately not among
38
+ * them. A shark keeps a rest between crossings measured in minutes, so a plate
39
+ * of one is a plate of empty water for most of the time somebody is looking at
40
+ * it, and a shark wound on until it is there is a shark on a treadmill.
41
+ */
42
+ export type Kind = "crab" | "jellyfish" | "octopus" | "pufferfish" | "swordfish"
43
+
44
+ export const KINDS: readonly Kind[] = ["swordfish", "jellyfish", "octopus", "crab", "pufferfish"]
45
+
46
+ /** What each one is, for a page that has to say. */
47
+ export const GLOSS: Readonly<Record<Kind, string>> = {
48
+ crab: "It goes where it decides to go, and the current has no say in it.",
49
+ jellyfish: "A moon jelly, which gets a second push out of its own refill.",
50
+ octopus: "The only thing down there with a repertoire rather than a rhythm.",
51
+ pufferfish: "It is a fish until something crowds it, and then it is a ball of spines.",
52
+ swordfish: "A billfish over a reef is a visitor from the water beyond it.",
53
+ }
54
+
55
+ /**
56
+ * How much bigger the water is than the window on it.
57
+ *
58
+ * Enough that the animal is never against a wall, and no more: everything in
59
+ * this water is stepped whether it is in the frame or not, so room is paid for
60
+ * every frame.
61
+ */
62
+ const ROOM = 5
63
+
64
+ /** How many numbers a plate's frame is given. One animal is not a sea. */
65
+ const BLOCK = 1 << 16
66
+
67
+ /** How much of the window the animal is meant to take up. */
68
+ const FILLS = 0.72
69
+
70
+ /**
71
+ * How far a drawing actually reaches, against the size the animal reports.
72
+ *
73
+ * An animal's size is the size of the animal, and a drawing of one is bigger
74
+ * than that in every case where something trails: an octopus is measured across
75
+ * its head and drawn with two head-lengths of arms under it, a crab is measured
76
+ * across its shell and stands on legs half again as wide, a pufferfish is
77
+ * measured deflated and spends its interesting seconds twice that with its
78
+ * spines out, and a moon jelly is a bell with a curtain hanging off it. `drop`
79
+ * is the other half of the same fact, because none of it trails evenly: what
80
+ * hangs, hangs downwards, so the middle of the drawing is below the middle of
81
+ * the animal.
82
+ *
83
+ * It is only ever a plate's business. A sea draws these at the size they are
84
+ * and never has to frame one.
85
+ */
86
+ const REACH: Readonly<Record<Kind, { drop: number; wide: number }>> = {
87
+ crab: { drop: 0.06, wide: 2.05 },
88
+ jellyfish: { drop: 0.34, wide: 1.6 },
89
+ octopus: { drop: 0.28, wide: 1.9 },
90
+ pufferfish: { drop: 0, wide: 2.15 },
91
+ swordfish: { drop: 0, wide: 1.15 },
92
+ }
93
+
94
+ /**
95
+ * What crowds the pufferfish, and how often.
96
+ *
97
+ * A pufferfish at rest is a slightly lumpy fish sculling about, and the drawing
98
+ * worth judging is the other one. In a sea the thing that comes near is a shark
99
+ * or a hull; on a plate it is whoever is looking, so that is what this is:
100
+ * something several times the animal's size, with its whole attention on it,
101
+ * leaning in for a few seconds out of every `every` and going away again.
102
+ *
103
+ * It is the one thing on a plate that is not in the water. It is there because
104
+ * the drawing this plate exists to show is the ball of spines, and the animal
105
+ * will not make one to order: it swells at what the water is doing, and a plate
106
+ * with nothing in it is a plate of water doing nothing.
107
+ */
108
+ const LOOKING = { every: 24, menace: 1, near: 3, size: 3 }
109
+
110
+ /** How light it is on a plate. */
111
+ const DAYLIGHT = 1
112
+
113
+ /** How many numbers stand in front of a frame's count; see `frame.ts`. */
114
+ const HEAD = 6
115
+
116
+ export interface PlateOptions {
117
+ height: number
118
+ seed?: number
119
+ width: number
120
+ }
121
+
122
+ export interface Plate {
123
+ /** The window's own size, which is not the water's. */
124
+ readonly box: Box
125
+ readonly geometry: Float32Array
126
+ /** Where to point the camera, and how far in. */
127
+ look(): { scale: number; x: number; y: number }
128
+ over(): number
129
+ step(seconds: number): void
130
+ wind(seconds: number): void
131
+ }
132
+
133
+ /**
134
+ * A plate of one animal.
135
+ *
136
+ * Everything it does not need is left out rather than made and ignored. A crab
137
+ * and an octopus need a floor, because neither of them is ever off it. Nothing
138
+ * else here has a bed at all, and a jellyfish in open water is a jellyfish.
139
+ */
140
+ export function createPlate(kind: Kind, options: PlateOptions): Plate {
141
+ const box = { height: options.height, width: options.width }
142
+ const water = { height: box.height * ROOM, width: box.width * ROOM }
143
+ const seed = options.seed ?? 1
144
+ const geometry = new Float32Array(BLOCK)
145
+
146
+ const grounded = kind === "crab" || kind === "octopus" || kind === "pufferfish"
147
+ const seabed = grounded ? createSeabed({ ...water, cliffs: 0, ranges: 1, seed, stones: 0 }) : null
148
+
149
+ const shoal =
150
+ kind === "swordfish"
151
+ ? createShoal({
152
+ count: 1,
153
+ cruise: 0.5,
154
+ height: water.height,
155
+ seed,
156
+ species: { swordfish: 1 } as Partial<Record<FishKind, number>>,
157
+ width: water.width,
158
+ })
159
+ : null
160
+
161
+ const jellies =
162
+ kind === "jellyfish"
163
+ ? createJellies({
164
+ count: 1,
165
+ height: water.height,
166
+ kinds: ["moon"] as readonly JellyKind[],
167
+ seed,
168
+ width: water.width,
169
+ })
170
+ : null
171
+
172
+ const inklings =
173
+ kind === "octopus" && seabed
174
+ ? createCephalopods({
175
+ floor: seabed.floorAt,
176
+ height: water.height,
177
+ octopuses: 1,
178
+ seed,
179
+ squids: 0,
180
+ width: water.width,
181
+ })
182
+ : null
183
+
184
+ let watched = 0
185
+
186
+ const looking = (): readonly Presence[] => {
187
+ const one = puffers?.puffers[0]
188
+ if (!one || watched % LOOKING.every > LOOKING.near) return []
189
+ return [
190
+ {
191
+ depth: one.depth,
192
+ menace: LOOKING.menace,
193
+ size: one.size * LOOKING.size,
194
+ x: one.x,
195
+ y: one.y,
196
+ },
197
+ ]
198
+ }
199
+
200
+ const puffers: Puffers | null =
201
+ kind === "pufferfish" && seabed
202
+ ? createPuffers({
203
+ about: looking,
204
+ count: 1,
205
+ floor: seabed.floorAt,
206
+ height: water.height,
207
+ seed,
208
+ width: water.width,
209
+ })
210
+ : null
211
+
212
+ const walkers =
213
+ kind === "crab" && seabed
214
+ ? createWalkers({
215
+ crabs: 1,
216
+ floor: seabed.floorAt,
217
+ seed,
218
+ starfish: 0,
219
+ width: water.width,
220
+ })
221
+ : null
222
+
223
+ /**
224
+ * Where the animal is, kept from the last frame it was in.
225
+ *
226
+ * A camera that snapped back to the middle of the water whenever the animal
227
+ * was between two states would be a camera nobody could watch. The last known
228
+ * place is a better answer than the middle, and it is always nearly right.
229
+ */
230
+ let held = { x: water.width / 2, y: water.height / 2 }
231
+ let across = box.width / 6
232
+
233
+ const find = () => {
234
+ const one =
235
+ shoal?.fish[0] ??
236
+ jellies?.jellies[0] ??
237
+ inklings?.octopuses[0] ??
238
+ puffers?.puffers[0] ??
239
+ walkers?.walkers[0]
240
+ if (!one) return
241
+
242
+ held = { x: one.x, y: one.y }
243
+ if (one.size > 0) across = one.size
244
+ }
245
+
246
+ return {
247
+ box,
248
+
249
+ geometry,
250
+
251
+ look() {
252
+ find()
253
+ const reach = REACH[kind]
254
+ const drawn = Math.max(1, across * reach.wide)
255
+ return {
256
+ scale: (Math.min(box.height, box.width) * FILLS) / drawn,
257
+ x: held.x,
258
+ y: held.y + across * reach.drop,
259
+ }
260
+ },
261
+
262
+ over(): number {
263
+ // The same six numbers a sea puts in front of its frame, so one reader
264
+ // walks either. A plate is not held by anybody: it is a drawing being
265
+ // looked at, and a drawing that wandered would be a drawing on a boat.
266
+ geometry[0] = 0
267
+ geometry[1] = 0
268
+ geometry[2] = 0
269
+ geometry[3] = 1
270
+ geometry[4] = 0
271
+ geometry[5] = DAYLIGHT
272
+
273
+ const pen = new Pen(geometry, HEAD)
274
+ if (shoal) paintShoal(pen, shoal)
275
+ // A plate is looked at rather than glanced past, so it is drawn in full
276
+ // daylight whatever the hour: everything in this water is the theme's own
277
+ // ink either way, and what the hour changes is a wash over the top, which
278
+ // on a plate is something to squint through.
279
+ if (jellies) paintJellies(pen, jellies, DAYLIGHT)
280
+ if (inklings) paintInklings(pen, inklings)
281
+ if (puffers) paintPuffers(pen, puffers)
282
+ if (walkers) paintWalkers(pen, walkers)
283
+ return pen.close()
284
+ },
285
+
286
+ step(seconds: number): void {
287
+ shoal?.step(seconds, null, null)
288
+ jellies?.step(seconds, null)
289
+ inklings?.step(seconds, null)
290
+ watched += seconds
291
+ puffers?.step(seconds)
292
+ walkers?.step(seconds)
293
+ },
294
+
295
+ wind(seconds: number): void {
296
+ const step = 1 / 10
297
+ for (let at = 0; at < Math.round(Math.max(0, seconds) / step); at++) this.step(step)
298
+ },
299
+ }
300
+ }
@@ -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
+ }