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