@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/LICENSE +661 -0
- package/README.md +62 -2
- package/package.json +35 -3
- package/src/biome.ts +157 -0
- package/src/canvas/flat.ts +336 -0
- package/src/caustics.ts +81 -0
- package/src/census.ts +262 -0
- package/src/cephalopods.ts +1041 -0
- package/src/cloud.ts +308 -0
- package/src/crags.ts +974 -0
- package/src/drift.ts +435 -0
- package/src/eels.ts +667 -0
- package/src/fish_shape.ts +410 -0
- package/src/flora.ts +1955 -0
- package/src/flow_field.ts +491 -0
- package/src/jellies.ts +1794 -0
- package/src/nemos.ts +606 -0
- package/src/paint/bed.ts +231 -0
- package/src/paint/colour.ts +86 -0
- package/src/paint/frame.ts +204 -0
- package/src/paint/ink.ts +134 -0
- package/src/paint/life.ts +611 -0
- package/src/paint/light.ts +132 -0
- package/src/paint/pen.ts +225 -0
- package/src/paint/rock.ts +325 -0
- package/src/paint/sea.ts +809 -0
- package/src/paint/shade.ts +109 -0
- package/src/paint/sky.ts +377 -0
- package/src/paint/specimen.ts +300 -0
- package/src/paint/svg.ts +326 -0
- package/src/paint/trace.ts +164 -0
- package/src/passers.ts +730 -0
- package/src/perlin.ts +168 -0
- package/src/plenty.ts +65 -0
- package/src/puffers.ts +510 -0
- package/src/rays.ts +225 -0
- package/src/reef.ts +945 -0
- package/src/relics.ts +451 -0
- package/src/seabed.ts +525 -0
- package/src/shoal.ts +1658 -0
- package/src/sizes.ts +86 -0
- package/src/sun.ts +500 -0
- package/src/swarm.ts +587 -0
- package/src/visitors.ts +1936 -0
- package/src/walkers.ts +861 -0
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
|