@codincod/ornament 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +52 -2
- package/package.json +35 -3
- package/src/biome.ts +157 -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/frame.ts +204 -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/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/nemos.ts
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clownfish, which live on one reef and go nowhere else.
|
|
3
|
+
*
|
|
4
|
+
* The simulation for the animals `reef.ts` houses, kept free of the DOM the way
|
|
5
|
+
* the rest of the ornament is. Everything else that swims in this water is
|
|
6
|
+
* passing through: the shoal crosses the picture and comes back, the visitors
|
|
7
|
+
* arrive and leave. A nemo is the one animal here with an address.
|
|
8
|
+
*
|
|
9
|
+
* ## Why it never leaves
|
|
10
|
+
*
|
|
11
|
+
* Because the real one does not. A clownfish is stung by everything on a reef
|
|
12
|
+
* except the anemone it grew up in, so the anemone is the whole of its world
|
|
13
|
+
* and it spends its life within a few body lengths of it. That is the rarest
|
|
14
|
+
* thing a background can show you: an animal that is somewhere rather than
|
|
15
|
+
* going somewhere. A shoal drifting past says the water is wide, and one fish
|
|
16
|
+
* fussing over the same patch of rock says the water is lived in.
|
|
17
|
+
*
|
|
18
|
+
* It is also the only reason the reef reads as a place at all. A mound with
|
|
19
|
+
* nothing tending it is scenery, and the same mound with something that keeps
|
|
20
|
+
* coming back to one corner of it is a home with a shape you learn.
|
|
21
|
+
*
|
|
22
|
+
* So there is no wandering off. Every station one picks is a place it asked the
|
|
23
|
+
* reef about first, and anything that would take it off the rock is refused
|
|
24
|
+
* rather than corrected later, which is what makes the claim in the name of
|
|
25
|
+
* this file true on every frame and not merely most of them.
|
|
26
|
+
*
|
|
27
|
+
* ## The turn
|
|
28
|
+
*
|
|
29
|
+
* `shoal.ts` forbids a fish to turn round, and the reason is sound: mirroring a
|
|
30
|
+
* drawing mid-swim turns the animal inside out, and easing the mirror instead
|
|
31
|
+
* foreshortens it through edge-on, which reads as paper turning in the wind. A
|
|
32
|
+
* fish crossing the picture never has to turn, so it never does.
|
|
33
|
+
*
|
|
34
|
+
* A fish tied to one anemone has to turn constantly, so the rule cannot hold
|
|
35
|
+
* here and the reason it was made has to be answered instead. What made the
|
|
36
|
+
* foreshortening read as paper was that it happened at speed: an animal going
|
|
37
|
+
* somewhere does not narrow. So a nemo turns only where a real one does, at the
|
|
38
|
+
* end of a run, having stopped. It arrives, hovers, and comes about while it is
|
|
39
|
+
* barely moving, and the drawing narrows through nothing at the moment nothing
|
|
40
|
+
* about it is travelling. See `Nemo.face`.
|
|
41
|
+
*
|
|
42
|
+
* ## Fright is looking, not being told
|
|
43
|
+
*
|
|
44
|
+
* This used to be a call. Something outside worked out that a shark had come
|
|
45
|
+
* near enough to matter and told the fish how frightened to be, which is a
|
|
46
|
+
* director's note, and there is no director in the sea. Nothing tells a
|
|
47
|
+
* clownfish anything. It watches the water, and what it does about what it sees
|
|
48
|
+
* is its own.
|
|
49
|
+
*
|
|
50
|
+
* So a nemo is handed a way of asking what else is out there, `about`, and it
|
|
51
|
+
* looks for itself every step. What it minds is anything bigger than it is, and
|
|
52
|
+
* how far off it minds one from is set by how big that thing is, because a
|
|
53
|
+
* silhouette twice the size is one you notice twice as far away. Something its
|
|
54
|
+
* own size or smaller it ignores, whatever it is doing: a clownfish is not
|
|
55
|
+
* afraid of a shrimp, and it will let a fish its own size swim through the
|
|
56
|
+
* anemone without moving. It weighs all of it by how nearly abreast the thing
|
|
57
|
+
* is swimming, so a shark cruising past the glass leaves a reef in the murk
|
|
58
|
+
* alone however neatly the two line up on the screen.
|
|
59
|
+
*
|
|
60
|
+
* There are three answers and which one a fish gives is its own business.
|
|
61
|
+
* Something merely large, or something hunting a long way off, and it quickens:
|
|
62
|
+
* it stays out, keeps doing what it was doing, and does it faster, which is
|
|
63
|
+
* what a reef looks like when a shape goes over. Something hunting and close,
|
|
64
|
+
* and it gives up on the outing and goes home. A fish that bolted for the
|
|
65
|
+
* anemone every time anything crossed the picture would be a fish nobody ever
|
|
66
|
+
* saw out in the water.
|
|
67
|
+
*
|
|
68
|
+
* ## The third answer is going at it
|
|
69
|
+
*
|
|
70
|
+
* A household is ranked by size and the largest is the female, and she does not
|
|
71
|
+
* hide. She swims out at whatever it is. It is the most reliably observable
|
|
72
|
+
* thing a clownfish does, it is why anybody who has been on a reef with a
|
|
73
|
+
* camera has been bitten by a fish the length of a finger, and a reef where the
|
|
74
|
+
* only answer to a shark is a fish disappearing is a reef with the best of it
|
|
75
|
+
* left out.
|
|
76
|
+
*
|
|
77
|
+
* So `nerve` runs down the household. The female barely minds a shadow and
|
|
78
|
+
* comes out to meet a hunter; the smallest is in the tentacles before anybody
|
|
79
|
+
* else has noticed. What that draws is the picture a reef actually gives you:
|
|
80
|
+
* the little ones gone, and one fish out in front of the anemone facing
|
|
81
|
+
* something a hundred times its size.
|
|
82
|
+
*
|
|
83
|
+
* Nothing has to be wired up for this to work. A water with nothing in it hands
|
|
84
|
+
* back nothing and the fish go about their day.
|
|
85
|
+
*
|
|
86
|
+
* Everything is seeded, so the same seed keeps the same fish on the same rock.
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
import { abreast, type Presence } from "./biome.ts"
|
|
90
|
+
import { makeRandom } from "./perlin.ts"
|
|
91
|
+
import type { Head, Reef } from "./reef.ts"
|
|
92
|
+
import type { Point } from "./seabed.ts"
|
|
93
|
+
import { drawnTilt } from "./shoal.ts"
|
|
94
|
+
|
|
95
|
+
export interface Nemo {
|
|
96
|
+
/** Where it is going, which is always somewhere it asked the reef about. */
|
|
97
|
+
aim: Point
|
|
98
|
+
/** Tail beat, in radians. */
|
|
99
|
+
beat: number
|
|
100
|
+
/** How far into going at something it is, 1 fresh and 0 over it. */
|
|
101
|
+
charge: number
|
|
102
|
+
/** How far into the tentacles it is: 0 out in the water, 1 gone from sight. */
|
|
103
|
+
cover: number
|
|
104
|
+
/**
|
|
105
|
+
* The drawn width, from -1 to 1, whose sign is which way it points.
|
|
106
|
+
*
|
|
107
|
+
* A renderer scales the animal by this and takes the sign as the mirror, so a
|
|
108
|
+
* fish coming about is squeezed to nothing and opens out the other way. It is
|
|
109
|
+
* the mirroring `shoal.ts` refuses to do, allowed here because it only ever
|
|
110
|
+
* happens to a fish that has stopped; see the note at the top of this file.
|
|
111
|
+
*/
|
|
112
|
+
face: number
|
|
113
|
+
/** How frightened it is: 1 fresh, 0 over it. */
|
|
114
|
+
fright: number
|
|
115
|
+
/** Seconds left on the station it is holding. */
|
|
116
|
+
hold: number
|
|
117
|
+
/** The anemone it lives in. Its address, its distance and its way out. */
|
|
118
|
+
host: Head
|
|
119
|
+
/** Its own phase, so no two of them waggle together. */
|
|
120
|
+
readonly lane: number
|
|
121
|
+
/** Body length in px, its anemone's distance already taken out of it. */
|
|
122
|
+
length: number
|
|
123
|
+
/**
|
|
124
|
+
* How easily this one is put off, against what it is looking at.
|
|
125
|
+
*
|
|
126
|
+
* Low is steady. It runs down the household from the female, so the same
|
|
127
|
+
* shadow crossing the same anemone empties it of everything but one fish.
|
|
128
|
+
*/
|
|
129
|
+
readonly nerve: number
|
|
130
|
+
/** Where it comes in the household. 0 is the female, and she is the largest. */
|
|
131
|
+
readonly rank: number
|
|
132
|
+
/** How far to turn the drawing, in radians, after mirroring it by `face`. */
|
|
133
|
+
tilt: number
|
|
134
|
+
/** px a second. */
|
|
135
|
+
vx: number
|
|
136
|
+
vy: number
|
|
137
|
+
x: number
|
|
138
|
+
y: number
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface Nemos {
|
|
142
|
+
readonly nemos: readonly Nemo[]
|
|
143
|
+
/**
|
|
144
|
+
* Take the reef again, after it has been rebuilt under them.
|
|
145
|
+
*
|
|
146
|
+
* A resize raises a fresh reef, so every anemone a fish knew about is gone,
|
|
147
|
+
* and how many anemones there are is not what it was either. The households
|
|
148
|
+
* are settled again from what the new reef offers.
|
|
149
|
+
*/
|
|
150
|
+
resettle(): void
|
|
151
|
+
step(seconds: number): void
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface NemosOptions {
|
|
155
|
+
/**
|
|
156
|
+
* What else is in this water, asked afresh every step.
|
|
157
|
+
*
|
|
158
|
+
* Left out, the water is empty as far as the fish can tell, which is what a
|
|
159
|
+
* reef on its own page is. Whatever supplies it is not commanding anything:
|
|
160
|
+
* it says what is there, and each fish decides for itself whether that is
|
|
161
|
+
* worth minding.
|
|
162
|
+
*/
|
|
163
|
+
about?: () => readonly Presence[]
|
|
164
|
+
/**
|
|
165
|
+
* The most fish to settle, for a caller on a budget.
|
|
166
|
+
*
|
|
167
|
+
* Left out, the reef decides: every anemone on it gets its household, and a
|
|
168
|
+
* reef with no anemone on it holds no clownfish at all.
|
|
169
|
+
*/
|
|
170
|
+
count?: number
|
|
171
|
+
reef: Reef
|
|
172
|
+
seed: number
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* How many fish one anemone keeps, fewest to most.
|
|
177
|
+
*
|
|
178
|
+
* How many there are is a fact about the reef rather than a number a caller
|
|
179
|
+
* thought of. A clownfish anemone holds a household: a large female, her
|
|
180
|
+
* mate, and whatever young are waiting their turn, and they do not leave and
|
|
181
|
+
* they do not join another. So the reef gets the fish its anemones can house,
|
|
182
|
+
* a reef with one anemone has one huddle on one side of it, and nobody has to
|
|
183
|
+
* keep a fish count and an anemone count in step by hand.
|
|
184
|
+
*/
|
|
185
|
+
const GROUP_LEAST = 2
|
|
186
|
+
|
|
187
|
+
const GROUP_SPAN = 3
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* How much smaller each fish in a household is than the one above it.
|
|
191
|
+
*
|
|
192
|
+
* A clownfish household is a queue, and the queue is visible: they are ranked
|
|
193
|
+
* by size, everybody knows their place in it, and when the female goes the next
|
|
194
|
+
* one up grows into the gap. Drawing them all one size is drawing a shoal that
|
|
195
|
+
* happens to be standing in an anemone.
|
|
196
|
+
*/
|
|
197
|
+
const RANK_STEP = 0.16
|
|
198
|
+
|
|
199
|
+
/** The smallest a fish gets however far down the queue it is. */
|
|
200
|
+
const RANK_LEAST = 0.45
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* How steady the female is, and how much jumpier each one below her.
|
|
204
|
+
*
|
|
205
|
+
* The spread is the point rather than either end of it. What makes a reef read
|
|
206
|
+
* as inhabited is that one thing crossing it gets several different answers,
|
|
207
|
+
* and the cheapest way to several answers is one figure that is not the same
|
|
208
|
+
* for any two animals.
|
|
209
|
+
*/
|
|
210
|
+
const NERVE_BOLD = 0.34
|
|
211
|
+
|
|
212
|
+
const NERVE_RUNG = 0.3
|
|
213
|
+
|
|
214
|
+
const NERVE_JUMPY = 1.2
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* How long a fish is against the anemone it lives in, and how much that varies.
|
|
218
|
+
*
|
|
219
|
+
* Against the anemone, because that is what it is always next to and the only
|
|
220
|
+
* thing anybody can judge its size by. It also settles a question a fixed size
|
|
221
|
+
* in px cannot: the reef is drawn against the window it is in, so a clownfish
|
|
222
|
+
* measured in px is a whale on a phone. A hand's length of fish against a
|
|
223
|
+
* dinner plate of host is what the two of them come to in the water, and this
|
|
224
|
+
* keeps it that way at any size.
|
|
225
|
+
*/
|
|
226
|
+
const BODY = 0.3
|
|
227
|
+
|
|
228
|
+
const BODY_SPAN = 0.3
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* How fast it swims, in body lengths a second, and how much of that it keeps
|
|
232
|
+
* once it has arrived somewhere.
|
|
233
|
+
*
|
|
234
|
+
* The hover is the important half. A clownfish at station is not still, it is
|
|
235
|
+
* holding against the water with its whole body going, and a fish that stopped
|
|
236
|
+
* dead between runs would read as a sticker somebody moved.
|
|
237
|
+
*/
|
|
238
|
+
const PACE = 2.6
|
|
239
|
+
|
|
240
|
+
const HOVER = 0.35
|
|
241
|
+
|
|
242
|
+
/** How much faster a frightened one goes. */
|
|
243
|
+
const BOLT = 2.2
|
|
244
|
+
|
|
245
|
+
/** How long it holds a station before choosing another, in seconds. */
|
|
246
|
+
const HOLD_LEAST = 0.6
|
|
247
|
+
|
|
248
|
+
const HOLD_SPAN = 1.9
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* How far from its anemone it will go, in body lengths.
|
|
252
|
+
*
|
|
253
|
+
* Short, and shorter than the reef is wide, which is the point of it. The fish
|
|
254
|
+
* is bound to the reef because it is bound to one anemone on the reef, so a
|
|
255
|
+
* reef with anemones at both ends has fish at both ends and a bare stretch in
|
|
256
|
+
* between, rather than a herd milling evenly over the whole rock.
|
|
257
|
+
*/
|
|
258
|
+
const RANGE = 9
|
|
259
|
+
|
|
260
|
+
/** How close counts as arrived, in body lengths. */
|
|
261
|
+
const ARRIVE = 0.6
|
|
262
|
+
|
|
263
|
+
/** How far above the rock it keeps, in body lengths. */
|
|
264
|
+
const CLEAR = 0.9
|
|
265
|
+
|
|
266
|
+
/** How quickly it eases onto the line it wants, as a share a second. */
|
|
267
|
+
const EASE = 4.5
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* How far it waggles off its line, in body lengths a second.
|
|
271
|
+
*
|
|
272
|
+
* A clownfish does not hold a line. It rows itself along on its pectorals with
|
|
273
|
+
* its whole body rocking, so the path is a wobble with a direction to it, and
|
|
274
|
+
* the wobble is most of what tells this animal from a fish that is going
|
|
275
|
+
* somewhere. It is taken across the line of travel rather than up the picture,
|
|
276
|
+
* because a fish rising to the top of the anemone rocks side to side as much as
|
|
277
|
+
* one crossing it does.
|
|
278
|
+
*/
|
|
279
|
+
const WAGGLE = 1.4
|
|
280
|
+
|
|
281
|
+
/** How far it goes per beat of its tail, in body lengths. */
|
|
282
|
+
const STRIDE = 0.42
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* How fast it comes about, in units of `Nemo.face` a second.
|
|
286
|
+
*
|
|
287
|
+
* Quick. The turn is the one moment the drawing is not an animal, so what it
|
|
288
|
+
* has to be is over: slow enough that nothing jumps between two frames, fast
|
|
289
|
+
* enough that nobody watching gets a good look at the edge-on frame.
|
|
290
|
+
*/
|
|
291
|
+
const FLIP = 7
|
|
292
|
+
|
|
293
|
+
/** How slowly it has to be going before it will come about, in body lengths. */
|
|
294
|
+
const FLIP_BELOW = 0.9
|
|
295
|
+
|
|
296
|
+
/** How long a fright lasts, in seconds. */
|
|
297
|
+
const FRIGHT_FADE = 4.5
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* How far off a thing is minded from, against how big across it is.
|
|
301
|
+
*
|
|
302
|
+
* Against its size, because that is what being noticed actually depends on. A
|
|
303
|
+
* fixed reach makes a passing turtle and a passing whale equally alarming at
|
|
304
|
+
* the same distance and neither of them alarming at all a moment before, and
|
|
305
|
+
* what that reads as is a trigger line drawn on the water.
|
|
306
|
+
*/
|
|
307
|
+
const NOTICE = 7
|
|
308
|
+
|
|
309
|
+
/** How much bigger than the fish a thing has to be before it minds it. */
|
|
310
|
+
const MINDS = 1.6
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* How alarming something with no menace about it is, at its worst.
|
|
314
|
+
*
|
|
315
|
+
* Not nothing. A turtle going over is a shadow going over, and every small fish
|
|
316
|
+
* on a reef looks up at a shadow; what it does not do is leave. So the plain
|
|
317
|
+
* bulk of a thing buys a quickening and never more than one, and the rest of
|
|
318
|
+
* the reading is what it looks like it eats.
|
|
319
|
+
*/
|
|
320
|
+
const BULK = 0.4
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* How frightened it has to be to give up on where it was going.
|
|
324
|
+
*
|
|
325
|
+
* Above what the bulk of a harmless thing can buy on its own, deliberately. A
|
|
326
|
+
* shadow quickens a fish and a hunter sends it home, and the line between them
|
|
327
|
+
* is here.
|
|
328
|
+
*/
|
|
329
|
+
const FRIGHT_HOME = 0.55
|
|
330
|
+
|
|
331
|
+
/** How fast it works its way into the tentacles and back out, a share a second. */
|
|
332
|
+
const COVER_IN = 2.4
|
|
333
|
+
|
|
334
|
+
const COVER_OUT = 0.7
|
|
335
|
+
|
|
336
|
+
/** How close to home it has to be to get inside, in body lengths. */
|
|
337
|
+
const COVER_NEAR = 1.2
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* How alarming a thing has to be before the female goes out to meet it.
|
|
341
|
+
*
|
|
342
|
+
* Read off what she is looking at rather than off her own fright, because she
|
|
343
|
+
* is the one fish here who barely has any, and a threshold on the fright of an
|
|
344
|
+
* animal picked for not being frightened would never be crossed.
|
|
345
|
+
*/
|
|
346
|
+
const CHARGE_AT = 0.3
|
|
347
|
+
|
|
348
|
+
/** How long she keeps at it after the thing has gone, in seconds. */
|
|
349
|
+
const CHARGE_FADE = 2.4
|
|
350
|
+
|
|
351
|
+
/** How many places it will ask the reef about before settling for home. */
|
|
352
|
+
const TRIES = 12
|
|
353
|
+
|
|
354
|
+
export function createNemos(options: NemosOptions): Nemos {
|
|
355
|
+
const reef = options.reef
|
|
356
|
+
const random = makeRandom(options.seed ^ 0x6f2d)
|
|
357
|
+
|
|
358
|
+
const nemos: Nemo[] = []
|
|
359
|
+
|
|
360
|
+
/** The anemones on this reef, which is the whole of where a fish may live. */
|
|
361
|
+
const anemones = (): Head[] => reef.heads.filter((one) => one.kind === "anemone")
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* A station somewhere over the reef, or home if the reef offered nothing.
|
|
365
|
+
*
|
|
366
|
+
* The fish proposes and the reef disposes. Nothing here works out where the
|
|
367
|
+
* rock is or how far the reef reaches; it asks, and asks again if the answer
|
|
368
|
+
* was no, which is why the same routine serves a reef of any shape.
|
|
369
|
+
*/
|
|
370
|
+
function station(one: Nemo): Point {
|
|
371
|
+
const reach = RANGE * one.length
|
|
372
|
+
|
|
373
|
+
for (let tries = 0; tries < TRIES; tries++) {
|
|
374
|
+
const angle = random() * Math.PI * 2
|
|
375
|
+
const out = Math.sqrt(random()) * reach
|
|
376
|
+
const x = one.host.x + Math.cos(angle) * out
|
|
377
|
+
const y = one.host.y + Math.sin(angle) * out
|
|
378
|
+
|
|
379
|
+
if (!reef.holds(x, y, one.host.depth)) continue
|
|
380
|
+
if (y > reef.surfaceAt(x, one.host.depth) - CLEAR * one.length) continue
|
|
381
|
+
|
|
382
|
+
return { x, y }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { x: one.host.x, y: one.host.y - CLEAR * one.length }
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** One fish, at its place in the queue, facing whichever way it turned out. */
|
|
389
|
+
function settle(host: Head, rank: number): Nemo {
|
|
390
|
+
const queue = Math.max(RANK_LEAST, 1 - rank * RANK_STEP)
|
|
391
|
+
const length = host.span * BODY * queue * (1 - BODY_SPAN / 2 + random() * BODY_SPAN)
|
|
392
|
+
const x = host.x + (random() - 0.5) * host.span
|
|
393
|
+
// Above the rock at its own x rather than above the anemone's. A household
|
|
394
|
+
// is spread across the width of its host, and the rock under one end of
|
|
395
|
+
// that spread is not the rock under the middle of it.
|
|
396
|
+
const y = Math.min(host.y, reef.surfaceAt(x, host.depth)) - CLEAR * length
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
aim: { x, y },
|
|
400
|
+
beat: random() * Math.PI * 2,
|
|
401
|
+
charge: 0,
|
|
402
|
+
cover: 0,
|
|
403
|
+
face: random() < 0.5 ? -1 : 1,
|
|
404
|
+
fright: 0,
|
|
405
|
+
hold: random() * HOLD_SPAN,
|
|
406
|
+
host,
|
|
407
|
+
lane: random() * Math.PI * 2,
|
|
408
|
+
length,
|
|
409
|
+
nerve: Math.min(NERVE_JUMPY, NERVE_BOLD + rank * NERVE_RUNG),
|
|
410
|
+
rank,
|
|
411
|
+
tilt: 0,
|
|
412
|
+
vx: 0,
|
|
413
|
+
vy: 0,
|
|
414
|
+
x,
|
|
415
|
+
y,
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function populate(): void {
|
|
420
|
+
const most = Math.max(0, options.count ?? Number.POSITIVE_INFINITY)
|
|
421
|
+
nemos.length = 0
|
|
422
|
+
|
|
423
|
+
for (const host of anemones()) {
|
|
424
|
+
const group = GROUP_LEAST + Math.floor(random() * (GROUP_SPAN + 1))
|
|
425
|
+
for (let made = 0; made < group && nemos.length < most; made++) {
|
|
426
|
+
nemos.push(settle(host, made))
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Onto the line it wants, at the speed it wants, with its tail going. */
|
|
432
|
+
function swim(one: Nemo, dt: number): void {
|
|
433
|
+
const toX = one.aim.x - one.x
|
|
434
|
+
const toY = one.aim.y - one.y
|
|
435
|
+
const away = Math.hypot(toX, toY)
|
|
436
|
+
const near = away < ARRIVE * one.length
|
|
437
|
+
|
|
438
|
+
const stirred = Math.max(one.fright, one.charge)
|
|
439
|
+
const pace = (near ? HOVER : 1) * PACE * (1 + stirred * BOLT) * one.length
|
|
440
|
+
const wantX = away > 0 ? (toX / away) * pace : 0
|
|
441
|
+
const wantY = away > 0 ? (toY / away) * pace : 0
|
|
442
|
+
|
|
443
|
+
const rate = Math.min(1, EASE * dt)
|
|
444
|
+
one.vx += (wantX - one.vx) * rate
|
|
445
|
+
one.vy += (wantY - one.vy) * rate
|
|
446
|
+
|
|
447
|
+
const speed = Math.hypot(one.vx, one.vy)
|
|
448
|
+
one.beat += (speed / Math.max(1, STRIDE * one.length)) * dt * Math.PI * 2
|
|
449
|
+
|
|
450
|
+
const roll = Math.sin(one.beat + one.lane) * WAGGLE * one.length
|
|
451
|
+
const sideX = speed > 0 ? -(one.vy / speed) * roll : 0
|
|
452
|
+
const sideY = speed > 0 ? (one.vx / speed) * roll : 0
|
|
453
|
+
|
|
454
|
+
one.x += (one.vx + sideX) * dt
|
|
455
|
+
one.y += (one.vy + sideY) * dt
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Back onto the reef, if that step took it off.
|
|
460
|
+
*
|
|
461
|
+
* A refusal rather than a correction, and it is given the place the fish came
|
|
462
|
+
* from rather than working one out from the speed, because the waggle moves
|
|
463
|
+
* it as well and a step undone by halves lands somewhere it never was. Where
|
|
464
|
+
* it wanted to go was somewhere the reef agreed to, so this only fires when
|
|
465
|
+
* the reef has moved under it or the waggle threw it wide, and in both cases
|
|
466
|
+
* the honest answer is that the fish never went there.
|
|
467
|
+
*/
|
|
468
|
+
function keep(one: Nemo, fromX: number, fromY: number): void {
|
|
469
|
+
const top = reef.surfaceAt(one.x, one.host.depth) - CLEAR * one.length
|
|
470
|
+
|
|
471
|
+
if (one.y > top && reef.holds(one.x, top, one.host.depth)) {
|
|
472
|
+
one.y = top
|
|
473
|
+
one.vy = Math.min(one.vy, 0)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (reef.holds(one.x, one.y, one.host.depth)) return
|
|
477
|
+
|
|
478
|
+
one.x = fromX
|
|
479
|
+
one.y = fromY
|
|
480
|
+
one.vx = 0
|
|
481
|
+
one.vy = 0
|
|
482
|
+
one.hold = 0
|
|
483
|
+
one.aim = { x: one.host.x, y: one.host.y - CLEAR * one.length }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Coming about, which it will only do once it has stopped. */
|
|
487
|
+
function turn(one: Nemo, dt: number): void {
|
|
488
|
+
const speed = Math.hypot(one.vx, one.vy)
|
|
489
|
+
const slow = speed < FLIP_BELOW * one.length
|
|
490
|
+
const want = slow && Math.abs(one.vx) > 0 ? Math.sign(one.vx) : Math.sign(one.face) || 1
|
|
491
|
+
const step = FLIP * dt
|
|
492
|
+
|
|
493
|
+
one.face += Math.min(Math.max(want - one.face, -step), step)
|
|
494
|
+
one.tilt = drawnTilt(Math.atan2(one.vy, Math.abs(one.vx)), Math.sign(one.face) || 1)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* How alarmed this fish is by what it can see of the water, right now.
|
|
499
|
+
*
|
|
500
|
+
* Its own reading, taken fresh every step rather than remembered, so a thing
|
|
501
|
+
* that came near and went away leaves nothing behind but the fright already
|
|
502
|
+
* fading. The nearest thing wins; a fish being approached by two of something
|
|
503
|
+
* is not twice as frightened, it is frightened.
|
|
504
|
+
*/
|
|
505
|
+
function minded(one: Nemo, water: readonly Presence[]): { alarm: number; at: Presence | null } {
|
|
506
|
+
let alarm = 0
|
|
507
|
+
let at: Presence | null = null
|
|
508
|
+
|
|
509
|
+
for (const thing of water) {
|
|
510
|
+
if (thing.size < one.length * MINDS) continue
|
|
511
|
+
|
|
512
|
+
const reach = thing.size * NOTICE
|
|
513
|
+
const away = Math.hypot(one.x - thing.x, one.y - thing.y)
|
|
514
|
+
if (away >= reach) continue
|
|
515
|
+
|
|
516
|
+
const near = (1 - away / reach) * abreast(one.host.depth, thing.depth)
|
|
517
|
+
const worry = near * (BULK + (thing.menace ?? 0) * (1 - BULK))
|
|
518
|
+
if (worry <= alarm) continue
|
|
519
|
+
|
|
520
|
+
alarm = worry
|
|
521
|
+
at = thing
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return { alarm, at }
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Where the female goes to meet something, which is towards it.
|
|
529
|
+
*
|
|
530
|
+
* As far towards it as the reef will let her and no further, so the fish that
|
|
531
|
+
* never leaves the rock does not leave the rock to see something off. She
|
|
532
|
+
* works out along that line until the reef stops agreeing, which is the same
|
|
533
|
+
* asking `station` does and the same refusal.
|
|
534
|
+
*/
|
|
535
|
+
function meet(one: Nemo, at: Presence): Point {
|
|
536
|
+
const home = { x: one.host.x, y: one.host.y - CLEAR * one.length }
|
|
537
|
+
const toX = at.x - one.host.x
|
|
538
|
+
const toY = at.y - one.host.y
|
|
539
|
+
const away = Math.hypot(toX, toY)
|
|
540
|
+
if (away < 1) return home
|
|
541
|
+
|
|
542
|
+
const out = Math.min(away, RANGE * one.length)
|
|
543
|
+
for (let back = TRIES; back >= 1; back--) {
|
|
544
|
+
const reach = out * (back / TRIES)
|
|
545
|
+
const x = one.host.x + (toX / away) * reach
|
|
546
|
+
const y = one.host.y + (toY / away) * reach
|
|
547
|
+
|
|
548
|
+
if (!reef.holds(x, y, one.host.depth)) continue
|
|
549
|
+
if (y > reef.surfaceAt(x, one.host.depth) - CLEAR * one.length) continue
|
|
550
|
+
|
|
551
|
+
return { x, y }
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return home
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Into the tentacles while it is frightened, and out again once it is not. */
|
|
558
|
+
function hide(one: Nemo, dt: number): void {
|
|
559
|
+
const home = Math.hypot(one.x - one.host.x, one.y - one.host.y) < COVER_NEAR * one.length
|
|
560
|
+
const want = one.fright > FRIGHT_HOME && home ? 1 : 0
|
|
561
|
+
const rate = (want > one.cover ? COVER_IN : COVER_OUT) * dt
|
|
562
|
+
|
|
563
|
+
one.cover += Math.min(Math.max(want - one.cover, -rate), rate)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
populate()
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
nemos,
|
|
570
|
+
|
|
571
|
+
resettle() {
|
|
572
|
+
populate()
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
step(seconds) {
|
|
576
|
+
const dt = Math.min(Math.max(seconds, 0), 0.1)
|
|
577
|
+
const water = options.about?.() ?? []
|
|
578
|
+
|
|
579
|
+
for (const one of nemos) {
|
|
580
|
+
const seen = minded(one, water)
|
|
581
|
+
const goes = one.rank === 0 && seen.at != null && seen.alarm > CHARGE_AT
|
|
582
|
+
|
|
583
|
+
one.fright = Math.max(0, Math.max(one.fright - dt / FRIGHT_FADE, seen.alarm * one.nerve))
|
|
584
|
+
one.charge = Math.max(0, Math.max(one.charge - dt / CHARGE_FADE, goes ? seen.alarm : 0))
|
|
585
|
+
one.hold -= dt
|
|
586
|
+
|
|
587
|
+
if (goes && seen.at != null) {
|
|
588
|
+
one.aim = meet(one, seen.at)
|
|
589
|
+
} else if (one.fright > FRIGHT_HOME) {
|
|
590
|
+
one.aim = { x: one.host.x, y: one.host.y - CLEAR * one.length }
|
|
591
|
+
} else if (one.hold <= 0) {
|
|
592
|
+
one.hold = HOLD_LEAST + random() * HOLD_SPAN
|
|
593
|
+
one.aim = station(one)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const fromX = one.x
|
|
597
|
+
const fromY = one.y
|
|
598
|
+
|
|
599
|
+
swim(one, dt)
|
|
600
|
+
keep(one, fromX, fromY)
|
|
601
|
+
turn(one, dt)
|
|
602
|
+
hide(one, dt)
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
}
|
|
606
|
+
}
|