@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
|
@@ -0,0 +1,1041 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Squid in the water, octopuses on the floor.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of the DOM the way the rest of the ornament is, and kept apart from
|
|
5
|
+
* `shoal.ts` because a cephalopod is not a fish with different steering. A fish
|
|
6
|
+
* is a body that beats continuously and travels continuously; these two are
|
|
7
|
+
* neither, and the whole reason for having them in the scene is that they break
|
|
8
|
+
* the rhythm everything else keeps.
|
|
9
|
+
*
|
|
10
|
+
* ## The squid, and why it is worth the trouble
|
|
11
|
+
*
|
|
12
|
+
* A shoal, however well steered, settles into one tempo. Every animal in it is
|
|
13
|
+
* always swimming, so the water has a background rate and after a minute the
|
|
14
|
+
* eye has it. A squid does not have a rate. It hangs, does nothing, then goes,
|
|
15
|
+
* hard, and hangs again.
|
|
16
|
+
*
|
|
17
|
+
* That is one cycle with two very unequal halves:
|
|
18
|
+
*
|
|
19
|
+
* * **Filling.** Most of the cycle. The mantle swells, the animal barely
|
|
20
|
+
* moves, and what movement there is is the water carrying it. The arms
|
|
21
|
+
* spread because nothing is holding them.
|
|
22
|
+
* * **The jet.** A fraction of it. The mantle snaps thin, the arms are pulled
|
|
23
|
+
* into a point, and the animal covers more ground than it did in the whole
|
|
24
|
+
* of the fill.
|
|
25
|
+
*
|
|
26
|
+
* The mantle drives the speed rather than the other way round, which is the
|
|
27
|
+
* same rule `shoal.ts` uses for a tail and for the same reason: an animal whose
|
|
28
|
+
* drawing and whose travel come off one number cannot fall out of step with
|
|
29
|
+
* itself.
|
|
30
|
+
*
|
|
31
|
+
* It never turns round, for the reason `Fish.facing` gives. A squid mirrored
|
|
32
|
+
* mid-glide would be worse than a fish mirrored, because there is no beat to
|
|
33
|
+
* hide the frame it happens on.
|
|
34
|
+
*
|
|
35
|
+
* ## The octopus
|
|
36
|
+
*
|
|
37
|
+
* Not in the water at all. It is on the bottom, which is the point: everything
|
|
38
|
+
* else in this scene is suspended, and an animal in contact with the ground is
|
|
39
|
+
* what tells you the ground is solid.
|
|
40
|
+
*
|
|
41
|
+
* It works along the floor rather than swimming over it, so its height comes
|
|
42
|
+
* from the seabed under it and never from a decision of its own. The arms are
|
|
43
|
+
* the whole animal, each on its own phase and none of them the same length.
|
|
44
|
+
*
|
|
45
|
+
* ## What it does with the day, and where the numbers came from
|
|
46
|
+
*
|
|
47
|
+
* It had two behaviours to begin with, a crawl and a jet, and it alternated
|
|
48
|
+
* them for as long as anybody watched. That is not an animal. It is a duty
|
|
49
|
+
* cycle, and the eye has the whole of it inside a few seconds.
|
|
50
|
+
*
|
|
51
|
+
* So the repertoire is taken off field work rather than invented. Sultan et
|
|
52
|
+
* al., "Deception in the dark, an ethogram of Callistoctopus furvus", Journal
|
|
53
|
+
* of Molluscan Studies 91(3), 2025, scored wild octopuses frame by frame and
|
|
54
|
+
* reported what share of watched time went to each behaviour. `DOINGS` is that
|
|
55
|
+
* table, one row a behaviour, and two of its findings are the whole reason for
|
|
56
|
+
* this rewrite:
|
|
57
|
+
*
|
|
58
|
+
* * **Most of an octopus is not travel.** Between sitting still and working
|
|
59
|
+
* its arms into crevices without moving the body, close to two fifths of
|
|
60
|
+
* the day goes to being somewhere rather than going somewhere. The old one
|
|
61
|
+
* had no way of not crawling except to jet.
|
|
62
|
+
* * **Jetting is a small part of it, and it is an answer.** It is the second
|
|
63
|
+
* line of defence, after not being seen has failed, so most of it is
|
|
64
|
+
* provoked. `UNPROVOKED` splits the measured share: that much is the
|
|
65
|
+
* animal's own idea and the rest waits for a `Startle`, which is what a
|
|
66
|
+
* hull going over hands down.
|
|
67
|
+
*
|
|
68
|
+
* The pick weights come off `DOINGS` rather than being tuned by hand, because a
|
|
69
|
+
* bout is a length of time and the long behaviours would otherwise swallow the
|
|
70
|
+
* day. Adding another behaviour is a row here and a posture in `POSTURES`.
|
|
71
|
+
*
|
|
72
|
+
* The speeds come off the same shelf. Huffard, "Locomotion by Abdopus
|
|
73
|
+
* aculeatus", Journal of Experimental Biology 209, 2006, timed the modes: a
|
|
74
|
+
* crawl at 0.62 body lengths a second, a walk on the back arms at rather more
|
|
75
|
+
* than twice that, a jet at under three times it and five at the very top. The
|
|
76
|
+
* old jet was nearly nine times the crawl, which is why it read as a firework
|
|
77
|
+
* rather than an escape. What is kept off that paper is the ratios; this water
|
|
78
|
+
* runs slower than the sea on purpose, and everything in it does.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
import { makeRandom } from "./perlin.ts"
|
|
82
|
+
import type { Point } from "./seabed.ts"
|
|
83
|
+
import { drawnTilt, felt, type Startle } from "./shoal.ts"
|
|
84
|
+
import { BED_OFF, drawnAt, SWIMMING_OFF } from "./sizes.ts"
|
|
85
|
+
|
|
86
|
+
export interface Squid {
|
|
87
|
+
/** 0 is as far off as this water goes, 1 is up against the glass. */
|
|
88
|
+
readonly depth: number
|
|
89
|
+
/** Which way it points, once and for life: 1 rightward, -1 left. */
|
|
90
|
+
readonly facing: number
|
|
91
|
+
/** Radians, and always inside a shallow cone off level. */
|
|
92
|
+
heading: number
|
|
93
|
+
/** Nose to arm tip in px, depth already taken out of it. */
|
|
94
|
+
readonly size: number
|
|
95
|
+
/** px/s, which is whatever the mantle is doing. */
|
|
96
|
+
speed: number
|
|
97
|
+
/**
|
|
98
|
+
* How hard the mantle is squeezed, 0 slack and 1 shut.
|
|
99
|
+
*
|
|
100
|
+
* The one number the drawing and the travel are both read off.
|
|
101
|
+
*/
|
|
102
|
+
squeeze: number
|
|
103
|
+
/**
|
|
104
|
+
* How far to rotate the drawing, in radians, after mirroring it by `facing`.
|
|
105
|
+
*
|
|
106
|
+
* Not the heading; see `Fish.tilt`, which this is the same fact as and for
|
|
107
|
+
* the same reason. A squid mirrors exactly as a fish does, and every
|
|
108
|
+
* renderer got the angle backwards for a leftward one, so it jetted up the
|
|
109
|
+
* screen while pointing down it.
|
|
110
|
+
*/
|
|
111
|
+
tilt: number
|
|
112
|
+
x: number
|
|
113
|
+
y: number
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** One of the behaviours `DOINGS` scores, and the whole of what an octopus does. */
|
|
117
|
+
export type OctopusDoing =
|
|
118
|
+
| "bury"
|
|
119
|
+
| "crawl"
|
|
120
|
+
| "handle"
|
|
121
|
+
| "jet"
|
|
122
|
+
| "pounce"
|
|
123
|
+
| "probe"
|
|
124
|
+
| "rest"
|
|
125
|
+
| "stilt"
|
|
126
|
+
|
|
127
|
+
export interface Octopus {
|
|
128
|
+
/**
|
|
129
|
+
* How full the mantle is, 0 empty and 1 swelled.
|
|
130
|
+
*
|
|
131
|
+
* Ventilation, which never stops. It is the only thing a stopped octopus has
|
|
132
|
+
* to say it is alive: at this size an animal holding still with nothing
|
|
133
|
+
* moving on it is a stone, which is a compliment to the camouflage and a
|
|
134
|
+
* problem for the picture.
|
|
135
|
+
*/
|
|
136
|
+
breath: number
|
|
137
|
+
/** How far through its own gait it is, which is what moves the arms. */
|
|
138
|
+
crawl: number
|
|
139
|
+
readonly depth: number
|
|
140
|
+
/** What it is doing. `DOINGS` says how likely that was and how long it runs. */
|
|
141
|
+
doing: OctopusDoing
|
|
142
|
+
/**
|
|
143
|
+
* Which way it points: 1 rightward, -1 left.
|
|
144
|
+
*
|
|
145
|
+
* It turns, which a squid never does, and the reason the two differ is that
|
|
146
|
+
* an octopus has moments of standing still to turn in. See `take`.
|
|
147
|
+
*/
|
|
148
|
+
facing: number
|
|
149
|
+
/**
|
|
150
|
+
* How hard it is being driven right now, 0 at rest and 1 mid-jet.
|
|
151
|
+
*
|
|
152
|
+
* The arms are read off this rather than off the speed, because the two are
|
|
153
|
+
* not the same thing: an arm streams because the water is going past it, and
|
|
154
|
+
* the water is going past it hardest at the moment of the push.
|
|
155
|
+
*/
|
|
156
|
+
haul: number
|
|
157
|
+
/** How far off the floor it is, in its own head widths. Below it when buried. */
|
|
158
|
+
lift: number
|
|
159
|
+
/** How far the arms are thrown out, 0 gathered under it and 1 stretched. */
|
|
160
|
+
reach: number
|
|
161
|
+
/** Head width in px, depth already taken out of it. */
|
|
162
|
+
readonly size: number
|
|
163
|
+
x: number
|
|
164
|
+
/** Where it is. The floor decides this while it is on the floor. */
|
|
165
|
+
y: number
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface Cephalopods {
|
|
169
|
+
readonly octopuses: readonly Octopus[]
|
|
170
|
+
resize(width: number, height: number, floor: (x: number) => number): void
|
|
171
|
+
readonly squids: readonly Squid[]
|
|
172
|
+
/**
|
|
173
|
+
* Advance by `seconds`. `startle` is whatever the water is being told to do
|
|
174
|
+
* this tick, which the octopuses answer and the squid do not: a squid is
|
|
175
|
+
* already somewhere else, and an animal sitting on the ground is the one with
|
|
176
|
+
* a reason to care that something went over.
|
|
177
|
+
*/
|
|
178
|
+
step(seconds: number, startle?: Startle | null): void
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface CephalopodOptions {
|
|
182
|
+
floor: (x: number) => number
|
|
183
|
+
height: number
|
|
184
|
+
octopuses?: number
|
|
185
|
+
seed: number
|
|
186
|
+
squids?: number
|
|
187
|
+
width: number
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** How long one jet cycle lasts, slowest to fastest, in seconds. */
|
|
191
|
+
const CYCLE_SLOWEST = 3.4
|
|
192
|
+
|
|
193
|
+
const CYCLE_FASTEST = 6.8
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* What share of the cycle is the jet itself.
|
|
197
|
+
*
|
|
198
|
+
* Small, and it has to be. Give the push a third of the cycle and the animal
|
|
199
|
+
* reads as swimming badly rather than as jetting; give it a tenth and the
|
|
200
|
+
* stillness before it becomes the thing you notice, which is correct.
|
|
201
|
+
*/
|
|
202
|
+
const JET_SHARE = 0.16
|
|
203
|
+
|
|
204
|
+
/** How fast a squid travels at the peak of a jet, in its own lengths a second. */
|
|
205
|
+
const JET_PEAK = 4.2
|
|
206
|
+
|
|
207
|
+
/** What it keeps while filling, as a share of that. Nearly nothing. */
|
|
208
|
+
const GLIDE = 0.07
|
|
209
|
+
|
|
210
|
+
/** How fast it sheds speed once the jet is over, per second. */
|
|
211
|
+
const DRAG = 2.1
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The ends of a squid's length, in metres, mantle and arms together.
|
|
215
|
+
*
|
|
216
|
+
* A reef squid rather than anything from deep water, and `sizes.ts` puts it at
|
|
217
|
+
* the same distance the rest of the water swims at, so a squid crossing in
|
|
218
|
+
* front of a shoal is the size a squid is beside those fish.
|
|
219
|
+
*/
|
|
220
|
+
const SQUID_SHORTEST = 0.16
|
|
221
|
+
|
|
222
|
+
const SQUID_LONGEST = 0.38
|
|
223
|
+
|
|
224
|
+
/** How steeply a squid may travel, either side of level. */
|
|
225
|
+
const SQUID_PITCH = 0.42
|
|
226
|
+
|
|
227
|
+
/** How far its line wanders while it fills, and how fast it takes a new one. */
|
|
228
|
+
const WANDER = 0.5
|
|
229
|
+
|
|
230
|
+
const RETHINK_LEAST = 2.5
|
|
231
|
+
|
|
232
|
+
const RETHINK_SPAN = 5
|
|
233
|
+
|
|
234
|
+
/** How far down the water a squid is ever born. They belong to the open water
|
|
235
|
+
* above the bed, and one that starts in the weeds looks like a mistake. */
|
|
236
|
+
const ROOF = 0.72
|
|
237
|
+
|
|
238
|
+
/** How far past the sides one goes before it comes back on the other. */
|
|
239
|
+
const MARGIN = 1.1
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* How near the top or the bed a squid gets before it starts turning away, in
|
|
243
|
+
* its own lengths, and how hard it leans on the line it was holding.
|
|
244
|
+
*
|
|
245
|
+
* It used to have no opinion about either. The step ended by clamping it back
|
|
246
|
+
* inside the water, so an animal that had drifted up spent the next several
|
|
247
|
+
* seconds sliding along the ceiling still nosed at it, which reads exactly like
|
|
248
|
+
* an animal swimming in one direction and facing another. The clamp is still
|
|
249
|
+
* there underneath, because a jet is fast enough to cross this band inside one
|
|
250
|
+
* frame, but nothing should ever reach it.
|
|
251
|
+
*
|
|
252
|
+
* The pull is stronger than `WANDER` on purpose. A squid that argued with the
|
|
253
|
+
* surface would be a squid that lost.
|
|
254
|
+
*/
|
|
255
|
+
const SHY_BAND = 2.4
|
|
256
|
+
|
|
257
|
+
const SHY_PULL = 1.4
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The ends of an octopus's head across, in metres.
|
|
261
|
+
*
|
|
262
|
+
* The head and not the animal, which is the number this file draws from and is
|
|
263
|
+
* about a fifth of what the arms reach. `sizes.ts` turns it into a share of the
|
|
264
|
+
* box at the distance the floor lies at, so an octopus at the mouth of a den
|
|
265
|
+
* and a crab going past it are the sizes they are against each other.
|
|
266
|
+
*/
|
|
267
|
+
const OCTOPUS_SMALLEST = 0.09
|
|
268
|
+
|
|
269
|
+
const OCTOPUS_LARGEST = 0.17
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The repertoire, and what share of a life each behaviour takes.
|
|
273
|
+
*
|
|
274
|
+
* `share` is measurement: the percentage of watched time the ethogram in the
|
|
275
|
+
* module note scored for each, with its burying and its going to ground added
|
|
276
|
+
* together because at this size they are the same picture. The rest of the
|
|
277
|
+
* table is judgement, and the note says which is which so nobody tunes the
|
|
278
|
+
* wrong column. `least` and `span` are how long one bout runs here, in
|
|
279
|
+
* seconds, which no ethogram reports.
|
|
280
|
+
*/
|
|
281
|
+
const DOINGS: Readonly<Record<OctopusDoing, { least: number; share: number; span: number }>> = {
|
|
282
|
+
/** Into the sand and out of sight, which is its first answer to anything. */
|
|
283
|
+
bury: { least: 7, share: 9.65, span: 13 },
|
|
284
|
+
crawl: { least: 2.5, share: 41.29, span: 4.5 },
|
|
285
|
+
/** Hunched over whatever it caught, arms knotted underneath it. */
|
|
286
|
+
handle: { least: 3, share: 0.85, span: 4 },
|
|
287
|
+
jet: { least: 1.2, share: 4.79, span: 0.8 },
|
|
288
|
+
/** The parachute: arms opened into a bell and dropped over a stone. */
|
|
289
|
+
pounce: { least: 0.9, share: 0.65, span: 0.6 },
|
|
290
|
+
/** Body still, arms into the crevices without it. The one it lives on. */
|
|
291
|
+
probe: { least: 3, share: 21.82, span: 6 },
|
|
292
|
+
rest: { least: 5, share: 17.41, span: 11 },
|
|
293
|
+
/** Walking on the back pair, with the rest coiled up over it. */
|
|
294
|
+
stilt: { least: 1.5, share: 0.8, span: 2.5 },
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* What share of jetting is the animal's own idea.
|
|
299
|
+
*
|
|
300
|
+
* The ethogram counts every jet it saw, and a good deal of what a wild octopus
|
|
301
|
+
* jets away from is the person holding the camera. Here the thing that turns up
|
|
302
|
+
* is a `Startle`, so the measured share is split in two: this much happens
|
|
303
|
+
* unasked and the rest waits to be provoked. Left whole, every octopus in the
|
|
304
|
+
* scene would be bolting from nothing several times a minute, which is what the
|
|
305
|
+
* old one did and what this is for.
|
|
306
|
+
*/
|
|
307
|
+
const UNPROVOKED = 0.2
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* How near and how hard a startle has to be felt before it lets go.
|
|
311
|
+
*
|
|
312
|
+
* Under this the answer is to stop being visible rather than to leave, and that
|
|
313
|
+
* is the right way round. An octopus's first defence is that you did not see
|
|
314
|
+
* it. The jet is what is left when that has failed.
|
|
315
|
+
*/
|
|
316
|
+
const BOLT = 0.45
|
|
317
|
+
|
|
318
|
+
/** The behaviours it is standing still through, and so can turn round in. */
|
|
319
|
+
const STILL: ReadonlySet<OctopusDoing> = new Set<OctopusDoing>(["bury", "handle", "probe", "rest"])
|
|
320
|
+
|
|
321
|
+
/** How likely it is to have turned round by the end of one of those. */
|
|
322
|
+
const TURN_ODDS = 0.35
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* How far it gets in one pull, in head widths a second at the peak of it.
|
|
326
|
+
*
|
|
327
|
+
* Higher than the old steady pace it replaces, and it has to be: half the
|
|
328
|
+
* cycle is now a gather where it does not move at all, so the same ground has
|
|
329
|
+
* to be covered in half the time or the animal is slower than it was as well
|
|
330
|
+
* as jerkier.
|
|
331
|
+
*/
|
|
332
|
+
const CRAWL_REACH = 0.62
|
|
333
|
+
|
|
334
|
+
/** How far it comes up off the floor as it pulls, in its own head widths. */
|
|
335
|
+
const CRAWL_RISE = 0.06
|
|
336
|
+
|
|
337
|
+
/** How fast its arms work through their gait, in cycles a second. */
|
|
338
|
+
const GAIT = 0.55
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* How fast it walks on the back pair, in head widths a second.
|
|
342
|
+
*
|
|
343
|
+
* Steady rather than pulsed, which is the difference between the two gaits: a
|
|
344
|
+
* crawl is a haul and a rest and a haul, and a walk on two arms is a walk. The
|
|
345
|
+
* timing work put it at rather more than twice a crawl and that is what this is.
|
|
346
|
+
*/
|
|
347
|
+
const STILT_PACE = 0.48
|
|
348
|
+
|
|
349
|
+
/** How far up it stands to do it, in its own head widths. */
|
|
350
|
+
const STILT_RISE = 0.42
|
|
351
|
+
|
|
352
|
+
/** How deep it sinks when it buries, in its own head widths. */
|
|
353
|
+
const BURY_DEPTH = 0.62
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* How fast a jet goes at the peak of it, in head widths a second.
|
|
357
|
+
*
|
|
358
|
+
* It was nearly nine times the crawl and it read as a firework going off. The
|
|
359
|
+
* timing work puts a jet at under three times a crawl on the mean and five at
|
|
360
|
+
* the very top of the range, so this is the top of that and no further.
|
|
361
|
+
*/
|
|
362
|
+
const JET_RUSH = 1.6
|
|
363
|
+
|
|
364
|
+
/** How far off the floor a jet takes it, in its own head widths. */
|
|
365
|
+
const JET_HEIGHT = 2.2
|
|
366
|
+
|
|
367
|
+
/** How fast it ventilates when it is idle, in cycles a second. */
|
|
368
|
+
const BREATH_CALM = 0.42
|
|
369
|
+
|
|
370
|
+
/** And how much more of that a hard haul adds, because working costs air. */
|
|
371
|
+
const BREATH_WORKED = 1.6
|
|
372
|
+
|
|
373
|
+
/** How far the arms stream behind, standing still and at full drive. */
|
|
374
|
+
const TRAIL_HELD = 0.22
|
|
375
|
+
|
|
376
|
+
const TRAIL_DRIVEN = 1.15
|
|
377
|
+
|
|
378
|
+
/** How far back these stand. Squid keep out of the very front of the glass. */
|
|
379
|
+
const DEPTH_FAR = 0.15
|
|
380
|
+
|
|
381
|
+
const DEPTH_NEAR = 0.85
|
|
382
|
+
|
|
383
|
+
/** How much of a body's drawn size, and with it its speed, is its depth. */
|
|
384
|
+
const DEPTH_SIZE = 0.6
|
|
385
|
+
|
|
386
|
+
const MIN_SPAN = 1
|
|
387
|
+
|
|
388
|
+
interface Beat {
|
|
389
|
+
/** The line it is holding, and how long before it takes another. */
|
|
390
|
+
aim: number
|
|
391
|
+
/** Seconds into the current cycle. */
|
|
392
|
+
at: number
|
|
393
|
+
hold: number
|
|
394
|
+
/** How long this cycle runs. */
|
|
395
|
+
span: number
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** One octopus's turn at one behaviour, and its breathing, which outlasts it. */
|
|
399
|
+
interface Bout {
|
|
400
|
+
/** Seconds into it. */
|
|
401
|
+
at: number
|
|
402
|
+
/** How far through a ventilation it is, in turns. */
|
|
403
|
+
puff: number
|
|
404
|
+
/** How long this one runs. */
|
|
405
|
+
span: number
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function createCephalopods(options: CephalopodOptions): Cephalopods {
|
|
409
|
+
const random = makeRandom(options.seed ^ 0x0c70)
|
|
410
|
+
|
|
411
|
+
let width = Math.max(MIN_SPAN, options.width)
|
|
412
|
+
let height = Math.max(MIN_SPAN, options.height)
|
|
413
|
+
let floor = options.floor
|
|
414
|
+
|
|
415
|
+
const squids: Squid[] = []
|
|
416
|
+
const octopuses: Octopus[] = []
|
|
417
|
+
const beats: Beat[] = []
|
|
418
|
+
const bouts: Bout[] = []
|
|
419
|
+
|
|
420
|
+
function bornSquid(): Squid {
|
|
421
|
+
const depth = DEPTH_FAR + random() * (DEPTH_NEAR - DEPTH_FAR)
|
|
422
|
+
const facing = random() < 0.5 ? -1 : 1
|
|
423
|
+
const long = SQUID_SHORTEST + random() * (SQUID_LONGEST - SQUID_SHORTEST)
|
|
424
|
+
const length = drawnAt(long, SWIMMING_OFF) * width
|
|
425
|
+
const aim = (random() - 0.5) * WANDER
|
|
426
|
+
const x = random() * width
|
|
427
|
+
const size = length * (1 - DEPTH_SIZE + DEPTH_SIZE * depth)
|
|
428
|
+
|
|
429
|
+
// Inside the water it is allowed, and not merely inside the box. One born
|
|
430
|
+
// over the surface was shoved back under it on its first frame, which is a
|
|
431
|
+
// squid travelling a body length sideways in a thirtieth of a second.
|
|
432
|
+
const top = size * MARGIN
|
|
433
|
+
const bed = Math.max(top, Math.min(floor(x) - size * 0.4, height * ROOF))
|
|
434
|
+
|
|
435
|
+
beats.push({
|
|
436
|
+
aim,
|
|
437
|
+
at: random() * CYCLE_SLOWEST,
|
|
438
|
+
hold: RETHINK_LEAST + random() * RETHINK_SPAN,
|
|
439
|
+
span: CYCLE_SLOWEST + random() * (CYCLE_FASTEST - CYCLE_SLOWEST),
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
depth,
|
|
444
|
+
facing,
|
|
445
|
+
heading: point(facing, aim),
|
|
446
|
+
size,
|
|
447
|
+
speed: 0,
|
|
448
|
+
squeeze: 0,
|
|
449
|
+
tilt: drawnTilt(point(facing, aim), facing),
|
|
450
|
+
x,
|
|
451
|
+
y: top + random() * (bed - top),
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** How long one bout of this lasts, drawn out of `DOINGS`. */
|
|
456
|
+
function span(doing: OctopusDoing): number {
|
|
457
|
+
const row = DOINGS[doing]
|
|
458
|
+
return row.least + random() * row.span
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Put it on to something else.
|
|
463
|
+
*
|
|
464
|
+
* It turns while it is standing still and never while it is going, which is
|
|
465
|
+
* the same rule `Squid.facing` states in its harder form. Flipping a moving
|
|
466
|
+
* animal swaps which way its arms stream between two frames, and what that
|
|
467
|
+
* reads as is the drawing being replaced rather than the animal turning
|
|
468
|
+
* round. Standing still there is nothing streaming to swap.
|
|
469
|
+
*/
|
|
470
|
+
function take(one: Octopus, bout: Bout, doing: OctopusDoing): void {
|
|
471
|
+
if (STILL.has(one.doing) && random() < TURN_ODDS) one.facing = -one.facing
|
|
472
|
+
one.doing = doing
|
|
473
|
+
bout.at = 0
|
|
474
|
+
bout.span = span(doing)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function bornOctopus(): Octopus {
|
|
478
|
+
const depth = DEPTH_FAR + random() * (DEPTH_NEAR - DEPTH_FAR)
|
|
479
|
+
const doing = pick(random)
|
|
480
|
+
const across = OCTOPUS_SMALLEST + random() * (OCTOPUS_LARGEST - OCTOPUS_SMALLEST)
|
|
481
|
+
const head = drawnAt(across, BED_OFF) * width
|
|
482
|
+
const run = span(doing)
|
|
483
|
+
const x = random() * width
|
|
484
|
+
|
|
485
|
+
// Part way through it already. A scene that opened with every octopus on
|
|
486
|
+
// the first frame of the same fresh bout would spend its first minute in
|
|
487
|
+
// lockstep, which is the one minute anybody is certain to watch.
|
|
488
|
+
bouts.push({ at: random() * run, puff: random(), span: run })
|
|
489
|
+
|
|
490
|
+
return {
|
|
491
|
+
breath: 0,
|
|
492
|
+
crawl: random() * Math.PI * 2,
|
|
493
|
+
depth,
|
|
494
|
+
doing,
|
|
495
|
+
facing: random() < 0.5 ? -1 : 1,
|
|
496
|
+
haul: 0,
|
|
497
|
+
lift: 0,
|
|
498
|
+
reach: 1,
|
|
499
|
+
size: head * (1 - DEPTH_SIZE + DEPTH_SIZE * depth),
|
|
500
|
+
x,
|
|
501
|
+
y: floor(x),
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function stock(): void {
|
|
506
|
+
squids.length = 0
|
|
507
|
+
octopuses.length = 0
|
|
508
|
+
beats.length = 0
|
|
509
|
+
bouts.length = 0
|
|
510
|
+
|
|
511
|
+
for (let made = 0; made < Math.max(0, options.squids ?? 0); made++) squids.push(bornSquid())
|
|
512
|
+
for (let made = 0; made < Math.max(0, options.octopuses ?? 0); made++) {
|
|
513
|
+
octopuses.push(bornOctopus())
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
stock()
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
octopuses,
|
|
521
|
+
|
|
522
|
+
resize(nextWidth, nextHeight, nextFloor) {
|
|
523
|
+
const scaleX = Math.max(MIN_SPAN, nextWidth) / width
|
|
524
|
+
const scaleY = Math.max(MIN_SPAN, nextHeight) / height
|
|
525
|
+
width = Math.max(MIN_SPAN, nextWidth)
|
|
526
|
+
height = Math.max(MIN_SPAN, nextHeight)
|
|
527
|
+
floor = nextFloor
|
|
528
|
+
|
|
529
|
+
for (const one of squids) {
|
|
530
|
+
one.x *= scaleX
|
|
531
|
+
one.y *= scaleY
|
|
532
|
+
}
|
|
533
|
+
for (const one of octopuses) {
|
|
534
|
+
one.x *= scaleX
|
|
535
|
+
one.y = floor(one.x) - one.lift * one.size
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
squids,
|
|
539
|
+
|
|
540
|
+
step(seconds, startle) {
|
|
541
|
+
const dt = Math.min(Math.max(seconds, 0), 0.1)
|
|
542
|
+
|
|
543
|
+
for (let at = 0; at < squids.length; at++) {
|
|
544
|
+
const one = squids[at]
|
|
545
|
+
const beat = beats[at]
|
|
546
|
+
if (!one || !beat) continue
|
|
547
|
+
|
|
548
|
+
beat.at += dt
|
|
549
|
+
if (beat.at >= beat.span) {
|
|
550
|
+
beat.at -= beat.span
|
|
551
|
+
beat.span = CYCLE_SLOWEST + random() * (CYCLE_FASTEST - CYCLE_SLOWEST)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// The mantle first, and everything else read off it.
|
|
555
|
+
const through = beat.at / beat.span
|
|
556
|
+
one.squeeze = through < JET_SHARE ? Math.sin((through / JET_SHARE) * Math.PI) : 0
|
|
557
|
+
|
|
558
|
+
const push = one.squeeze * JET_PEAK * one.size
|
|
559
|
+
const glide = GLIDE * JET_PEAK * one.size
|
|
560
|
+
one.speed =
|
|
561
|
+
push > one.speed ? push : glide + (one.speed - glide) * Math.max(0, 1 - DRAG * dt)
|
|
562
|
+
|
|
563
|
+
beat.hold -= dt
|
|
564
|
+
if (beat.hold <= 0) {
|
|
565
|
+
beat.aim = (random() - 0.5) * WANDER
|
|
566
|
+
beat.hold = RETHINK_LEAST + random() * RETHINK_SPAN
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Eased rather than set. `beat.aim` is a fresh number every few
|
|
570
|
+
// seconds, and a body that took it whole snapped onto the new line
|
|
571
|
+
// between two frames: the squid appeared to twitch round rather than
|
|
572
|
+
// turn, which is the one thing a drifting animal must not do.
|
|
573
|
+
const edge = one.size * MARGIN
|
|
574
|
+
const bed = floor(one.x) - one.size * 0.4
|
|
575
|
+
|
|
576
|
+
one.heading = ease(
|
|
577
|
+
one.heading,
|
|
578
|
+
point(one.facing, beat.aim + shy(one.y, edge, bed, one.size)),
|
|
579
|
+
TURN * dt,
|
|
580
|
+
)
|
|
581
|
+
one.tilt = drawnTilt(one.heading, one.facing)
|
|
582
|
+
one.x += Math.cos(one.heading) * one.speed * dt
|
|
583
|
+
one.y += Math.sin(one.heading) * one.speed * dt
|
|
584
|
+
|
|
585
|
+
if (one.x > width + edge) one.x = -edge
|
|
586
|
+
if (one.x < -edge) one.x = width + edge
|
|
587
|
+
one.y = Math.min(Math.max(one.y, edge), bed)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
for (let at = 0; at < octopuses.length; at++) {
|
|
591
|
+
const one = octopuses[at]
|
|
592
|
+
const bout = bouts[at]
|
|
593
|
+
if (!one || !bout) continue
|
|
594
|
+
|
|
595
|
+
// Breathing outlasts the bout and is the one thing here that never
|
|
596
|
+
// stops, so it is stepped before anything gets a say in it.
|
|
597
|
+
bout.puff = (bout.puff + (BREATH_CALM + one.haul * BREATH_WORKED) * dt) % 1
|
|
598
|
+
one.breath = (1 - Math.cos(bout.puff * Math.PI * 2)) / 2
|
|
599
|
+
|
|
600
|
+
// Something went over. Hard and near enough and it leaves; anything
|
|
601
|
+
// less and it goes into the sand, which is the answer it prefers.
|
|
602
|
+
const felt = pressure(startle, one)
|
|
603
|
+
if (felt > BOLT) {
|
|
604
|
+
if (one.doing !== "jet") take(one, bout, "jet")
|
|
605
|
+
} else if (felt > 0 && one.doing !== "jet" && one.doing !== "bury") {
|
|
606
|
+
take(one, bout, "bury")
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
bout.at += dt
|
|
610
|
+
if (bout.at >= bout.span) take(one, bout, pick(random))
|
|
611
|
+
|
|
612
|
+
const through = Math.min(1, bout.at / bout.span)
|
|
613
|
+
advance(one, through, dt)
|
|
614
|
+
|
|
615
|
+
const edge = one.size * MARGIN
|
|
616
|
+
if (one.x > width + edge) one.x = -edge
|
|
617
|
+
if (one.x < -edge) one.x = width + edge
|
|
618
|
+
one.y = floor(one.x) - one.lift * one.size
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* The ladder the next behaviour is drawn off, each rung a running total.
|
|
626
|
+
*
|
|
627
|
+
* Built once, because `DOINGS` never moves.
|
|
628
|
+
*/
|
|
629
|
+
const ODDS = weigh()
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* How likely each behaviour is to be chosen, given how long it then lasts.
|
|
633
|
+
*
|
|
634
|
+
* A bout is a length of time, so drawing straight off `share` would hand the
|
|
635
|
+
* long behaviours far more of the day than the field work gave them: burying is
|
|
636
|
+
* a tenth of a life in bouts of a dozen seconds, and a pounce is under a
|
|
637
|
+
* hundredth in bouts of one. Dividing the share by the mean bout undoes that,
|
|
638
|
+
* and what comes out at the far end is time spent, which is what was measured.
|
|
639
|
+
*/
|
|
640
|
+
function weigh(): { doing: OctopusDoing; upto: number }[] {
|
|
641
|
+
const rungs: { doing: OctopusDoing; upto: number }[] = []
|
|
642
|
+
let total = 0
|
|
643
|
+
|
|
644
|
+
for (const doing of Object.keys(DOINGS).sort() as OctopusDoing[]) {
|
|
645
|
+
const row = DOINGS[doing]
|
|
646
|
+
const share = doing === "jet" ? row.share * UNPROVOKED : row.share
|
|
647
|
+
total += share / (row.least + row.span / 2)
|
|
648
|
+
rungs.push({ doing, upto: total })
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
for (const rung of rungs) rung.upto /= total
|
|
652
|
+
return rungs
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** What it does next, which is nothing to do with what it was doing. */
|
|
656
|
+
function pick(random: () => number): OctopusDoing {
|
|
657
|
+
const roll = random()
|
|
658
|
+
for (const rung of ODDS) if (roll <= rung.upto) return rung.doing
|
|
659
|
+
return "crawl"
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* How hard a startle is felt where this one is standing, 0 to 1.
|
|
664
|
+
*
|
|
665
|
+
* Linear out to `reach` and nothing past it, and weighed by how much of the
|
|
666
|
+
* startle reaches this distance at all; see `felt`. A fish and an octopus
|
|
667
|
+
* disagree about what to do with a startle and agree about how much of it there
|
|
668
|
+
* is, which is why both of those come off the same two lines in `shoal.ts`.
|
|
669
|
+
*/
|
|
670
|
+
function pressure(startle: Startle | null | undefined, one: Octopus): number {
|
|
671
|
+
if (!startle || startle.force <= 0 || startle.reach <= 0) return 0
|
|
672
|
+
|
|
673
|
+
const gap = Math.hypot(one.x - startle.x, one.y - startle.y)
|
|
674
|
+
if (gap >= startle.reach) return 0
|
|
675
|
+
|
|
676
|
+
return startle.force * (1 - gap / startle.reach) * felt(one.depth, startle)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* One octopus, moved on by whatever it happens to be doing.
|
|
681
|
+
*
|
|
682
|
+
* Every arm of this sets `haul`, `lift` and `reach`, because the drawing reads
|
|
683
|
+
* all three every frame and a behaviour that left one of them alone would be
|
|
684
|
+
* wearing the last one's posture. Only the three that travel touch `x`.
|
|
685
|
+
*/
|
|
686
|
+
function advance(one: Octopus, through: number, dt: number): void {
|
|
687
|
+
switch (one.doing) {
|
|
688
|
+
case "bury": {
|
|
689
|
+
// Down, a while under, and back up. Most of the bout is spent buried, so
|
|
690
|
+
// the going down and the coming up are quick at either end of it.
|
|
691
|
+
const under = Math.min(1, Math.min(through, 1 - through) * 6)
|
|
692
|
+
one.haul = 0
|
|
693
|
+
one.lift = -BURY_DEPTH * under
|
|
694
|
+
one.reach = 1 - under * 0.55
|
|
695
|
+
break
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
case "crawl": {
|
|
699
|
+
one.crawl += GAIT * dt * Math.PI * 2
|
|
700
|
+
|
|
701
|
+
// A step rather than a slide. The old gait never let the pace reach
|
|
702
|
+
// zero, and an animal that is always moving a little is an animal being
|
|
703
|
+
// dragged: what says walking is the stop. So the pull is the positive
|
|
704
|
+
// half of the cycle and the other half is a gather, where it stands
|
|
705
|
+
// still and brings its arms up under it.
|
|
706
|
+
one.haul = Math.max(0, Math.sin(one.crawl)) ** 1.4
|
|
707
|
+
one.lift = one.haul * CRAWL_RISE
|
|
708
|
+
one.reach = 1
|
|
709
|
+
one.x += one.facing * CRAWL_REACH * one.haul * one.size * dt
|
|
710
|
+
break
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
case "handle": {
|
|
714
|
+
// Hunched over something, arms knotted under the head, and the only
|
|
715
|
+
// movement is the worrying at it.
|
|
716
|
+
one.crawl += GAIT * 0.6 * dt * Math.PI * 2
|
|
717
|
+
one.haul = 0
|
|
718
|
+
one.lift = 0
|
|
719
|
+
one.reach = 0.5 + Math.sin(one.crawl) * 0.08
|
|
720
|
+
break
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
case "jet": {
|
|
724
|
+
// Off the ground and travelling, arms streamed. The arc is the whole of
|
|
725
|
+
// it: an octopus that jetted along the floor would be an octopus on
|
|
726
|
+
// rails, and the reason to jet at all is to leave.
|
|
727
|
+
one.haul = Math.sin(through * Math.PI) ** 0.6
|
|
728
|
+
one.lift = Math.sin(through * Math.PI) * JET_HEIGHT
|
|
729
|
+
one.reach = 1
|
|
730
|
+
one.x += one.facing * JET_RUSH * one.size * one.haul * dt
|
|
731
|
+
break
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
case "pounce": {
|
|
735
|
+
// Open, then shut. Arms thrown wide into a bell, lifted, and dropped
|
|
736
|
+
// over a stone, and whatever was under the stone is now inside. The
|
|
737
|
+
// closing is faster than the opening because it is a strike.
|
|
738
|
+
one.crawl += GAIT * dt * Math.PI * 2
|
|
739
|
+
one.haul = 0
|
|
740
|
+
one.lift = Math.sin(Math.min(1, through * 1.6) * Math.PI) * 0.3
|
|
741
|
+
one.reach =
|
|
742
|
+
through < 0.55
|
|
743
|
+
? 0.5 + Math.min(1, through * 2.2) * 0.6
|
|
744
|
+
: Math.max(0.25, 1.1 - (through - 0.55) * 6)
|
|
745
|
+
break
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
case "probe": {
|
|
749
|
+
// Still, with the arms going where the body does not. This is the one it
|
|
750
|
+
// spends most of its standing life on, so the sweep is slow enough to
|
|
751
|
+
// watch: an arm in a crevice takes seconds over it.
|
|
752
|
+
one.crawl += GAIT * 0.45 * dt * Math.PI * 2
|
|
753
|
+
one.haul = 0
|
|
754
|
+
one.lift = 0
|
|
755
|
+
one.reach = 0.72 + Math.sin(one.crawl * 0.6) * 0.28
|
|
756
|
+
break
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
case "rest": {
|
|
760
|
+
one.crawl += GAIT * 0.2 * dt * Math.PI * 2
|
|
761
|
+
one.haul = 0
|
|
762
|
+
one.lift = 0
|
|
763
|
+
one.reach = 0.45 + Math.sin(one.crawl) * 0.05
|
|
764
|
+
break
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
case "stilt": {
|
|
768
|
+
// Up on the back pair with the rest coiled over it, and the pace steady:
|
|
769
|
+
// two arms alternating is a walk, and a walk does not stall the way the
|
|
770
|
+
// crawl does. It stands up at the start and sits back down at the end,
|
|
771
|
+
// because appearing at full height is a cut rather than a movement.
|
|
772
|
+
one.crawl += GAIT * 1.7 * dt * Math.PI * 2
|
|
773
|
+
const up = Math.min(1, Math.min(through, 1 - through) * 5)
|
|
774
|
+
one.haul = 0.25
|
|
775
|
+
one.lift = STILT_RISE * up
|
|
776
|
+
one.reach = 1
|
|
777
|
+
one.x += one.facing * STILT_PACE * one.size * up * dt
|
|
778
|
+
break
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Which way the water is telling it to go, if it is telling it anything.
|
|
785
|
+
*
|
|
786
|
+
* Positive is down, the way `point` reads an aim. Nothing at all through the
|
|
787
|
+
* middle of the water, which is the point: this is a wall it notices when it is
|
|
788
|
+
* near one, not a force pulling it to the centre of the picture.
|
|
789
|
+
*/
|
|
790
|
+
function shy(y: number, top: number, bed: number, size: number): number {
|
|
791
|
+
// Never more than a share of the water, or in a shallow scene the two bands
|
|
792
|
+
// overlap and the animal is told to go up and down at once. What it did then
|
|
793
|
+
// was hold still against whichever of the two was winning.
|
|
794
|
+
const band = Math.max(1, Math.min(size * SHY_BAND, (bed - top) * 0.4))
|
|
795
|
+
const under = Math.max(0, top + band - y)
|
|
796
|
+
const over = Math.max(0, y - (bed - band))
|
|
797
|
+
|
|
798
|
+
return ((under - over) / band) * SHY_PULL
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/** How fast a squid comes round onto a new line, in radians a second. */
|
|
802
|
+
const TURN = 0.9
|
|
803
|
+
|
|
804
|
+
/** A step of the way from one number to another, and never past it. */
|
|
805
|
+
function ease(from: number, to: number, step: number): number {
|
|
806
|
+
const gap = to - from
|
|
807
|
+
return Math.abs(gap) <= step ? to : from + Math.sign(gap) * step
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/** The nearest heading a body facing this way is allowed to hold. */
|
|
811
|
+
function point(facing: number, want: number): number {
|
|
812
|
+
const asked = Math.atan2(Math.sin(want), facing * Math.cos(want))
|
|
813
|
+
const rel = SQUID_PITCH * Math.tanh(asked / SQUID_PITCH)
|
|
814
|
+
return facing > 0 ? rel : Math.PI - rel
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* A squid, drawn at whatever its mantle is doing.
|
|
819
|
+
*
|
|
820
|
+
* Built nose-first along positive x with the arms trailing behind the origin,
|
|
821
|
+
* in a body a unit long, so a renderer scales it by `Squid.size` and mirrors it
|
|
822
|
+
* by `Squid.facing` exactly as it does a fish.
|
|
823
|
+
*
|
|
824
|
+
* The mantle is a leaf: wide where the fins are, drawn to a point at the nose,
|
|
825
|
+
* and narrowing to the collar where the arms leave. Squeezing it takes width
|
|
826
|
+
* out and puts length in, because that is where the water went.
|
|
827
|
+
*/
|
|
828
|
+
export function squidBody(squeeze: number): string {
|
|
829
|
+
const fat = 0.19 - squeeze * 0.07
|
|
830
|
+
const long = 0.62 + squeeze * 0.1
|
|
831
|
+
const fin = 0.2 - squeeze * 0.05
|
|
832
|
+
|
|
833
|
+
return [
|
|
834
|
+
`M${long.toFixed(3)} 0`,
|
|
835
|
+
`Q${(long * 0.55).toFixed(3)} ${(-fat).toFixed(3)} 0 ${(-fat * 0.72).toFixed(3)}`,
|
|
836
|
+
`L0 ${(fat * 0.72).toFixed(3)}`,
|
|
837
|
+
`Q${(long * 0.55).toFixed(3)} ${fat.toFixed(3)} ${long.toFixed(3)} 0`,
|
|
838
|
+
"Z",
|
|
839
|
+
// The fins, one either side, back where the mantle is widest.
|
|
840
|
+
`M${(long * 0.52).toFixed(3)} ${(-fat * 0.86).toFixed(3)}`,
|
|
841
|
+
`Q${(long * 0.86).toFixed(3)} ${(-fat - fin).toFixed(3)} ${(long * 0.97).toFixed(3)} ${(-fat * 0.2).toFixed(3)}`,
|
|
842
|
+
"Z",
|
|
843
|
+
`M${(long * 0.52).toFixed(3)} ${(fat * 0.86).toFixed(3)}`,
|
|
844
|
+
`Q${(long * 0.86).toFixed(3)} ${(fat + fin).toFixed(3)} ${(long * 0.97).toFixed(3)} ${(fat * 0.2).toFixed(3)}`,
|
|
845
|
+
"Z",
|
|
846
|
+
].join(" ")
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* The arms, as polylines leaving the collar.
|
|
851
|
+
*
|
|
852
|
+
* Gathered into a point during a jet and spread while filling, which is not a
|
|
853
|
+
* flourish: it is the only part of the animal that shows the water it is
|
|
854
|
+
* pushing against.
|
|
855
|
+
*/
|
|
856
|
+
export function squidArms(squeeze: number, count = 8): Point[][] {
|
|
857
|
+
const arms: Point[][] = []
|
|
858
|
+
// Gathered at full squeeze, never shut. Arms folded into a single line make
|
|
859
|
+
// the animal a dart, and a dart has no arms to notice: what is wanted is a
|
|
860
|
+
// handful of them held tight, which still reads as a handful.
|
|
861
|
+
const spread = (1 - squeeze) * 0.44 + 0.2
|
|
862
|
+
|
|
863
|
+
for (let made = 0; made < count; made++) {
|
|
864
|
+
const side = made / (count - 1) - 0.5
|
|
865
|
+
const lean = side * spread
|
|
866
|
+
const reach = 0.34 + Math.abs(side) * 0.16
|
|
867
|
+
const points: Point[] = []
|
|
868
|
+
|
|
869
|
+
for (let step = 0; step <= 5; step++) {
|
|
870
|
+
const t = step / 5
|
|
871
|
+
points.push({
|
|
872
|
+
x: -t * reach,
|
|
873
|
+
y: lean * t * (0.5 + t) + Math.sin(t * 2.6 + side * 5) * 0.03 * (1 - squeeze) * t,
|
|
874
|
+
})
|
|
875
|
+
}
|
|
876
|
+
arms.push(points)
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return arms
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* An octopus's mantle, in a head a unit wide sitting on the origin.
|
|
884
|
+
*
|
|
885
|
+
* A bag with a waist rather than a dome. The dome is what it was first and the
|
|
886
|
+
* dome is a mushroom: what tells an octopus from a jellyfish above the arms is
|
|
887
|
+
* that the mantle narrows where it meets them, so there is a head and then a
|
|
888
|
+
* gather, and the arms leave the gather.
|
|
889
|
+
*
|
|
890
|
+
* `breath` is the ventilation, 0 empty and 1 full, and it takes width in and
|
|
891
|
+
* lets a little height out, because that is where the water went. It is a small
|
|
892
|
+
* enough movement that nobody will name it, which is correct: what it is for is
|
|
893
|
+
* that a stopped octopus is still doing something. Most of this animal's day is
|
|
894
|
+
* now spent not travelling, and without a breath under it that day is a sprite
|
|
895
|
+
* that has been left on the ground.
|
|
896
|
+
*/
|
|
897
|
+
export function octopusHead(breath: number): string {
|
|
898
|
+
const full = Math.min(Math.max(breath, 0), 1)
|
|
899
|
+
const wide = 1 + full * 0.08
|
|
900
|
+
const tall = 1 - full * 0.05
|
|
901
|
+
const w = (at: number): string => (at * wide).toFixed(3)
|
|
902
|
+
const h = (at: number): string => (at * tall).toFixed(3)
|
|
903
|
+
|
|
904
|
+
return [
|
|
905
|
+
`M${w(-0.3)} ${h(0.14)}`,
|
|
906
|
+
`Q${w(-0.5)} ${h(-0.06)} ${w(-0.44)} ${h(-0.4)}`,
|
|
907
|
+
`Q${w(-0.36)} ${h(-0.78)} 0 ${h(-0.8)}`,
|
|
908
|
+
`Q${w(0.36)} ${h(-0.78)} ${w(0.44)} ${h(-0.4)}`,
|
|
909
|
+
`Q${w(0.5)} ${h(-0.06)} ${w(0.3)} ${h(0.14)}`,
|
|
910
|
+
"Z",
|
|
911
|
+
].join(" ")
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** The mantle with the breath out of it, for anything wanting a still one. */
|
|
915
|
+
export const OCTOPUS_HEAD = octopusHead(0)
|
|
916
|
+
|
|
917
|
+
/** How one behaviour holds its arms. Alongside `DOINGS`, a row a behaviour. */
|
|
918
|
+
interface Posture {
|
|
919
|
+
/** How far the tips curl back up and in towards the body. */
|
|
920
|
+
curl: number
|
|
921
|
+
/** How far an arm falls towards the floor along its own length. */
|
|
922
|
+
drop: number
|
|
923
|
+
/** How wide the arms are spread across the ground. */
|
|
924
|
+
fan: number
|
|
925
|
+
/**
|
|
926
|
+
* How much longer the leading arms are than the trailing ones.
|
|
927
|
+
*
|
|
928
|
+
* The sign is the whole of what tells a probe from a walk on two arms.
|
|
929
|
+
* Positive reaches forward and tucks the back: the body stays where it is and
|
|
930
|
+
* the front arms go into the crevice. Negative stands on the back pair and
|
|
931
|
+
* coils the front over the head, which is the bipedal gait. `curl` follows
|
|
932
|
+
* it, so whichever end is short is also the end that is curled up.
|
|
933
|
+
*/
|
|
934
|
+
lead: number
|
|
935
|
+
/** Arm length, in head widths. */
|
|
936
|
+
reach: number
|
|
937
|
+
/** How much the gait moves them at all, 0 held still. */
|
|
938
|
+
work: number
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* What each behaviour looks like, which is almost entirely a matter of arms.
|
|
943
|
+
*
|
|
944
|
+
* The head barely changes, so if these eight are not distinguishable from one
|
|
945
|
+
* another here then the animal has one behaviour however many `DOINGS` lists.
|
|
946
|
+
*/
|
|
947
|
+
const POSTURES: Readonly<Record<OctopusDoing, Posture>> = {
|
|
948
|
+
bury: { curl: 0.05, drop: 0.08, fan: 3.4, lead: 0, reach: 0.75, work: 0.12 },
|
|
949
|
+
crawl: { curl: 0, drop: 0.34, fan: 2.5, lead: 0.25, reach: 1, work: 1 },
|
|
950
|
+
handle: { curl: 0.32, drop: 0.26, fan: 1.3, lead: 0, reach: 0.66, work: 0.4 },
|
|
951
|
+
jet: { curl: 0, drop: 0.06, fan: 0.7, lead: 0, reach: 1.1, work: 0.15 },
|
|
952
|
+
pounce: { curl: 0, drop: 0.1, fan: 3.2, lead: 0, reach: 1, work: 0.25 },
|
|
953
|
+
probe: { curl: 0.22, drop: 0.3, fan: 2.6, lead: 1, reach: 1, work: 0.7 },
|
|
954
|
+
rest: { curl: 0.3, drop: 0.3, fan: 1.9, lead: 0, reach: 0.68, work: 0.25 },
|
|
955
|
+
stilt: { curl: 0.62, drop: 0.55, fan: 1.5, lead: -1.6, reach: 0.95, work: 0.9 },
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/** As much of an octopus as its arms are drawn off. An `Octopus` is one. */
|
|
959
|
+
export interface OctopusPose {
|
|
960
|
+
crawl: number
|
|
961
|
+
doing: OctopusDoing
|
|
962
|
+
facing: number
|
|
963
|
+
haul: number
|
|
964
|
+
reach: number
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* An octopus's arms, in a body a unit wide, rooted at the origin.
|
|
969
|
+
*
|
|
970
|
+
* Each arm has its own phase off the shared gait, so the animal is always part
|
|
971
|
+
* way through reaching with some of them and gathering with the rest. Arms all
|
|
972
|
+
* doing the same thing at once are a windscreen wiper with too many blades.
|
|
973
|
+
*
|
|
974
|
+
* It takes the animal rather than a number now, because there is no longer one
|
|
975
|
+
* number to take: which arm is long, how far it falls and whether the gait is
|
|
976
|
+
* running at all are all read off what it is doing.
|
|
977
|
+
*/
|
|
978
|
+
export function octopusArms(pose: OctopusPose, count = 8): Point[][] {
|
|
979
|
+
const arms: Point[][] = []
|
|
980
|
+
const drive = Math.min(Math.max(pose.haul, 0), 1)
|
|
981
|
+
const held = POSTURES[pose.doing] ?? POSTURES.crawl
|
|
982
|
+
const open = Math.min(Math.max(pose.reach, 0), 1.2)
|
|
983
|
+
|
|
984
|
+
for (let made = 0; made < count; made++) {
|
|
985
|
+
// Fanned across most of a half-turn rather than dropped straight down. An
|
|
986
|
+
// octopus is mostly arm and it is mostly sideways: a bundle of them hanging
|
|
987
|
+
// under a dome is a jellyfish, and it takes only spreading them out along
|
|
988
|
+
// the ground to be the other animal entirely.
|
|
989
|
+
const side = made / (count - 1) - 0.5
|
|
990
|
+
|
|
991
|
+
// Two groups out of phase rather than one each, and the split runs front to
|
|
992
|
+
// back. Every arm on its own phase is that many things happening at once
|
|
993
|
+
// and it reads as a spin; a leading half reaching while a trailing half
|
|
994
|
+
// pushes reads as a gait, which is what it is.
|
|
995
|
+
const phase = pose.crawl + (side > 0 ? 0 : Math.PI) + made * 0.12
|
|
996
|
+
const work = Math.sin(phase) * held.work
|
|
997
|
+
|
|
998
|
+
// Which end of the animal this arm is on: 1 leading, -1 trailing.
|
|
999
|
+
const ahead = Math.sign(side * pose.facing) || 1
|
|
1000
|
+
const grown = held.reach * open * (1 + ahead * held.lead * 0.35)
|
|
1001
|
+
const reach = grown * (0.9 + Math.abs(side) * 0.5) * (0.88 + work * 0.12)
|
|
1002
|
+
const curl = held.curl * (1 - ahead * held.lead * 0.8)
|
|
1003
|
+
|
|
1004
|
+
// Which way the water is going past it. An arm behind a moving animal is
|
|
1005
|
+
// pulled straight out behind it, and that is the single clearest statement
|
|
1006
|
+
// a shape this size can make about which way it is going: without it the
|
|
1007
|
+
// thing is symmetrical, and a symmetrical animal sliding sideways is being
|
|
1008
|
+
// dragged rather than swimming.
|
|
1009
|
+
const stream = -pose.facing * (TRAIL_HELD + drive * TRAIL_DRIVEN)
|
|
1010
|
+
|
|
1011
|
+
// Gathered as it goes. Arms spread wide are arms holding ground; arms
|
|
1012
|
+
// brought in are arms in the water, so the fan closes as the push builds.
|
|
1013
|
+
const fan = held.fan * (1 - drive * 0.62) * open
|
|
1014
|
+
|
|
1015
|
+
const points: Point[] = []
|
|
1016
|
+
|
|
1017
|
+
for (let step = 0; step <= 6; step++) {
|
|
1018
|
+
const t = step / 6
|
|
1019
|
+
// Out first and down after, so an arm lies along the floor for most of
|
|
1020
|
+
// its length instead of dangling. `t * t` on the drop is what does that.
|
|
1021
|
+
//
|
|
1022
|
+
// About a body and a bit either side, which is as far as this can go. An
|
|
1023
|
+
// octopus is mostly arm, but an animal whose arms are three times its own
|
|
1024
|
+
// width reads as a spider, and one whose arms are longer than the shot is
|
|
1025
|
+
// wide reads as a mistake.
|
|
1026
|
+
const along = side * fan * reach * t
|
|
1027
|
+
const wave = Math.sin(phase + t * 2.2) * 0.14 * t * (1 - t) * 4 * held.work
|
|
1028
|
+
// A gathered arm comes in and up, and only so far up. The mantle sits
|
|
1029
|
+
// above the origin and it is solid, so an arm lifted much past the line
|
|
1030
|
+
// it is rooted on is an arm drawn behind the head, which at this weight
|
|
1031
|
+
// is an arm that is not there at all. Gathered still has to be visible.
|
|
1032
|
+
points.push({
|
|
1033
|
+
x: along + stream * t * reach - ahead * curl * t * t * 0.28,
|
|
1034
|
+
y: (t * t * held.drop + t * 0.1) * (1 - drive * 0.7) - wave * 0.22 - curl * t * t * 0.5,
|
|
1035
|
+
})
|
|
1036
|
+
}
|
|
1037
|
+
arms.push(points)
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
return arms
|
|
1041
|
+
}
|