@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,1936 @@
1
+ /**
2
+ * The big animals, crossing.
3
+ *
4
+ * A turtle, a pod of dolphins, a manta, a shark, and one thing that is none of
5
+ * those. Kept free of the DOM the way the rest of the ornament is, and kept
6
+ * apart from `shoal.ts` for the reason `cephalopods.ts` gives: these are not
7
+ * fish with different constants.
8
+ * A fish in this water lives here and is steered by what it wants. A visitor is
9
+ * going somewhere else, and the picture is on its way.
10
+ *
11
+ * ## Why they are not passers
12
+ *
13
+ * `passers.ts` is the surface and what is looking for you, and it keeps
14
+ * appointments by the day: a boat crosses once, a submarine on one day in
15
+ * seven. That clock is right for machinery and wrong for an animal. A turtle is
16
+ * not a rare event, it is an uncommon one, and the difference is the whole feel
17
+ * of the thing. What happens once a day is something you wait for. What happens
18
+ * every few minutes is a sea with animals in it.
19
+ *
20
+ * So these run on watched time, one party at a time with a long rest between,
21
+ * and nothing is written down. See `REST_LEAST`.
22
+ *
23
+ * ## Every one of them is a silhouette
24
+ *
25
+ * That is not a saving. It is the only way anybody has ever seen one of these:
26
+ * you are under it, the light is behind it, and what reaches you is an outline.
27
+ * Colour on a shark at that distance would be an invention, and the outline is
28
+ * the part that carries the animal anyway.
29
+ *
30
+ * Which means the drawing has to be right where an outline is read, and that is
31
+ * the fins and the head. The tables below are all profile: where the body is
32
+ * deep, where it is thin, and what stands off it.
33
+ *
34
+ * ## Some of them swim and some of them do not
35
+ *
36
+ * The shark, the dolphin and the mermaid carry a wave down the body, the way
37
+ * `fish_shape.ts` builds a fish, and everything they have hangs off that spine:
38
+ * a tail sweeps because the body it is joined to swept. What separates the
39
+ * first two is a profile and a phase, which is honest, because from the side
40
+ * they really are the same animal with a different nose and a different tail.
41
+ *
42
+ * The third differs in where the wave is allowed to start. A shark bends a
43
+ * little at the shoulders and a great deal at the wrist; a person does not bend
44
+ * at the shoulders at all. `ENVELOPES` is that difference and it is the whole
45
+ * of it, so a mermaid comes out as a body held still down to the waist with
46
+ * everything below it doing the swimming, which is what a monofin actually
47
+ * looks like from the side.
48
+ *
49
+ * The turtle rows. Its shell is rigid, so the whole animal is two flippers
50
+ * beating and a body going along with it, and `blade` is what draws one.
51
+ *
52
+ * The manta is the odd one, and it is drawn from below because that is where
53
+ * the eye looking at it is. Its wings beat up and down, so what changes on the
54
+ * screen is how far out the tips reach: a wing at the top of its beat is
55
+ * foreshortened to nearly nothing and a wing held level is at full span. Drawn
56
+ * flat it is a kite. See `MANTA_FOLD`.
57
+ *
58
+ * ## What a visitor does to the water
59
+ *
60
+ * Almost nothing, on purpose. It leaves a `Startle`, and a soft wide one: the
61
+ * shoal gives way and closes behind it. Nothing here is hunting and nothing
62
+ * here is hunted, and a shoal scattering from a passing shark would make this a
63
+ * picture about danger, which this water is not. It is a picture about
64
+ * something large going by, and the water noticing.
65
+ *
66
+ * ## And one of them is not an animal
67
+ *
68
+ * There is a mermaid in here, and she is the one thing in this file that does
69
+ * not run on watched time. Odds could not get her rare enough, so she keeps an
70
+ * appointment by the day the way a submarine does, on about one day in thirty
71
+ * and never twice; `MERMAID_DAYS` has the argument. Nothing in the water treats
72
+ * her as other than a shape on its way somewhere.
73
+ *
74
+ * A silhouette is the only honest way to draw her, and it is the reason she can
75
+ * be here at all. Everything down here is an outline because that is what the
76
+ * light does to something large at eight metres, and at eight metres there is
77
+ * nothing to argue with. Somebody who saw it saw a shape with hair on it. The
78
+ * water declines to say more than that, and so does this file.
79
+ */
80
+
81
+ import type { Biome } from "./biome.ts"
82
+ import { mixOf } from "./census.ts"
83
+ import { makeRandom, stir } from "./perlin.ts"
84
+ import { drawnTilt, type Startle } from "./shoal.ts"
85
+ import { drawnAt, SWIMMING_OFF } from "./sizes.ts"
86
+
87
+ export type VisitKind = "dolphin" | "manta" | "mermaid" | "shark" | "turtle"
88
+
89
+ export interface Visitor {
90
+ /** How far through its crossing, 0 to 1. Negative until it has set off. */
91
+ along: number
92
+ /** 0 is as far off as this water goes, 1 is up against the glass. */
93
+ readonly depth: number
94
+ /** Which way it is going: 1 rightward, -1 left. */
95
+ readonly facing: number
96
+ readonly kind: VisitKind
97
+ /**
98
+ * How far off the party's own line this one holds, in px.
99
+ *
100
+ * Nothing for a leader and for anything that travels alone. It is already in
101
+ * `y`, so a renderer has no use for it; what it is here for is that the whole
102
+ * party shares one course and this is the only thing that differs.
103
+ */
104
+ readonly lift: number
105
+ /**
106
+ * Its own rhythm, as a multiple of what its kind beats at.
107
+ *
108
+ * Drawn once, as it arrives, and never rolled again. A beat that takes a
109
+ * fresh number every frame has no period at all, and what that looks like is
110
+ * an engine misfiring rather than an animal; `shoal.ts` says the same thing
111
+ * about a fish's burst and for the same reason. What this is for is that two
112
+ * of a kind crossing the same water should not be beating in time.
113
+ */
114
+ readonly pace: number
115
+ /** Nose to tail in px, with the distance already taken out of it. */
116
+ readonly size: number
117
+ /**
118
+ * How far through its stroke it is, 0 to 1.
119
+ *
120
+ * The one number every drawing is read off, which is the rule the squid's
121
+ * mantle keeps for the same reason: an animal whose travel and whose drawing
122
+ * come off one clock cannot fall out of step with itself.
123
+ */
124
+ stroke: number
125
+ /**
126
+ * How far to turn the drawing, in radians, after mirroring it by `facing`.
127
+ *
128
+ * `drawnTilt`, so a visitor mirrors exactly as a fish and a squid do. The
129
+ * note on that function is worth reading before a renderer touches this.
130
+ */
131
+ tilt: number
132
+ /** How loud it is now, 0 to 1, faded in and out at the ends. */
133
+ weight: number
134
+ x: number
135
+ y: number
136
+ }
137
+
138
+ export interface Visitors {
139
+ readonly crossing: readonly Visitor[]
140
+ resize(width: number, height: number): void
141
+ /** What the water is being told to do now, or null while nothing is by. */
142
+ readonly startle: Startle | null
143
+ step(seconds: number): void
144
+ }
145
+
146
+ export interface VisitorsOptions {
147
+ /** Cross one after another with no rest. For previews and for tests. */
148
+ eager?: boolean
149
+ height: number
150
+ kinds?: readonly VisitKind[]
151
+ seed: number
152
+ /**
153
+ * The water these are in, so that whatever lives down there can see them.
154
+ *
155
+ * Entered rather than announced. What a shark looks like to a crab is a fact
156
+ * about the shark, so it is stated here, and the crab reads the water and
157
+ * makes its own mind up; see `biome.ts`. Left out, these cross a water that
158
+ * nothing else is looking at, which is what a test and the website want.
159
+ */
160
+ water?: Biome
161
+ width: number
162
+ }
163
+
164
+ /**
165
+ * What each animal is, where it swims, and how long it takes to cross.
166
+ *
167
+ * Which of them crosses next is not settled here. Exactly one party crosses at
168
+ * a time and the only question is which, so the weights are how common each
169
+ * animal is over a reef, out of `census.ts` with everything else that lives in
170
+ * this water. The mermaid is in no draw at all: she keeps an appointment by the
171
+ * day instead, which `MERMAID_DAYS` sets out at length.
172
+ *
173
+ * `seat` is where in the column it swims and `far` is how far back, and between
174
+ * them they tell one from another before a single outline is read. A dolphin is
175
+ * at the top because that is where it breathes. A manta is high because you
176
+ * look up at one. A shark is out in the middle water where nothing else here
177
+ * goes. A turtle is anywhere, because a turtle is on its way to the surface
178
+ * from wherever it happened to be, and the mermaid is going the same way for
179
+ * the same reason.
180
+ *
181
+ * `party` is how many come at once, and only the dolphins do. That is the fact
182
+ * about dolphins: one reads as a fish and three read as a pod before the eye
183
+ * has resolved a fin.
184
+ *
185
+ * The sizes are lengths of real animals, in metres, at the distance they all
186
+ * swim past at; `sizes.ts` turns the pair into a share of the box. They are a
187
+ * bottlenose, a giant oceanic manta measured down the disc rather than across
188
+ * it, a grown great white, and a green turtle nose to the back of its shell.
189
+ * The manta's wings are getting on for twice its disc again, which the drawing
190
+ * already knows and this table does not have to. The mermaid is the one length
191
+ * here nobody can look up, so she is written as a tall woman with a tail on
192
+ * her, which is the only measurement anybody has ever offered.
193
+ */
194
+ interface Habit {
195
+ /** How fast it works through its stroke, in strokes a second. */
196
+ readonly beat: number
197
+ /** How far back it swims, from the murk at 0 to the glass at 1. */
198
+ readonly farLeast: number
199
+ readonly farSpan: number
200
+ /** How hard a startle it leaves, against the loudest there is; see `FELT`. */
201
+ readonly heft: number
202
+ /**
203
+ * How much it looks like something that hunts, 0 through 1.
204
+ *
205
+ * A different question from `heft`, and the two do not track each other. A
206
+ * pod of dolphins is the loudest thing that crosses this water and a manta
207
+ * is nearly as large, and only one of them is anything a small animal has
208
+ * ever had to think about. What this decides is not the push, which is
209
+ * physics; it is whether a crab on the floor stops walking.
210
+ */
211
+ readonly menace: number
212
+ /** How many come at once, at least and up to that again. */
213
+ readonly party: number
214
+ readonly partySpan: number
215
+ /** Where in the column it swims, as shares of the height. */
216
+ readonly seatLeast: number
217
+ readonly seatSpan: number
218
+ /**
219
+ * How big it is drawn, as shares of the box's width.
220
+ *
221
+ * Written as the length of a real animal in metres and the distance it swims
222
+ * at, and turned into a share by `sizes.ts`, so the four of them are the
223
+ * sizes they are against each other rather than four numbers that each
224
+ * looked right alone.
225
+ */
226
+ readonly sizeLeast: number
227
+ readonly sizeSpan: number
228
+ /** How long a crossing takes, at least and up to that again, in seconds. */
229
+ readonly takeLeast: number
230
+ readonly takeSpan: number
231
+ }
232
+
233
+ const HABITS: Readonly<Record<VisitKind, Habit>> = {
234
+ dolphin: {
235
+ beat: 1.05,
236
+ farLeast: 0.5,
237
+ farSpan: 0.45,
238
+ heft: 0.5,
239
+ menace: 0.35,
240
+ party: 2,
241
+ partySpan: 3,
242
+ seatLeast: 0.08,
243
+ seatSpan: 0.16,
244
+ sizeLeast: drawnAt(2.4, SWIMMING_OFF),
245
+ sizeSpan: drawnAt(3.6, SWIMMING_OFF) - drawnAt(2.4, SWIMMING_OFF),
246
+ takeLeast: 16,
247
+ takeSpan: 9,
248
+ },
249
+ /**
250
+ * A reef manta, measured nose to tail like everything else in this table.
251
+ *
252
+ * Which is not what a reader sees. A manta is about twice as wide as it is
253
+ * long and the drawing is too, so the animal that crosses the picture reaches
254
+ * getting on for twice these numbers, which is the disc a diver would report.
255
+ * Written as a span it would come out an oceanic giant, and one of those over
256
+ * a reef is a different picture altogether.
257
+ */
258
+ manta: {
259
+ beat: 0.3,
260
+ farLeast: 0.35,
261
+ farSpan: 0.45,
262
+ heft: 0.35,
263
+ menace: 0,
264
+ party: 1,
265
+ partySpan: 0,
266
+ seatLeast: 0.12,
267
+ seatSpan: 0.18,
268
+ sizeLeast: drawnAt(1.7, SWIMMING_OFF),
269
+ sizeSpan: drawnAt(2.7, SWIMMING_OFF) - drawnAt(1.7, SWIMMING_OFF),
270
+ takeLeast: 30,
271
+ takeSpan: 16,
272
+ },
273
+ mermaid: {
274
+ beat: 0.62,
275
+ farLeast: 0.5,
276
+ farSpan: 0.38,
277
+ heft: 0.35,
278
+ menace: 0,
279
+ party: 1,
280
+ partySpan: 0,
281
+ seatLeast: 0.2,
282
+ seatSpan: 0.3,
283
+ sizeLeast: drawnAt(1.9, SWIMMING_OFF),
284
+ sizeSpan: drawnAt(2.4, SWIMMING_OFF) - drawnAt(1.9, SWIMMING_OFF),
285
+ takeLeast: 26,
286
+ takeSpan: 14,
287
+ },
288
+ shark: {
289
+ beat: 0.42,
290
+ farLeast: 0.3,
291
+ farSpan: 0.45,
292
+ heft: 0.6,
293
+ menace: 0.9,
294
+ party: 1,
295
+ partySpan: 1,
296
+ seatLeast: 0.34,
297
+ seatSpan: 0.22,
298
+ sizeLeast: drawnAt(3.6, SWIMMING_OFF),
299
+ sizeSpan: drawnAt(5.5, SWIMMING_OFF) - drawnAt(3.6, SWIMMING_OFF),
300
+ takeLeast: 34,
301
+ takeSpan: 18,
302
+ },
303
+ turtle: {
304
+ beat: 0.24,
305
+ farLeast: 0.45,
306
+ farSpan: 0.5,
307
+ heft: 0.3,
308
+ menace: 0,
309
+ party: 1,
310
+ partySpan: 0,
311
+ seatLeast: 0.24,
312
+ seatSpan: 0.36,
313
+ sizeLeast: drawnAt(1.0, SWIMMING_OFF),
314
+ sizeSpan: drawnAt(1.5, SWIMMING_OFF) - drawnAt(1.0, SWIMMING_OFF),
315
+ takeLeast: 40,
316
+ takeSpan: 22,
317
+ },
318
+ }
319
+
320
+ /**
321
+ * How the crossing itself is shaped, past going from one side to the other.
322
+ *
323
+ * A straight line across a picture is a cutout on a rail, and these three
324
+ * numbers are how each of them declines to be one. `rise` is how much of the
325
+ * box the animal gains over the whole crossing, negative for a climb: the
326
+ * turtle is going up, because a turtle crossing your view is a turtle on its
327
+ * way to breathe. `roll` and `waves` are the slow swing it takes on the way.
328
+ *
329
+ * The dolphin's swing is the large one and it is not a wander at all. It is
330
+ * porpoising: a run of shallow dives under a surface it keeps coming back to.
331
+ */
332
+ const COURSES: Readonly<Record<VisitKind, Course>> = {
333
+ dolphin: { rise: -0.03, roll: 0.07, waves: 2.6 },
334
+ manta: { rise: -0.02, roll: 0.05, waves: 0.8 },
335
+ mermaid: { rise: -0.08, roll: 0.06, waves: 1.5 },
336
+ shark: { rise: 0, roll: 0.03, waves: 0.6 },
337
+ turtle: { rise: -0.2, roll: 0.04, waves: 1.1 },
338
+ }
339
+
340
+ /** How much of a crossing is spent fading in, and the same again fading out. */
341
+ const FADE = 0.14
342
+
343
+ interface Course {
344
+ rise: number
345
+ roll: number
346
+ waves: number
347
+ }
348
+
349
+ /**
350
+ * How far a party's own swing may differ from the one its kind is written with.
351
+ *
352
+ * What is in `COURSES` is what a crossing is like rather than what it is. Two
353
+ * turtles a week apart taking the identical line through the same water is the
354
+ * thing that turns a visitor into a screensaver, and one number drawn per party
355
+ * is all it takes for them not to. The climb is left alone: the climb is the
356
+ * reason the animal is crossing at all, and an animal that half of the time is
357
+ * not going up is an animal that half of the time has no errand.
358
+ */
359
+ const COURSE_SWING = 0.55
360
+
361
+ /** How far either side of its kind's beat an animal's own rhythm is drawn. */
362
+ const PACE_LEAST = 0.8
363
+
364
+ const PACE_SPAN = 0.45
365
+
366
+ /** How far past each side one starts and finishes, as shares of the width. */
367
+ const OFFING = 0.22
368
+
369
+ /**
370
+ * Watched seconds between one party leaving and the next arriving.
371
+ *
372
+ * Six minutes to a quarter of an hour, and it is the number this whole file
373
+ * turns on. Under it the sea is a procession and an animal stops being worth
374
+ * looking up for; a shark every few minutes is scenery. Far past it, somebody
375
+ * who leaves a desktop uncovered all afternoon never sees one at all. It is
376
+ * also deliberately not a day, which is the whole of what separates this file
377
+ * from `passers.ts`.
378
+ */
379
+ const REST_LEAST = 360
380
+
381
+ const REST_SPAN = 540
382
+
383
+ /** How much of that rest the water waits before the first party of a sitting. */
384
+ const OPENING = 0.35
385
+
386
+ /**
387
+ * How far behind the leader the rest of a pod swims, and how far off to a side.
388
+ *
389
+ * In lengths of the animal. A pod is not a rank and it is not a queue: it holds
390
+ * a loose echelon off one shoulder, so what is wanted is a lag that grows down
391
+ * the party and a shoulder that alternates.
392
+ */
393
+ const WING_BACK = 1.6
394
+
395
+ const WING_SIDE = 0.6
396
+
397
+ /** How much smaller each one down a party is drawn than the leader. */
398
+ const WING_SHRINK = 0.1
399
+
400
+ /** How far apart in its own stroke each one down a party is. */
401
+ const WING_PHASE = 0.17
402
+
403
+ /** How much of a drawn size is distance rather than the animal. */
404
+ const DEPTH_SIZE = 0.55
405
+
406
+ /**
407
+ * How far a visitor is felt through the water, in its own lengths, and how hard
408
+ * the loudest of them is felt at all.
409
+ *
410
+ * Wide and soft, which is the opposite of a hull. A hull is a hard thing
411
+ * shoving water aside; this is a shape going by, and what the shoal does about
412
+ * it is lean away and drift back. `shoal.ts` reads the same `Startle` either
413
+ * way and knows nothing about which it has been handed.
414
+ */
415
+ const FELT_REACH = 1.9
416
+
417
+ const FELT = 0.34
418
+
419
+ const MIN_SPAN = 1
420
+
421
+ /** How much of a visitor has to have arrived before the water can see it. */
422
+ const SEEN = 0.05
423
+
424
+ /**
425
+ * The share of days that have a mermaid on them.
426
+ *
427
+ * She is the one thing in this file that does not run on watched time, and the
428
+ * reason is that no share could get her rare enough. A party crosses every ten
429
+ * minutes or so of uncovered wallpaper, so the longest odds anybody would write
430
+ * down still put her in the water most afternoons, and a thing you have seen
431
+ * twice is a thing you are sure about. She has no business being that.
432
+ *
433
+ * So she keeps an appointment instead, which is `passers.ts`'s clock rather
434
+ * than this file's: the desktop seeds this water with the calendar day, so one
435
+ * day in thirty draws her and the rest of the month does not. About once a
436
+ * month, and never twice.
437
+ *
438
+ * The other half of it is that the water only advances while the wallpaper is
439
+ * uncovered. On the day that has her she is a few parties in rather than at
440
+ * some hour of the clock, so the day she turns up is the day you actually see
441
+ * her, instead of the day she crossed an empty screen behind a browser.
442
+ */
443
+ const MERMAID_DAYS = 1 / 30
444
+
445
+ /** How many parties into her day she may be. */
446
+ const MERMAID_PLACE = 3
447
+
448
+ /**
449
+ * The visitors of a piece of water.
450
+ *
451
+ * `eager` throws the rest away and crosses one party after another, for the
452
+ * preview harness and for tests. Nothing should pass it in earnest, for the
453
+ * reason `createPassers` gives at greater length: the rarity is the feature.
454
+ */
455
+ export function createVisitors(options: VisitorsOptions): Visitors {
456
+ const random = makeRandom(options.seed ^ 0x71a5)
457
+ const kinds = options.kinds ?? (["dolphin", "manta", "mermaid", "shark", "turtle"] as const)
458
+ const eager = options.eager ?? false
459
+
460
+ // Everything that is drawn on a share, which is everything but her. Asked for
461
+ // her alone, which is what the preview harness and the drawing tests do, the
462
+ // pool is what was asked for and the appointment is off: a sheet with one
463
+ // animal on it wants that animal every time.
464
+ const pool = kinds.filter((kind) => kind !== "mermaid")
465
+ const drawn = pool.length > 0 ? pool : kinds
466
+
467
+ /**
468
+ * Which party of today's is hers, or -1 on the days that have none.
469
+ *
470
+ * Off a stream of its own, and a stirred one, which is two decisions and both
471
+ * of them were bought. Stirred because the desktop's seed is the calendar
472
+ * day: a xorshift handed two seeds a day apart returns nearly the same first
473
+ * number, so a month of days asked this question got a month of the same
474
+ * answer, and the answer was no. `stir` in `perlin.ts` is the fix and says
475
+ * why. Its own stream so that adding her moved nothing else: the water shares
476
+ * one sequence, and a draw taken out of the middle of it is every sea after
477
+ * this one laid out somewhere else.
478
+ */
479
+ const plan = makeRandom(stir(options.seed ^ 0x4d61))
480
+
481
+ const owed =
482
+ kinds.includes("mermaid") && pool.length > 0 && plan() < MERMAID_DAYS
483
+ ? Math.floor(plan() * MERMAID_PLACE)
484
+ : -1
485
+
486
+ /** How many parties this water has put up, which is how her turn is known. */
487
+ let parties = 0
488
+
489
+ let width = Math.max(MIN_SPAN, options.width)
490
+ let height = Math.max(MIN_SPAN, options.height)
491
+
492
+ const crossing: Visitor[] = []
493
+
494
+ /** The course the party is on, which every member of it shares. */
495
+ let take = 1
496
+ let course: Course = COURSES.turtle
497
+ let seat = 0
498
+
499
+ /** What the water is being told to do this tick; see `Startle`. */
500
+ let felt: Startle | null = null
501
+
502
+ /** Watched seconds still owed before the next party arrives. */
503
+ let rest = (REST_LEAST + random() * REST_SPAN) * OPENING
504
+
505
+ // Only what is actually showing. A party fades in over the first of its
506
+ // crossing and fades out over the last, and a shark nobody can see yet is a
507
+ // shark nothing should be reacting to.
508
+ options.water?.enter(() =>
509
+ crossing
510
+ .filter((one) => one.weight > SEEN)
511
+ .map((one) => ({
512
+ depth: one.depth,
513
+ menace: HABITS[one.kind].menace * one.weight,
514
+ size: one.size,
515
+ x: one.x,
516
+ y: one.y,
517
+ })),
518
+ )
519
+
520
+ /** How common each of them is over this reef today; see `census.ts`. */
521
+ const shares = mixOf(drawn, options.seed)
522
+
523
+ /** Which kind comes next, on how much of the water each of them is. */
524
+ function which(): VisitKind {
525
+ let total = 0
526
+ for (const kind of drawn) total += shares[kind]
527
+
528
+ let at = random() * total
529
+ for (const kind of drawn) {
530
+ at -= shares[kind]
531
+ if (at <= 0) return kind
532
+ }
533
+
534
+ return drawn[0] ?? "turtle"
535
+ }
536
+
537
+ /** The kind's course, as this party is actually going to take it. */
538
+ function taken(kind: VisitKind): Course {
539
+ const own = COURSES[kind]
540
+ const swung = (by: number, much: number): number => by * (1 - much + random() * much * 2)
541
+
542
+ return {
543
+ rise: own.rise,
544
+ roll: swung(own.roll, COURSE_SWING),
545
+ waves: swung(own.waves, COURSE_SWING / 2),
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Put a party in the water, off the side of the picture.
551
+ *
552
+ * Every member shares the leader's course and its clock, and differs in where
553
+ * it sits against that course, how far behind it is, and how far through its
554
+ * own stroke. Pods do not beat in time. They beat near enough in time that
555
+ * the eye reads one animal's worth of decision, which is what the lag and
556
+ * `WING_PHASE` are between them.
557
+ */
558
+ function arrive(): void {
559
+ const kind = parties++ === owed ? "mermaid" : which()
560
+ const habit = HABITS[kind]
561
+ const facing = random() < 0.5 ? -1 : 1
562
+ const far = habit.farLeast + random() * habit.farSpan
563
+ const size = width * (habit.sizeLeast + random() * habit.sizeSpan)
564
+ const count = habit.party + Math.floor(random() * (habit.partySpan + 1))
565
+ const run = width * (1 + OFFING * 2)
566
+
567
+ take = habit.takeLeast + random() * habit.takeSpan
568
+ course = taken(kind)
569
+ seat = height * (habit.seatLeast + random() * habit.seatSpan)
570
+
571
+ for (let made = 0; made < count; made++) {
572
+ const side = made % 2 === 0 ? 1 : -1
573
+ const out = Math.ceil(made / 2)
574
+ const depth = Math.max(0.06, far - out * 0.05)
575
+
576
+ crossing.push({
577
+ along: -(made * WING_BACK * size) / run,
578
+ depth,
579
+ facing,
580
+ kind,
581
+ lift: side * out * WING_SIDE * size,
582
+ pace: PACE_LEAST + random() * PACE_SPAN,
583
+ size: size * (1 - made * WING_SHRINK) * (1 - DEPTH_SIZE + DEPTH_SIZE * depth),
584
+ stroke: (random() + made * WING_PHASE) % 1,
585
+ tilt: 0,
586
+ weight: 0,
587
+ x: 0,
588
+ y: 0,
589
+ })
590
+ }
591
+ }
592
+
593
+ /** How far up the box the course is, at `at` through the crossing. */
594
+ const rides = (at: number): number =>
595
+ height * (course.rise * at + course.roll * Math.sin(at * Math.PI * 2 * course.waves))
596
+
597
+ /**
598
+ * Where the course has this one, and which way it is therefore pointing.
599
+ *
600
+ * The heading is read off the course rather than set, so an animal always
601
+ * points along the line it is actually travelling and a renderer never has to
602
+ * be told twice. A step either side is enough to read a slope from: none of
603
+ * these courses has a corner in it.
604
+ */
605
+ function courseAt(one: Visitor): { heading: number; x: number; y: number } {
606
+ const from = -OFFING * width
607
+ const to = width * (1 + OFFING)
608
+ const at = Math.max(0, Math.min(1, one.along))
609
+ const step = 0.01
610
+ const ahead = Math.min(1, at + step)
611
+ const back = Math.max(0, at - step)
612
+ const run = (to - from) * (ahead - back) * one.facing
613
+
614
+ return {
615
+ heading: Math.atan2(rides(ahead) - rides(back), run || 1),
616
+ x: one.facing > 0 ? from + at * (to - from) : to - at * (to - from),
617
+ y: seat + rides(at) + one.lift,
618
+ }
619
+ }
620
+
621
+ return {
622
+ crossing,
623
+
624
+ resize(nextWidth, nextHeight) {
625
+ width = Math.max(MIN_SPAN, nextWidth)
626
+ height = Math.max(MIN_SPAN, nextHeight)
627
+ // Dropped rather than rescaled, the way a passer is. A shark that changed
628
+ // length halfway across would be worse than no shark, and a window being
629
+ // dragged is the one moment nobody is watching the water.
630
+ crossing.length = 0
631
+ felt = null
632
+ rest = REST_LEAST + random() * REST_SPAN
633
+ },
634
+
635
+ get startle() {
636
+ return felt
637
+ },
638
+
639
+ step(seconds) {
640
+ const dt = Math.min(Math.max(seconds, 0), 0.1)
641
+
642
+ if (crossing.length === 0) {
643
+ rest -= dt
644
+ if (eager || rest <= 0) {
645
+ arrive()
646
+ rest = REST_LEAST + random() * REST_SPAN
647
+ }
648
+ }
649
+
650
+ for (let at = crossing.length - 1; at >= 0; at--) {
651
+ const one = crossing[at]
652
+ if (!one) continue
653
+
654
+ one.along += (dt * driven(one.kind, one.stroke)) / Math.max(take, 0.001)
655
+ if (one.along >= 1) {
656
+ crossing.splice(at, 1)
657
+ continue
658
+ }
659
+
660
+ const where = courseAt(one)
661
+ one.stroke = (one.stroke + HABITS[one.kind].beat * paced(one) * dt) % 1
662
+ one.tilt = drawnTilt(where.heading + pitched(one.kind, one.stroke), one.facing)
663
+ one.weight = Math.max(0, Math.min(1, one.along / FADE, (1 - one.along) / FADE))
664
+ one.x = where.x
665
+ // The course says where the animal is going; its own beat says where it
666
+ // is between one stroke and the next, which is not the same question.
667
+ one.y = where.y + heaved(one.kind, one.stroke) * one.size
668
+ }
669
+
670
+ // The loudest of them, which for a pod is whichever is furthest into its
671
+ // crossing. There is one slot in the water for this, and a fish has no
672
+ // use for a second push from the same animal's friend.
673
+ felt = null
674
+ for (const one of crossing) {
675
+ const force = FELT * HABITS[one.kind].heft * one.weight
676
+ if (force <= 0 || (felt && felt.force >= force)) continue
677
+
678
+ // Its own distance goes with it. A shape crossing the near water has no
679
+ // business shoving fish that are a long way behind it; a hull leaves its
680
+ // depth off for the opposite reason, which `Startle.depth` sets out.
681
+ felt = { depth: one.depth, force, reach: one.size * FELT_REACH, x: one.x, y: one.y }
682
+ }
683
+ },
684
+ }
685
+ }
686
+
687
+ /**
688
+ * One station of a body: how far along, and how deep above and below the spine.
689
+ *
690
+ * A table rather than a curve, because a silhouette is read at its shoulders
691
+ * and its wrist and nowhere else, and a table is the only form in which those
692
+ * are numbers somebody can move. `over` is towards the back and `under`
693
+ * towards the belly, and they differ everywhere on every animal here. A body
694
+ * as deep below its spine as above it is a leaf.
695
+ *
696
+ * `x` runs from the nose at 1 back to the tail joint, in a body two units long.
697
+ */
698
+ interface Station {
699
+ readonly over: number
700
+ readonly under: number
701
+ readonly x: number
702
+ }
703
+
704
+ /**
705
+ * The shark, from the side.
706
+ *
707
+ * Slim, with the depth well forward and a long taper into a narrow wrist, which
708
+ * is what separates a shark's outline from a fish's before a single fin is
709
+ * drawn. A fish carries its depth in the middle. A shark carries it at the
710
+ * shoulders and then runs away to a point.
711
+ */
712
+ const SHARK: readonly Station[] = [
713
+ { over: 0.005, under: 0.005, x: 1 },
714
+ { over: 0.045, under: 0.05, x: 0.88 },
715
+ { over: 0.085, under: 0.088, x: 0.7 },
716
+ { over: 0.113, under: 0.105, x: 0.46 },
717
+ { over: 0.115, under: 0.098, x: 0.2 },
718
+ { over: 0.098, under: 0.078, x: -0.06 },
719
+ { over: 0.072, under: 0.055, x: -0.3 },
720
+ { over: 0.05, under: 0.036, x: -0.46 },
721
+ { over: 0.032, under: 0.026, x: -0.58 },
722
+ ]
723
+
724
+ /**
725
+ * The dolphin, from the side.
726
+ *
727
+ * The beak is the whole animal. Everything else about a dolphin's outline is
728
+ * shared with a shark of the same size, and the step from a thin rostrum up
729
+ * into the melon is the one place the eye tells them apart at a distance. It is
730
+ * worth four stations of a nine-station table.
731
+ */
732
+ const DOLPHIN: readonly Station[] = [
733
+ { over: 0.02, under: 0.028, x: 1 },
734
+ { over: 0.032, under: 0.05, x: 0.92 },
735
+ { over: 0.055, under: 0.062, x: 0.84 },
736
+ { over: 0.1, under: 0.078, x: 0.78 },
737
+ { over: 0.15, under: 0.115, x: 0.68 },
738
+ { over: 0.185, under: 0.165, x: 0.44 },
739
+ { over: 0.18, under: 0.175, x: 0.14 },
740
+ { over: 0.14, under: 0.13, x: -0.2 },
741
+ { over: 0.095, under: 0.088, x: -0.44 },
742
+ { over: 0.062, under: 0.056, x: -0.62 },
743
+ ]
744
+
745
+ /** The ones whose whole body carries a wave, and is therefore built on a spine. */
746
+ type Swimmer = "dolphin" | "mermaid" | "shark"
747
+
748
+ /** Where each one's tail is joined on, in the same units the tables use. */
749
+ const JOINTS: Readonly<Record<Swimmer, number>> = {
750
+ dolphin: -0.62,
751
+ mermaid: -0.62,
752
+ shark: -0.58,
753
+ }
754
+
755
+ /**
756
+ * How much of a wave each carries, and how far the tail swings.
757
+ *
758
+ * `sweep` is the swing at the joint, in body lengths, and it is the number
759
+ * `fish_shape.ts` argues about at length: a tail sweeping further than the
760
+ * animal's travel accounts for reads as flapping in place rather than as
761
+ * swimming. Both of these are under a fish's fifth, because both animals are
762
+ * large, and a large swimmer beats less far and much more slowly.
763
+ */
764
+ const SWEEP: Readonly<Record<Swimmer, { sweep: number; waves: number }>> = {
765
+ dolphin: { sweep: 0.075, waves: 0.62 },
766
+ mermaid: { sweep: 0.105, waves: 0.58 },
767
+ shark: { sweep: 0.085, waves: 0.78 },
768
+ }
769
+
770
+ /** Where along the body the still point is, and how the swing grows past it. */
771
+ const finned = (u: number): number => 0.16 - 0.7 * u + 1.54 * u * u
772
+
773
+ /**
774
+ * The same question answered by something with a spine that does not do that.
775
+ *
776
+ * A fish is still a fifth of the way back and moving everywhere else, head
777
+ * included. A person swimming is the other arrangement: the head is held,
778
+ * because a swimmer looks where they are going, and the back above the waist
779
+ * has no joints that would let it do anything else. Everything the animal has
780
+ * happens below that and it happens all at once, which is why this is a power
781
+ * rather than a curve with a low point in it.
782
+ */
783
+ const waisted = (u: number): number => 0.05 + 0.95 * u ** 2.4
784
+
785
+ /** Which of the two each swimmer is read off. */
786
+ const ENVELOPES: Readonly<Record<Swimmer, (u: number) => number>> = {
787
+ dolphin: finned,
788
+ mermaid: waisted,
789
+ shark: finned,
790
+ }
791
+
792
+ /**
793
+ * How far the mermaid's body is held off the straight, and how high she rides.
794
+ *
795
+ * The arch is the pose, and it is most of what makes the drawing read. Laid out
796
+ * along a line she is a diver: head down, body level, tail astern, and what the
797
+ * eye gets is somebody in a wetsuit. Every mermaid anybody has ever drawn is
798
+ * bent instead, head carried high and the tail swept away below, and that curve
799
+ * says more about what she is than the fluke on the end of it does.
800
+ *
801
+ * `MERMAID_ARCH` is how much of the box the body gives up from the crown to the
802
+ * tail joint, and `MERMAID_RIDE` is how much of that is taken back off so the
803
+ * curve straddles the line she travels along rather than hanging under it. The
804
+ * shape between them is a smoothstep, which is flat at both ends and steepest
805
+ * in the middle: the head stays level, the fluke stays level, and the waist is
806
+ * where the body turns. A curve that bent at the ends instead would be an
807
+ * animal with a broken neck.
808
+ */
809
+ const MERMAID_ARCH = 0.52
810
+
811
+ const MERMAID_RIDE = 0.21
812
+
813
+ /**
814
+ * The shape of each swimmer at rest.
815
+ *
816
+ * A shark at rest is a straight line and so is a dolphin, which is why nothing
817
+ * needed this until something with a spine that curves turned up.
818
+ */
819
+ const CAMBERS: Readonly<Record<Swimmer, (u: number) => number>> = {
820
+ dolphin: () => 0,
821
+ mermaid: (u) => MERMAID_ARCH * u * u * (3 - 2 * u) - MERMAID_RIDE,
822
+ shark: () => 0,
823
+ }
824
+
825
+ /** How far either side of a station its own slope is read, in body units. */
826
+ const SLOPE = 0.05
827
+
828
+ /** How many points a blade's own curve is cut into, along each side. */
829
+ const CURVE_STEPS = 8
830
+
831
+ /**
832
+ * A number as a path writes it, with the sign taken off a zero.
833
+ *
834
+ * Every drawing here is periodic, so the same moment of a beat comes round
835
+ * again and has to come out as the same string; a renderer that keeps a parsed
836
+ * path is otherwise reparsing an identical one. A sine at the end of its turn
837
+ * lands a hair under nothing rather than on it, and `-0.000` is a different
838
+ * string and the same point.
839
+ */
840
+ const fixed = (value: number): string => (Math.abs(value) < 0.0005 ? 0 : value).toFixed(3)
841
+
842
+ /**
843
+ * A ring of points wound the way every other ring here is wound.
844
+ *
845
+ * The whole animal is one filled path in several subpaths, for the reason
846
+ * `SUBMARINE` states: under an odd-even rule a fin sitting on the body cuts a
847
+ * hole out of it exactly where the two are meant to join. A winding rule wants
848
+ * every subpath turning the same way, and a fin under the belly is naturally
849
+ * written the other way round from a fin on the back.
850
+ *
851
+ * So nothing here has to be written in any particular order. The area says
852
+ * which way a ring turns and this turns the ones that disagree.
853
+ */
854
+ function wound(
855
+ points: readonly (readonly [number, number])[],
856
+ ): readonly (readonly [number, number])[] {
857
+ let area = 0
858
+ for (let at = 0; at < points.length; at++) {
859
+ const here = points[at]
860
+ const next = points[(at + 1) % points.length]
861
+ if (!here || !next) continue
862
+ area += here[0] * next[1] - next[0] * here[1]
863
+ }
864
+
865
+ return area <= 0 ? points : [...points].reverse()
866
+ }
867
+
868
+ /**
869
+ * A closed outline through its own points, rounded.
870
+ *
871
+ * The same smoothing `fish_shape.ts` uses and for the same reason: corners land
872
+ * on the midpoints and the points themselves become the controls, so a table of
873
+ * nine stations comes out as a body rather than as a polygon. A nose that is
874
+ * meant to be round is round.
875
+ */
876
+ function rounded(given: readonly (readonly [number, number])[]): string {
877
+ const points = wound(given)
878
+ const mid = (a: readonly [number, number], b: readonly [number, number]): string =>
879
+ `${fixed((a[0] + b[0]) / 2)} ${fixed((a[1] + b[1]) / 2)}`
880
+
881
+ const last = points[points.length - 1]
882
+ const first = points[0]
883
+ if (!last || !first) return ""
884
+
885
+ let d = `M${mid(last, first)}`
886
+ for (let at = 0; at < points.length; at++) {
887
+ const here = points[at]
888
+ const next = points[(at + 1) % points.length]
889
+ if (!here || !next) continue
890
+ d += `Q${fixed(here[0])} ${fixed(here[1])} ${mid(here, next)}`
891
+ }
892
+
893
+ return `${d}Z`
894
+ }
895
+
896
+ /** A straight-sided piece, as its own subpath, wound the way the body is. */
897
+ function piece(given: readonly (readonly [number, number])[]): string {
898
+ const points = wound(given)
899
+ if (points.length === 0) return ""
900
+
901
+ return `M${points.map(([x, y]) => `${fixed(x)} ${fixed(y)}`).join("L")}Z`
902
+ }
903
+
904
+ /** A bent spine, and where a thing standing on it at `x` has got to. */
905
+ interface Spine {
906
+ /** Which way the spine runs at `x`, in radians. */
907
+ angleAt(x: number): number
908
+ /** How far off the straight the spine is at `x`. */
909
+ at(x: number): number
910
+ /** A point `lift` clear of the spine at `x`, square to the way it runs. */
911
+ clear(x: number, lift: number): [number, number]
912
+ /** Where the tail is joined on. */
913
+ readonly joint: number
914
+ }
915
+
916
+ /**
917
+ * The wave a swimming body carries, as a spine anything can be hung on.
918
+ *
919
+ * `fish_shape.ts`'s, cut down to what is wanted here: an envelope nearly still
920
+ * a fifth of the way back from the nose, growing the whole excursion into the
921
+ * wrist. Everything hung on the body asks this where its own station has got
922
+ * to, so a dorsal leans because the back under it leaned, and a tail sweeps
923
+ * because the peduncle swept. Nothing in here rotates a fin.
924
+ */
925
+ function bend(kind: Swimmer, phase: number): Spine {
926
+ const { sweep, waves } = SWEEP[kind]
927
+ const joint = JOINTS[kind]
928
+ const run = 1 - joint
929
+ const camber = CAMBERS[kind]
930
+ const envelope = ENVELOPES[kind]
931
+
932
+ const at = (x: number): number => {
933
+ const u = Math.max(0, Math.min(1, (1 - x) / run))
934
+ return camber(u) + sweep * envelope(u) * Math.sin(2 * Math.PI * waves * u - phase)
935
+ }
936
+
937
+ const angleAt = (x: number): number => Math.atan2(at(x + SLOPE) - at(x - SLOPE), 2 * SLOPE)
938
+
939
+ return {
940
+ angleAt,
941
+ at,
942
+ clear(x, lift) {
943
+ const angle = angleAt(x)
944
+ return [x + Math.sin(angle) * lift, at(x) - Math.cos(angle) * lift]
945
+ },
946
+ joint,
947
+ }
948
+ }
949
+
950
+ /** A body lofted along its own bent spine, with whatever stands off it. */
951
+ function swimmer(
952
+ kind: Swimmer,
953
+ table: readonly Station[],
954
+ phase: number,
955
+ fins: (spine: Spine, phase: number) => string,
956
+ ): string {
957
+ const spine = bend(kind, phase)
958
+
959
+ const over: [number, number][] = []
960
+ const under: [number, number][] = []
961
+ for (const station of table) {
962
+ over.push(spine.clear(station.x, station.over))
963
+ under.push(spine.clear(station.x, -station.under))
964
+ }
965
+
966
+ return rounded([...over, ...under.reverse()]) + fins(spine, phase)
967
+ }
968
+
969
+ /**
970
+ * The tail's own frame, carried by whatever the peduncle is doing.
971
+ *
972
+ * A tail is given in its own coordinates, tip astern at negative x, and this is
973
+ * what puts it on the end of the animal. Nothing about it is a rotation of a
974
+ * drawing: the joint has swung, so the fin joined to it has swung.
975
+ */
976
+ function hung(spine: Spine): (point: readonly [number, number]) => [number, number] {
977
+ const angle = spine.angleAt(spine.joint)
978
+ const cos = Math.cos(angle)
979
+ const sin = Math.sin(angle)
980
+ const base = spine.at(spine.joint)
981
+
982
+ return ([x, y]) => [spine.joint + x * cos - y * sin, base + x * sin + y * cos]
983
+ }
984
+
985
+ /**
986
+ * The shark's fins, standing wherever the body has got to.
987
+ *
988
+ * The first dorsal is the one anybody actually reads, so it is tall and raked
989
+ * hard. The second dorsal and the anal are small, and they are there because a
990
+ * back with one fin on it reads as a dolphin. The tail is heterocercal, which
991
+ * is to say the upper lobe is much the longer, and that one asymmetry says
992
+ * shark from across a room.
993
+ */
994
+ function sharkFins(spine: Spine): string {
995
+ const carry = hung(spine)
996
+
997
+ return (
998
+ piece([
999
+ spine.clear(0.32, 0.05),
1000
+ spine.clear(0.13, 0.32),
1001
+ spine.clear(0.03, 0.15),
1002
+ spine.clear(-0.05, 0.05),
1003
+ ]) +
1004
+ piece([spine.clear(-0.3, 0.045), spine.clear(-0.37, 0.11), spine.clear(-0.44, 0.04)]) +
1005
+ piece([spine.clear(-0.26, -0.04), spine.clear(-0.34, -0.11), spine.clear(-0.42, -0.03)]) +
1006
+ piece([spine.clear(-0.04, -0.07), spine.clear(-0.13, -0.15), spine.clear(-0.2, -0.055)]) +
1007
+ blade(spine.clear(0.46, -0.05), 2.95, 0.26, 0.045, -0.02) +
1008
+ blade(spine.clear(0.42, -0.09), 2.55, 0.38, 0.055, 0.035) +
1009
+ piece([
1010
+ carry([0.04, 0]),
1011
+ carry([-0.08, -0.06]),
1012
+ carry([-0.36, -0.3]),
1013
+ carry([-0.22, -0.045]),
1014
+ carry([-0.2, 0.13]),
1015
+ carry([-0.05, 0.035]),
1016
+ ])
1017
+ )
1018
+ }
1019
+
1020
+ /**
1021
+ * The dolphin's, which are three shapes and a fluke.
1022
+ *
1023
+ * The dorsal is falcate: it leans back off its own base into a hook, and that
1024
+ * curve is what separates a dolphin from a shark's straight triangle at any
1025
+ * distance where both are grey.
1026
+ *
1027
+ * The flukes are drawn spread rather than edge on, which is a small lie. A
1028
+ * dolphin's tail is horizontal, so seen exactly side on it is a blade a few
1029
+ * pixels deep; nobody has ever drawn one that way and nobody would read it.
1030
+ *
1031
+ * The lie is worth telling properly. Spread as a sharp fork of straight-edged
1032
+ * lobes it is a tuna's tail, which is the animal it is trying not to be, so
1033
+ * the planform is the one a dolphin actually has: swept back off a narrow
1034
+ * peduncle, hollowed along the trailing edge into a crescent, and parted at
1035
+ * the middle by a notch small enough to read as a nick rather than as a fork.
1036
+ */
1037
+ function dolphinFins(spine: Spine): string {
1038
+ const carry = hung(spine)
1039
+
1040
+ return (
1041
+ piece([spine.clear(0.28, 0.13), spine.clear(0.03, 0.34), spine.clear(-0.08, 0.15)]) +
1042
+ blade(spine.clear(0.46, -0.12), 2.4, 0.44, 0.06, 0.04) +
1043
+ rounded([
1044
+ carry([0.1, 0]),
1045
+ carry([0.03, -0.07]),
1046
+ carry([-0.09, -0.15]),
1047
+ carry([-0.22, -0.21]),
1048
+ carry([-0.33, -0.245]),
1049
+ carry([-0.28, -0.19]),
1050
+ carry([-0.22, -0.13]),
1051
+ carry([-0.185, -0.07]),
1052
+ carry([-0.16, -0.02]),
1053
+ carry([-0.13, 0]),
1054
+ carry([-0.16, 0.02]),
1055
+ carry([-0.185, 0.07]),
1056
+ carry([-0.22, 0.13]),
1057
+ carry([-0.28, 0.19]),
1058
+ carry([-0.33, 0.245]),
1059
+ carry([-0.22, 0.21]),
1060
+ carry([-0.09, 0.15]),
1061
+ carry([0.03, 0.07]),
1062
+ ])
1063
+ )
1064
+ }
1065
+
1066
+ /**
1067
+ * A tapered blade from a root, at an angle, as a filled shape.
1068
+ *
1069
+ * What every flipper, wing and rowing fin here is made of. It tapers along a
1070
+ * curve rather than along a straight line, and it carries a bow, so a limb has
1071
+ * a trailing edge that gives. A rowing limb drawn as a triangle reads as an
1072
+ * oar, which is a thing pinned to a boat rather than a thing joined to an
1073
+ * animal.
1074
+ */
1075
+ function blade(
1076
+ root: readonly [number, number],
1077
+ angle: number,
1078
+ reach: number,
1079
+ wide: number,
1080
+ bow: number,
1081
+ ): string {
1082
+ const cos = Math.cos(angle)
1083
+ const sin = Math.sin(angle)
1084
+ const points: [number, number][] = []
1085
+
1086
+ const side = (way: number): void => {
1087
+ for (let step = 0; step <= CURVE_STEPS; step++) {
1088
+ const u = way > 0 ? step / CURVE_STEPS : 1 - step / CURVE_STEPS
1089
+ const off = wide * (1 - u ** 1.4) * way + bow * Math.sin(u * Math.PI)
1090
+ const along = u * reach
1091
+
1092
+ points.push([root[0] + along * cos - off * sin, root[1] + along * sin + off * cos])
1093
+ }
1094
+ }
1095
+
1096
+ side(1)
1097
+ side(-1)
1098
+
1099
+ return rounded(points)
1100
+ }
1101
+
1102
+ /** The shark, mid-stroke, nose at +x and two units nose to tail joint. */
1103
+ export const sharkBody = (stroke: number): string =>
1104
+ swimmer("shark", SHARK, stroke * Math.PI * 2, sharkFins)
1105
+
1106
+ /** The dolphin, the same way. */
1107
+ export const dolphinBody = (stroke: number): string =>
1108
+ swimmer("dolphin", DOLPHIN, stroke * Math.PI * 2, dolphinFins)
1109
+
1110
+ /**
1111
+ * The middle of a fore flipper's swing, and how far either side of it the
1112
+ * flipper gets.
1113
+ *
1114
+ * A turtle does not row like an oar. It flies, and the two halves of the beat
1115
+ * are not the same event: the downstroke sweeps from above the shell to below
1116
+ * it and is the whole of the thrust, and the recovery lifts the limb back up
1117
+ * doing nothing. So the swing is wide, and what the eye follows is a flipper
1118
+ * crossing the line of the shell twice a beat. The rear pair barely move: they
1119
+ * are a rudder, and a turtle steering with them is a turtle turning, which this
1120
+ * one is not.
1121
+ */
1122
+ const TURTLE_SEAT = 0.7
1123
+
1124
+ const TURTLE_SWING = 1.15
1125
+
1126
+ /**
1127
+ * How much of a beat pushes, and how much of it is spent going along with what
1128
+ * the push did.
1129
+ *
1130
+ * Off wild green turtles filmed from a drone on the Great Barrier Reef rather
1131
+ * than off a guess: van der Geest and colleagues, `Scientific Reports` 2022,
1132
+ * put the limb beat cycle at 4.2 seconds and found the animal producing thrust
1133
+ * for about three tenths of it, with the rest of the cycle a long low-drag
1134
+ * glide. `HABITS.turtle.beat` is that cycle, and this is that share.
1135
+ *
1136
+ * It is the one number that makes a turtle read as a turtle rather than as a
1137
+ * slow bird. An even beat is a wing. What the eye is actually looking for is a
1138
+ * shove followed by a coast, and the shove has to be the short half.
1139
+ */
1140
+ const TURTLE_PULL = 0.3
1141
+
1142
+ /**
1143
+ * Where a flipper is, and what it is doing, this far into a beat.
1144
+ *
1145
+ * One statement of the rhythm for both halves of the animal, since the drawing
1146
+ * and the going along are the same event: a turtle that is drawn shoving and
1147
+ * not moving any faster for it is a turtle on a conveyor.
1148
+ *
1149
+ * The phase runs through half a turn while it pushes and the other half while
1150
+ * it recovers, so the same swing takes a third of the time going down as coming
1151
+ * back up. Each half eases in and out of its ends, because a flipper does not
1152
+ * change direction in a frame, and the pause at the top and the bottom is where
1153
+ * a real one spends its stages.
1154
+ *
1155
+ * A phase of nothing is the flipper at the top, and half a turn is the flipper
1156
+ * at the bottom, so the push is the half that goes downwards. That is the way
1157
+ * round it has to be: the stroke that moves the animal is the one sweeping down
1158
+ * and back against the water, and a turtle drawn lifting on the short half and
1159
+ * sinking through the long one is a turtle whose limbs are hanging off it.
1160
+ *
1161
+ * `feather` is how far the limb has turned edge-on, which is nothing at all
1162
+ * through the push and most of the way over in the middle of the recovery. It
1163
+ * is the other half of why a beat has two halves: the same limb at the same
1164
+ * angle going up and going down is the same drawing, and what tells them apart
1165
+ * is that one of them is broadside and slow and the other is thin and quick.
1166
+ */
1167
+ function turtleBeat(cycle: number): {
1168
+ feather: number
1169
+ phase: number
1170
+ thrust: number
1171
+ trail: number
1172
+ } {
1173
+ const at = cycle - Math.floor(cycle)
1174
+ const ease = (part: number): number => (1 - Math.cos(Math.PI * part)) / 2
1175
+
1176
+ if (at < TURTLE_PULL) {
1177
+ const through = at / TURTLE_PULL
1178
+ return {
1179
+ feather: 0,
1180
+ phase: Math.PI * ease(through),
1181
+ thrust: Math.sin(Math.PI * through),
1182
+ trail: turtleHeave(cycle),
1183
+ }
1184
+ }
1185
+
1186
+ const back = (at - TURTLE_PULL) / (1 - TURTLE_PULL)
1187
+ return {
1188
+ feather: Math.sin(Math.PI * back),
1189
+ phase: Math.PI + Math.PI * ease(back),
1190
+ thrust: 0,
1191
+ trail: turtleHeave(cycle),
1192
+ }
1193
+ }
1194
+
1195
+ /**
1196
+ * How much faster a turtle is going while it is pushing than while it is not.
1197
+ *
1198
+ * A beat that is three parts glide is a beat the animal is losing way through,
1199
+ * so the crossing is not a constant speed with a flipper waving over it. The
1200
+ * mean is left alone: what this does is take from the glide and give to the
1201
+ * stroke, so the same crossing takes the same time and stops looking towed.
1202
+ */
1203
+ const TURTLE_SURGE = 0.55
1204
+
1205
+ /** The mean thrust over a whole beat, which is the pace the animal was given. */
1206
+ const TURTLE_HELD = (2 * TURTLE_PULL) / Math.PI
1207
+
1208
+ /** What a kind's speed is worth this far into its own beat, 1 being its mean. */
1209
+ function driven(kind: VisitKind, cycle: number): number {
1210
+ if (kind !== "turtle") return 1
1211
+
1212
+ // The mean of half a sine over the share of the beat that has one, which is
1213
+ // what has to come back off so the animal keeps the pace it was given.
1214
+ return 1 + TURTLE_SURGE * (turtleBeat(cycle).thrust - TURTLE_HELD)
1215
+ }
1216
+
1217
+ /**
1218
+ * How far a turtle has risen off its own course, this far into a beat.
1219
+ *
1220
+ * The limbs are not the animal. A flipper sweeping down against the water
1221
+ * shoves the whole turtle up, and the long glide after it is the animal slowly
1222
+ * losing that height again, so a beat is a small climb and a long sink. Leave
1223
+ * this out and every limb in the drawing can be right and the turtle still
1224
+ * reads as a cutout going along a rail, because that is what it is: a rigid
1225
+ * shell holding one height while something waves underneath it.
1226
+ *
1227
+ * The rise is the thrust added up, with the mean pace taken back off, which is
1228
+ * the same arithmetic `driven` does to the speed and for the same reason. Doing
1229
+ * it in closed form rather than by keeping a running total is what makes the
1230
+ * loop close: a beat that ends anywhere but where it started leaves a turtle
1231
+ * climbing out of the picture over a crossing.
1232
+ *
1233
+ * Zero at the top and the bottom of the range, and centred, so what comes out
1234
+ * is a share of a body either side of the course.
1235
+ */
1236
+ function turtleHeave(cycle: number): number {
1237
+ const at = cycle - Math.floor(cycle)
1238
+ const most = (2 * TURTLE_PULL * (1 - TURTLE_PULL)) / Math.PI
1239
+
1240
+ const raised =
1241
+ at < TURTLE_PULL
1242
+ ? (TURTLE_PULL / Math.PI) * (1 - Math.cos((Math.PI * at) / TURTLE_PULL)) - TURTLE_HELD * at
1243
+ : most - TURTLE_HELD * (at - TURTLE_PULL)
1244
+
1245
+ return raised / most - 0.5
1246
+ }
1247
+
1248
+ /**
1249
+ * How much of its own size a turtle rises and falls over one beat, and how far
1250
+ * it pitches doing it.
1251
+ *
1252
+ * Both are small. What they are for is that the animal is being pushed rather
1253
+ * than carried: it gains a little on the stroke and gives it back through the
1254
+ * glide, and it is nose up while it is gaining and nose down while it is not.
1255
+ * A tenth of a body and seven degrees is under the threshold at which anybody
1256
+ * would call it bobbing, and over the one at which the thing stops looking
1257
+ * towed.
1258
+ */
1259
+ const TURTLE_HEAVE = 0.1
1260
+
1261
+ const TURTLE_PITCH = 0.12
1262
+
1263
+ /**
1264
+ * How far a turtle's beat wanders either side of its own, and how many times it
1265
+ * does that over a crossing.
1266
+ *
1267
+ * A turtle does not beat to a metronome. It works at it for a few strokes and
1268
+ * then hangs there doing very little, and the difference between those two is
1269
+ * most of what says animal rather than mechanism. Drawn as a slow wander rather
1270
+ * than as meshing gears, because a rate that changes inside one frame is a
1271
+ * flipper that jumps, and a jump reads as a dropped frame rather than as a rest.
1272
+ */
1273
+ const TURTLE_BOUT = 0.55
1274
+
1275
+ const TURTLE_BOUTS = 6
1276
+
1277
+ /** How fast this one is working its own beat, this far into its crossing. */
1278
+ function paced(one: Visitor): number {
1279
+ if (one.kind !== "turtle") return one.pace
1280
+
1281
+ // Off `along` and its own `pace` rather than a clock, so the wander is a
1282
+ // thing the animal is doing over its crossing and two of them are never in
1283
+ // step: the pace sets the rate and starts the wander somewhere of its own.
1284
+ return (
1285
+ one.pace * (1 + TURTLE_BOUT * Math.sin(2 * Math.PI * (TURTLE_BOUTS * one.along + one.pace)))
1286
+ )
1287
+ }
1288
+
1289
+ /** How far off its course a kind's own beat carries it, in its own sizes. */
1290
+ function heaved(kind: VisitKind, cycle: number): number {
1291
+ return kind === "turtle" ? TURTLE_HEAVE * turtleHeave(cycle) : 0
1292
+ }
1293
+
1294
+ /** How far a kind's own beat tips its nose off the course it is following. */
1295
+ function pitched(kind: VisitKind, cycle: number): number {
1296
+ if (kind !== "turtle") return 0
1297
+
1298
+ // Nose up while it is climbing, which is while it is pushing. The same number
1299
+ // the rise is made of, so the two cannot disagree about which way it is going.
1300
+ return -TURTLE_PITCH * (turtleBeat(cycle).thrust - TURTLE_HELD)
1301
+ }
1302
+
1303
+ /**
1304
+ * How far the rear pair shift, and it is deliberately almost nothing.
1305
+ *
1306
+ * A grown sea turtle swims on its fore flippers alone. The rear pair are a
1307
+ * rudder and elevators: they trail, they hold the line, and they come
1308
+ * into it when the animal turns, which this one is not doing. Beating them in
1309
+ * time with the fore pair is the single thing that makes a turtle read as
1310
+ * something rowing, so what little they do here follows the body's own rise
1311
+ * rather than the stroke that drives it.
1312
+ */
1313
+ const TURTLE_REAR = 0.06
1314
+
1315
+ /**
1316
+ * How far behind the near pair the far pair is, as a share of a beat.
1317
+ *
1318
+ * Not half of one, which is what this was and what made the animal read as
1319
+ * something crawling. A turtle's fore flippers are wings and they beat
1320
+ * together: an alternating pair is a gait, and a gait is a thing with feet.
1321
+ * What separates the two sides on a silhouette is that the far one is shorter,
1322
+ * seated higher and a moment behind, and a moment is all this is.
1323
+ */
1324
+ const TURTLE_LAG = 0.06
1325
+
1326
+ /** How much of its length a flipper loses when it turns edge-on to come back. */
1327
+ const TURTLE_FEATHER = 0.22
1328
+
1329
+ /**
1330
+ * The turtle, from the side, mid-stroke.
1331
+ *
1332
+ * The shell is the animal and it does not bend, so this is the one drawing here
1333
+ * with no spine in it: a dome, a head on a short neck, and four limbs hung off
1334
+ * it. The fore flippers are what the eye follows. The far pair are drawn first
1335
+ * and half a beat behind, which is the whole of what tells a reader they are on
1336
+ * the other side of the animal.
1337
+ */
1338
+ export function turtleBody(stroke: number): string {
1339
+ const near = turtleBeat(stroke)
1340
+ const far = turtleBeat(stroke - TURTLE_LAG)
1341
+
1342
+ /**
1343
+ * One fore flipper, at whatever it is doing this far into the beat.
1344
+ *
1345
+ * The angle is the obvious half, and it is the one the eye reads: the limb
1346
+ * comes up past the line of the shell and goes down past it again. The rest
1347
+ * of it is the feathering, which is what keeps the two crossings from being
1348
+ * the same drawing: the limb is broadside on the stroke that pushes and
1349
+ * turned edge-on for the one that does not, so it is shorter, thinner and
1350
+ * flatter on the way back up.
1351
+ */
1352
+ const oar = (
1353
+ root: readonly [number, number],
1354
+ beat: { feather: number; phase: number },
1355
+ reach: number,
1356
+ wide: number,
1357
+ ): string => {
1358
+ const turned = 1 - TURTLE_FEATHER * beat.feather
1359
+
1360
+ return blade(
1361
+ root,
1362
+ TURTLE_SEAT - TURTLE_SWING * Math.cos(beat.phase),
1363
+ reach * turned,
1364
+ wide * turned,
1365
+ 0.06 * (1 - 0.6 * beat.feather),
1366
+ )
1367
+ }
1368
+
1369
+ return (
1370
+ // The far pair first, so the near pair is drawn over them. They are the
1371
+ // same limbs half a beat behind, which is the whole of what tells a reader
1372
+ // they are on the other side of the animal.
1373
+ oar([0.29, -0.01], far, 0.7, 0.085) +
1374
+ blade([-0.6, 0.06], 2.5 + TURTLE_REAR * far.trail, 0.26, 0.06, 0.02) +
1375
+ rounded([
1376
+ [0.45, -0.1],
1377
+ [0.2, -0.28],
1378
+ [-0.15, -0.32],
1379
+ [-0.5, -0.24],
1380
+ [-0.88, -0.02],
1381
+ [-0.55, 0.14],
1382
+ [-0.15, 0.2],
1383
+ [0.25, 0.16],
1384
+ [0.48, 0.06],
1385
+ ]) +
1386
+ rounded([
1387
+ [0.42, -0.05],
1388
+ [0.66, -0.11],
1389
+ [0.88, -0.12],
1390
+ [1.02, -0.03],
1391
+ [1.02, 0.06],
1392
+ [0.86, 0.12],
1393
+ [0.62, 0.12],
1394
+ [0.44, 0.1],
1395
+ ]) +
1396
+ oar([0.38, 0.07], near, 0.84, 0.115) +
1397
+ blade([-0.62, 0.11], 2.5 + TURTLE_REAR * near.trail, 0.3, 0.07, 0.02)
1398
+ )
1399
+ }
1400
+
1401
+ /**
1402
+ * How wide a manta is against its own length, and how far a wing folds away.
1403
+ *
1404
+ * A manta is a good deal wider than it is long, and drawn square it is a skate.
1405
+ * `MANTA_FOLD` is how much of a wing comes in at the top of a beat, and it is
1406
+ * foreshortening rather than a fold: seen from below, a wing that has beaten up
1407
+ * is pointing away from you, so what reaches the eye is a shorter wing with a
1408
+ * curve along its leading edge. That is the whole of the animation, and it is
1409
+ * why this one is drawn from below while the rest are drawn from the side.
1410
+ */
1411
+ const MANTA_SPAN = 1.95
1412
+
1413
+ const MANTA_FOLD = 0.34
1414
+
1415
+ /** How far a wing tip trails behind the shoulder that is driving it. */
1416
+ const MANTA_LAG = 0.1
1417
+
1418
+ /**
1419
+ * The manta, from below, mid-beat.
1420
+ *
1421
+ * Two units from the horns to the root of the tail, and getting on for twice
1422
+ * that across, which is the first thing to get right: a manta is much wider
1423
+ * than it is long and drawn square it is a skate. The wings are swept, so a tip
1424
+ * sits well behind the shoulder it grew from, and the trailing edge falls away
1425
+ * behind it. A diamond with none of that is a kite.
1426
+ *
1427
+ * The horns are small and they are most of the animal's name. A disc with two
1428
+ * points on its leading edge is a manta; a disc without them is a ray of some
1429
+ * sort.
1430
+ */
1431
+ export function mantaBody(stroke: number): string {
1432
+ const beat = Math.sin(stroke * Math.PI * 2)
1433
+ const span = MANTA_SPAN * (1 - MANTA_FOLD * Math.abs(beat))
1434
+ const bow = 0.22 * beat
1435
+ // A wing tip is not rigid and does not keep up with the shoulder driving it.
1436
+ // It trails furthest when the wing is moving fastest, which is as it passes
1437
+ // level, and further on the way up than on the way down. Without it the two
1438
+ // level moments of a beat are the same drawing and the animal has half the
1439
+ // beat it should have.
1440
+ const lag = -MANTA_LAG * (1 + 0.8 * Math.cos(stroke * Math.PI * 2))
1441
+
1442
+ /** One wing, shoulder to tip and back along the trailing edge to the tail. */
1443
+ const wing = (side: number): [number, number][] => {
1444
+ const tip: [number, number] = [-0.22 + lag, side * (span + bow)]
1445
+
1446
+ return [
1447
+ [0.9, side * 0.3],
1448
+ [0.42, side * (0.6 * span + bow * 0.4)],
1449
+ [0.05, side * (0.9 * span + bow * 0.8)],
1450
+ // Twice, so the corner survives the smoothing. A rounded wing tip is a
1451
+ // ray of some other sort, and the eye reads the tips before anything else.
1452
+ tip,
1453
+ tip,
1454
+ [-0.5, side * (0.5 * span + bow * 0.4)],
1455
+ [-0.74, side * 0.34],
1456
+ ]
1457
+ }
1458
+
1459
+ const horn = (side: number): string =>
1460
+ piece([
1461
+ [0.96, side * 0.3],
1462
+ [1.28, side * (0.4 + 0.06 * beat)],
1463
+ [1.22, side * 0.22],
1464
+ [0.94, side * 0.14],
1465
+ ])
1466
+
1467
+ return (
1468
+ rounded([
1469
+ [1.02, -0.24],
1470
+ [1.04, 0],
1471
+ [1.02, 0.24],
1472
+ ...wing(1),
1473
+ [-0.92, 0.12],
1474
+ [-0.92, -0.12],
1475
+ ...wing(-1).reverse(),
1476
+ ]) +
1477
+ horn(1) +
1478
+ horn(-1) +
1479
+ piece([
1480
+ [-0.86, -0.035],
1481
+ [-1.9, -0.012 - 0.05 * beat],
1482
+ [-1.91, 0.006 - 0.05 * beat],
1483
+ [-0.86, 0.035],
1484
+ ])
1485
+ )
1486
+ }
1487
+
1488
+ /**
1489
+ * The mermaid, from the side, below the neck.
1490
+ *
1491
+ * She is a woman, and in profile that is four numbers rather than any detail: a
1492
+ * shoulder narrower than the hip below it, a bust standing forward of the ribs,
1493
+ * a waist under both, and hips wider than the shoulder. Nothing else in a
1494
+ * silhouette carries it. The first version of this table was a swimmer's torso
1495
+ * of no particular kind and it read as a seal.
1496
+ *
1497
+ * Every one of them had to be pushed past what a body actually measures. The
1498
+ * smoothing this file draws with runs the outline through the midpoints between
1499
+ * stations, so a pinch written at its true depth comes out at half of it, and a
1500
+ * waist at half depth is not a waist, it is a body. The waist is stated twice
1501
+ * for the same reason: a station repeated is a station the curve has to reach.
1502
+ * The bust is on the `under` column alone, because a chest that is deep both
1503
+ * ways is a barrel.
1504
+ *
1505
+ * Past the hips the tail is thicker than the woman above it, which is the other
1506
+ * half of the read. Two halves of the same girth are one animal. The step at
1507
+ * the hip is what makes them two.
1508
+ *
1509
+ * The head is not in here. It is `HEAD`, laid on the front of this as a shape
1510
+ * of its own, because a head averaged into a shoulder is a shoulder. What is
1511
+ * here instead is a neck: two stations of almost nothing, running forward far
1512
+ * enough to be swallowed by the head that sits over them.
1513
+ */
1514
+ const MERMAID: readonly Station[] = [
1515
+ { over: 0.04, under: 0.037, x: 0.83 },
1516
+ { over: 0.036, under: 0.033, x: 0.765 },
1517
+ { over: 0.082, under: 0.055, x: 0.685 },
1518
+ { over: 0.08, under: 0.082, x: 0.6 },
1519
+ { over: 0.078, under: 0.128, x: 0.52 },
1520
+ { over: 0.074, under: 0.115, x: 0.46 },
1521
+ { over: 0.05, under: 0.05, x: 0.34 },
1522
+ { over: 0.046, under: 0.046, x: 0.29 },
1523
+ { over: 0.125, under: 0.14, x: 0.12 },
1524
+ { over: 0.125, under: 0.132, x: -0.02 },
1525
+ { over: 0.105, under: 0.105, x: -0.18 },
1526
+ { over: 0.078, under: 0.076, x: -0.34 },
1527
+ { over: 0.05, under: 0.048, x: -0.48 },
1528
+ { over: 0.028, under: 0.026, x: -0.62 },
1529
+ ]
1530
+
1531
+ /**
1532
+ * The head, on the front of the neck, in the body's own coordinates.
1533
+ *
1534
+ * A skull and a jaw and nothing else: at this distance an ear is an invention
1535
+ * and a nose is two pixels. What has to survive is that it is longer than it is
1536
+ * deep, that the back of it overhangs the neck, and that the chin stands
1537
+ * forward of the throat, because those three are what separate a head from the
1538
+ * end of a shoulder.
1539
+ *
1540
+ * It is carried above the line of the body and tipped nose up, which is not a
1541
+ * flourish. Anybody swimming face down is looking along their own eyebrows, so
1542
+ * the head rides high and the chin comes off the chest, and the gap that opens
1543
+ * under it is the throat. That gap is doing more work than the head is: laid
1544
+ * level, the chin sits on the collarbone, and what is left is a bump on the
1545
+ * front of a shoulder.
1546
+ *
1547
+ * It is drawn a good deal larger than a head measures against a body this long,
1548
+ * and that is deliberate. Everything else about her is legible at any size and
1549
+ * a head at its true share is four px of the drawing, which is a smudge with
1550
+ * the whole argument resting on it.
1551
+ */
1552
+ const HEAD: readonly (readonly [number, number])[] = [
1553
+ [1.046, -0.08],
1554
+ [0.999, -0.132],
1555
+ [0.929, -0.153],
1556
+ [0.852, -0.142],
1557
+ [0.785, -0.089],
1558
+ [0.773, -0.01],
1559
+ [0.824, 0.049],
1560
+ [0.903, 0.066],
1561
+ [0.981, 0.045],
1562
+ [1.038, -0.011],
1563
+ ]
1564
+
1565
+ /** How much of a wave there is along a lock, per unit of its length. */
1566
+ const HAIR_WAVES = 1.15
1567
+
1568
+ /** Where the hair is rooted. */
1569
+ const HAIR_ROOT = 0.9
1570
+
1571
+ /** How many points a lock is drawn through, along one edge of it. */
1572
+ const HAIR_STEPS = 9
1573
+
1574
+ /** How far the hair falls away behind her, and over what length it does it. */
1575
+ const HAIR_FALL = 0.12
1576
+ const HAIR_REACH = 0.9
1577
+
1578
+ /** How much further apart the locks are at the ends than at the roots. */
1579
+ const HAIR_SPREAD = 0.9
1580
+
1581
+ /**
1582
+ * The line the hair streams along, this far back of the crown.
1583
+ *
1584
+ * Every lock shares it, and that is what makes them one head of hair rather
1585
+ * than ten things that happen to start in the same place. It leaves the crown
1586
+ * flat and settles towards her back, because she is arched: the body drops away
1587
+ * under the line she is travelling, and hair that kept going straight would end
1588
+ * up flying a body's width above her.
1589
+ */
1590
+ function streaming(back: number): number {
1591
+ return HAIR_FALL * Math.min(1, Math.max(0, back) / HAIR_REACH) ** 1.3 - 0.105
1592
+ }
1593
+
1594
+ /**
1595
+ * One lock of hair.
1596
+ *
1597
+ * `at` is where it leaves the scalp and `lift` is how far off the streamline it
1598
+ * rides there, both in the crown's frame, where negative is up. `dense` puts a
1599
+ * lock in the silhouette rather than in the veil, which is what the fat short
1600
+ * ones nearest the head get: hair packs where it grows and separates as it
1601
+ * goes, so the mass on her shoulders stops light the way her head does and only
1602
+ * what has left the mass is sheer.
1603
+ *
1604
+ * The dense ones are also what covers her skull, so their lifts are a stack:
1605
+ * each overlaps the one under it by more than the beat can ever move them
1606
+ * apart, and the bottom one is buried in her head. A separate shape for the
1607
+ * scalp, which is what this had first, leaves a pocket of water wherever its
1608
+ * end fails to meet a lock, and the end moves.
1609
+ */
1610
+ type Lock = {
1611
+ at: number
1612
+ dense: boolean
1613
+ lift: number
1614
+ long: number
1615
+ off: number
1616
+ swing: number
1617
+ wide: number
1618
+ }
1619
+
1620
+ /**
1621
+ * Her hair, lock by lock.
1622
+ *
1623
+ * The first pass drew it as one shape at one weight, which is what everything
1624
+ * else in this water is, and it read as a scarf. Hair is not a shape. It is
1625
+ * many things of the same shape that agree about where they start and disagree
1626
+ * about everything after, and the disagreement is the whole of what makes it
1627
+ * read: two locks crossing tell you they are separate, and a mass that parts at
1628
+ * the ends tells you it is fine. Thinning one outline tells you nothing, so no
1629
+ * amount of weight was ever going to buy this.
1630
+ *
1631
+ * The fat ones are the mass and are short. The fine ones are what has come out
1632
+ * of the mass, and they are the long ones, because a lock only gets to wander
1633
+ * once nothing is holding it. They overlap into one thing at the roots, since
1634
+ * `wound` merges what overlaps rather than cutting it, and they come apart past
1635
+ * the shoulders where they are on their own.
1636
+ */
1637
+ const LOCKS: readonly Lock[] = [
1638
+ { at: 0.95, dense: true, lift: 0.035, long: 0.42, off: 0.2, swing: 0.02, wide: 0.05 },
1639
+ { at: 0.93, dense: true, lift: -0.005, long: 0.55, off: 0.8, swing: 0.03, wide: 0.052 },
1640
+ { at: 0.91, dense: true, lift: -0.032, long: 0.5, off: 1.5, swing: 0.035, wide: 0.048 },
1641
+ { at: 0.89, dense: true, lift: -0.052, long: 0.4, off: 2.2, swing: 0.03, wide: 0.04 },
1642
+ { at: 0.93, dense: false, lift: 0.035, long: 0.72, off: 2.0, swing: 0.05, wide: 0.032 },
1643
+ { at: 0.89, dense: false, lift: -0.06, long: 0.86, off: 2.6, swing: 0.075, wide: 0.03 },
1644
+ { at: 0.92, dense: false, lift: -0.005, long: 0.95, off: 3.3, swing: 0.06, wide: 0.034 },
1645
+ { at: 0.86, dense: false, lift: -0.075, long: 0.68, off: 4.0, swing: 0.085, wide: 0.024 },
1646
+ { at: 0.9, dense: false, lift: 0.05, long: 1.04, off: 4.7, swing: 0.07, wide: 0.026 },
1647
+ { at: 0.88, dense: false, lift: -0.035, long: 1.12, off: 5.5, swing: 0.09, wide: 0.022 },
1648
+ ]
1649
+
1650
+ /**
1651
+ * One lock, this far into the beat.
1652
+ *
1653
+ * The wave is measured along the lock's own length rather than along its share
1654
+ * of itself, so a long lock gets more of a wave than a short one instead of the
1655
+ * same wave stretched, and it opens from nothing at the root: hair moves at the
1656
+ * end it is not held by.
1657
+ */
1658
+ function lock(spine: Spine, phase: number, hair: Lock): string {
1659
+ const crown = spine.at(HAIR_ROOT)
1660
+ const over: [number, number][] = []
1661
+ const under: [number, number][] = []
1662
+
1663
+ for (let step = 0; step <= HAIR_STEPS; step += 1) {
1664
+ const along = step / HAIR_STEPS
1665
+ const back = HAIR_ROOT - hair.at + hair.long * along
1666
+ const wave = Math.sin(2 * Math.PI * HAIR_WAVES * along * hair.long - phase - hair.off)
1667
+ const half = hair.wide * (1 - along) ** 0.55
1668
+ const y =
1669
+ crown +
1670
+ streaming(back) +
1671
+ hair.lift * (1 + HAIR_SPREAD * along) +
1672
+ hair.swing * along * along * wave
1673
+
1674
+ over.push([hair.at - hair.long * along, y - half])
1675
+ under.push([hair.at - hair.long * along, y + half])
1676
+ }
1677
+
1678
+ return rounded([...over, ...under.reverse()])
1679
+ }
1680
+
1681
+ /** The locks of one density, at this point in the beat, as one drawing. */
1682
+ function hair(stroke: number, dense: boolean): string {
1683
+ const phase = stroke * Math.PI * 2
1684
+ const spine = bend("mermaid", phase)
1685
+
1686
+ return LOCKS.filter((each) => each.dense === dense)
1687
+ .map((each) => lock(spine, phase, each))
1688
+ .join("")
1689
+ }
1690
+
1691
+ /**
1692
+ * An arm, in its two pieces, as shares of reach and of thickness.
1693
+ *
1694
+ * `blade` is what every other limb in this file is made of and it is wrong for
1695
+ * this one. A fin tapers to a point because a fin is a fin; an arm does not,
1696
+ * and drawn that way it came out as a whisker off the chest. An arm at this
1697
+ * size is a bar that thins into the wrist and then widens again, and that one
1698
+ * widening is the whole of what makes it an arm rather than a stick.
1699
+ *
1700
+ * Three pieces rather than one because of what a straight one looked like,
1701
+ * which was a pole hung off a body. An elbow is the joint nothing else in this
1702
+ * water has, and a limb with a bend in it is read as a limb before anything
1703
+ * else about the animal is read at all.
1704
+ */
1705
+ const UPPER_ARM: readonly (readonly [number, number])[] = [
1706
+ [0, 1],
1707
+ [0.5, 0.88],
1708
+ [1, 0.8],
1709
+ ]
1710
+
1711
+ /** The forearm, which is a taper into the wrist and nothing else. */
1712
+ const FOREARM: readonly (readonly [number, number])[] = [
1713
+ [0, 0.86],
1714
+ [0.42, 0.72],
1715
+ [0.75, 0.58],
1716
+ [1, 0.48],
1717
+ ]
1718
+
1719
+ /**
1720
+ * The hand, off the end of it.
1721
+ *
1722
+ * A piece of its own, and the whole of the reason is the angle. The first one
1723
+ * was the last two stations of the forearm, pinched at the wrist and swelled
1724
+ * again past it, and what that draws is a bulb on a stick. A hand is not wider
1725
+ * than the arm it is on and that is not what makes it read: what makes it read
1726
+ * is that it is not in line with the arm. A wrist bends, and a shape that
1727
+ * leaves the forearm at an angle is a hand before it is anything else.
1728
+ *
1729
+ * Short and blunt. Fingers at this distance are one shape, and a hand drawn to
1730
+ * a point is a knife.
1731
+ */
1732
+ const HAND: readonly (readonly [number, number])[] = [
1733
+ [0, 0.72],
1734
+ [0.32, 0.8],
1735
+ [0.7, 0.72],
1736
+ [1, 0.34],
1737
+ ]
1738
+
1739
+ /**
1740
+ * Where an arm is held, how far it swings, how far it reaches, how thick.
1741
+ *
1742
+ * Held forward and down rather than back along the body, which is the pose that
1743
+ * survives being a silhouette: an arm laid against the ribs is inside the
1744
+ * outline and the animal has none. Rooted at the chest rather than at the top
1745
+ * of the shoulder for the same kind of reason, since an arm off the shoulder
1746
+ * crosses the throat, and the throat is the gap that says this is a head on a
1747
+ * neck.
1748
+ *
1749
+ * The swing is a share of a turn per stroke, which makes the arms look driven
1750
+ * by the tail. They are not driving anything and they are not meant to: what
1751
+ * moves them is the same body the tail is joined to.
1752
+ */
1753
+ const ARM_SEAT = 0.58
1754
+
1755
+ const ARM_SWING = 0.2
1756
+
1757
+ /** How much lower than the near arm the far one is carried. */
1758
+ const ARM_APART = 0.34
1759
+
1760
+ const ARM_REACH = 0.5
1761
+
1762
+ const ARM_GIRTH = 0.042
1763
+
1764
+ /** How much of an arm is above the elbow, and how far the elbow is bent. */
1765
+ const ARM_UPPER = 0.46
1766
+
1767
+ const ARM_BEND = 0.5
1768
+
1769
+ /**
1770
+ * How far the hand falls away from the forearm, how long it is, and how far
1771
+ * back up the forearm it is rooted.
1772
+ *
1773
+ * Rooted short of the wrist rather than on it, which is not anatomy, it is
1774
+ * arithmetic: two shapes meeting end to end at an angle leave a wedge open on
1775
+ * the outside of the turn, and a wedge in a filled outline is a hole. Started
1776
+ * inside the forearm, the joint is covered whatever the angle does.
1777
+ */
1778
+ const ARM_WRIST = 0.34
1779
+
1780
+ const ARM_HAND = 0.26
1781
+
1782
+ const ARM_JOINT = 0.86
1783
+
1784
+ /** How much of the near arm the far one is, being on the other side of her. */
1785
+ const ARM_FAR = 0.84
1786
+
1787
+ /** A limb of nearly one thickness, out from a root at an angle. */
1788
+ function limb(
1789
+ taper: readonly (readonly [number, number])[],
1790
+ root: readonly [number, number],
1791
+ angle: number,
1792
+ reach: number,
1793
+ wide: number,
1794
+ ): string {
1795
+ const cos = Math.cos(angle)
1796
+ const sin = Math.sin(angle)
1797
+ const at = (along: number, off: number): [number, number] => [
1798
+ root[0] + along * cos - off * sin,
1799
+ root[1] + along * sin + off * cos,
1800
+ ]
1801
+
1802
+ return rounded([
1803
+ ...taper.map(([along, thick]) => at(along * reach, thick * wide)),
1804
+ ...[...taper].reverse().map(([along, thick]) => at(along * reach, -thick * wide)),
1805
+ ])
1806
+ }
1807
+
1808
+ /**
1809
+ * The head, the hair, the arms, the veils and the fluke, wherever she has got
1810
+ * to.
1811
+ *
1812
+ * The far arm is drawn first and shorter and half a beat behind, which is the
1813
+ * same lie the turtle's far flippers tell and it is what gives an outline two
1814
+ * sides. Everything else is one thing each, because a silhouette has one of
1815
+ * everything a silhouette can see.
1816
+ */
1817
+ function mermaidFins(spine: Spine, phase: number): string {
1818
+ const carry = hung(spine)
1819
+
1820
+ /** One arm, out from the shoulder at whatever the beat has it doing. */
1821
+ const arm = (turn: number, reach: number, under: number): string => {
1822
+ const root = spine.clear(0.6, -0.04)
1823
+ const shoulder = ARM_SEAT + under + ARM_SWING * Math.sin(turn)
1824
+ const upper = reach * ARM_UPPER
1825
+ const held = shoulder - ARM_BEND
1826
+ const elbow: [number, number] = [
1827
+ root[0] + Math.cos(shoulder) * upper,
1828
+ root[1] + Math.sin(shoulder) * upper,
1829
+ ]
1830
+ const wrist: [number, number] = [
1831
+ elbow[0] + Math.cos(held) * (reach - upper) * ARM_JOINT,
1832
+ elbow[1] + Math.sin(held) * (reach - upper) * ARM_JOINT,
1833
+ ]
1834
+
1835
+ return (
1836
+ limb(UPPER_ARM, root, shoulder, upper, ARM_GIRTH) +
1837
+ limb(FOREARM, elbow, held, reach - upper, ARM_GIRTH * 0.9) +
1838
+ limb(HAND, wrist, held + ARM_WRIST, reach * ARM_HAND, ARM_GIRTH * 0.84)
1839
+ )
1840
+ }
1841
+
1842
+ return (
1843
+ arm(phase + Math.PI, ARM_REACH * ARM_FAR, ARM_APART) +
1844
+ rounded(HEAD.map(([x, y]) => spine.clear(x, -y))) +
1845
+ hair(phase / (Math.PI * 2), true) +
1846
+ arm(phase, ARM_REACH, 0) +
1847
+ blade(spine.clear(-0.28, 0.06), 3.48, 0.22, 0.032, 0.055) +
1848
+ blade(spine.clear(-0.28, -0.06), 2.66, 0.22, 0.032, -0.055) +
1849
+ rounded([
1850
+ carry([0.08, 0]),
1851
+ carry([-0.03, -0.09]),
1852
+ carry([-0.14, -0.2]),
1853
+ carry([-0.26, -0.3]),
1854
+ carry([-0.4, -0.36]),
1855
+ carry([-0.38, -0.25]),
1856
+ carry([-0.33, -0.14]),
1857
+ carry([-0.26, -0.06]),
1858
+ carry([-0.21, -0.02]),
1859
+ carry([-0.18, 0]),
1860
+ carry([-0.21, 0.02]),
1861
+ carry([-0.26, 0.06]),
1862
+ carry([-0.33, 0.14]),
1863
+ carry([-0.38, 0.25]),
1864
+ carry([-0.4, 0.36]),
1865
+ carry([-0.26, 0.3]),
1866
+ carry([-0.14, 0.2]),
1867
+ carry([-0.03, 0.09]),
1868
+ ])
1869
+ )
1870
+ }
1871
+
1872
+ /** The mermaid, mid-stroke, drawn the way every other swimmer here is. */
1873
+ export const mermaidBody = (stroke: number): string =>
1874
+ swimmer("mermaid", MERMAID, stroke * Math.PI * 2, mermaidFins)
1875
+
1876
+ /**
1877
+ * Her hair, mid-stroke, as a drawing of its own.
1878
+ *
1879
+ * The locks that have left the mass, and only those: the rest of her hair is in
1880
+ * the silhouette with her head. Everything else in this water stops the light
1881
+ * and comes out as one flat shape, which is what a body at eight metres does.
1882
+ * The loose end of a head of hair does not: it is a fine mass, the light goes
1883
+ * through as much of it as it catches, and what reaches the eye is softer than
1884
+ * the head it grew out of. So it is handed over separately for a renderer to
1885
+ * lay in under the animal at a share of the animal's own weight; `VEILS` has
1886
+ * the contract.
1887
+ */
1888
+ export const mermaidHair = (stroke: number): string => hair(stroke, false)
1889
+
1890
+ /**
1891
+ * Each animal's drawing, by kind, so a renderer needs to know none of the above.
1892
+ *
1893
+ * It asks for the animal it has been handed at the stroke it is at, and gets a
1894
+ * path in a body two units long with the nose at +x. What it does about
1895
+ * mirroring and turning is `Visitor.tilt`, which is the contract every other
1896
+ * animal in this water keeps.
1897
+ */
1898
+ export const BODIES: Readonly<Record<VisitKind, (stroke: number) => string>> = {
1899
+ dolphin: dolphinBody,
1900
+ manta: mantaBody,
1901
+ mermaid: mermaidBody,
1902
+ shark: sharkBody,
1903
+ turtle: turtleBody,
1904
+ }
1905
+
1906
+ /**
1907
+ * What the light gets through, by kind, for the kinds that have any.
1908
+ *
1909
+ * A second drawing, laid in behind the animal's own and at a share of its
1910
+ * weight. It exists because one of these is not all body. An animal is a shape
1911
+ * the light stops, which is why everything here is a silhouette and why one
1912
+ * flat weight is the honest way to draw one; hair is a fine mass that scatters
1913
+ * light rather than stopping it, so a head of it reads softer than the head
1914
+ * under it, and drawn at the body's own weight it is a helmet.
1915
+ *
1916
+ * Behind rather than over, so the part lying on her back and shoulders is
1917
+ * covered by the body at full weight and only what is streaming free is sheer.
1918
+ * Painted over instead, the overlap comes out as a pale stripe down the animal.
1919
+ *
1920
+ * A renderer that draws `BODIES` and skips this is not drawing a plainer
1921
+ * mermaid, it is drawing a bald one. Both or neither.
1922
+ */
1923
+ export const VEILS: Readonly<Partial<Record<VisitKind, (stroke: number) => string>>> = {
1924
+ mermaid: mermaidHair,
1925
+ }
1926
+
1927
+ /**
1928
+ * How much of the animal's own weight a veil is drawn at.
1929
+ *
1930
+ * Enough to read as its own thing against the body and not so little that it
1931
+ * goes back into the water. It is a share rather than a weight of its own
1932
+ * because everything about a visitor already fades with distance and with the
1933
+ * ends of its crossing, and hair that did not fade with the rest of her would
1934
+ * be a wig arriving separately.
1935
+ */
1936
+ export const VEIL_INK = 0.55