@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/shoal.ts ADDED
@@ -0,0 +1,1658 @@
1
+ /**
2
+ * A shoal of fish, wandering, with a reader on the other side of the glass.
3
+ *
4
+ * The simulation behind the site's `water.ts`, kept free of the DOM the way `flow_field.ts`
5
+ * is: what is here is where a fish goes and why, and the canvas half stays a
6
+ * canvas half.
7
+ *
8
+ * The porthole used to draw a shoal as a row of fish sliding across the glass
9
+ * on one keyframe, which is all a stylesheet can offer and is fine behind a
10
+ * window the size of a saucer. Across the width of a hero it reads as what it
11
+ * is, cutouts on a conveyor. So the fish here are steered instead of tweened,
12
+ * and everything that makes them read as alive comes from that:
13
+ *
14
+ * * A fish eases onto the line it wants and then eases its heading onto that,
15
+ * which is two lags in series, so nothing it does has a corner in it.
16
+ * Nothing in here ever sets a heading outright.
17
+ * * Where it wants to go is its own decision, held for its own stretch of
18
+ * seconds and then taken again. See `SWEEP` for why it is not a field.
19
+ * * It has a depth. Depth is its own slow field, so a fish wanders off into
20
+ * the murk, shrinks, fades, and comes back a minute later without anybody
21
+ * having decided that it should.
22
+ * * Its tail beats because it is getting somewhere, not the other way round.
23
+ * Speed is set in body lengths a second and the beat is whatever covers a
24
+ * `STRIDE` at that speed, so a fish that has slowed has slowed its tail
25
+ * with it, and the beat that carries it is the beat it is drawn on.
26
+ * * It never turns round. See `Fish.facing`, which is the one rule here that
27
+ * was arrived at by watching the alternatives fail.
28
+ * * It knows whether the pointer is moving. Hold still and one veers over for
29
+ * a look on its way past; move, and the ones near you bolt and dive.
30
+ * * It may be one of several kinds of animal, which is `SPECIES`.
31
+ *
32
+ * Depth is the piece worth arguing for. Fish drawn on one plane are wallpaper
33
+ * whatever they do, because the eye reads a flat scatter as a pattern; give
34
+ * them a near and a far and the same handful of fish become water with a volume
35
+ * to it. It also earns the site's own rule about ornament twice over: a fish in
36
+ * the distance is fainter, so the copy on top of it is safer, and the reader who
37
+ * notices anything notices something moving away rather than a decoration
38
+ * asking to be looked at.
39
+ *
40
+ * Everything is seeded, so a shoal handed a seed swims the same way twice. What
41
+ * the seed itself is, a caller decides; see `freshSeed`.
42
+ */
43
+
44
+ import { abreast, type Presence } from "./biome.ts"
45
+ import { mixOf } from "./census.ts"
46
+ import { makeNoise2, makeRandom, type Noise2, stir } from "./perlin.ts"
47
+ import { drawnAt, SWIMMING_OFF } from "./sizes.ts"
48
+
49
+ export type Kind = "cruiser" | "darter" | "drifter" | "escort" | "swordfish"
50
+
51
+ /**
52
+ * What sort of animal a fish is.
53
+ *
54
+ * Every field is a multiplier on a number the shoal already had, which is the
55
+ * whole design of this: there is one set of rules for swimming and the species
56
+ * only argue about the constants. A kind that needed its own branch in `step`
57
+ * would be a second simulation wearing the first one's interface, and the two
58
+ * would drift apart the first time either was tuned.
59
+ *
60
+ * What separates them, in order of how much it shows:
61
+ *
62
+ * * `hold`, how long one keeps a line. A drifter picks a heading and stays on
63
+ * it for the best part of a minute; a darter changes its mind every second.
64
+ * This is the single most legible difference between two animals, more than
65
+ * size and much more than speed, because it is the only one you can read
66
+ * from a path rather than from a frame.
67
+ * * `verve`, whether it bursts and coasts instead of cruising. Small fish do
68
+ * not swim, they go and then stop, and a small fish moving at a steady rate
69
+ * reads as a large fish far away.
70
+ * * `long`, how big it really is, which `sizes.ts` turns into how big it is
71
+ * drawn and which carries depth with it, because size and distance are the
72
+ * same statement here.
73
+ * * `deep`, where in the column it prefers to be. Something long and slow
74
+ * belongs in the murk at the back, and putting it there means it never has
75
+ * to compete with the shoal for attention.
76
+ * * `stride`, how far it goes per beat of its tail. A big slow animal takes
77
+ * one long stroke where a small one takes six.
78
+ */
79
+ export interface Species {
80
+ /**
81
+ * Whether it carries a bill, and how long against the drawing's own.
82
+ *
83
+ * A drawing fact in a table of swimming facts, and it earns its place: the
84
+ * only thing that knows a swordfish from a cruiser is this table, and a
85
+ * renderer asking `SPECIES[fish.kind].bill` needs to know nothing else. The
86
+ * alternative is every renderer carrying its own list of which names have
87
+ * bills, which is the same fact written down twice.
88
+ */
89
+ readonly bill: number
90
+ /** Where in the depth field it sits, as `DEPTH_MID` is for a cruiser. */
91
+ readonly deep: number
92
+ /** How long it holds a line, against `HOLD_LEAST` and `HOLD_SPAN`. */
93
+ readonly hold: number
94
+ /**
95
+ * Nose to tail in metres, which is what it is rather than how it is drawn.
96
+ *
97
+ * The drawing follows from `sizes.ts`. The range the box was given is what a
98
+ * cruiser gets, and every other kind is the root of how much longer it really
99
+ * is, so a billfish comes out over a grouper because a billfish is longer and
100
+ * not because a number here was tuned until it looked right. A reader who
101
+ * doubts one can look the animal up.
102
+ */
103
+ readonly long: number
104
+ /**
105
+ * How much it picks up its pace with something hunting about, against
106
+ * `WARY`. 0 is a fish that carries on regardless.
107
+ *
108
+ * A kind's own, and one of them is nothing on purpose. Everything in a sea
109
+ * reacting to the same shape is a sea with one animal in it wearing several
110
+ * outlines, and what a reef or an open water actually looks like when a shark
111
+ * goes through is most things quickening, some things leaving, and a few
112
+ * carrying on as though nothing had happened. The last of those is not an
113
+ * oversight anywhere: it is a big enough animal, or a far enough one, or one
114
+ * that has seen a hundred sharks and been eaten by none of them.
115
+ */
116
+ readonly nerve: number
117
+ /** Its cruising speed, against the water's own. */
118
+ readonly pace: number
119
+ /** How steeply it will swim, against `MAX_PITCH`. */
120
+ readonly pitch: number
121
+ /** Body lengths per beat, against `STRIDE`. */
122
+ readonly stride: number
123
+ /** How hard it bursts and coasts. 0 is a steady swimmer. */
124
+ readonly verve: number
125
+ }
126
+
127
+ /**
128
+ * The animals this water holds.
129
+ *
130
+ * A handful, and no more than that. Every kind added past the point where a
131
+ * glance can tell them apart is a kind nobody can see, and the cost of it is
132
+ * paid on every frame; what makes a sea look full is a few animals behaving
133
+ * differently, not many behaving similarly.
134
+ *
135
+ * The test for whether one has earned its place is whether it changes the
136
+ * silhouette or changes the path. A swordfish changes the silhouette. A darter
137
+ * changes the path. Another cruiser with a different fin would change neither.
138
+ */
139
+ export const SPECIES: Readonly<Record<Kind, Species>> = {
140
+ /** The one that was here first. Everything else is described against it. */
141
+ cruiser: {
142
+ bill: 0,
143
+ deep: 0.52,
144
+ hold: 1,
145
+ long: 0.3,
146
+ nerve: 1,
147
+ pace: 1,
148
+ pitch: 1,
149
+ stride: 1,
150
+ verve: 0,
151
+ },
152
+ /** Small, quick, and never going anywhere for long. */
153
+ darter: {
154
+ bill: 0,
155
+ deep: 0.58,
156
+ hold: 0.3,
157
+ long: 0.09,
158
+ nerve: 1.7,
159
+ pace: 1.5,
160
+ pitch: 1.4,
161
+ stride: 0.58,
162
+ verve: 1,
163
+ },
164
+ /**
165
+ * Long, slow and far back, crossing the murk on one heading.
166
+ *
167
+ * The one that does nothing about anything. It is already the largest thing
168
+ * in this water and it is already at the back of it, and an animal that size
169
+ * has no business hurrying out of the way of a shape, so a shark goes through
170
+ * and the drifter carries on exactly as it was. It is the most useful animal
171
+ * in the shoal for that: without it every crossing makes the whole picture
172
+ * flinch at once, which is a picture of one animal rather than of a sea.
173
+ */
174
+ drifter: {
175
+ bill: 0,
176
+ deep: 0.2,
177
+ hold: 4.5,
178
+ long: 1.4,
179
+ nerve: 0,
180
+ pace: 0.6,
181
+ pitch: 0.45,
182
+ stride: 1.7,
183
+ verve: 0,
184
+ },
185
+ /** Comes in twos and holds station with the other. See `Fish.mate`. */
186
+ escort: {
187
+ bill: 0,
188
+ deep: 0.62,
189
+ hold: 1.4,
190
+ long: 0.18,
191
+ nerve: 1.25,
192
+ pace: 1.1,
193
+ pitch: 0.9,
194
+ stride: 0.85,
195
+ verve: 0,
196
+ },
197
+ /**
198
+ * Big, quick, near the front of the water, and holding one line for a long
199
+ * time. The bill is most of it, but the rest has to agree with the bill: an
200
+ * animal built like that and pottering about would read as a drawing error.
201
+ *
202
+ * A long stride with real bursts in it, which is what a thunniform swimmer
203
+ * does. It hardly bends: everything happens at the tail, so the pitch is held
204
+ * shallow and the body reads as a rigid thing being driven.
205
+ */
206
+ swordfish: {
207
+ bill: 1,
208
+ deep: 0.72,
209
+ hold: 3.2,
210
+ long: 2.6,
211
+ nerve: 0.25,
212
+ pace: 1.55,
213
+ pitch: 0.55,
214
+ stride: 1.5,
215
+ verve: 0.45,
216
+ },
217
+ }
218
+
219
+ /** Every kind there is, which is what a sea holds when nobody says otherwise. */
220
+ export const KINDS: readonly Kind[] = ["cruiser", "darter", "drifter", "escort", "swordfish"]
221
+
222
+ /**
223
+ * What a sea is made of, when nobody has an opinion about it.
224
+ *
225
+ * The shares every renderer passes as `species`: the porthole, the water behind
226
+ * a hero, and the desktop background built from this directory. It is one call
227
+ * rather than three tables because the point of the ornament being one piece of
228
+ * code is that a fish looks the same wherever somebody meets it, and a mix that
229
+ * drifts apart per surface undoes that quietly.
230
+ *
231
+ * The shares are `census.ts`'s, which is to say they are what a reef holds
232
+ * rather than what looks well in a window: mostly the small ones over the
233
+ * coral, a good few of the middling ones a reef reads as, and a billfish that
234
+ * turns up in a shoal about as often as one turns up over a reef. A porthole
235
+ * holds a few fish, so most windows never show that last one. Taking the day
236
+ * with it is the other half: the same water is a darter week and then a week
237
+ * with something larger in it.
238
+ */
239
+ export function wild(seed: number): Readonly<Partial<Record<Kind, number>>> {
240
+ return mixOf(KINDS, seed)
241
+ }
242
+
243
+ export interface Fish {
244
+ /** The line it has decided to hold, in radians off level; see `decide`. */
245
+ aim: number
246
+ /** 0 is as far off as this water goes, 1 is up against the glass. */
247
+ depth: number
248
+ /**
249
+ * Which way it is pointed, once and for life: 1 for rightward, -1 for left.
250
+ *
251
+ * A drawn fish faces one way, so one swimming the other way has to be
252
+ * mirrored, and mirroring is what wrecks a shoal. Do it the instant a heading
253
+ * crosses the vertical and the fish turns inside out between two frames. Ease
254
+ * it, and the body foreshortens through edge-on, which reads as a sheet of
255
+ * paper turning over in the wind rather than an animal.
256
+ *
257
+ * So no fish ever turns round. Each one is born facing a way, keeps a shallow
258
+ * pitch either side of it (`MAX_PITCH`), and crosses the picture and comes
259
+ * back on the far side. That is what a shoal drifting past a window is doing
260
+ * anyway, and it means nothing ever mirrors, foreshortens or flips.
261
+ */
262
+ readonly facing: number
263
+ /** Radians. 0 is swimming right, and it stays inside the fish's own cone. */
264
+ heading: number
265
+ /** Seconds left on that decision, after which it takes another. */
266
+ hold: number
267
+ readonly kind: Kind
268
+ /** Its own lane through the depth field, so no two rise together. */
269
+ readonly lane: number
270
+ /** What it is actually steering to, easing onto `aim` and never jumping. */
271
+ lean: number
272
+ /** Body length in px at depth 1, before depth takes its share. */
273
+ readonly length: number
274
+ /**
275
+ * The one it is holding station with, or null, which is nearly all of them.
276
+ *
277
+ * One-way on purpose: the follower watches the leader and the leader does not
278
+ * know it is being followed. Two fish each steering at the other is a feedback
279
+ * loop with no damping in it, and what it produces is a slow orbit that gets
280
+ * tighter until they are one shape. A leader and a shadow is also what a pair
281
+ * of fish actually looks like.
282
+ */
283
+ mate: Fish | null
284
+ /**
285
+ * Its own cruising speed, as a share of the shoal's. A shoal where everything
286
+ * moves at one speed reads as a conveyor however curved the paths are.
287
+ */
288
+ readonly pace: number
289
+ /**
290
+ * How long it is drawn, in the box's units, with depth already taken out of
291
+ * it. What a drawer wants, and what a body length means here.
292
+ */
293
+ size: number
294
+ /** px/s across the picture, depth already in it. */
295
+ speed: number
296
+ /**
297
+ * Where it is in its burst, 0 through 1, for a kind with any `verve`.
298
+ *
299
+ * A steady swimmer sits at 0 and never moves off it.
300
+ */
301
+ spurt: number
302
+ /** Tail beat, radians. */
303
+ tail: number
304
+ /**
305
+ * How far to rotate the drawing, in radians, after mirroring it by `facing`.
306
+ *
307
+ * Not the heading. A drawing that has been mirrored is in a left-handed
308
+ * frame, and a rotation applied in one of those goes the opposite way on the
309
+ * screen from the one it goes on paper: a fish heading down and to the left
310
+ * came out drawn nose-up while it swam nose-down. Every renderer worked that
311
+ * out for itself and every one of them got it wrong the same way, which is
312
+ * the argument for the animal knowing its own angle and handing it over.
313
+ */
314
+ tilt: number
315
+ x: number
316
+ y: number
317
+ }
318
+
319
+ export interface Pointer {
320
+ /** Whether it has moved recently. The whole of the interaction turns on this. */
321
+ moving: boolean
322
+ x: number
323
+ y: number
324
+ }
325
+
326
+ /** Somewhere in the box. Anything a fish measures itself against is one. */
327
+ interface Place {
328
+ readonly x: number
329
+ readonly y: number
330
+ }
331
+
332
+ /**
333
+ * Something in the water worth getting away from, that is not a reader.
334
+ *
335
+ * A hull going over, a ping going through. It is a push out from a point, felt
336
+ * to `reach` and no further, and the shoal answers it the way it answers a
337
+ * moving pointer: veer off out at the edge of it, dive and bolt near the middle
338
+ * of it. What made it is none of a fish's business, which is why this says
339
+ * nothing about boats; `passers.ts` hands one over and does the deciding.
340
+ */
341
+ export interface Startle {
342
+ /**
343
+ * How far off it is, or nothing if it is felt through the whole column.
344
+ *
345
+ * A hull says nothing, and that is right: a boat going over disturbs the
346
+ * water under it at every distance, so a fish in the murk feels it as much as
347
+ * one at the glass. Anything that is a shape rather than a disturbance should
348
+ * say, or it pushes fish it is nowhere near. See `abreast`.
349
+ */
350
+ depth?: number
351
+ /** How hard it is felt at the middle of it, 0 to 1. */
352
+ force: number
353
+ /** How far out it is felt at all, in px. */
354
+ reach: number
355
+ x: number
356
+ y: number
357
+ }
358
+
359
+ /**
360
+ * How much of a startle reaches something at `depth`, all of it to none.
361
+ *
362
+ * A screen has two distances and this water has three, and the third is most of
363
+ * the room in it. Something that says where it is in the column is felt by what
364
+ * is swimming alongside it and not by what is a water away, however neatly the
365
+ * two line up on the glass. Something that says nothing is felt by everybody,
366
+ * which is what a hull going over actually does.
367
+ *
368
+ * Exported beside `Startle` because it is the other half of that declaration
369
+ * rather than a fish's private business. Anything handed a startle has the same
370
+ * question to answer, and `cephalopods.ts` grew its own copy of the answer
371
+ * before this one was here to take.
372
+ */
373
+ export function felt(depth: number, startle: Startle): number {
374
+ return startle.depth == null ? 1 : abreast(depth, startle.depth)
375
+ }
376
+
377
+ export interface Shoal {
378
+ /** The one that has come over to look, if the reader has been still. */
379
+ readonly attentive: Fish | null
380
+ readonly fish: readonly Fish[]
381
+ /**
382
+ * How many should be about, which is not how many are.
383
+ *
384
+ * A number to work towards rather than a number to become. The water's one
385
+ * promise is that nothing appears out of nothing and nothing vanishes in
386
+ * front of you, and a shoal that resized itself would break both: a fish
387
+ * would blink into the middle of the picture and another would blink out of
388
+ * it. So a shoal over its number is one that stops replacing what leaves, and
389
+ * a shoal under it walks one in from the side at a time.
390
+ *
391
+ * It is here because a reef at three in the afternoon is not a reef at three
392
+ * in the morning. Most of what lives on one is out in the day and sheltering
393
+ * at night, and the difference is most of what tells the two apart from the
394
+ * shore.
395
+ */
396
+ hold(count: number): void
397
+ /**
398
+ * Re-fit to a resized box, carrying the fish across in proportion. When
399
+ * `count` is given and differs, the shoal grows or thins to it, so a window
400
+ * dragged wide is not left with a phone's pair of fish in it.
401
+ */
402
+ resize(width: number, height: number, count?: number): void
403
+ /**
404
+ * Advance by `seconds`. `pointer` is in the box's own coordinates, or null,
405
+ * and so is `startle`: whatever else the water is doing to them this tick.
406
+ */
407
+ step(seconds: number, pointer: Pointer | null, startle?: Startle | null): void
408
+ }
409
+
410
+ export interface ShoalOptions {
411
+ /**
412
+ * What else is in this water, asked for once a step.
413
+ *
414
+ * The same optional call the rest of the ornament takes. Nothing is fired at
415
+ * a fish: it looks, and what it makes of what it sees is its kind's own
416
+ * business. Left out, the water is empty as far as the shoal can tell, which
417
+ * is the state a page with nothing crossing it is in anyway.
418
+ */
419
+ about?: () => readonly Presence[]
420
+ count: number
421
+ /**
422
+ * How fast this water goes, in body lengths a second; `CRUISE` if left out.
423
+ *
424
+ * A framing decides this, not a fish. The same length a second is a shoal
425
+ * ambling across a hero and a shoal commuting across a porthole, because the
426
+ * porthole holds a fifth of a fish per unit width and the hero holds a
427
+ * thirtieth: the animal is the same and the crop is not.
428
+ */
429
+ cruise?: number
430
+ height: number
431
+ /**
432
+ * Nose to tail, longest and shortest, in whatever units the box is in.
433
+ *
434
+ * A cruiser's range, since every other kind is measured off it. Left out, it
435
+ * is what `sizes.ts` makes of a real fish at the distance the rest of the
436
+ * ornament swims at, which is the right answer for any window on open water.
437
+ * A close-up states its own: a porthole is a hand's breadth of glass and a
438
+ * fish behind it is not eight metres off.
439
+ */
440
+ longest?: number
441
+ seed: number
442
+ shortest?: number
443
+ /**
444
+ * How the shoal is made up, as relative shares by kind. All cruisers if left
445
+ * out, which is the water the site opened with and the reason this is opt-in:
446
+ * a porthole the size of a saucer has three fish in it, and three fish of
447
+ * three species is a specimen case rather than a shoal.
448
+ *
449
+ * Shares rather than counts, so the same mix describes a shoal of five and a
450
+ * shoal of forty, and a caller never has to know how many fish the box holds.
451
+ */
452
+ species?: Partial<Record<Kind, number>>
453
+ width: number
454
+ }
455
+
456
+ /** A floor, so a sliver of a window never empties the water completely. */
457
+ const MIN_SHOAL = 1
458
+
459
+ /** How long the water takes to walk one more fish in, in seconds. */
460
+ const FILL_EVERY = 2.5
461
+
462
+ /**
463
+ * How many of the wander field's features fit across the box.
464
+ *
465
+ * Reach for this one first if the shoal ever needs retuning. A few in the
466
+ * picture at once means a fish meets a bend now and then instead of every
467
+ * second, and two fish swimming together are reading nearly the same part of
468
+ * the field, which is what makes them look like company.
469
+ *
470
+ * Counted across the box and not per pixel, which it was, and that was a bug
471
+ * with a visible symptom. The number was set on a hero a thousand px wide; a
472
+ * porthole is two hundred units across, so less than one feature spanned the
473
+ * whole window and every fish in it was handed the same direction at the same
474
+ * moment. A shoal steering as one body is not a shoal.
475
+ */
476
+ const FIELD_CELLS = 3.6
477
+
478
+ /** How fast the field slides under the fish, so no path ever closes on itself. */
479
+ const DRIFT = 0.06
480
+
481
+ /**
482
+ * Cruising speed, in its own body lengths a second.
483
+ *
484
+ * Body lengths and not pixels, which is the change that made the shoal read as
485
+ * animals. A rate in pixels is a rate for one size of fish: the same 26px/s was
486
+ * two thirds of a body a second for the big ones and most of a body a second
487
+ * for the small, so the big ones laboured and the small ones were the only ones
488
+ * getting anywhere, and the picture had no perspective in it at all. Measured
489
+ * in its own lengths, a fish twice the size crosses twice as fast because it is
490
+ * twice as long, which is what a shoal at two distances looks like.
491
+ *
492
+ * Around one length a second is a fish going nowhere in particular. Sustained
493
+ * swimming runs to about three, and a bolt is ten, so this is the bottom of the
494
+ * range on purpose: it is scenery.
495
+ */
496
+ const CRUISE = 1.05
497
+
498
+ /**
499
+ * How far a fish travels for each beat of its tail, in body lengths.
500
+ *
501
+ * The one number that ties the drawing to the simulation. A fish's stride is
502
+ * its own, near enough constant whatever speed it is going: it holds the
503
+ * amplitude and changes the beat, so speed is stride times frequency and the
504
+ * frequency here is read back off the speed rather than set.
505
+ *
506
+ * Between two thirds and four fifths of a body is what steady swimmers get, and
507
+ * this is the middle of that. With `fish_shape.ts` sweeping a fifth of a body,
508
+ * it puts every fish here at a Strouhal number of 0.29, inside the 0.2 to 0.4
509
+ * band that swimming animals converge on. The beat used to be a constant plus a
510
+ * slice of the speed, which meant a fish drifting at a third of a length a
511
+ * second still worked its tail most of once a second: effort with nothing to
512
+ * show for it, and the first thing anybody said about this ornament.
513
+ */
514
+ export const STRIDE = 0.7
515
+
516
+ /** What a startled fish multiplies its cruise by, and how long it keeps it. */
517
+ const DART = 3.4
518
+
519
+ /**
520
+ * How much faster a fish swims with something hunting nearby, at its worst.
521
+ *
522
+ * A quickening and not a bolt. Everything in this water is peaceful and nothing
523
+ * in it is hunted, and a shoal scattering from a passing shark would make the
524
+ * page a picture about danger, which it is not. What it is a picture of is
525
+ * something large going by and the water noticing, and the water noticing looks
526
+ * like this: the same fish on the same lines, going at them harder.
527
+ */
528
+ const WARY = 0.9
529
+
530
+ /**
531
+ * How far off a hunting thing is minded from, against how big across it is.
532
+ *
533
+ * Wide. A shark is noticed a long way before it is a problem, which is the
534
+ * difference between a shoal that read the water and a shoal that got a fright.
535
+ */
536
+ const WARY_REACH = 6
537
+
538
+ /** How fast a fish can turn, radians/s, at ease and startled. */
539
+ const TURN = 0.85
540
+
541
+ const TURN_STARTLED = 3.4
542
+
543
+ /** How fast speed follows the speed it wants, per second. */
544
+ const HASTE = 2.2
545
+
546
+ /**
547
+ * How steeply a fish may swim, either side of level.
548
+ *
549
+ * This is the price of never mirroring one (`Fish.facing`), and it turns out to
550
+ * be worth paying twice over. A fish held near level reads as a fish; the same
551
+ * drawing pointed up at fifty degrees reads as a dart, whatever it is doing,
552
+ * because a real one climbing that steeply would be doing something urgent.
553
+ */
554
+ const MAX_PITCH = 0.6
555
+
556
+ /**
557
+ * How far from the top or the bottom the water starts turning a fish back, as a
558
+ * share of the height, and how hard it pushes.
559
+ *
560
+ * Both are small on purpose. A reach of a third of the height from each side
561
+ * leaves no middle at all, so every fish is being shoved at all times and the shove is the only
562
+ * thing steering; what the shoal does then is bounce between two walls. The
563
+ * water has to be mostly water.
564
+ *
565
+ * Only up and down. Sideways there is no wall at all: a fish that has crossed
566
+ * the picture carries on out of it and comes back on the far side, which is
567
+ * what lets every one of them keep the way it faces for life. Turning them at a
568
+ * side would mean turning them round, and turning them round is the thing this
569
+ * whole ornament will not do.
570
+ */
571
+ const EDGE_REACH = 0.18
572
+
573
+ const EDGE_PUSH = 0.9
574
+
575
+ /**
576
+ * How steep a line a fish will pick for itself, in radians either way.
577
+ *
578
+ * ## What the field was doing wrong
579
+ *
580
+ * Steering used to come straight off a noise field: sample it, point there. A
581
+ * field is smooth and it is one field, so every fish read the same swell at the
582
+ * same rate and the shoal went up and down together in the same amounts, on the
583
+ * same period. Drawn out, the paths were W after W, all the same size. Nothing
584
+ * alive moves like that, and no amount of retuning a single field fixes it,
585
+ * because the sameness is the field.
586
+ *
587
+ * ## What a fish does instead
588
+ *
589
+ * It picks a line and holds it for a while, then picks another. So that is what
590
+ * this is: an aim, drawn per fish; a stretch of seconds to hold it for, also
591
+ * drawn per fish; and a small wobble off the field on top, so holding a line
592
+ * still is not holding a ruler. Two fish now differ in where they are going,
593
+ * how long they will keep going there, and how soon they will think again.
594
+ *
595
+ * The draw is biased toward the shallow, because a steep line is a decision and
596
+ * decisions are rarer than drifting.
597
+ */
598
+ const SWEEP = 1.1
599
+
600
+ /** How much of a fish's own line it gives up to come back toward mid-water. */
601
+ const HOMING = 0.34
602
+
603
+ /** How long a fish holds a line: at least this, plus up to this again. */
604
+ const HOLD_LEAST = 1.6
605
+
606
+ const HOLD_SPAN = 6.5
607
+
608
+ /**
609
+ * How fast a fish eases onto a line it has just picked, per second.
610
+ *
611
+ * A decision is a step and a body is not. Without this the target jumped and
612
+ * the heading chased it at whatever its turn limit was, which is the corner
613
+ * anybody watching would report: a fish going one way and then, over about two
614
+ * frames, going another. Easing the target and then easing the heading toward
615
+ * it is two lags in a row, so a turn now starts slowly, peaks in the middle and
616
+ * settles, and there is no frame where the angle jumps.
617
+ */
618
+ const BEND = 0.85
619
+
620
+ /** How far off its line the water nudges a fish while it is holding one. */
621
+ const WOBBLE = 0.18
622
+
623
+ /**
624
+ * How far past the sides a fish swims before it comes back on the other one, as
625
+ * a multiple of its own body.
626
+ *
627
+ * Its own body rather than a number of pixels, because the two things drawing
628
+ * these are a hero the width of a page and a porthole the size of a saucer. A
629
+ * margin wide enough to hide a fish leaving the hero is a third of the glass,
630
+ * and a shoal that spends a third of its life outside a window it never leaves
631
+ * is a window with nothing in it.
632
+ *
633
+ * A shade under one body, because `x` is the tail joint and the nose reaches
634
+ * about three quarters of a length past it. That is the whole distance needed
635
+ * to put the animal out of sight, and any more than that is time it spends
636
+ * being nowhere.
637
+ */
638
+ const MARGIN = 0.8
639
+
640
+ /**
641
+ * How near two escorts have to be, in body lengths, before they fall in
642
+ * together in open water.
643
+ *
644
+ * Near enough that the pair is already a pair to look at, because that is the
645
+ * whole of what it has to be: an escort steers at its station rather than at
646
+ * its leader, and a station half a picture away is a chase nobody asked to
647
+ * watch. Pairs made while the shoal is being built are exempt; see `pair`.
648
+ */
649
+ const JOIN_REACH = 5
650
+
651
+ /** How close two fish get before they part, and how firmly. */
652
+ const NEIGHBOUR_REACH = 70
653
+
654
+ const NEIGHBOUR_PUSH = 0.8
655
+
656
+ /** How near a moving pointer has to be before a fish gives way, and how hard. */
657
+ const FLEE_REACH = 340
658
+
659
+ const FLEE_PUSH = 3.6
660
+
661
+ /**
662
+ * The share of that reach inside which a fish does not merely veer off but
663
+ * bolts: dives, and swims flat out.
664
+ *
665
+ * Two distances because they are two things. Fish notice a shape moving well
666
+ * before it is on top of them and start easing away from it, and that is most
667
+ * of what a reader sees. Panic is the near half of it. Running both off one
668
+ * radius meant either nothing reacted until the pointer was almost touching, or
669
+ * the whole shoal bolted from across the surface.
670
+ */
671
+ const PANIC = 0.62
672
+
673
+ /**
674
+ * How long the pointer has to hold still before a fish will come over, in
675
+ * seconds.
676
+ *
677
+ * Long enough that it never fires while somebody is on their way to a button,
678
+ * short enough that it happens while they are still reading the sentence they
679
+ * stopped at.
680
+ */
681
+ const LOOK_DELAY = 1.1
682
+
683
+ /** How far a fish will come from, and how strongly the rest of them lean over. */
684
+ const LOOK_REACH = 320
685
+
686
+ const LOOK_PUSH = 2.2
687
+
688
+ const LEAN_PUSH = 0.5
689
+
690
+ /**
691
+ * How long one fish stays interested, and how long the water waits before
692
+ * sending another, both in seconds.
693
+ *
694
+ * Without these a reader who stops moving has a fish parked on their cursor for
695
+ * as long as they leave it there, which is a pet rather than a wild animal, and
696
+ * the ornament's whole claim is that it is doing its own thing and you happen to
697
+ * be watching. So the visit ends, the shoal has a moment, and then somebody else
698
+ * comes over: the one that has just been is passed over while there is anybody
699
+ * else in reach, so a long stillness is a procession rather than the same fish
700
+ * on a loop.
701
+ */
702
+ const LOOK_SPAN = 13
703
+
704
+ const LOOK_REST = 5
705
+
706
+ /**
707
+ * How close the curious one settles, in px, and what it does then.
708
+ *
709
+ * Inside the stand-off it steers along the tangent instead of inward, which
710
+ * makes it circle rather than dock. A fish that swims into the cursor and stops
711
+ * dead is a cursor with a sticker on it; one that comes over, turns, and holds
712
+ * a loop is a fish having a look at you.
713
+ */
714
+ const STANDOFF = 70
715
+
716
+ const HOVER = 0.45
717
+
718
+ /** How far it may be dragged before it gives up and rejoins the water. */
719
+ const LOSE_INTEREST = 1.8
720
+
721
+ /** How far the depth field swings either side of a kind's own seat, and how
722
+ * fast it slides. Where it is seated is the kind's business; see `Species`. */
723
+ const DEPTH_SWING = 0.46
724
+
725
+ const DEPTH_DRIFT = 0.035
726
+
727
+ /** The nearest and furthest a fish is ever drawn. */
728
+ const DEPTH_NEAR = 1
729
+
730
+ const DEPTH_FAR = 0.06
731
+
732
+ /** How fast depth follows the depth it wants, at ease and startled. */
733
+ const DEPTH_EASE = 0.5
734
+
735
+ const DEPTH_DIVE = 1.4
736
+
737
+ /** What a startled fish aims for: away as well as off, so it leaves twice over. */
738
+ const DIVE = 0.35
739
+
740
+ /** Where a fish that has come to look sits, however deep it was. */
741
+ const RISE = 0.94
742
+
743
+ /**
744
+ * How much of a fish's drawn size is depth, and with it how much of its speed.
745
+ *
746
+ * One constant for both, because they are one fact. A fish at the back of the
747
+ * water is smaller and crosses slower in the same proportion, which is all
748
+ * perspective is; two knobs for it, which is what this had, means a distant
749
+ * fish can be given a size that says one distance and a speed that says
750
+ * another, and the eye picks that apart without being able to say why.
751
+ *
752
+ * Since speed is set in body lengths, the second half comes for free: a fish
753
+ * that has shrunk to a third of the way back is holding the same length a
754
+ * second and so crossing at a third of the pixels.
755
+ */
756
+ const DEPTH_SIZE = 0.6
757
+
758
+ /**
759
+ * How much of a fish's speed comes and goes with its own tail, either side of
760
+ * the speed it is holding.
761
+ *
762
+ * A fish does not travel at a rate. It beats and glides, and the beat is
763
+ * already here for the body to swing on, so the same phase drives the travel
764
+ * and the two cannot drift apart. Held small: when the speed varies by more
765
+ * than a third of its baseline, the glide turns into a stall and the shoal
766
+ * starts to look like it is being tugged.
767
+ *
768
+ * Twice a beat, not once. Thrust comes off the tail whichever way it is
769
+ * sweeping, so a fish gets a push at each end of the stroke and coasts through
770
+ * the middle of it. A once-a-beat surge is a fish being pushed from one side.
771
+ */
772
+ const SURGE = 0.26
773
+
774
+ /**
775
+ * How a fish with `verve` bursts: how long one round takes, how much of that is
776
+ * the push, and how far above and below its cruise the two halves go.
777
+ *
778
+ * Burst-and-coast, which is how nearly every small fish actually gets anywhere.
779
+ * The push is the short half and the coast is the long one, and the coast falls
780
+ * well under the cruising speed rather than to it, because a fish coasting is
781
+ * losing speed to the water the whole way: a coast that held its rate would be a
782
+ * fish under power with its tail still, which is the one thing the eye reads as
783
+ * wrong without being able to name.
784
+ */
785
+ const SPURT_SLOWEST = 0.9
786
+
787
+ const SPURT_FASTEST = 1.9
788
+
789
+ const SPURT_SHARE = 0.32
790
+
791
+ const SPURT_PUSH = 2.4
792
+
793
+ const SPURT_COAST = 0.22
794
+
795
+ /**
796
+ * How far behind and to the side a follower sits, in the leader's own body
797
+ * lengths, and how hard it holds that.
798
+ *
799
+ * Behind and off to one side, never alongside. Two fish level with each other
800
+ * are one wide fish at any distance where this is worth drawing, and the offset
801
+ * is what keeps two silhouettes readable as two. It also happens to be where a
802
+ * second fish actually swims, in the water the first one has already moved.
803
+ */
804
+ const STATION_BACK = 1.3
805
+
806
+ const STATION_SIDE = 0.55
807
+
808
+ const STATION_PUSH = 1.6
809
+
810
+ /**
811
+ * How hard a follower works the gap, and the speeds it may reach doing it.
812
+ *
813
+ * Steering alone will not keep two fish together, and it took watching them
814
+ * drift apart to see why: every fish is drawn its own cruising speed, so a
815
+ * follower slower than its leader points at the right spot for as long as you
816
+ * like and never arrives. A pair has to agree about pace, so a follower gives
817
+ * up its own and takes the leader's, trimmed by how far off station it is.
818
+ *
819
+ * The bounds are what stop that being a tow rope. Uncapped, a follower that
820
+ * fell a long way back would come up at any speed at all, which reads as the
821
+ * second fish being fired out of a gun.
822
+ */
823
+ const STATION_CATCH = 0.55
824
+
825
+ const STATION_SLOWEST = 0.35
826
+
827
+ const STATION_BRISKEST = 2.1
828
+
829
+ /**
830
+ * The spread of cruising speeds across a shoal, as shares of its own.
831
+ *
832
+ * Wide, and drawn toward the bottom of it. Where every fish is within a third
833
+ * of one speed the eye picks up the shared rate long before it picks up any
834
+ * individual path, and what it has then is a conveyor with curved rails. What a
835
+ * shoal actually looks like is most of them going nowhere in particular and one
836
+ * with somewhere to be, so the draw is squared: about three in ten come out
837
+ * above the middle of this range and the rest idle below it.
838
+ */
839
+ const SLOWEST = 0.45
840
+
841
+ const FASTEST = 1.9
842
+
843
+ /** How hard that draw leans on the slow end. */
844
+ const LEISURE = 2
845
+
846
+ /**
847
+ * What a cruiser is, nose to tail, at its shortest and at its longest.
848
+ *
849
+ * In metres, and everything else in `SPECIES` is measured off the shorter of
850
+ * them. A box that says nothing about size gets these at `SWIMMING_OFF`, which
851
+ * is how far off the rest of the ornament's animals swim, so a shoal behind a
852
+ * hero and a shark crossing the same water are the sizes they are against each
853
+ * other for free.
854
+ */
855
+ const CRUISER_SHORT = 0.18
856
+
857
+ const CRUISER_LONG = 0.42
858
+
859
+ /** The length every other kind is the root of; see `Species.long`. */
860
+ const BASE = SPECIES.cruiser.long
861
+
862
+ /**
863
+ * What a cruiser comes to in a box this wide, at its shortest and its longest.
864
+ *
865
+ * Out here rather than inside the water because a surface with a reason to
866
+ * measure the box differently needs the same sum: a strip behind a hero on a
867
+ * phone is a crop of a sea rather than a smaller one, and what it hands over is
868
+ * the width it would be a whole window at. Everything else takes the default
869
+ * and never asks.
870
+ */
871
+ export function swimming(width: number): { longest: number; shortest: number } {
872
+ return {
873
+ longest: drawnAt(CRUISER_LONG, SWIMMING_OFF) * width,
874
+ shortest: drawnAt(CRUISER_SHORT, SWIMMING_OFF) * width,
875
+ }
876
+ }
877
+
878
+ /**
879
+ * A seed that differs from one visit to the next, mixed with `from` so two
880
+ * shoals asking in the same millisecond still get different water.
881
+ *
882
+ * Nothing else in here reads a clock, and that is the point of keeping this in
883
+ * one function: a shoal handed a seed is the same shoal every time, which is
884
+ * what the tests lean on and what makes anything wrong with it findable. All
885
+ * the clock decides is which of those shoals a visitor gets.
886
+ *
887
+ * It is worth deciding it. A fixed seed was the earlier answer, on the argument
888
+ * that a page should open the same way every time, and that argument holds for
889
+ * a layout and not for water: somebody who signs in twice a week gets the same
890
+ * three fish in the same three places, and a background that repeats stops
891
+ * reading as water and starts reading as a picture of water.
892
+ */
893
+ export function freshSeed(from = 0): number {
894
+ return stir(Date.now() ^ from)
895
+ }
896
+
897
+ /**
898
+ * A seed that is the same all day and a different one tomorrow.
899
+ *
900
+ * For a sea somebody lives with rather than one they visit. A page is opened
901
+ * and left, so `freshSeed` is right there: new water every time, and nothing is
902
+ * owed to what was on screen yesterday. A desktop background is the opposite
903
+ * case. It is the same rectangle behind everything you do for months, and both
904
+ * of the obvious answers are wrong for it. Fixed, and the floor never changes,
905
+ * so whatever that one seed happens to hold is all you ever get: the chest with
906
+ * the laptop on its lid falls to about one sea in ten, and on a machine whose
907
+ * number missed it, the joke the site is named after is never told at all.
908
+ * Fresh every boot, and there is no place to come back to; a wreck seen last
909
+ * week was somewhere else, and a background that is different every time you
910
+ * look at it is asking to be looked at.
911
+ *
912
+ * A day is the unit that answers both. It is long enough that the sea is a
913
+ * place while you are working in it, and short enough that the rare things come
914
+ * round: a year gives you the chest about thirty-five times.
915
+ *
916
+ * The calendar is read in local time, so the water turns over at midnight where
917
+ * the reader is rather than at some hour in Greenwich. Two screens on one
918
+ * machine, and two machines in one house, agree for free.
919
+ *
920
+ * It comes back signed rather than as `freshSeed` does, because the renderer
921
+ * that wants it holds it in a 32-bit signed integer. Nothing downstream can
922
+ * tell: every seed here is folded through `| 0` on its way into the generator.
923
+ */
924
+ export function daySeed(now = new Date(), from = 0): number {
925
+ const day = now.getFullYear() * 10000 + (now.getMonth() + 1) * 100 + now.getDate()
926
+ return stir(day ^ from) | 0
927
+ }
928
+
929
+ export function createShoal(options: ShoalOptions): Shoal {
930
+ const noise = makeNoise2(options.seed)
931
+ const depths = makeNoise2(options.seed ^ 0x9e37)
932
+ const random = makeRandom(options.seed ^ 0x5f3d)
933
+
934
+ let width = Math.max(1, options.width)
935
+ let height = Math.max(1, options.height)
936
+ let drift = 0
937
+ let sank = 0
938
+ let still = 0
939
+ let attentive: Fish | null = null
940
+ let visit = 0
941
+ let rest = 0
942
+ let last: Fish | null = null
943
+
944
+ const cruise = options.cruise ?? CRUISE
945
+ const mix = weigh(options.species)
946
+ // Read off the width each time rather than once, so a fish walked into a
947
+ // window that has been dragged wider is the size the window is now. A box
948
+ // that stated its own range keeps it, since that range is the caller's.
949
+ const born = (): Fish => {
950
+ const spread = swimming(width)
951
+ return spawn(random, draw(mix, random), {
952
+ cruise,
953
+ height,
954
+ longest: options.longest ?? spread.longest,
955
+ shortest: options.shortest ?? spread.shortest,
956
+ width,
957
+ })
958
+ }
959
+
960
+ const fish: Fish[] = Array.from({ length: Math.max(MIN_SHOAL, options.count) }, born)
961
+ pair(fish)
962
+
963
+ /** How many should be about, and how long since one was walked in. */
964
+ let asked = fish.length
965
+ let filled = 0
966
+
967
+ /**
968
+ * What leaves is gone, and what arrives has never been here before.
969
+ *
970
+ * A fish used to come off one edge and back on the other, which is a shoal of
971
+ * `count` animals on a belt: watch a hero for a minute and the same swordfish
972
+ * comes past at the same size, at the same depth, in the same direction, for
973
+ * ever. Nobody times it and everybody feels it, because the thing an eye is
974
+ * best at is noticing that it has seen this before.
975
+ *
976
+ * So the slot is refilled rather than the animal reused: a fresh kind, size,
977
+ * depth, pace and heading out of the same seeded stream, entering from the
978
+ * side its own facing brings it in from. The water keeps its number and
979
+ * nothing else about it, which is the point: what a picture of a sea has to
980
+ * be is a picture of a sea that is bigger than the picture.
981
+ *
982
+ * The one that has gone is dropped from everything that was holding it. A
983
+ * follower's leader, the fish that came over for a look, the fish it was: all
984
+ * of them are references to an animal that is no longer in the water, and a
985
+ * follower steering at one would swim at a place nothing is.
986
+ */
987
+ /** A fresh animal, on the edge its own heading brings it in from. */
988
+ function entering(): Fish {
989
+ const made = born()
990
+ made.x = made.facing > 0 ? -made.size * MARGIN : width + made.size * MARGIN
991
+ return made
992
+ }
993
+
994
+ /** The one at `at` is gone and nothing takes its place. */
995
+ function drop(at: number): void {
996
+ const going = fish[at]
997
+ if (!going) return
998
+
999
+ fish.splice(at, 1)
1000
+ if (attentive === going) attentive = null
1001
+ if (last === going) last = null
1002
+ for (const one of fish) if (one.mate === going) one.mate = null
1003
+ }
1004
+
1005
+ function renew(at: number): void {
1006
+ const going = fish[at]
1007
+ if (!going) return
1008
+
1009
+ // A follower is the one thing that comes back around, and only while the
1010
+ // fish it is following is still in the water and still outside the picture
1011
+ // itself. It fell behind its station and off the edge; putting it back on
1012
+ // that station is where it was going anyway, and a pair that lost half of
1013
+ // itself at the frame's edge would be a pair for as long as it took to
1014
+ // cross the picture once. A station anybody can see is a fish appearing out
1015
+ // of nothing, so that one is replaced like everything else.
1016
+ const lead = going.mate
1017
+ if (lead && fish.includes(lead)) {
1018
+ const x = lead.x - lead.facing * lead.size * STATION_BACK
1019
+ if (gone(x, width, going.size)) {
1020
+ going.x = x
1021
+ going.y = lead.y + lead.size * STATION_SIDE
1022
+ return
1023
+ }
1024
+ }
1025
+
1026
+ fish[at] = entering()
1027
+
1028
+ if (attentive === going) attentive = null
1029
+ if (last === going) last = null
1030
+ for (const one of fish) if (one.mate === going) one.mate = null
1031
+ }
1032
+
1033
+ return {
1034
+ get attentive() {
1035
+ return attentive
1036
+ },
1037
+ fish,
1038
+
1039
+ hold(count) {
1040
+ asked = Math.max(MIN_SHOAL, Math.round(count))
1041
+ },
1042
+
1043
+ resize(nextWidth, nextHeight, count) {
1044
+ const scaleX = Math.max(1, nextWidth) / width
1045
+ const scaleY = Math.max(1, nextHeight) / height
1046
+ width = Math.max(1, nextWidth)
1047
+ height = Math.max(1, nextHeight)
1048
+
1049
+ for (const one of fish) {
1050
+ one.x *= scaleX
1051
+ one.y *= scaleY
1052
+ }
1053
+
1054
+ if (count == null) return
1055
+ asked = Math.max(MIN_SHOAL, Math.round(count))
1056
+ while (fish.length > count && fish.length > MIN_SHOAL) {
1057
+ const going = fish.at(-1)
1058
+ if (going === attentive) attentive = null
1059
+ // A follower whose leader has just been thinned out of the water would
1060
+ // otherwise steer at an animal nobody is drawing.
1061
+ for (const one of fish) if (one.mate === going) one.mate = null
1062
+ fish.pop()
1063
+ }
1064
+ while (fish.length < count) fish.push(born())
1065
+ pair(fish)
1066
+ },
1067
+
1068
+ step(seconds, pointer, startle = null) {
1069
+ const dt = Math.min(Math.max(seconds, 0), 0.1)
1070
+ const water = options.about?.() ?? []
1071
+ drift += DRIFT * dt
1072
+ sank += DEPTH_DRIFT * dt
1073
+
1074
+ still = pointer && !pointer.moving ? still + dt : 0
1075
+ const looking = still > LOOK_DELAY
1076
+ const fleeing = pointer?.moving === true
1077
+
1078
+ if (!looking || !pointer) {
1079
+ attentive = null
1080
+ last = null
1081
+ rest = 0
1082
+ visit = 0
1083
+ } else if (attentive) {
1084
+ visit += dt
1085
+ const strayed =
1086
+ away(attentive, pointer) > LOOK_REACH * LOSE_INTEREST ||
1087
+ (away(attentive, pointer) > STANDOFF && !ahead(attentive, pointer))
1088
+ if (visit > LOOK_SPAN || strayed) {
1089
+ last = attentive
1090
+ attentive = null
1091
+ visit = 0
1092
+ rest = LOOK_REST
1093
+ }
1094
+ } else if (rest > 0) {
1095
+ rest -= dt
1096
+ } else {
1097
+ attentive = nearest(fish, pointer, LOOK_REACH, last)
1098
+ }
1099
+
1100
+ let arrived = false
1101
+
1102
+ for (let at = 0; at < fish.length; at++) {
1103
+ const one = fish[at]
1104
+ if (!one) continue
1105
+ const sort = SPECIES[one.kind]
1106
+
1107
+ one.hold -= dt
1108
+ if (one.hold <= 0) decide(one, random, height, sort)
1109
+ one.lean = ease(one.lean, one.aim, BEND * dt)
1110
+
1111
+ const startled =
1112
+ (fleeing && pointer != null && away(one, pointer) < FLEE_REACH * PANIC) ||
1113
+ (startle != null &&
1114
+ startle.force * felt(one.depth, startle) > 0 &&
1115
+ away(one, startle) < startle.reach * PANIC)
1116
+
1117
+ const [wantX, wantY] = want(
1118
+ one,
1119
+ fish,
1120
+ noise,
1121
+ drift,
1122
+ looking ? pointer : null,
1123
+ fleeing ? pointer : null,
1124
+ startle,
1125
+ one === attentive,
1126
+ width,
1127
+ height,
1128
+ )
1129
+
1130
+ one.heading = toward(
1131
+ one.heading,
1132
+ steady(one.facing, Math.atan2(wantY, wantX), MAX_PITCH * sort.pitch),
1133
+ startled ? AGILITY_BOLT : AGILITY,
1134
+ startled ? TURN_STARTLED : TURN,
1135
+ dt,
1136
+ )
1137
+ one.tilt = drawnTilt(one.heading, one.facing)
1138
+
1139
+ // A follower takes its leader's depth as well as its pace. Two fish on
1140
+ // different planes are two fish, whatever they are doing about each
1141
+ // other: one is nearer and larger and the other is not, and the eye has
1142
+ // read them as unrelated before it reads anything they have in common.
1143
+ const ambient = one.mate
1144
+ ? one.mate.depth
1145
+ : clamp(sort.deep + DEPTH_SWING * depths(one.lane, sank), DEPTH_FAR, DEPTH_NEAR)
1146
+ const wanted = startled ? ambient * DIVE : one === attentive ? RISE : ambient
1147
+ one.depth = ease(one.depth, wanted, (startled ? DEPTH_DIVE : DEPTH_EASE) * dt)
1148
+ one.size = one.length * (1 - DEPTH_SIZE + DEPTH_SIZE * one.depth)
1149
+
1150
+ const hovering = one === attentive && pointer && away(one, pointer) < STANDOFF
1151
+ const gear = burst(one, sort, dt)
1152
+ const own = cruise * one.pace * one.size * gear
1153
+ const quick = 1 + wary(one, water) * sort.nerve * WARY
1154
+ const wants = station(one, own) * quick * (startled ? DART : hovering ? HOVER : 1)
1155
+ one.speed = ease(one.speed, wants, HASTE * dt)
1156
+
1157
+ one.tail += ((2 * Math.PI * one.speed) / (STRIDE * sort.stride * one.size)) * dt
1158
+
1159
+ const stroke = one.speed * (1 + SURGE * Math.cos(2 * one.tail))
1160
+ one.x += Math.cos(one.heading) * stroke * dt
1161
+ one.y = clamp(one.y + Math.sin(one.heading) * stroke * dt, -EDGE_HOLD, height + EDGE_HOLD)
1162
+
1163
+ // A follower whose leader has left is on its own, and a follower that
1164
+ // has itself gone is replaced like anything else. What is gone is not
1165
+ // brought back around: see `renew`.
1166
+ if (gone(one.x, width, one.size)) {
1167
+ // Over its number, this is where the water gives one up: at the edge,
1168
+ // where a fish leaving is the ordinary thing that happens anyway.
1169
+ if (fish.length > asked && fish.length > MIN_SHOAL) drop(at)
1170
+ else renew(at)
1171
+ arrived = true
1172
+ }
1173
+ }
1174
+
1175
+ // And under it, one is walked in from the side, slowly enough that a
1176
+ // shoal filling out over an afternoon is not a shoal arriving.
1177
+ filled += dt
1178
+ if (fish.length < asked && filled >= FILL_EVERY) {
1179
+ filled = 0
1180
+ fish.push(entering())
1181
+ arrived = true
1182
+ }
1183
+
1184
+ // Somebody has left, so somebody may be short of company. Escorts already
1185
+ // swimming within sight of each other fall in; nothing is moved to make
1186
+ // it happen.
1187
+ if (arrived) pair(fish, JOIN_REACH, false)
1188
+ },
1189
+ }
1190
+ }
1191
+
1192
+ /**
1193
+ * Where a fish would rather be heading, as everything that has an opinion
1194
+ * summed into one vector.
1195
+ *
1196
+ * Summed rather than ranked, so no rule ever needs a special case in the
1197
+ * steering: the edge of the picture, the fish alongside, and the reader are all
1198
+ * just things the water has an opinion about, and the fish reads the total. The
1199
+ * weights are what order them. The reader outranks the frame, which outranks
1200
+ * the neighbours, which outrank the field the fish would otherwise follow.
1201
+ */
1202
+ function want(
1203
+ one: Fish,
1204
+ shoal: readonly Fish[],
1205
+ noise: Noise2,
1206
+ drift: number,
1207
+ looking: Pointer | null,
1208
+ fleeing: Pointer | null,
1209
+ startle: Startle | null,
1210
+ attentive: boolean,
1211
+ width: number,
1212
+ height: number,
1213
+ ): [number, number] {
1214
+ const scale = FIELD_CELLS / width
1215
+ const nx = one.x * scale + drift * 0.3
1216
+ const ny = one.y * scale + drift
1217
+
1218
+ const pitched = one.lean + noise(nx + one.lane, ny + one.lane) * WOBBLE
1219
+ let x = one.facing * Math.cos(pitched)
1220
+ let y = Math.sin(pitched)
1221
+
1222
+ const reach = height * EDGE_REACH
1223
+ y += edge(one.y, reach) - edge(height - one.y, reach)
1224
+
1225
+ if (one.mate) {
1226
+ // The spot behind and beside the leader, rather than the leader itself. A
1227
+ // follower steering at the animal is a follower steering into it, and the
1228
+ // separation rule below then shoves it off again: the two together make a
1229
+ // pair that jostles the whole way across, which is a fight and not company.
1230
+ const spot = {
1231
+ moving: false,
1232
+ x: one.mate.x - one.mate.facing * one.mate.size * STATION_BACK,
1233
+ y: one.mate.y + one.mate.size * STATION_SIDE,
1234
+ }
1235
+ const [awayX, awayY, distance] = separation(one, spot)
1236
+ x -= (awayX / Math.max(distance, 0.5)) * STATION_PUSH
1237
+ y -= (awayY / Math.max(distance, 0.5)) * STATION_PUSH
1238
+ }
1239
+
1240
+ for (const other of shoal) {
1241
+ // A pair is exempt from separation both ways round: they are meant to be
1242
+ // inside each other's reach, and that is the one place the rule is wrong.
1243
+ if (other === one || other === one.mate || other.mate === one) continue
1244
+ const [awayX, awayY, distance] = separation(one, other)
1245
+ if (distance >= NEIGHBOUR_REACH) continue
1246
+
1247
+ const t = 1 - distance / NEIGHBOUR_REACH
1248
+ const push = (t * t * (3 - 2 * t) * NEIGHBOUR_PUSH) / Math.max(distance, 0.5)
1249
+ x += awayX * push
1250
+ y += awayY * push
1251
+ }
1252
+
1253
+ if (fleeing) {
1254
+ const [awayX, awayY, distance] = separation(one, fleeing)
1255
+ if (distance < FLEE_REACH) {
1256
+ const push = (((FLEE_REACH - distance) / FLEE_REACH) * FLEE_PUSH) / Math.max(distance, 0.5)
1257
+ x += awayX * push
1258
+ y += awayY * push
1259
+ }
1260
+ }
1261
+
1262
+ // The same rule as the reader's, on the reach and the strength whatever sent
1263
+ // it says. A hull is a small hard one and a ping is a wide soft one, and
1264
+ // neither of them needs a rule of its own down here.
1265
+ const force = startle ? startle.force * felt(one.depth, startle) : 0
1266
+ if (startle && force > 0) {
1267
+ const [awayX, awayY, distance] = separation(one, startle)
1268
+ if (distance < startle.reach) {
1269
+ const fall = (startle.reach - distance) / startle.reach
1270
+ const push = (fall * FLEE_PUSH * force) / Math.max(distance, 0.5)
1271
+ x += awayX * push
1272
+ y += awayY * push
1273
+ }
1274
+ }
1275
+
1276
+ if (looking) {
1277
+ const [awayX, awayY, distance] = separation(one, looking)
1278
+ const push = attentive ? LOOK_PUSH : distance < LOOK_REACH ? LEAN_PUSH : 0
1279
+
1280
+ if (push > 0) {
1281
+ x -= (awayX / Math.max(distance, 0.5)) * push
1282
+ y -= (awayY / Math.max(distance, 0.5)) * push
1283
+ }
1284
+ }
1285
+
1286
+ return [x, y]
1287
+ }
1288
+
1289
+ /** The mix as a list to walk, with the shares run into a total. */
1290
+ function weigh(shares: Partial<Record<Kind, number>> | undefined): [Kind, number][] {
1291
+ const wanted = shares ?? { cruiser: 1 }
1292
+ const run: [Kind, number][] = []
1293
+ let total = 0
1294
+
1295
+ for (const kind of ["cruiser", "darter", "drifter", "escort", "swordfish"] as const) {
1296
+ const share = Math.max(0, wanted[kind] ?? 0)
1297
+ if (share <= 0) continue
1298
+ total += share
1299
+ run.push([kind, total])
1300
+ }
1301
+
1302
+ if (run.length === 0) return [["cruiser", 1]]
1303
+ return run.map(([kind, upto]) => [kind, upto / total])
1304
+ }
1305
+
1306
+ /**
1307
+ * One kind off the mix.
1308
+ *
1309
+ * A mix of one is answered without rolling, and that is load-bearing rather
1310
+ * than an optimisation: every fish afterwards is drawn from the same seeded
1311
+ * stream, so spending a number here would shift the whole sequence and hand a
1312
+ * caller that asked for no mix at all a different shoal than it had before.
1313
+ * Water that changed because a feature it does not use was added is water
1314
+ * nobody can reason about.
1315
+ */
1316
+ function draw(mix: readonly [Kind, number][], random: () => number): Kind {
1317
+ if (mix.length <= 1) return mix[0]?.[0] ?? "cruiser"
1318
+ const roll = random()
1319
+ return mix.find(([, upto]) => roll < upto)?.[0] ?? mix[mix.length - 1]![0]
1320
+ }
1321
+
1322
+ /**
1323
+ * Put every unattached escort behind another one.
1324
+ *
1325
+ * Same way round, since a pair swimming into each other is not a pair, and no
1326
+ * chains: a follower is never given a follower of its own, because what that
1327
+ * makes is a queue, and a queue of fish is a train. Any escort left without a
1328
+ * partner keeps `mate` null and swims as an ordinary fish, which is the right
1329
+ * answer for an odd one out and needs no code of its own.
1330
+ *
1331
+ * Pairs are made twice: once when a shoal is built, and again whenever the
1332
+ * water has taken somebody back. That second one matters because a water that
1333
+ * refills itself loses pairs at the frame's edge, and a mechanic that only ever
1334
+ * ran at the start is a mechanic nobody sees after the first minute.
1335
+ *
1336
+ * `reach` is how near two escorts have to be, in the follower's own lengths,
1337
+ * before they fall in together, and `place` is whether the follower is then put
1338
+ * on its station outright. A shoal being built pairs at any distance and places,
1339
+ * because nobody is looking yet. A shoal in the water pairs only animals
1340
+ * already swimming within sight of each other and never moves one: a fish that
1341
+ * jumped onto another's shoulder is worse than a fish swimming alone.
1342
+ */
1343
+ function pair(shoal: readonly Fish[], reach = Number.POSITIVE_INFINITY, place = true): void {
1344
+ const spare = shoal.filter((one) => one.kind === "escort" && !one.mate)
1345
+
1346
+ for (const one of spare) {
1347
+ if (one.mate) continue
1348
+ const led = shoal.some((other) => other.mate === one)
1349
+ if (led) continue
1350
+
1351
+ const leader = spare.find(
1352
+ (other) =>
1353
+ other !== one &&
1354
+ !other.mate &&
1355
+ other.facing === one.facing &&
1356
+ // Ahead of it, in its own direction of travel. A leader astern is a
1357
+ // follower that has to hold back for the rest of the crossing, and the
1358
+ // station rule will not let it slow down that far.
1359
+ (other.x - one.x) * one.facing > 0 &&
1360
+ Math.hypot(other.x - one.x, other.y - one.y) < one.size * reach,
1361
+ )
1362
+ if (!leader) continue
1363
+ if (shoal.some((other) => other.mate === leader)) continue
1364
+
1365
+ one.mate = leader
1366
+ if (!place) continue
1367
+ one.x = leader.x - leader.facing * leader.size * STATION_BACK
1368
+ one.y = leader.y + leader.size * STATION_SIDE
1369
+ }
1370
+ }
1371
+
1372
+ /**
1373
+ * How much of its cruise a fish is asking for this instant.
1374
+ *
1375
+ * One for a steady swimmer, and for one with `verve` a hard push followed by a
1376
+ * long coast. The gear is what the speed eases toward rather than what it is, so
1377
+ * `HASTE` rounds the corners off both ends of the burst and nothing here has to
1378
+ * shape the curve itself.
1379
+ */
1380
+ function burst(one: Fish, sort: Species, dt: number): number {
1381
+ if (sort.verve <= 0) return 1
1382
+
1383
+ // Its own rate, off the fraction of its depth lane, which is a number drawn
1384
+ // per fish and never read for anything but its whole part. Taking it from
1385
+ // here rather than rolling the dice each frame is what makes the burst a
1386
+ // rhythm: a fish drawing a fresh rate every step has no period at all, and
1387
+ // what that looks like is an engine misfiring rather than an animal.
1388
+ const rate = SPURT_SLOWEST + (one.lane % 1) * (SPURT_FASTEST - SPURT_SLOWEST)
1389
+
1390
+ one.spurt += dt * rate
1391
+ if (one.spurt >= 1) one.spurt -= 1
1392
+
1393
+ const gear = one.spurt < SPURT_SHARE ? SPURT_PUSH : SPURT_COAST
1394
+ return 1 + (gear - 1) * sort.verve
1395
+ }
1396
+
1397
+ /**
1398
+ * The speed a fish is asking for, once keeping company has had its say.
1399
+ *
1400
+ * Its own, unless it is following somebody, in which case it is the leader's
1401
+ * trimmed by how far off station it has fallen: behind and it hurries, in front
1402
+ * and it eases off. That trim is the whole of the station-keeping loop, and it
1403
+ * needs nothing to damp it because `HASTE` already stands between this and the
1404
+ * speed a fish actually has.
1405
+ */
1406
+ function station(one: Fish, own: number): number {
1407
+ if (!one.mate) return own
1408
+
1409
+ const behind = (one.mate.x - one.x) * one.facing
1410
+ const slip = (behind - one.mate.size * STATION_BACK) / Math.max(one.mate.size, 1)
1411
+
1412
+ return one.mate.speed * clamp(1 + slip * STATION_CATCH, STATION_SLOWEST, STATION_BRISKEST)
1413
+ }
1414
+
1415
+ /**
1416
+ * Fish are placed from the seeded PRNG rather than from a noise field, for the
1417
+ * reason `flow_field.ts` gives: noise values cluster around zero, so a shoal
1418
+ * placed that way starts in a knot in the middle of an otherwise empty picture.
1419
+ */
1420
+ function spawn(
1421
+ random: () => number,
1422
+ kind: Kind,
1423
+ box: Required<Omit<ShoalOptions, "about" | "count" | "seed" | "species">>,
1424
+ ): Fish {
1425
+ const sort = SPECIES[kind]
1426
+ const facing = random() < 0.5 ? -1 : 1
1427
+
1428
+ const aim = (random() - 0.5) * SWEEP * sort.pitch
1429
+
1430
+ const depth = DEPTH_FAR + random() * (DEPTH_NEAR - DEPTH_FAR)
1431
+ const length =
1432
+ (box.shortest + random() * (box.longest - box.shortest)) * Math.sqrt(sort.long / BASE)
1433
+ const pace = (SLOWEST + random() ** LEISURE * (FASTEST - SLOWEST)) * sort.pace
1434
+ const size = length * (1 - DEPTH_SIZE + DEPTH_SIZE * depth)
1435
+
1436
+ return {
1437
+ aim,
1438
+ depth,
1439
+ facing,
1440
+ heading: steady(facing, aim, MAX_PITCH * sort.pitch),
1441
+ hold: random() * (HOLD_LEAST + HOLD_SPAN) * sort.hold,
1442
+ kind,
1443
+ lane: random() * 200,
1444
+ lean: aim,
1445
+ length,
1446
+ mate: null,
1447
+ pace,
1448
+ size,
1449
+ speed: box.cruise * pace * size,
1450
+ // Only a kind that bursts spends a draw on where in the burst it starts,
1451
+ // for the reason `draw` gives: a shoal that asked for none of this has to
1452
+ // come out of the seeded stream exactly as it did before there was any.
1453
+ spurt: sort.verve > 0 ? random() : 0,
1454
+ tail: random() * Math.PI * 2,
1455
+ tilt: drawnTilt(steady(facing, aim, MAX_PITCH * sort.pitch), facing),
1456
+ x: random() * box.width,
1457
+ y: random() * box.height,
1458
+ }
1459
+ }
1460
+
1461
+ /**
1462
+ * Which fish comes over, if any is placed to.
1463
+ *
1464
+ * It has to be near enough to bother and it has to have the reader ahead of it,
1465
+ * because no fish here turns round (`Fish.facing`). That is a happier rule than
1466
+ * it sounds: what a reader gets is one that veers out of the shoal, swims over
1467
+ * for a look and carries on past, which is what a fish does. One that stopped
1468
+ * and hovered on the cursor would be a pet.
1469
+ *
1470
+ * `over` is the one that has just had its look, passed over unless nothing else
1471
+ * qualifies, so holding still meets the shoal rather than one fish.
1472
+ */
1473
+ function nearest(
1474
+ shoal: readonly Fish[],
1475
+ to: Pointer,
1476
+ reach: number,
1477
+ over: Fish | null,
1478
+ ): Fish | null {
1479
+ let closest: Fish | null = null
1480
+ let best = reach
1481
+ let spare: Fish | null = null
1482
+
1483
+ for (const one of shoal) {
1484
+ const distance = away(one, to)
1485
+ if (distance >= best || !ahead(one, to)) continue
1486
+ if (one === over) {
1487
+ spare = one
1488
+ continue
1489
+ }
1490
+ best = distance
1491
+ closest = one
1492
+ }
1493
+
1494
+ return closest ?? spare
1495
+ }
1496
+
1497
+ /** Whether a point is in front of a fish rather than behind it. */
1498
+ function ahead(one: Fish, to: Pointer): boolean {
1499
+ return (to.x - one.x) * Math.cos(one.heading) + (to.y - one.y) * Math.sin(one.heading) > 0
1500
+ }
1501
+
1502
+ const away = (one: Fish, to: Place): number => Math.hypot(one.x - to.x, one.y - to.y)
1503
+
1504
+ /** From `to` toward `one`, and how far that is. */
1505
+ function separation(one: Fish, to: Place): [number, number, number] {
1506
+ const x = one.x - to.x
1507
+ const y = one.y - to.y
1508
+ return [x, y, Math.hypot(x, y)]
1509
+ }
1510
+
1511
+ /**
1512
+ * How much of what a fish can see of the water looks like it hunts.
1513
+ *
1514
+ * Its own reading, taken fresh, and it is only ever about menace: something
1515
+ * merely large going over is what `Startle` is for and the shoal already has an
1516
+ * answer to it. This is the other thing, the one a fish keeps half an eye on
1517
+ * while carrying on with its day.
1518
+ */
1519
+ function wary(one: Fish, water: readonly Presence[]): number {
1520
+ let worst = 0
1521
+
1522
+ for (const thing of water) {
1523
+ const menace = thing.menace ?? 0
1524
+ if (menace <= 0) continue
1525
+
1526
+ const reach = thing.size * WARY_REACH
1527
+ const distance = Math.hypot(one.x - thing.x, one.y - thing.y)
1528
+ if (distance >= reach) continue
1529
+
1530
+ worst = Math.max(worst, (1 - distance / reach) * menace * abreast(one.depth, thing.depth))
1531
+ }
1532
+
1533
+ return worst
1534
+ }
1535
+
1536
+ /**
1537
+ * A fish thinks again: where it is going now, and for how long.
1538
+ *
1539
+ * The aim is a signed square of a flat draw, which crowds it toward level: most
1540
+ * of the time a fish carries on much as it was, and now and then it commits to
1541
+ * a climb. `HOMING` is the only thing here that is not chance, and it is what
1542
+ * stops a shoal collecting along the top and the bottom, since a fish high in
1543
+ * the water is likelier to pick a line that brings it down.
1544
+ *
1545
+ * Every draw is its own, so no two fish are on the same clock. That is the
1546
+ * whole point of this over a field.
1547
+ */
1548
+ function decide(one: Fish, random: () => number, height: number, sort: Species): void {
1549
+ const roll = random() * 2 - 1
1550
+ const homing = -((one.y / Math.max(height, 1) - 0.5) * 2) * HOMING
1551
+ const most = MAX_PITCH * sort.pitch
1552
+
1553
+ one.aim = clamp(homing + roll * Math.abs(roll) * SWEEP * sort.pitch, -most, most)
1554
+ one.hold = (HOLD_LEAST + random() * HOLD_SPAN) * sort.hold
1555
+ }
1556
+
1557
+ /**
1558
+ * Turn from one angle toward another, the short way round.
1559
+ *
1560
+ * How fast is how far it has to go, up to `most` radians a second. That word
1561
+ * "up to" is the whole of it. This used to turn at `most` or not at all, which
1562
+ * is a steering wheel with two positions, and it is exactly what a fish here
1563
+ * looked like: dead straight for a second, one snap, dead straight again. The
1564
+ * measured turn rate was zero half the time and the ceiling the rest.
1565
+ *
1566
+ * Proportional means a fish arriving on a heading slows into it, the way
1567
+ * anything with a body in a fluid does. `AGILITY` is how many times a second
1568
+ * what is left of the error is worked off, and the cap only ever bites on the
1569
+ * first frames of a real change of mind.
1570
+ */
1571
+ function toward(from: number, to: number, gain: number, most: number, dt: number): number {
1572
+ let by = (to - from) % (Math.PI * 2)
1573
+ if (by > Math.PI) by -= Math.PI * 2
1574
+ if (by < -Math.PI) by += Math.PI * 2
1575
+
1576
+ const rate = Math.max(-most, Math.min(most, by * gain))
1577
+ return from + rate * dt
1578
+ }
1579
+
1580
+ /**
1581
+ * How much of the heading it has left to make up a fish works off a second,
1582
+ * cruising and frightened.
1583
+ *
1584
+ * The cap alone is not the answer to how quick a fish is. A cruising one is
1585
+ * lazy about a wrong heading and a bolting one is not, and if both work at one
1586
+ * gain then raising the cap for the fright changes nothing, because the gain is
1587
+ * what it was hitting.
1588
+ */
1589
+ const AGILITY = 1.5
1590
+
1591
+ const AGILITY_BOLT = 4.2
1592
+
1593
+ /**
1594
+ * The nearest heading to `want` that a fish facing this way is allowed to hold.
1595
+ *
1596
+ * Everything the water asks for goes through here, so a fish never has a target
1597
+ * behind it and never has one steeper than `MAX_PITCH`. Asked to turn round it
1598
+ * takes the shallowest way it can toward that answer, which is a fish that
1599
+ * cannot oblige rather than a fish that flips.
1600
+ *
1601
+ * Squashed and not clipped. A clip means every steep request lands on the same
1602
+ * two headings, so a fish spends its life pinned to one edge of its cone and
1603
+ * then crosses to the other, which is a windscreen wiper. Squashing leaves the
1604
+ * cone as an asymptote nothing quite reaches and keeps the shallow requests,
1605
+ * which are most of them, almost untouched.
1606
+ */
1607
+ /**
1608
+ * A heading as the angle a mirrored drawing has to be turned by.
1609
+ *
1610
+ * Shared with `cephalopods.ts`, because a squid mirrors the same way a fish
1611
+ * does and got the same thing wrong. `asin(sin(h))` is what folds a leftward
1612
+ * heading of `pi - rel` back onto `rel`, and the `facing` is what undoes the
1613
+ * handedness the mirror introduced.
1614
+ *
1615
+ * **This is the whole angle.** Turn the drawing by it and stop; a renderer that
1616
+ * multiplies it by `facing` again lands a leftward animal at `-rel`, nose up
1617
+ * while it swims down, which is the bug this function was written to end and
1618
+ * which it then caused in every renderer at once. The three of them compose
1619
+ * their transforms the same way, canvas, SVG and Qt alike: the drawing is
1620
+ * mirrored first and the turn is applied to the mirrored thing.
1621
+ */
1622
+ export function drawnTilt(heading: number, facing: number): number {
1623
+ return Math.asin(clamp(Math.sin(heading), -1, 1)) * facing
1624
+ }
1625
+
1626
+ function steady(facing: number, want: number, pitch: number): number {
1627
+ const asked = Math.atan2(Math.sin(want), facing * Math.cos(want))
1628
+ const rel = pitch * Math.tanh(asked / pitch)
1629
+ return facing > 0 ? rel : Math.PI - rel
1630
+ }
1631
+
1632
+ const ease = (from: number, to: number, rate: number): number =>
1633
+ from + (to - from) * Math.min(1, Math.max(0, rate))
1634
+
1635
+ /** One edge's inward shove on a fish `distance` px from it. */
1636
+ function edge(distance: number, reach: number): number {
1637
+ if (reach <= 0 || distance >= reach) return 0
1638
+ return ((reach - distance) / reach) * EDGE_PUSH
1639
+ }
1640
+
1641
+ const clamp = (value: number, low: number, high: number): number =>
1642
+ Math.min(Math.max(value, low), high)
1643
+
1644
+ /**
1645
+ * Whether a fish has left the picture, by a body's length beyond the frame.
1646
+ *
1647
+ * Every surface this is drawn on hides its overflow, so the margin is the
1648
+ * distance at which the animal is genuinely out of sight rather than merely
1649
+ * near the edge. It is asked both of a fish that is leaving and of one that has
1650
+ * just been let in, which is how `pair` is told which of them is which.
1651
+ */
1652
+ function gone(x: number, span: number, body: number): boolean {
1653
+ const margin = body * MARGIN
1654
+ return x > span + margin || x < -margin
1655
+ }
1656
+
1657
+ /** How far past the top or the bottom a fish is allowed to get. */
1658
+ const EDGE_HOLD = 8