@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.
package/src/jellies.ts ADDED
@@ -0,0 +1,1794 @@
1
+ /**
2
+ * The jellyfish, of which there are eight and no two of them swim alike.
3
+ *
4
+ * A drifting animal is the hardest thing in this water to get right, because
5
+ * the wrong version of it is so cheap: a dome, a few streamers, a sine wave on
6
+ * the vertical, and the eye files it under decoration within a second. What
7
+ * makes a real one worth watching is that it is barely in charge of where it
8
+ * goes. It holds itself the right way up, it pushes when it wants to be
9
+ * higher, and the rest of its afternoon belongs to the water.
10
+ *
11
+ * So the model here is the animal's, and the horizontal is the current's. A
12
+ * bell points at the sky, a contraction drives it along the axis it is
13
+ * pointing, and how deep it ends up is settled by how often it bothers. Let
14
+ * that run and the sawtooth comes out on its own: a climb on every pulse, a
15
+ * slow sink between them, and a whole animal's day of holding one depth
16
+ * without anything ever setting a depth.
17
+ *
18
+ * ## Eight of them, and the differences are not decoration
19
+ *
20
+ * `Drive` is the argument for the file. Three of these animals do not pulse at
21
+ * all and two of them are not jellyfish, which is the sort of fact that reads
22
+ * as pedantry until you watch the water: a comb jelly gliding past a pulsing
23
+ * moon jelly is the clearest statement of scale and difference the scene can
24
+ * make with two small shapes.
25
+ *
26
+ * * **Moon** (*Aurelia aurita*), the saucer with four horseshoes in it. Its
27
+ * bell is a spring: the contraction throws a starting vortex off the rim
28
+ * and the elastic refill catches it, so one beat gives two pushes and the
29
+ * second is free. Gemmell and others measured it in PNAS in 2013 and
30
+ * called it passive energy recapture, and it is `Habit.recapture`.
31
+ * * **Barrel** (*Rhizostoma pulmo*), a bell to 90 cm with no marginal
32
+ * tentacles whatever and eight fused oral arms hanging under it. The
33
+ * strongest swimmer of the pulsers here and the steadiest.
34
+ * * **Lion's mane** (*Cyanea capillata*), the largest of them, its tentacles
35
+ * in eight clusters and the whole mass reported past 30 m in the north
36
+ * Atlantic. Slow, cold, and drawn as hair rather than as a body; see the
37
+ * note on `veil` below.
38
+ * * **Box** (*Chironex fleckeri*), the one with eyes. Twenty-four of them in
39
+ * four rhopalia, a tentacle bundle off each of the four pedalia, and a
40
+ * documented habit of steering round an obstacle instead of into it. It is
41
+ * the only animal in this file handed `about`, and that is the reason.
42
+ * * **Crown** (the coronates, *Atolla* and *Periphylla*), a deep red disc
43
+ * with a groove cut round it and one trailing tentacle far longer than the
44
+ * rest. Attacked, an *Atolla* runs a wave of blue light round its margin
45
+ * and keeps running it: the burglar alarm, meant to call in something
46
+ * bigger than whatever has hold of it. `Answer` is where that lives.
47
+ * * **Man o' war** (*Physalia physalis*), which is not one animal and cannot
48
+ * swim. A colony under a gas float, sailing at an angle to the wind, with
49
+ * left-handed and right-handed forms that leave on opposite tacks. It
50
+ * never pulses and it never leaves the surface.
51
+ * * **Comb** (*Mnemiopsis leidyi*), a ctenophore, so not a jellyfish and not
52
+ * even close: no stinging cells, eight rows of fused cilia beating in a
53
+ * travelling wave, and two oral lobes held open like a scoop. It swims
54
+ * mouth first, which is the detail worth drawing, because everything else
55
+ * in this water leads with the end that is not its mouth.
56
+ * * **Mauve stinger** (*Pelagia noctiluca*), a bell the size of a fist
57
+ * covered in warts, with only eight marginal tentacles and four oral arms,
58
+ * all of them long. It has no seabed stage at all, which is why it arrives
59
+ * in the millions or not at all, and its warts sting, which is why a
60
+ * swimmer can be stung by a part of it that is not a tentacle.
61
+ *
62
+ * ## What a startle is worth to something with no brain
63
+ *
64
+ * Almost nothing it does with its body. A hull going over empties a reef and a
65
+ * moon jelly does not notice, because there is nothing in a moon jelly that
66
+ * could. Only the box jelly moves, and only because it can see; the comb jelly
67
+ * and the man o' war gather themselves in and stop. That is the whole of
68
+ * `Answer`.
69
+ *
70
+ * What three of them do instead is make light. Around half the medusae in the
71
+ * sea are bioluminescent, which is more of them than anybody guesses, and none
72
+ * of it is left on: touched, then fading over seconds. `Light` is
73
+ * a column of its own for that reason, since a crown jelly answers with light
74
+ * and nothing else while a comb jelly does both at once. Three displays, and
75
+ * they are not the same display at three strengths: a wave that runs round a
76
+ * margin, a wash over a whole animal, and eight rows lit end to end.
77
+ *
78
+ * It is worth nothing in daylight, and that is not a defect to be tuned out.
79
+ * A flash is measured against the water around it, so these are a night thing,
80
+ * and the one light in this file that runs the other way is the comb jelly's
81
+ * diffraction, which needs a sun to take apart and dies in the dark. Both are
82
+ * the renderer's to weigh; see `combRows` and `washOf`.
83
+ *
84
+ * ## Hair is not a body
85
+ *
86
+ * `visitors.ts` reached this first and the reasoning carries over whole: a fine
87
+ * mass scatters light instead of stopping it, so a lion's mane drawn at the
88
+ * weight of its own bell is a solid animal wearing a solid skirt. What hangs
89
+ * under these is handed over separately and `VEIL_INK` in `visitors.ts` is the
90
+ * share it is drawn at.
91
+ */
92
+
93
+ import { abreast, type Biome, type Presence } from "./biome.ts"
94
+ import { mixOf } from "./census.ts"
95
+ import { makeNoise2, makeRandom } from "./perlin.ts"
96
+ import type { Point } from "./seabed.ts"
97
+ import { felt, type Startle } from "./shoal.ts"
98
+ import { drawnAt } from "./sizes.ts"
99
+
100
+ export type JellyKind =
101
+ | "barrel"
102
+ | "box"
103
+ | "comb"
104
+ | "crown"
105
+ | "mane"
106
+ | "manowar"
107
+ | "moon"
108
+ | "stinger"
109
+
110
+ /**
111
+ * How a thing gets about.
112
+ *
113
+ * The three ways anything in this file moves, and they are three different
114
+ * animals rather than three settings.
115
+ *
116
+ * * `pulse` squeezes a bell and coasts. Nothing happens between the beats,
117
+ * and the gap is most of the cycle.
118
+ * * `cilia` never stops. Eight rows of hairs beating in a travelling wave
119
+ * give a push that has no peak in it, so a ctenophore glides where a
120
+ * jellyfish lurches.
121
+ * * `sail` has no engine at all. A float on the surface goes where the wind
122
+ * puts it and heels over on the way.
123
+ */
124
+ export type Drive = "cilia" | "pulse" | "sail"
125
+
126
+ /**
127
+ * What one does with its body when something goes over.
128
+ *
129
+ * `none` is the common answer and the honest one. The other two belong to the
130
+ * animals here with the equipment to give another. What a jellyfish does with
131
+ * light is a separate question and a separate column; see `Light`.
132
+ */
133
+ export type Answer = "bolt" | "furl" | "none"
134
+
135
+ /**
136
+ * What one does with light when something takes hold of it.
137
+ *
138
+ * Around half of the medusae in the sea make their own, which is more of them
139
+ * than most people would guess and is why this is a column rather than a
140
+ * special case. None of it is on all the time. Bioluminescence costs the
141
+ * animal something and it is spent when there is something to spend it on:
142
+ * every display here is fired by contact and fades over seconds.
143
+ *
144
+ * * `wave` runs round the margin, turn after turn. A crown jelly's.
145
+ * * `wash` lights the whole animal at once, bell, arms and tentacles. The
146
+ * mauve stinger is named for it: *noctiluca* is the night light of the sea.
147
+ * * `rows` lights the eight rows a comb jelly already has, which is the
148
+ * detail worth having, because those rows are lit twice by two different
149
+ * things. See `combRows`.
150
+ *
151
+ * It is blue in the sea, near 470 nm, since blue is what gets furthest through
152
+ * water. This scene has no blue in it, so every renderer here draws it in the
153
+ * moon's colour and nothing else in the water glows at all.
154
+ */
155
+ export type Light = "none" | "rows" | "wash" | "wave"
156
+
157
+ export interface Jelly {
158
+ /** Where it is in its own cycle, 0 to 1. A sail has one and ignores it. */
159
+ beat: number
160
+ /** 0 is as far off as this water goes, 1 is up against the glass. */
161
+ readonly depth: number
162
+ /** A mirror, 1 or -1. For a man o' war it is which tack it sails on. */
163
+ readonly facing: number
164
+ /**
165
+ * How brightly it is lighting itself, 0 dark and 1 full.
166
+ *
167
+ * Light the animal made, and nothing else. It is 0 until something touches
168
+ * it and 0 again a few seconds later, and for the four kinds that cannot
169
+ * make any it is 0 for their whole lives. What a comb jelly's rows scatter
170
+ * out of the water's own light is not this and is not read off it.
171
+ */
172
+ glow: number
173
+ readonly kind: JellyKind
174
+ /** Bell across in px, distance already taken out of it. */
175
+ readonly size: number
176
+ /**
177
+ * How gathered it is, 0 open and 1 shut.
178
+ *
179
+ * The number the drawing and the travel are both read off, which is the rule
180
+ * `Squid.squeeze` keeps and for the same reason. What it means depends on
181
+ * what has one: a bell contracting, oral lobes drawn in, or a
182
+ * float letting its crest down.
183
+ */
184
+ squeeze: number
185
+ /** How far the apex leans off straight up, in radians. */
186
+ tilt: number
187
+ /**
188
+ * How far the light has run, in radians.
189
+ *
190
+ * Round the margin for a crown jelly, whose alarm is a wave that keeps going
191
+ * round; down the rows for a comb jelly, whose cilia beat in sequence from
192
+ * the far end towards the mouth. Nothing else uses it.
193
+ */
194
+ wave: number
195
+ x: number
196
+ y: number
197
+ }
198
+
199
+ export interface Jellies {
200
+ readonly jellies: readonly Jelly[]
201
+ resize(width: number, height: number, count?: number): void
202
+ /**
203
+ * Advance by `seconds`. `startle` is whatever the water is being told to do,
204
+ * which three of the seven kinds can answer and four cannot.
205
+ */
206
+ step(seconds: number, startle?: Startle | null): void
207
+ }
208
+
209
+ export interface JelliesOptions {
210
+ count: number
211
+ /**
212
+ * Where the ground is, so that nothing settles into it.
213
+ *
214
+ * A box jelly hunts along the bottom and a lion's mane sinks whenever it
215
+ * stops pulsing, so both of them arrive at the sand sooner or later. Without
216
+ * this they arrive at the bottom of the box, which in a scene with hills in
217
+ * it is a bell buried up to its margin.
218
+ *
219
+ * Asked at the animal's own distance, since the ground has one. A bell that
220
+ * took the near sand for its floor would sink through the hill it is drifting
221
+ * over and come to rest among the roots of plants standing in front of it.
222
+ */
223
+ floor?: (x: number, depth?: number) => number
224
+ height: number
225
+ /** Which kinds may turn up. The whole table, unless a caller narrows it. */
226
+ kinds?: readonly JellyKind[]
227
+ seed: number
228
+ /**
229
+ * The water these are in, so that whatever swims past can see them.
230
+ *
231
+ * Entered rather than announced, the way `visitors.ts` enters a shark: what
232
+ * a lion's mane looks like to a small fish is a fact about the lion's mane.
233
+ * It is read as well as written, by the box jelly, which has the eyes to
234
+ * read it with. Left out, these drift a water nothing is watching, and
235
+ * nothing is watching them back.
236
+ */
237
+ water?: Biome
238
+ width: number
239
+ }
240
+
241
+ /**
242
+ * What one kind is, where it lives, and how it gets about.
243
+ *
244
+ * A row an animal, and the row is the whole difference between them: nothing
245
+ * in `step` branches on a kind except the three drives, so a jellyfish that
246
+ * needed a branch of its own would be a second simulation wearing this one's
247
+ * interface. The two would part company the first time either was tuned.
248
+ *
249
+ * The bells are real animals measured across, in metres, at the distance these
250
+ * all drift past at; `sizes.ts` turns the pair into a share of the box. A moon
251
+ * jelly runs to 40 cm, a barrel to 90, a north Atlantic lion's mane past a
252
+ * metre and a half, a box jelly's cube to 30 cm, and a comb jelly is the size
253
+ * of a plum. A man o' war is measured along its float rather than across a
254
+ * bell, since it has no bell.
255
+ *
256
+ * How common each is against the others is not a column here. It is the same
257
+ * question as how common a jellyfish is against a fish, so `census.ts` answers
258
+ * both, and a bloom is a day on which the water is nearly all of one of them.
259
+ *
260
+ * `beats` is contractions a second at rest, and the numbers are the published
261
+ * ones held to this water's own pace: an adult *Aurelia* runs near one a
262
+ * second, a barrel jelly slower, a lion's mane slower again, and a box jelly
263
+ * fast enough to run a fish down. `sink` is what happens the rest of the time,
264
+ * so a kind that pulses rarely had better be near neutral or it spends its day
265
+ * on the floor.
266
+ */
267
+ interface Habit {
268
+ answer: Answer
269
+ /** How far the oral arms hang, in bell widths. */
270
+ armFall: number
271
+ /** How many hang under it. */
272
+ arms: number
273
+ /** Contractions a second while it is holding its depth. */
274
+ beats: number
275
+ /** How many bunches the marginal tentacles are gathered into. */
276
+ bunches: number
277
+ drive: Drive
278
+ /** How far back it lives: 0 the murk, 1 the glass. */
279
+ farLeast: number
280
+ farSpan: number
281
+ /** How far the marginal tentacles trail, in bell widths. */
282
+ hairFall: number
283
+ /** How many there are of them. */
284
+ hairs: number
285
+ /** What it does with light, which for half of them is nothing. */
286
+ light: Light
287
+ /** How much it looks like it stings, to anything that can see it. */
288
+ menace: number
289
+ /** What share of the cycle the contraction itself takes. */
290
+ push: number
291
+ /** What share of a pulse's push arrives on the refill instead. */
292
+ recapture: number
293
+ /** How far it leans off upright as it goes, in radians. */
294
+ roll: number
295
+ /** Where in the column it holds: 0 the surface, 1 the floor. */
296
+ seatLeast: number
297
+ seatSpan: number
298
+ /** Whether it can make out what is in the water with it. One of them can. */
299
+ sees: boolean
300
+ /** How fast it falls with nothing driving it, in bell widths a second. */
301
+ sink: number
302
+ /** How much way one pulse adds, in bell widths a second. */
303
+ thrust: number
304
+ /** How far the one long tentacle goes, in bell widths. Nothing, mostly. */
305
+ trail: number
306
+ /** How big it is across, in metres. */
307
+ wideLeast: number
308
+ wideSpan: number
309
+ }
310
+
311
+ const HABITS: Readonly<Record<JellyKind, Habit>> = {
312
+ barrel: {
313
+ answer: "none",
314
+ armFall: 0.85,
315
+ arms: 8,
316
+ beats: 0.55,
317
+ bunches: 1,
318
+ drive: "pulse",
319
+ farLeast: 0.32,
320
+ farSpan: 0.46,
321
+ hairFall: 0,
322
+ hairs: 0,
323
+ light: "none",
324
+ menace: 0.1,
325
+ push: 0.32,
326
+ recapture: 0.1,
327
+ roll: 0.1,
328
+ seatLeast: 0.3,
329
+ seatSpan: 0.34,
330
+ sees: false,
331
+ sink: 0.3,
332
+ thrust: 0.62,
333
+ trail: 0,
334
+ wideLeast: 0.4,
335
+ wideSpan: 0.5,
336
+ },
337
+ box: {
338
+ answer: "bolt",
339
+ armFall: 0,
340
+ arms: 0,
341
+ beats: 1.4,
342
+ bunches: 4,
343
+ drive: "pulse",
344
+ farLeast: 0.4,
345
+ farSpan: 0.4,
346
+ hairFall: 3.2,
347
+ hairs: 16,
348
+ light: "none",
349
+ menace: 0.55,
350
+ push: 0.3,
351
+ recapture: 0,
352
+ roll: 0.5,
353
+ seatLeast: 0.48,
354
+ seatSpan: 0.26,
355
+ sees: true,
356
+ sink: 0.22,
357
+ thrust: 0.95,
358
+ trail: 0,
359
+ wideLeast: 0.14,
360
+ wideSpan: 0.16,
361
+ },
362
+ comb: {
363
+ answer: "furl",
364
+ armFall: 0,
365
+ arms: 0,
366
+ beats: 3.2,
367
+ bunches: 1,
368
+ drive: "cilia",
369
+ farLeast: 0.5,
370
+ farSpan: 0.44,
371
+ hairFall: 0,
372
+ hairs: 0,
373
+ light: "rows",
374
+ menace: 0,
375
+ push: 0.5,
376
+ recapture: 0,
377
+ roll: 0.22,
378
+ seatLeast: 0.24,
379
+ seatSpan: 0.46,
380
+ sees: false,
381
+ sink: 0.05,
382
+ thrust: 0.34,
383
+ trail: 0,
384
+ wideLeast: 0.06,
385
+ wideSpan: 0.06,
386
+ },
387
+ crown: {
388
+ answer: "none",
389
+ armFall: 0,
390
+ arms: 0,
391
+ beats: 0.5,
392
+ bunches: 1,
393
+ drive: "pulse",
394
+ farLeast: 0.1,
395
+ farSpan: 0.3,
396
+ hairFall: 0.5,
397
+ hairs: 20,
398
+ light: "wave",
399
+ menace: 0.15,
400
+ push: 0.34,
401
+ recapture: 0.05,
402
+ roll: 0.12,
403
+ seatLeast: 0.54,
404
+ seatSpan: 0.2,
405
+ sees: false,
406
+ sink: 0.3,
407
+ thrust: 0.5,
408
+ trail: 3.4,
409
+ wideLeast: 0.09,
410
+ wideSpan: 0.09,
411
+ },
412
+ mane: {
413
+ answer: "none",
414
+ armFall: 1.05,
415
+ arms: 4,
416
+ beats: 0.34,
417
+ bunches: 8,
418
+ drive: "pulse",
419
+ farLeast: 0.18,
420
+ farSpan: 0.42,
421
+ hairFall: 2.6,
422
+ hairs: 44,
423
+ light: "none",
424
+ menace: 0.4,
425
+ push: 0.36,
426
+ recapture: 0.12,
427
+ roll: 0.09,
428
+ seatLeast: 0.38,
429
+ seatSpan: 0.32,
430
+ sees: false,
431
+ sink: 0.14,
432
+ thrust: 0.5,
433
+ trail: 0,
434
+ wideLeast: 0.7,
435
+ wideSpan: 0.8,
436
+ },
437
+ manowar: {
438
+ answer: "furl",
439
+ armFall: 0.7,
440
+ arms: 5,
441
+ beats: 0.12,
442
+ bunches: 1,
443
+ drive: "sail",
444
+ farLeast: 0.55,
445
+ farSpan: 0.35,
446
+ hairFall: 0.9,
447
+ hairs: 7,
448
+ light: "none",
449
+ menace: 0.5,
450
+ push: 0.5,
451
+ recapture: 0,
452
+ roll: 0.18,
453
+ seatLeast: 0.03,
454
+ seatSpan: 0.02,
455
+ sees: false,
456
+ sink: 0,
457
+ thrust: 0,
458
+ trail: 5.5,
459
+ wideLeast: 0.1,
460
+ wideSpan: 0.18,
461
+ },
462
+ moon: {
463
+ answer: "none",
464
+ armFall: 0.5,
465
+ arms: 4,
466
+ beats: 0.9,
467
+ bunches: 1,
468
+ drive: "pulse",
469
+ farLeast: 0.28,
470
+ farSpan: 0.56,
471
+ hairFall: 0.16,
472
+ hairs: 40,
473
+ light: "none",
474
+ menace: 0.05,
475
+ push: 0.3,
476
+ recapture: 0.36,
477
+ roll: 0.08,
478
+ seatLeast: 0.26,
479
+ seatSpan: 0.38,
480
+ sees: false,
481
+ sink: 0.28,
482
+ thrust: 0.5,
483
+ trail: 0,
484
+ wideLeast: 0.2,
485
+ wideSpan: 0.2,
486
+ },
487
+ stinger: {
488
+ answer: "none",
489
+ armFall: 1.5,
490
+ arms: 4,
491
+ beats: 0.7,
492
+ bunches: 1,
493
+ drive: "pulse",
494
+ farLeast: 0.34,
495
+ farSpan: 0.5,
496
+ hairFall: 2.2,
497
+ hairs: 8,
498
+ light: "wash",
499
+ menace: 0.45,
500
+ push: 0.31,
501
+ recapture: 0.18,
502
+ roll: 0.11,
503
+ seatLeast: 0.14,
504
+ seatSpan: 0.34,
505
+ sees: false,
506
+ sink: 0.29,
507
+ thrust: 0.55,
508
+ trail: 0,
509
+ wideLeast: 0.06,
510
+ wideSpan: 0.06,
511
+ },
512
+ }
513
+
514
+ export const KINDS: readonly JellyKind[] = Object.keys(HABITS) as JellyKind[]
515
+
516
+ /**
517
+ * How each of them gets about, and what each does when something goes over.
518
+ *
519
+ * The two columns of `HABITS` anybody outside this file has a use for. A
520
+ * renderer needs the first, since a float with a sail on it is not a bell and
521
+ * is not drawn like one; a test needs the second, to hold the three that
522
+ * answer against the four that cannot.
523
+ */
524
+ export const DRIVES: Readonly<Record<JellyKind, Drive>> = Object.fromEntries(
525
+ KINDS.map((kind) => [kind, HABITS[kind].drive]),
526
+ ) as Record<JellyKind, Drive>
527
+
528
+ export const ANSWERS: Readonly<Record<JellyKind, Answer>> = Object.fromEntries(
529
+ KINDS.map((kind) => [kind, HABITS[kind].answer]),
530
+ ) as Record<JellyKind, Answer>
531
+
532
+ export const LIGHTS: Readonly<Record<JellyKind, Light>> = Object.fromEntries(
533
+ KINDS.map((kind) => [kind, HABITS[kind].light]),
534
+ ) as Record<JellyKind, Light>
535
+
536
+ /**
537
+ * How far off these drift past, in metres.
538
+ *
539
+ * Nearer than the sharks and the turtles, which cross at `SWIMMING_OFF`. A
540
+ * jellyfish is something you find yourself already among, so it is drawn at
541
+ * the distance of a thing you would rather not touch.
542
+ */
543
+ export const DRIFTING_OFF = 5
544
+
545
+ /** How much of the drawn size is distance. A far one is smaller as well as fainter. */
546
+ const DEPTH_SIZE = 0.55
547
+
548
+ /**
549
+ * How fast one loses way, per second.
550
+ *
551
+ * High, because a bell is a bluff body and a pulse is spent almost as soon as
552
+ * it is made. It is what makes the sawtooth: without it a moon jelly's beats
553
+ * would add up and it would leave the top of the picture inside a minute.
554
+ */
555
+ const DRAG = 2.4
556
+
557
+ /** What share of the cycle the refill push occupies, after the contraction. */
558
+ const RECOVER = 0.34
559
+
560
+ /**
561
+ * The lowest and highest the pulse rate is ever driven to, against its own.
562
+ *
563
+ * The floor is not zero. A jellyfish that had risen above where it wanted to
564
+ * be and stopped altogether would be a dead one, and the picture reads that
565
+ * immediately: everything else in the water is moving.
566
+ */
567
+ const RATE_LEAST = 0.22
568
+ const RATE_MOST = 1.9
569
+
570
+ /** How hard being off its depth drives the pulse. */
571
+ const URGE = 0.8
572
+
573
+ /** How far off its seat one has to be for that to be the whole of its mind. */
574
+ const BAND = 0.13
575
+
576
+ /** The water everything here is carried by, in px a second, and how it turns over. */
577
+ const CURRENT = 15
578
+ const CURRENT_LIFT = 4
579
+ const CURRENT_GRAIN = 1 / 620
580
+ const CURRENT_CREEP = 0.014
581
+
582
+ /** How fast a lean is taken up, per second. */
583
+ const TURN = 1.1
584
+
585
+ /** How far a bolt lifts the pulse rate, and the push with it. */
586
+ const BOLT_RATE = 1.6
587
+ const BOLT_PUSH = 1.2
588
+
589
+ /** How long an alarm takes to run out, in seconds. */
590
+ const ALARM_HOLD = 4.5
591
+
592
+ /** How fast the crown jelly's light goes round its own margin, in turns a second. */
593
+ const FLASH_SPIN = 1.4
594
+
595
+ /** How far a comb jelly turns over to go down, at the bottom of its band. */
596
+ const DIVE = 1.2
597
+
598
+ /** How far a man o' war's crest goes over as it wets it. */
599
+ const WET = 0.3
600
+
601
+ /** How far a man o' war's tack takes it off the water it is sitting on. */
602
+ const TACK = 0.45
603
+
604
+ /** How far outside the box one drifts before it is made again, in bell widths. */
605
+ const MARGIN = 1.6
606
+
607
+ /** How near a moon jelly is born to the last one, in bell widths. A smack. */
608
+ const SMACK = 5
609
+
610
+ /** How far the top of the water is from the top of the box, as a share of it. */
611
+ const SKY = 0.02
612
+
613
+ /**
614
+ * How far a bell is held off the ground, in bell widths, before what hangs
615
+ * under it is counted.
616
+ *
617
+ * The oral arms are counted, because they are the body and a body laid over the
618
+ * sand reads as an animal in the sand. The marginal hair is not: it is slack
619
+ * and it streams, and a lion's mane sweeping the bottom with it is what one
620
+ * looks like.
621
+ */
622
+ const BELL_CLEAR = 0.35
623
+
624
+ const MIN_SPAN = 1
625
+
626
+ /**
627
+ * A jellyfish with the parts of it a renderer has no use for.
628
+ *
629
+ * Its way through the water and how frightened it is. Neither belongs on
630
+ * `Jelly`: what is drawn is a shape at a place, and a drawing that took a
631
+ * velocity would be a drawing deciding how the animal moves.
632
+ */
633
+ interface Held extends Jelly {
634
+ alarm: number
635
+ /** Where in the column it means to be, in px down the box. */
636
+ seat: number
637
+ /** Which way it turns over to descend, and which tack it sails on. */
638
+ side: number
639
+ vx: number
640
+ vy: number
641
+ /** Where the box jelly is going, in px. Nothing else has anywhere to be. */
642
+ wantX: number
643
+ wantY: number
644
+ }
645
+
646
+ /**
647
+ * How hard the bell is squeezed at this point of the cycle, 0 open and 1 shut.
648
+ *
649
+ * The contraction is quick and the refill is not, which is the shape of the
650
+ * whole animal's rhythm: about a third of the cycle is the push and the rest
651
+ * is the bell letting itself back out. Drawn as a rise on a sine's shoulder
652
+ * and a longer fall on a cosine's, so neither end has a corner in it.
653
+ */
654
+ export function squeezeAt(kind: JellyKind, beat: number): number {
655
+ const habit = HABITS[kind]
656
+ const at = beat - Math.floor(beat)
657
+ if (at < habit.push) return Math.sin((at / habit.push) * (Math.PI / 2))
658
+
659
+ const back = (at - habit.push) / Math.max(1e-6, 1 - habit.push)
660
+ return (1 + Math.cos(back * Math.PI)) / 2
661
+ }
662
+
663
+ /**
664
+ * What the bell is pushing with at this point of the cycle.
665
+ *
666
+ * Weighted so that one whole cycle of it comes to 1, whatever the shape: a
667
+ * caller multiplies by `Habit.thrust` and gets bell widths a second added per
668
+ * pulse, which is a number that can be argued about.
669
+ *
670
+ * The second lobe is the moon jelly's. A contraction throws a ring of water
671
+ * off the rim and the bell's own elasticity brings it back in against that
672
+ * ring, so the refill pushes as well, at rather less than the contraction did
673
+ * and for nothing. `Habit.recapture` is the share, and a kind that has none
674
+ * has a flat second half of the cycle.
675
+ */
676
+ export function thrustAt(kind: JellyKind, beat: number): number {
677
+ const habit = HABITS[kind]
678
+ const at = beat - Math.floor(beat)
679
+ if (at < habit.push) {
680
+ const along = at / habit.push
681
+ return (1 - habit.recapture) * (Math.PI / (2 * habit.push)) * Math.sin(along * Math.PI)
682
+ }
683
+
684
+ const after = (at - habit.push) / RECOVER
685
+ if (after >= 1 || habit.recapture <= 0) return 0
686
+ return habit.recapture * (Math.PI / (2 * RECOVER)) * Math.sin(after * Math.PI)
687
+ }
688
+
689
+ export function createJellies(options: JelliesOptions): Jellies {
690
+ const random = makeRandom(options.seed ^ 0x5e11)
691
+ const flow = makeNoise2(options.seed ^ 0x1f0a)
692
+ const kinds = options.kinds && options.kinds.length > 0 ? options.kinds : KINDS
693
+
694
+ let width = Math.max(MIN_SPAN, options.width)
695
+ let height = Math.max(MIN_SPAN, options.height)
696
+ let clock = 0
697
+
698
+ const held: Held[] = []
699
+ const gathered: Presence[] = []
700
+
701
+ /** How common each of them is in this water today; see `census.ts`. */
702
+ const shares = mixOf(kinds, options.seed)
703
+
704
+ /** Which kind this one is, drawn on the day's own shares. */
705
+ function draw(): JellyKind {
706
+ let total = 0
707
+ for (const kind of kinds) total += shares[kind]
708
+
709
+ let ticket = random() * total
710
+ for (const kind of kinds) {
711
+ ticket -= shares[kind]
712
+ if (ticket <= 0) return kind
713
+ }
714
+ return kinds[kinds.length - 1] ?? "moon"
715
+ }
716
+
717
+ /**
718
+ * Make one, either scattered through the water or walked in from a side.
719
+ *
720
+ * `from` is which side it enters by, or nothing for the first filling of the
721
+ * box. Nothing appears in the middle of the picture after that, which is the
722
+ * promise every animal in this scene keeps.
723
+ */
724
+ function born(from?: number): Held {
725
+ const kind = draw()
726
+ const habit = HABITS[kind]
727
+ const depth = habit.farLeast + random() * habit.farSpan
728
+ const across = habit.wideLeast + random() * habit.wideSpan
729
+ const size = drawnAt(across, DRIFTING_OFF) * width * (1 - DEPTH_SIZE + DEPTH_SIZE * depth)
730
+ const seat = height * (SKY + (habit.seatLeast + random() * habit.seatSpan) * (1 - SKY))
731
+
732
+ const x = from == null ? random() * width : from < 0 ? -size * MARGIN : width + size * MARGIN
733
+
734
+ // The ground is a ceiling on how deep a band can be. A crown jelly lives
735
+ // in the bottom third of the column and a hill in the bottom third is
736
+ // still a hill, so where it means to be is wherever the sand allows.
737
+ const bed = (options.floor?.(x, depth) ?? height) - size * clearOf(habit)
738
+ const seated = Math.min(seat, bed)
739
+
740
+ const one: Held = {
741
+ alarm: 0,
742
+ beat: random(),
743
+ depth,
744
+ facing: random() < 0.5 ? -1 : 1,
745
+ glow: 0,
746
+ kind,
747
+ seat: seated,
748
+ side: random() < 0.5 ? -1 : 1,
749
+ size,
750
+ squeeze: 0,
751
+ tilt: 0,
752
+ vx: 0,
753
+ vy: 0,
754
+ wantX: 0,
755
+ wantY: 0,
756
+ wave: random() * Math.PI * 2,
757
+ x,
758
+ y: clamp(seated + (random() - 0.5) * height * BAND, height * SKY + size * 0.6, bed),
759
+ }
760
+
761
+ one.squeeze = habit.drive === "pulse" ? squeezeAt(kind, one.beat) : 0
762
+ aim(one)
763
+
764
+ // A smack. Moon jellies are found in company rather than one at a time,
765
+ // and the water they came in on is the reason, so a new one takes the
766
+ // height of whichever is already about instead of picking its own.
767
+ if (kind === "moon") {
768
+ for (const other of held) {
769
+ if (other.kind !== "moon") continue
770
+ one.y = other.y + (random() - 0.5) * SMACK * one.size
771
+ one.seat = other.seat
772
+ break
773
+ }
774
+ }
775
+
776
+ return one
777
+ }
778
+
779
+ /** Somewhere for the one animal here with eyes to be going. */
780
+ function aim(one: Held): void {
781
+ one.wantX = random() * width
782
+ one.wantY = one.seat + (random() - 0.5) * height * BAND * 2
783
+ }
784
+
785
+ for (let made = 0; made < Math.max(0, options.count); made++) held.push(born())
786
+
787
+ options.water?.enter(() => {
788
+ gathered.length = 0
789
+ for (const one of held) {
790
+ const habit = HABITS[one.kind]
791
+ if (habit.menace <= 0) continue
792
+
793
+ // Measured across whatever it can reach with, which for a lion's mane is
794
+ // most of a picture and for a box jelly is several times its own bell.
795
+ // What a fish has to keep clear of is the hair.
796
+ gathered.push({
797
+ depth: one.depth,
798
+ menace: habit.menace,
799
+ size: one.size * (1 + Math.max(habit.hairFall, habit.trail) * 0.6),
800
+ x: one.x,
801
+ y: one.y,
802
+ })
803
+ }
804
+ return gathered
805
+ })
806
+
807
+ /** The water's own movement where this animal is, in px a second. */
808
+ function carry(one: Held): { x: number; y: number } {
809
+ const u = one.x * CURRENT_GRAIN
810
+ const v = one.y * CURRENT_GRAIN + clock * CURRENT_CREEP
811
+ return {
812
+ x: flow(u, v) * CURRENT * (0.4 + 0.6 * one.depth),
813
+ y: flow(u + 31.7, v - 12.3) * CURRENT_LIFT,
814
+ }
815
+ }
816
+
817
+ /** How far off its depth it is: 1 far below where it wants to be, -1 above. */
818
+ function urgeOf(one: Held): number {
819
+ return clamp((one.y - one.seat) / Math.max(1, height * BAND), -1, 1)
820
+ }
821
+
822
+ /**
823
+ * Where the box jelly means to point.
824
+ *
825
+ * Towards wherever it is going, and away from anything large it can make out
826
+ * on the way. *Chironex* has twenty-four eyes and a published habit of
827
+ * steering round a piling instead of into it, and this is the whole of what
828
+ * that is worth in a picture: a swerve nobody asked for, at a moment nothing
829
+ * else in the water reacted to.
830
+ */
831
+ function steer(one: Held): number {
832
+ let toX = one.wantX - one.x
833
+ let toY = one.wantY - one.y
834
+
835
+ for (const seen of options.water?.about() ?? []) {
836
+ const gap = Math.hypot(seen.x - one.x, seen.y - one.y)
837
+ const notice = seen.size * 1.4 + one.size
838
+ if (gap >= notice || gap <= 0.001) continue
839
+
840
+ const shove = (1 - gap / notice) * abreast(one.depth, seen.depth) * notice
841
+ toX -= ((seen.x - one.x) / gap) * shove
842
+ toY -= ((seen.y - one.y) / gap) * shove
843
+ }
844
+
845
+ return Math.atan2(toX, -toY)
846
+ }
847
+
848
+ /**
849
+ * Whether something is actually on this animal, 0 clear and 1 square on it.
850
+ *
851
+ * Touch, and touch is not sight. `Habit.sees` is a fact about eyes and it
852
+ * gates steering, since deciding to go round something means making it out
853
+ * at a distance. Knowing that a thing has hold of you needs no eyes at all:
854
+ * a jellyfish is a nerve net and being struck is exactly what a nerve net
855
+ * is for. So every kind that can make light reads the water for contact,
856
+ * and the four with none never ask.
857
+ *
858
+ * This is what actually fires the displays. A hull going over is minutes
859
+ * apart and a shark is rarer than that, and a light that only ever answered
860
+ * those would be a light nobody ever saw. What sets a real one off is a fish
861
+ * blundering into it, which happens all afternoon.
862
+ */
863
+ function touched(one: Held): number {
864
+ let worst = 0
865
+
866
+ for (const seen of options.water?.about() ?? []) {
867
+ const gap = Math.hypot(seen.x - one.x, seen.y - one.y)
868
+ const reach = (seen.size + one.size) * 0.5
869
+ if (gap >= reach || gap <= 0.001) continue
870
+
871
+ worst = Math.max(worst, (1 - gap / reach) * abreast(one.depth, seen.depth))
872
+ }
873
+
874
+ return worst
875
+ }
876
+
877
+ function pulse(one: Held, habit: Habit, dt: number, bolt: number): void {
878
+ const rate =
879
+ habit.beats * clamp(1 + urgeOf(one) * URGE, RATE_LEAST, RATE_MOST) * (1 + bolt * BOLT_RATE)
880
+
881
+ one.beat = (one.beat + rate * dt) % 1
882
+ one.squeeze = squeezeAt(one.kind, one.beat)
883
+
884
+ // Rate times the curve, because the curve is weighted per pulse and a
885
+ // faster animal is having more of them. A box jelly running from something
886
+ // gets its speed from beating oftener as much as from beating harder.
887
+ const kick = thrustAt(one.kind, one.beat) * habit.thrust * rate * (1 + bolt * BOLT_PUSH)
888
+ drive(one, dt, kick, (1 - one.squeeze) * habit.sink)
889
+ }
890
+
891
+ /**
892
+ * Eight rows of cilia, beating in sequence from the far end towards the
893
+ * mouth, which never stop and have no peak in them.
894
+ *
895
+ * So the push is flat and the animal glides. Everything a ctenophore does
896
+ * about its depth it does by turning over, since it has no way of pushing
897
+ * harder: `DIVE` is how far it is willing to go, and a comb jelly holding
898
+ * station at the top of its band is drawn most of the way upside down.
899
+ */
900
+ function cilia(one: Held, habit: Habit, dt: number, furl: number): void {
901
+ one.beat = (one.beat + habit.beats * dt) % 1
902
+ one.wave = (one.wave + habit.beats * Math.PI * 2 * dt) % (Math.PI * 2)
903
+ one.squeeze = furl
904
+
905
+ const working = 1 - furl
906
+ drive(one, dt, habit.thrust * DRAG * working, habit.sink)
907
+ }
908
+
909
+ /**
910
+ * A float on the surface, which is where it stays.
911
+ *
912
+ * *Physalia* cannot swim and does not try. What it has is a crest it holds
913
+ * up into the wind, and a handedness: the left-sailing and right-sailing
914
+ * forms leave on opposite tacks, at about 45 degrees off the wind's own
915
+ * line. A picture with one horizontal axis in it cannot show an angle, so
916
+ * what the tack comes to here is a float outrunning or lagging the water it
917
+ * is sitting on, which is what you would see from a beach anyway.
918
+ *
919
+ * Disturbed, it lets the crest down and rolls the float under to wet it,
920
+ * which is `furl` and is the nearest thing to a reflex a colony has.
921
+ */
922
+ function sail(one: Held, habit: Habit, dt: number, furl: number): void {
923
+ one.beat = (one.beat + habit.beats * dt) % 1
924
+
925
+ const wet = (1 - Math.cos(one.beat * Math.PI * 2)) / 2
926
+ one.squeeze = Math.max(furl, WET * wet)
927
+
928
+ const wind = carry(one)
929
+ one.vx = wind.x * TACK * one.side * (1 - one.squeeze * 0.5)
930
+ one.vy = 0
931
+ one.y = one.seat + Math.sin(one.beat * Math.PI * 2) * one.size * 0.04
932
+ one.tilt = swing(one.tilt, habit.roll * one.side * (0.4 + wet), TURN * dt)
933
+ one.x += (one.vx + wind.x) * dt
934
+ }
935
+
936
+ /** Way on, way off, and what the water does to it while it has neither. */
937
+ function drive(one: Held, dt: number, kick: number, fall: number): void {
938
+ const ax = Math.sin(one.tilt) * kick * one.size
939
+ const ay = -Math.cos(one.tilt) * kick * one.size
940
+
941
+ one.vx += (ax - one.vx * DRAG) * dt
942
+ one.vy += (ay + fall * one.size * DRAG - one.vy * DRAG) * dt
943
+
944
+ const water = carry(one)
945
+ one.x += (one.vx + water.x) * dt
946
+ one.y += (one.vy + water.y) * dt
947
+ }
948
+
949
+ /** A lazy lean, its own for each animal, going nowhere. */
950
+ function lean(one: Held, habit: Habit): number {
951
+ return habit.roll * flow(one.x * CURRENT_GRAIN * 0.5, clock * 0.05 + one.seat * 0.01)
952
+ }
953
+
954
+ return {
955
+ jellies: held,
956
+
957
+ resize(nextWidth, nextHeight, count) {
958
+ const scaleX = Math.max(MIN_SPAN, nextWidth) / width
959
+ const scaleY = Math.max(MIN_SPAN, nextHeight) / height
960
+ width = Math.max(MIN_SPAN, nextWidth)
961
+ height = Math.max(MIN_SPAN, nextHeight)
962
+
963
+ for (const one of held) {
964
+ one.x *= scaleX
965
+ one.y *= scaleY
966
+ one.seat *= scaleY
967
+ one.wantX *= scaleX
968
+ one.wantY *= scaleY
969
+ }
970
+
971
+ if (count == null) return
972
+ while (held.length > count) held.pop()
973
+ while (held.length < count) held.push(born(random() < 0.5 ? -1 : 1))
974
+ },
975
+
976
+ step(seconds, startle) {
977
+ const dt = clamp(seconds, 0, 0.1)
978
+ clock += dt
979
+
980
+ for (let at = 0; at < held.length; at++) {
981
+ const one = held[at]
982
+ if (!one) continue
983
+ const habit = HABITS[one.kind]
984
+
985
+ // What a startle is worth to this animal, which for four of the seven
986
+ // is nothing at all. Nothing is checking whether the thing that went
987
+ // over was frightening: a moon jelly has no equipment to check with.
988
+ if (startle && (habit.answer !== "none" || habit.light !== "none")) {
989
+ const gap = Math.hypot(startle.x - one.x, startle.y - one.y)
990
+ if (gap < startle.reach) {
991
+ const near = (1 - gap / startle.reach) * startle.force * felt(one.depth, startle)
992
+ one.alarm = Math.max(one.alarm, clamp(near, 0, 1))
993
+ }
994
+ }
995
+ // And whatever is on top of it this moment, which is the other half of
996
+ // being disturbed and the half that happens often.
997
+ if (habit.light !== "none") one.alarm = Math.max(one.alarm, touched(one))
998
+
999
+ one.alarm = Math.max(0, one.alarm - dt / ALARM_HOLD)
1000
+
1001
+ const bolt = habit.answer === "bolt" ? one.alarm : 0
1002
+ const furl = habit.answer === "furl" ? one.alarm : 0
1003
+
1004
+ // Light the animal made, which every kind that can make any makes the
1005
+ // same way: touched, then fading over seconds. A crown jelly's runs
1006
+ // round its own margin and keeps running, on the theory that whatever
1007
+ // turns up to look will be interested in the thing that has hold of
1008
+ // it. That is the only display here with a direction to it.
1009
+ if (habit.light !== "none") {
1010
+ one.glow = one.alarm
1011
+ if (habit.light === "wave" && one.alarm > 0) {
1012
+ one.wave = (one.wave + FLASH_SPIN * Math.PI * 2 * dt) % (Math.PI * 2)
1013
+ }
1014
+ }
1015
+
1016
+ if (habit.drive === "sail") {
1017
+ sail(one, habit, dt, furl)
1018
+ } else if (habit.drive === "cilia") {
1019
+ const want = one.side * (1 - urgeOf(one)) * DIVE + lean(one, habit)
1020
+ one.tilt = swing(one.tilt, want, TURN * dt)
1021
+ cilia(one, habit, dt, furl)
1022
+ } else {
1023
+ const want = habit.sees ? steer(one) : lean(one, habit)
1024
+ one.tilt = swing(one.tilt, want, TURN * dt)
1025
+ pulse(one, habit, dt, bolt)
1026
+ }
1027
+
1028
+ // Somewhere new to be, once it has arrived. Only the box jelly is ever
1029
+ // going anywhere, and this is how often it changes its mind.
1030
+ if (habit.sees && Math.hypot(one.wantX - one.x, one.wantY - one.y) < one.size) aim(one)
1031
+
1032
+ // The top of the water and the ground are both walls, and a bell that
1033
+ // met either would be a bell sliding along a line. What it gets is a
1034
+ // shove back into the water, which reads as an animal that thought
1035
+ // better of it.
1036
+ const roof = height * SKY + (habit.drive === "sail" ? 0 : one.size * 0.6)
1037
+ if (one.y < roof) {
1038
+ one.y = roof
1039
+ one.vy = Math.max(one.vy, 0)
1040
+ }
1041
+ const bed = (options.floor?.(one.x, one.depth) ?? height) - one.size * clearOf(habit)
1042
+ if (one.y > bed) {
1043
+ one.y = bed
1044
+ one.vy = Math.min(one.vy, 0)
1045
+ }
1046
+
1047
+ const gone = one.size * MARGIN
1048
+ if (one.x < -gone) held[at] = born(1)
1049
+ if (one.x > width + gone) held[at] = born(-1)
1050
+ }
1051
+ },
1052
+ }
1053
+ }
1054
+
1055
+ const clamp = (value: number, low: number, high: number): number =>
1056
+ Math.min(Math.max(value, low), high)
1057
+
1058
+ /** How far this kind holds its rim off the ground, in bell widths. */
1059
+ const clearOf = (habit: Habit): number => BELL_CLEAR + habit.armFall
1060
+
1061
+ const ease = (from: number, to: number, rate: number): number =>
1062
+ from + (to - from) * Math.min(1, Math.max(0, rate))
1063
+
1064
+ /**
1065
+ * The same, taken the short way round a circle.
1066
+ *
1067
+ * `Math.atan2` answers +π on one side of straight down and -π on the other, so
1068
+ * a bell that means to go down sits on a seam. Eased along the number line, a
1069
+ * hair of sideways drift across that seam sends the animal the whole way round
1070
+ * the compass and back, which reads as a jellyfish spinning on the spot. Taken
1071
+ * the short way, the same seam is a wobble. The answer is kept inside one
1072
+ * turn, because a wallpaper runs for days and a winding angle loses its
1073
+ * precision on the way to the card.
1074
+ */
1075
+ const swing = (from: number, to: number, rate: number): number => {
1076
+ const gone = ease(from, from + Math.atan2(Math.sin(to - from), Math.cos(to - from)), rate)
1077
+ return Math.atan2(Math.sin(gone), Math.cos(gone))
1078
+ }
1079
+
1080
+ /**
1081
+ * The drawings, all of them in a bell one unit across.
1082
+ *
1083
+ * The rim sits on the origin, the apex is above it and everything that hangs
1084
+ * hangs below, so a renderer scales by `Jelly.size`, mirrors by `Jelly.facing`
1085
+ * and turns by `Jelly.tilt`, exactly as it does a fish. A jelly at rest is
1086
+ * pointing at the sky, which is the one posture every animal here shares.
1087
+ *
1088
+ * The comb jelly is drawn in the same frame and comes out looking upside down,
1089
+ * because it is. A ctenophore swims mouth first, so the end that leads is the
1090
+ * end with the lobes on it, and putting the mouth where every other animal
1091
+ * here keeps its apex is the drawing telling the truth about it.
1092
+ */
1093
+
1094
+ /**
1095
+ * How many drawings one contraction is cut into.
1096
+ *
1097
+ * A bell squeezes continuously and a renderer keeps the paths it has walked,
1098
+ * so a path that never repeats is a cache with a leak in it. The same bargain
1099
+ * `fish_shape.ts` makes with a tail beat: enough steps that nobody counts
1100
+ * them, few enough that the whole vocabulary of the water fits in a map.
1101
+ */
1102
+ export const BELL_STEPS = 24
1103
+
1104
+ /** What a fine mass and a thick one are drawn as lines at, in bell widths. */
1105
+ export const HAIR_GIRTH: Readonly<Record<JellyKind, number>> = {
1106
+ barrel: 0.02,
1107
+ box: 0.016,
1108
+ comb: 0.012,
1109
+ crown: 0.014,
1110
+ mane: 0.009,
1111
+ manowar: 0.012,
1112
+ moon: 0.008,
1113
+ stinger: 0.011,
1114
+ }
1115
+
1116
+ export const ARM_GIRTH: Readonly<Record<JellyKind, number>> = {
1117
+ barrel: 0.075,
1118
+ box: 0.03,
1119
+ comb: 0.03,
1120
+ crown: 0.03,
1121
+ mane: 0.055,
1122
+ manowar: 0.04,
1123
+ moon: 0.03,
1124
+ stinger: 0.045,
1125
+ }
1126
+
1127
+ const fixed = (value: number): string => (Math.abs(value) < 0.0005 ? 0 : value).toFixed(3)
1128
+
1129
+ /**
1130
+ * A run of points as a curve that goes through all of them.
1131
+ *
1132
+ * Catmull-Rom, written out as cubics, because a bell is a shape somebody has
1133
+ * to be able to argue about and an argument about a bell is an argument about
1134
+ * where its shoulder is. Sampling the shape and smoothing it afterwards keeps
1135
+ * the numbers in the file the ones a reader can check.
1136
+ */
1137
+ function smooth(points: readonly Point[], shut: boolean): string {
1138
+ if (points.length < 2) return ""
1139
+
1140
+ const at = (i: number): Point => {
1141
+ const seat = shut
1142
+ ? (i + points.length) % points.length
1143
+ : Math.min(points.length - 1, Math.max(0, i))
1144
+ return points[seat] ?? { x: 0, y: 0 }
1145
+ }
1146
+
1147
+ const head = at(0)
1148
+ const parts = [`M${fixed(head.x)} ${fixed(head.y)}`]
1149
+ const last = shut ? points.length : points.length - 1
1150
+
1151
+ for (let i = 0; i < last; i++) {
1152
+ const back = at(i - 1)
1153
+ const from = at(i)
1154
+ const to = at(i + 1)
1155
+ const on = at(i + 2)
1156
+ parts.push(
1157
+ `C${fixed(from.x + (to.x - back.x) / 6)} ${fixed(from.y + (to.y - back.y) / 6)}` +
1158
+ ` ${fixed(to.x - (on.x - from.x) / 6)} ${fixed(to.y - (on.y - from.y) / 6)}` +
1159
+ ` ${fixed(to.x)} ${fixed(to.y)}`,
1160
+ )
1161
+ }
1162
+
1163
+ return shut ? `${parts.join(" ")} Z` : parts.join(" ")
1164
+ }
1165
+
1166
+ /**
1167
+ * The shape of one bell, as numbers rather than as a path.
1168
+ *
1169
+ * `shoulder` is what tells the seven apart before anything else does. Below
1170
+ * one the dome is drawn out to a point and reads as a helmet; at four the
1171
+ * sides go straight up and the top goes flat, which is a box jelly and is why
1172
+ * a box jelly is recognisable at forty pixels.
1173
+ */
1174
+ interface Dome {
1175
+ /** How deep the near rim bows below the far one, in bell widths. */
1176
+ bow: number
1177
+ /** How deep each scallop of the margin cuts, in bell widths. */
1178
+ cut: number
1179
+ /**
1180
+ * How far below the origin everything that hangs is rooted, in bell widths.
1181
+ *
1182
+ * The margin, for anything with a bell, which is where the origin already
1183
+ * is. A man o' war has no margin: what hangs off it hangs off the underside
1184
+ * of a float, and rooted at the origin it would come out of the middle of
1185
+ * the bag.
1186
+ */
1187
+ hang: number
1188
+ /** How many scallops show across the front of the margin. None is smooth. */
1189
+ lobes: number
1190
+ /** How square the shoulder is: under 1 pointed, 1 round, over 1 boxy. */
1191
+ shoulder: number
1192
+ /** How tall it stands, in bell widths, and how much taller it squeezes. */
1193
+ tall: number
1194
+ tallGain: number
1195
+ /**
1196
+ * How far the bumps on the dome stand off it, and how many show.
1197
+ *
1198
+ * One animal here has any. A mauve stinger's bell is covered in warts, and
1199
+ * they are not decoration: each one is a packed cluster of stinging cells,
1200
+ * which is why the animal can sting you with a part of itself that is not a
1201
+ * tentacle. It is also the whole of how you tell one from a moon jelly at a
1202
+ * distance, since neither of them is much bigger than a fist.
1203
+ */
1204
+ wart: number
1205
+ warts: number
1206
+ /** How wide it is when open, and how much of that a contraction takes. */
1207
+ wide: number
1208
+ wideLoss: number
1209
+ }
1210
+
1211
+ const DOMES: Readonly<Record<JellyKind, Dome>> = {
1212
+ barrel: {
1213
+ bow: 0.13,
1214
+ cut: 0.02,
1215
+ hang: 0,
1216
+ lobes: 8,
1217
+ shoulder: 1.15,
1218
+ tall: 0.5,
1219
+ tallGain: 0.14,
1220
+ wart: 0,
1221
+ warts: 0,
1222
+ wide: 1,
1223
+ wideLoss: 0.2,
1224
+ },
1225
+ box: {
1226
+ bow: 0.06,
1227
+ cut: 0,
1228
+ hang: 0,
1229
+ lobes: 0,
1230
+ shoulder: 3.4,
1231
+ tall: 0.92,
1232
+ tallGain: 0.12,
1233
+ wart: 0,
1234
+ warts: 0,
1235
+ wide: 1,
1236
+ wideLoss: 0.2,
1237
+ },
1238
+ comb: {
1239
+ bow: 0,
1240
+ cut: 0,
1241
+ hang: 0,
1242
+ lobes: 0,
1243
+ shoulder: 1,
1244
+ tall: 0,
1245
+ tallGain: 0,
1246
+ wart: 0,
1247
+ warts: 0,
1248
+ wide: 1,
1249
+ wideLoss: 0.14,
1250
+ },
1251
+ crown: {
1252
+ bow: 0.05,
1253
+ cut: 0.05,
1254
+ hang: 0,
1255
+ lobes: 6,
1256
+ shoulder: 1.6,
1257
+ tall: 0.3,
1258
+ tallGain: 0.1,
1259
+ wart: 0,
1260
+ warts: 0,
1261
+ wide: 1,
1262
+ wideLoss: 0.16,
1263
+ },
1264
+ mane: {
1265
+ bow: 0.11,
1266
+ cut: 0.07,
1267
+ hang: 0,
1268
+ lobes: 4,
1269
+ shoulder: 0.85,
1270
+ tall: 0.34,
1271
+ tallGain: 0.12,
1272
+ wart: 0,
1273
+ warts: 0,
1274
+ wide: 1,
1275
+ wideLoss: 0.18,
1276
+ },
1277
+ manowar: {
1278
+ bow: 0,
1279
+ cut: 0,
1280
+ hang: 0.12,
1281
+ lobes: 0,
1282
+ shoulder: 1,
1283
+ tall: 0,
1284
+ tallGain: 0,
1285
+ wart: 0,
1286
+ warts: 0,
1287
+ wide: 1,
1288
+ wideLoss: 0,
1289
+ },
1290
+ moon: {
1291
+ bow: 0.1,
1292
+ cut: 0.015,
1293
+ hang: 0,
1294
+ lobes: 5,
1295
+ shoulder: 0.95,
1296
+ tall: 0.28,
1297
+ tallGain: 0.14,
1298
+ wart: 0,
1299
+ warts: 0,
1300
+ wide: 1,
1301
+ wideLoss: 0.22,
1302
+ },
1303
+ stinger: {
1304
+ bow: 0.12,
1305
+ cut: 0.02,
1306
+ hang: 0,
1307
+ lobes: 5,
1308
+ shoulder: 1.3,
1309
+ tall: 0.46,
1310
+ tallGain: 0.14,
1311
+ wart: 0.04,
1312
+ warts: 6,
1313
+ wide: 1,
1314
+ wideLoss: 0.2,
1315
+ },
1316
+ }
1317
+
1318
+ /**
1319
+ * How many samples the top of a bell is worth, and how many its margin.
1320
+ *
1321
+ * The margin is counted off its own scallops, because a rim sampled at fewer
1322
+ * points than it has lobes is a rim with a beat frequency in it: eight
1323
+ * scallops read off sixteen samples come out as four, and the animal is drawn
1324
+ * with the wrong number of parts.
1325
+ */
1326
+ const OVER = 20
1327
+ const UNDER = 14
1328
+ const PER_LOBE = 4
1329
+ const PER_WART = 5
1330
+
1331
+ /**
1332
+ * A bell, squeezed by however much it is squeezed.
1333
+ *
1334
+ * A contraction takes width out and puts height in, because that is where the
1335
+ * water went and it is the whole of what a pulse looks like from the side. The
1336
+ * margin bows towards the reader, so the rim reads as a circle seen at an
1337
+ * angle instead of a line ruled under a dome.
1338
+ */
1339
+ function domePath(kind: JellyKind, squeeze: number): string {
1340
+ const shape = DOMES[kind]
1341
+ const wide = (shape.wide * (1 - shape.wideLoss * squeeze)) / 2
1342
+ const tall = shape.tall + shape.tallGain * squeeze
1343
+ const points: Point[] = []
1344
+
1345
+ const over = Math.max(OVER, shape.warts * PER_WART)
1346
+ for (let i = 0; i <= over; i++) {
1347
+ const turn = Math.PI - (i / over) * Math.PI
1348
+ // The warts ride on the dome rather than being stuck to it, so they stand
1349
+ // off along the outward normal and go flat where the dome turns under at
1350
+ // the sides. A bump added straight up would put a row of teeth on the top
1351
+ // and leave the shoulders smooth, which is the wrong animal.
1352
+ const bump =
1353
+ shape.warts > 0 ? shape.wart * Math.max(0, Math.sin((i / over) * Math.PI * shape.warts)) : 0
1354
+
1355
+ points.push({
1356
+ x: wide * Math.cos(turn) * (1 + bump),
1357
+ y: -tall * Math.sin(turn) ** (1 / shape.shoulder) - bump * Math.sin(turn) * tall * 0.5,
1358
+ })
1359
+ }
1360
+
1361
+ // The margin, coming back the other way. The scallops are cut into it rather
1362
+ // than hung off it, so a lion's mane comes apart into eight lobes and a moon
1363
+ // jelly's rim is merely not straight.
1364
+ const under = Math.max(UNDER, shape.lobes * PER_LOBE)
1365
+ for (let i = 1; i < under; i++) {
1366
+ const along = 1 - i / under
1367
+ const across = (along - 0.5) * 2
1368
+ const scallop = shape.lobes > 0 ? Math.abs(Math.sin(along * Math.PI * shape.lobes)) : 0
1369
+ points.push({
1370
+ x: wide * across,
1371
+ y: (shape.bow + shape.cut * scallop) * (1 - across * across) * (1 + squeeze * 0.3),
1372
+ })
1373
+ }
1374
+
1375
+ return smooth(points, true)
1376
+ }
1377
+
1378
+ /**
1379
+ * A comb jelly, which is a bag with a slit in it and two lobes either side.
1380
+ *
1381
+ * Drawn mouth up, since that is the end it leads with. The lobes close over
1382
+ * the mouth when it is disturbed and that is the whole of `squeeze` for this
1383
+ * animal: there is no bell here to contract.
1384
+ */
1385
+ function combBody(squeeze: number): string {
1386
+ const open = 1 - squeeze
1387
+ const wide = 0.5 * (1 - 0.2 * squeeze)
1388
+ const gape = 0.34 * open
1389
+
1390
+ const points: Point[] = [
1391
+ { x: -gape, y: -0.62 },
1392
+ { x: -wide, y: -0.26 },
1393
+ { x: -wide * 0.94, y: 0.16 },
1394
+ { x: -wide * 0.4, y: 0.62 },
1395
+ { x: 0, y: 0.78 },
1396
+ { x: wide * 0.4, y: 0.62 },
1397
+ { x: wide * 0.94, y: 0.16 },
1398
+ { x: wide, y: -0.26 },
1399
+ { x: gape, y: -0.62 },
1400
+ // The mouth itself, a notch between the two lobes rather than a gap
1401
+ // between two shapes: the lobes are folds of one animal.
1402
+ { x: gape * 0.3, y: -0.3 + 0.1 * squeeze },
1403
+ { x: -gape * 0.3, y: -0.3 + 0.1 * squeeze },
1404
+ ]
1405
+
1406
+ return smooth(points, true)
1407
+ }
1408
+
1409
+ /**
1410
+ * A man o' war's float, seen from the side with its crest up.
1411
+ *
1412
+ * The float is a gas bag the animal secretes and cannot refill quickly, and
1413
+ * the crest along the top of it is the sail. Disturbed, or merely drying out,
1414
+ * it lets the crest over and rolls the whole float under to wet it, which is
1415
+ * `squeeze` here and is the only movement the colony has.
1416
+ */
1417
+ function manowarFloat(squeeze: number): string {
1418
+ const lift = 1 - squeeze * 0.85
1419
+ const points: Point[] = [
1420
+ // The bag first: long, low, and lifted clear of the water at both ends.
1421
+ { x: -0.5, y: 0.02 },
1422
+ { x: -0.4, y: -0.05 },
1423
+ { x: -0.26, y: -0.08 },
1424
+ // Then the crest, which is a fin and not a bulge. Its base is a third of
1425
+ // the bag's length and it stands half of it high, so what a wind pushes on
1426
+ // is plainly a sail. Widen the base and the whole animal reads as a
1427
+ // mushroom, which is how the first draft of this drawing read.
1428
+ { x: -0.14, y: -0.22 * lift },
1429
+ { x: -0.02, y: -0.42 * lift },
1430
+ { x: 0.02, y: -0.44 * lift },
1431
+ { x: 0.14, y: -0.27 * lift },
1432
+ { x: 0.2, y: -0.1 },
1433
+ { x: 0.36, y: -0.07 },
1434
+ { x: 0.5, y: 0.03 },
1435
+ // And back along the underside, which is where everything hangs from.
1436
+ { x: 0.4, y: 0.1 },
1437
+ { x: 0.12, y: 0.15 },
1438
+ { x: -0.18, y: 0.14 },
1439
+ { x: -0.4, y: 0.08 },
1440
+ ]
1441
+
1442
+ return smooth(points, true)
1443
+ }
1444
+
1445
+ /** The bells, one a kind, kept by the step they were cut at. */
1446
+ const cut = new Map<string, string>()
1447
+
1448
+ /**
1449
+ * What this animal looks like now.
1450
+ *
1451
+ * Snapped to a step of the contraction, so the same drawing comes back as the
1452
+ * same string and a renderer's own cache of walked paths stays the size of the
1453
+ * vocabulary instead of the size of the afternoon.
1454
+ */
1455
+ export function bell(kind: JellyKind, squeeze: number): string {
1456
+ const step = Math.round(Math.min(Math.max(squeeze, 0), 1) * BELL_STEPS)
1457
+ const key = `${kind}${step}`
1458
+ const had = cut.get(key)
1459
+ if (had) return had
1460
+
1461
+ const at = step / BELL_STEPS
1462
+ const made =
1463
+ kind === "comb" ? combBody(at) : kind === "manowar" ? manowarFloat(at) : domePath(kind, at)
1464
+
1465
+ cut.set(key, made)
1466
+ return made
1467
+ }
1468
+
1469
+ /**
1470
+ * The four horseshoes on a moon jelly, which is how anybody tells one.
1471
+ *
1472
+ * Gonads, seen through the bell, and the reason *Aurelia* is the jellyfish
1473
+ * everybody can name. Four of them, set at the corners of a square, so from
1474
+ * the side the near pair sits low and the far pair rides high and small. They
1475
+ * are handed over as lines and drawn at a share of the bell's own weight, so
1476
+ * they read as the bell being see-through and not as marks painted on it.
1477
+ */
1478
+ export function moonMarks(squeeze: number): Point[][] {
1479
+ const wide = (1 - DOMES.moon.wideLoss * squeeze) / 2
1480
+ const seat = -DOMES.moon.tall * (0.3 + 0.1 * squeeze)
1481
+ const marks: Point[][] = []
1482
+
1483
+ for (let made = 0; made < 4; made++) {
1484
+ const round = (made / 4) * Math.PI * 2 + Math.PI / 4
1485
+ const spot = { x: Math.cos(round) * wide * 0.52, y: seat + Math.sin(round) * 0.1 }
1486
+ // Smaller at the back of the bell, because the back of the bell is further
1487
+ // away. It is the whole of the perspective this drawing has and it is
1488
+ // enough: without it the four marks are a row of beads.
1489
+ const size = wide * 0.2 * (1 - Math.sin(round) * 0.3)
1490
+ const shoe: Point[] = []
1491
+
1492
+ for (let i = 0; i <= 10; i++) {
1493
+ const turn = Math.PI * 0.22 + (i / 10) * Math.PI * 1.56
1494
+ shoe.push({ x: spot.x + size * Math.cos(turn), y: spot.y + size * Math.sin(turn) * 0.72 })
1495
+ }
1496
+ marks.push(shoe)
1497
+ }
1498
+
1499
+ return marks
1500
+ }
1501
+
1502
+ /**
1503
+ * A number between 0 and 1 that is the same every time it is asked for.
1504
+ *
1505
+ * The tentacles need to differ from one another and a scene has to open the
1506
+ * same way twice, so nothing here draws a random number: a strand's length and
1507
+ * its phase are a function of which strand it is. The constants are the usual
1508
+ * hash and mean nothing.
1509
+ */
1510
+ function jitter(at: number, salt: number): number {
1511
+ const spun = Math.sin(at * 12.9898 + salt * 78.233) * 43758.5453
1512
+ return spun - Math.floor(spun)
1513
+ }
1514
+
1515
+ /** How many pieces a strand and an arm are cut into. */
1516
+ const STRAND = 8
1517
+ const ARM = 7
1518
+
1519
+ /**
1520
+ * How much margin one strand is worth, in px, and the shortest one worth
1521
+ * drawing at all.
1522
+ *
1523
+ * A moon jelly carries hundreds of marginal tentacles and they are each about
1524
+ * a centimetre. Drawn at the count and drawn honestly they are forty lines
1525
+ * inside a fringe eleven pixels deep, which costs a renderer forty strokes and
1526
+ * buys the reader a smudge. So the drawing knows how big it is: the fringe
1527
+ * thins as the animal recedes and goes altogether when there is nothing left
1528
+ * of it, and the bunches survive, because how many clusters a lion's mane has
1529
+ * is part of what it is.
1530
+ */
1531
+ const PITCH = 5
1532
+ const WORTH = 3
1533
+
1534
+ /**
1535
+ * Everything fine that hangs off the margin.
1536
+ *
1537
+ * One rule for all of them and a table of numbers to argue about, which is the
1538
+ * same bargain `HABITS` makes with swimming. What separates a moon jelly's
1539
+ * fringe from a lion's mane is `hairs`, `hairFall` and `bunches`: forty
1540
+ * stubs evenly round the rim against thirty-two in eight clusters falling two
1541
+ * and a half bell widths.
1542
+ *
1543
+ * They gather as the bell contracts and splay again as it fills, which is not
1544
+ * a flourish. It is the only part of the animal that shows the water it is
1545
+ * pushing against, and it is the same argument `squidArms` makes.
1546
+ */
1547
+ export function hairOf(one: Jelly): Point[][] {
1548
+ const habit = HABITS[one.kind]
1549
+ const shape = DOMES[one.kind]
1550
+ const strands: Point[][] = []
1551
+ const wide = (shape.wide * (1 - shape.wideLoss * one.squeeze)) / 2
1552
+ const splay = 0.28 * (1 - one.squeeze)
1553
+ const room = Math.max(habit.bunches, Math.round(one.size / PITCH))
1554
+ const per = Math.max(
1555
+ 1,
1556
+ Math.min(Math.round(habit.hairs / habit.bunches), Math.round(room / habit.bunches)),
1557
+ )
1558
+ const shown = habit.hairFall * one.size < WORTH ? 0 : Math.min(habit.hairs, per * habit.bunches)
1559
+
1560
+ for (let made = 0; made < shown; made++) {
1561
+ const bunch = Math.floor(made / per)
1562
+ const inside = per > 1 ? ((made % per) / (per - 1) - 0.5) * 0.9 : 0
1563
+ const across =
1564
+ habit.bunches > 1
1565
+ ? ((bunch + 0.5) / habit.bunches - 0.5) * 2 + inside * (0.55 / habit.bunches)
1566
+ : shown > 1
1567
+ ? (made / (shown - 1) - 0.5) * 2
1568
+ : 0
1569
+
1570
+ strands.push(
1571
+ strand(
1572
+ wide * across,
1573
+ shape.hang + (shape.bow + shape.cut) * (1 - across * across),
1574
+ habit.hairFall * (0.62 + 0.38 * jitter(made, 1)),
1575
+ across * splay,
1576
+ one,
1577
+ made,
1578
+ ),
1579
+ )
1580
+ }
1581
+
1582
+ // The long one. A crown jelly trails a single tentacle several times its own
1583
+ // width and fishes with it; a man o' war trails one that averages ten metres
1584
+ // under a float the size of a hand. Both are the animal, so both are drawn,
1585
+ // and neither is a mistake in the numbers.
1586
+ if (habit.trail > 0) {
1587
+ strands.push(strand(wide * 0.1, shape.hang + shape.bow * 0.6, habit.trail, 0.06, one, 97))
1588
+ }
1589
+
1590
+ return strands
1591
+ }
1592
+
1593
+ /**
1594
+ * One strand, falling from where it is rooted.
1595
+ *
1596
+ * Its wave runs down its own length and it runs slower the further down it
1597
+ * goes, which is what makes a length of hair in water look heavy. Straight
1598
+ * while the bell is squeezed, since a strand behind something that has just
1599
+ * pushed is a strand being pulled.
1600
+ */
1601
+ function strand(
1602
+ rootX: number,
1603
+ rootY: number,
1604
+ fall: number,
1605
+ out: number,
1606
+ one: Jelly,
1607
+ at: number,
1608
+ ): Point[] {
1609
+ const points: Point[] = []
1610
+ const phase = jitter(at, 2) * Math.PI * 2 + one.beat * Math.PI * 2
1611
+ const wave = (1 - one.squeeze * 0.8) * 0.05 * (0.6 + jitter(at, 3))
1612
+
1613
+ for (let step = 0; step <= STRAND; step++) {
1614
+ const along = step / STRAND
1615
+ points.push({
1616
+ x: rootX + out * along * fall + Math.sin(phase + along * 3.4) * wave * along * fall,
1617
+ y: rootY + along * fall * (0.9 + 0.1 * one.squeeze),
1618
+ })
1619
+ }
1620
+
1621
+ return points
1622
+ }
1623
+
1624
+ /**
1625
+ * The oral arms, which are the thick things and not the stinging ones.
1626
+ *
1627
+ * A barrel jelly has eight and nothing else, no marginal tentacles at all, and
1628
+ * is the animal you can pick out of a bloom by that alone. A moon jelly has
1629
+ * four short frilled ones round its mouth. A lion's mane's hang in folded
1630
+ * curtains behind the hair. The box jelly and the crown have none worth
1631
+ * drawing, so they get none.
1632
+ */
1633
+ export function armsOf(one: Jelly): Point[][] {
1634
+ const habit = HABITS[one.kind]
1635
+ if (habit.arms <= 0) return []
1636
+
1637
+ const shape = DOMES[one.kind]
1638
+ const wide = (shape.wide * (1 - shape.wideLoss * one.squeeze)) / 2
1639
+ const arms: Point[][] = []
1640
+
1641
+ for (let made = 0; made < habit.arms; made++) {
1642
+ const across = habit.arms > 1 ? (made / (habit.arms - 1) - 0.5) * 2 : 0
1643
+ const root = across * wide * 0.3
1644
+ const phase = jitter(made, 4) * Math.PI * 2 + one.beat * Math.PI * 2
1645
+ const frill = 0.07 * (0.5 + jitter(made, 5)) * (1 - one.squeeze * 0.6)
1646
+ // No two the same length. A bunch of arms all ending on one line is a
1647
+ // fringe on a lampshade, and it is the single thing that gave the first
1648
+ // draft of these away as drawn instead of grown.
1649
+ const fall = habit.armFall * (0.66 + 0.34 * jitter(made, 6))
1650
+ const points: Point[] = []
1651
+
1652
+ for (let step = 0; step <= ARM; step++) {
1653
+ const along = step / ARM
1654
+ // Out and then down, so an arm leaves the mouth sideways and gathers
1655
+ // under the bell instead of dropping out of it like a rope. The tip
1656
+ // comes back in, which is what stops a bunch of them reading as pipes.
1657
+ points.push({
1658
+ x:
1659
+ root +
1660
+ across * wide * 0.62 * Math.sqrt(along) * (1 - along * along * 0.55) +
1661
+ Math.sin(phase + along * 4.2) * frill * along,
1662
+ y: shape.hang + shape.bow * 0.5 + along * fall * (0.88 + 0.12 * one.squeeze),
1663
+ })
1664
+ }
1665
+ arms.push(points)
1666
+ }
1667
+
1668
+ return arms
1669
+ }
1670
+
1671
+ /** How many rows a comb jelly shows the reader, out of the eight it has. */
1672
+ const ROWS = 4
1673
+
1674
+ /** How many pieces one row is cut into, and how far the wave lags down it. */
1675
+ const BEADS = 7
1676
+ const LAG = 2.2
1677
+
1678
+ /**
1679
+ * The comb rows, and where the light running along them has got to.
1680
+ *
1681
+ * Eight rows of fused cilia, beating in sequence from the aboral end towards
1682
+ * the mouth, each row a fraction behind the one before it. What runs along
1683
+ * them is not light the animal made: the rows work as a diffraction grating,
1684
+ * so what the reader sees is the water's own light taken apart. That is why a
1685
+ * ctenophore in a beam looks like it is wired up and a ctenophore in the dark
1686
+ * does not.
1687
+ *
1688
+ * So this is the borrowed light, and it needs a sun to borrow from. The comb
1689
+ * jelly's own light is `Jelly.glow`, it comes on when something jostles the
1690
+ * animal, it runs along these same eight rows, and it needs the dark. The two
1691
+ * are drawn on one set of lines from two different sources, and telling them
1692
+ * apart is the whole reason this animal is worth having in the water.
1693
+ *
1694
+ * The wave dies as the lobes come in, since a row that has stopped beating has
1695
+ * stopped diffracting. The lines are handed over anyway: that is the moment
1696
+ * the animal is flashing.
1697
+ *
1698
+ * Half the rows face away and are not drawn. Each piece carries its own share
1699
+ * of the wave, because the wave is the point and a row drawn at one weight is
1700
+ * a stripe.
1701
+ */
1702
+ export function combRows(one: Jelly): { glow: number; points: Point[] }[] {
1703
+ if (one.kind !== "comb") return []
1704
+
1705
+ const lit: { glow: number; points: Point[] }[] = []
1706
+ const beating = 1 - one.squeeze
1707
+
1708
+ for (let row = 0; row < ROWS; row++) {
1709
+ const across = (row / (ROWS - 1) - 0.5) * 2
1710
+ const seat = across * 0.34 * (1 - 0.2 * one.squeeze)
1711
+ // The rows near the edge of the body are seen almost end on, so they are
1712
+ // shorter on the screen and dimmer. Nothing about the animal changed.
1713
+ const edge = 1 - Math.abs(across) * 0.45
1714
+
1715
+ for (let bead = 0; bead < BEADS; bead++) {
1716
+ const from = bead / BEADS
1717
+ const to = (bead + 1) / BEADS
1718
+ const along = (from + to) / 2
1719
+ const swell = Math.sin(one.wave - along * LAG + row * 0.5)
1720
+ lit.push({
1721
+ glow: beating * edge * (0.25 + 0.75 * Math.max(0, swell)),
1722
+ points: [
1723
+ { x: seat * bodyAt(from), y: 0.7 - from * 1.24 },
1724
+ { x: seat * bodyAt(to), y: 0.7 - to * 1.24 },
1725
+ ],
1726
+ })
1727
+ }
1728
+ }
1729
+
1730
+ return lit
1731
+ }
1732
+
1733
+ /** How wide the body is at a point along a row, the aboral end at 0. */
1734
+ function bodyAt(along: number): number {
1735
+ return Math.sin(Math.min(1, along * 0.82 + 0.18) * Math.PI) ** 0.6
1736
+ }
1737
+
1738
+ /** How many waves of the alarm are running round the margin at once. */
1739
+ const ALARMS = 3
1740
+
1741
+ /**
1742
+ * The crown jelly's alarm: light running round its own margin.
1743
+ *
1744
+ * *Atolla wyvillei* held by something fires a wave of blue round the rim and
1745
+ * keeps firing it, turn after turn, for several seconds. It is not a warning
1746
+ * to whatever has hold of it. It is an advertisement, meant to bring in
1747
+ * something big enough to eat the thing that is eating it, and it works: a
1748
+ * lure built to imitate it is what put a live giant squid on camera in 2012.
1749
+ *
1750
+ * The far half of the margin is seen through the animal, so it is dimmer. That
1751
+ * is what turns a row of lights into a wheel.
1752
+ */
1753
+ export function sparks(one: Jelly): { glow: number; x: number; y: number }[] {
1754
+ if (one.glow <= 0.01 || HABITS[one.kind].light !== "wave") return []
1755
+
1756
+ const shape = DOMES[one.kind]
1757
+ const wide = (shape.wide * (1 - shape.wideLoss * one.squeeze)) / 2
1758
+ const lit: { glow: number; x: number; y: number }[] = []
1759
+
1760
+ for (let made = 0; made < ALARMS; made++) {
1761
+ const round = one.wave + (made / ALARMS) * Math.PI * 2
1762
+ const front = Math.sin(round)
1763
+ lit.push({
1764
+ glow: one.glow * (0.35 + (0.65 * (front + 1)) / 2),
1765
+ x: Math.cos(round) * wide,
1766
+ y: shape.bow * (1 + front * 0.4),
1767
+ })
1768
+ }
1769
+
1770
+ return lit
1771
+ }
1772
+
1773
+ /**
1774
+ * The whole animal alight, which is the commonest display in the sea.
1775
+ *
1776
+ * A mauve stinger touched in the dark lights bell, arms and tentacles at once
1777
+ * and goes on doing it wherever it was touched; *Pelagia noctiluca* is the
1778
+ * night light of the sea and was named that centuries before anybody knew what
1779
+ * the light was for. There is no shape to it and no direction, so what goes
1780
+ * over the wire is a middle and a reach.
1781
+ *
1782
+ * The reach covers the tentacles as well as the bell, because they light too,
1783
+ * and a glow stopping at the margin would draw a lamp with a jellyfish hanging
1784
+ * under it.
1785
+ */
1786
+ export function washOf(one: Jelly): { glow: number; reach: number } | null {
1787
+ const habit = HABITS[one.kind]
1788
+ if (habit.light !== "wash" || one.glow <= 0.01) return null
1789
+
1790
+ return {
1791
+ glow: one.glow,
1792
+ reach: one.size * (0.5 + Math.max(habit.hairFall, habit.armFall) * 0.45),
1793
+ }
1794
+ }