@codincod/ornament 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +62 -2
- package/package.json +35 -3
- package/src/biome.ts +157 -0
- package/src/canvas/flat.ts +336 -0
- package/src/caustics.ts +81 -0
- package/src/census.ts +262 -0
- package/src/cephalopods.ts +1041 -0
- package/src/cloud.ts +308 -0
- package/src/crags.ts +974 -0
- package/src/drift.ts +435 -0
- package/src/eels.ts +667 -0
- package/src/fish_shape.ts +410 -0
- package/src/flora.ts +1955 -0
- package/src/flow_field.ts +491 -0
- package/src/jellies.ts +1794 -0
- package/src/nemos.ts +606 -0
- package/src/paint/bed.ts +231 -0
- package/src/paint/colour.ts +86 -0
- package/src/paint/frame.ts +204 -0
- package/src/paint/ink.ts +134 -0
- package/src/paint/life.ts +611 -0
- package/src/paint/light.ts +132 -0
- package/src/paint/pen.ts +225 -0
- package/src/paint/rock.ts +325 -0
- package/src/paint/sea.ts +809 -0
- package/src/paint/shade.ts +109 -0
- package/src/paint/sky.ts +377 -0
- package/src/paint/specimen.ts +300 -0
- package/src/paint/svg.ts +326 -0
- package/src/paint/trace.ts +164 -0
- package/src/passers.ts +730 -0
- package/src/perlin.ts +168 -0
- package/src/plenty.ts +65 -0
- package/src/puffers.ts +510 -0
- package/src/rays.ts +225 -0
- package/src/reef.ts +945 -0
- package/src/relics.ts +451 -0
- package/src/seabed.ts +525 -0
- package/src/shoal.ts +1658 -0
- package/src/sizes.ts +86 -0
- package/src/sun.ts +500 -0
- package/src/swarm.ts +587 -0
- package/src/visitors.ts +1936 -0
- package/src/walkers.ts +861 -0
package/src/puffers.ts
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one animal in this water that does not run.
|
|
3
|
+
*
|
|
4
|
+
* Every other response in this sea is a way of leaving. A fish darts, a crab
|
|
5
|
+
* freezes and then bolts, an octopus jets, a jellyfish furls and sinks, an
|
|
6
|
+
* anemone shuts, a colony of eels drops into the sand. All of it is the same
|
|
7
|
+
* sentence said in six ways, and a sea that only knows one sentence is a sea
|
|
8
|
+
* where nothing that happens means anything, because the answer is always the
|
|
9
|
+
* same answer.
|
|
10
|
+
*
|
|
11
|
+
* A pufferfish stays. It swallows water until it is a ball with spines out and
|
|
12
|
+
* then hangs there, and what it is saying is not "I am leaving" but "you cannot
|
|
13
|
+
* get this in your mouth". It is the only argument in the picture, and it is
|
|
14
|
+
* the whole reason to draw one.
|
|
15
|
+
*
|
|
16
|
+
* ## Why it is not a fish in `shoal.ts`
|
|
17
|
+
*
|
|
18
|
+
* Because it is not driven the way a fish is. A fish in that file is a spine
|
|
19
|
+
* with a wave running down it and a heading it eases onto, and what makes it
|
|
20
|
+
* look like a fish is that the body it is drawn as is the body that is pushing
|
|
21
|
+
* it. A pufferfish sculls with two small fins on the sides of a body that is
|
|
22
|
+
* held rigid. It is closer to a helicopter than to a fish, and every constant
|
|
23
|
+
* in `shoal.ts` is about the thing it is not.
|
|
24
|
+
*
|
|
25
|
+
* The other half is that a shoal steers on fright and this one does not. A fish
|
|
26
|
+
* that stopped fleeing would break every rule that file is built out of.
|
|
27
|
+
*
|
|
28
|
+
* ## What swelling costs
|
|
29
|
+
*
|
|
30
|
+
* Everything, which is why it is not free here either. Inflating is expensive
|
|
31
|
+
* and it takes a while to undo, so a real one does not do it lightly and does
|
|
32
|
+
* not stop doing it the moment the danger has passed: `SWELL` and `SETTLE` are
|
|
33
|
+
* far apart for the same reason `eels.ts` has them far apart, and it is the same
|
|
34
|
+
* fact about animals. Half deflated with nothing about is what one looks like
|
|
35
|
+
* for a minute after something went by, and it is the state that makes the event
|
|
36
|
+
* before it legible.
|
|
37
|
+
*
|
|
38
|
+
* ## What a renderer gets
|
|
39
|
+
*
|
|
40
|
+
* `body` is a closed outline in px and `spines` are lines standing off it, both
|
|
41
|
+
* recut only when the animal has changed enough to be worth it. The outline goes
|
|
42
|
+
* from an oval to very nearly a circle, the tail shrinks against it as it goes,
|
|
43
|
+
* and the spines are nothing at rest: a pufferfish at rest is a slightly
|
|
44
|
+
* lumpy fish, and that is what makes the other state read.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { abreast, type Presence } from "./biome.ts"
|
|
48
|
+
import { makeRandom } from "./perlin.ts"
|
|
49
|
+
import type { Point } from "./seabed.ts"
|
|
50
|
+
import { BED_OFF, drawnAt } from "./sizes.ts"
|
|
51
|
+
|
|
52
|
+
export interface Puffer {
|
|
53
|
+
/** The outline, closed, in px. */
|
|
54
|
+
body: Point[]
|
|
55
|
+
/**
|
|
56
|
+
* How many times it has redrawn itself.
|
|
57
|
+
*
|
|
58
|
+
* The only thing a renderer has to watch. A pufferfish spends most of a
|
|
59
|
+
* sitting at rest and drifting slowly, so it holds a drawing for a long time.
|
|
60
|
+
*/
|
|
61
|
+
cut: number
|
|
62
|
+
/** 0 is as far off as this water goes, 1 is up against the glass. */
|
|
63
|
+
readonly depth: number
|
|
64
|
+
/** Where the eye sits, in px. */
|
|
65
|
+
eye: Point
|
|
66
|
+
/** Which way it is pointed, once and for life: 1 rightward, -1 left. */
|
|
67
|
+
readonly facing: number
|
|
68
|
+
/** How wide it is across at rest, in px. */
|
|
69
|
+
readonly size: number
|
|
70
|
+
/** The spines, each a triangle standing off the body. Empty at rest. */
|
|
71
|
+
spines: Point[][]
|
|
72
|
+
/** How swollen it is, 0 a fish and 1 a ball with spines out. */
|
|
73
|
+
swell: number
|
|
74
|
+
x: number
|
|
75
|
+
y: number
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface Puffers {
|
|
79
|
+
readonly puffers: readonly Puffer[]
|
|
80
|
+
resize(width: number, height: number, floor: (x: number, depth?: number) => number): void
|
|
81
|
+
step(seconds: number): void
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface PuffersOptions {
|
|
85
|
+
/**
|
|
86
|
+
* What else is in the water, asked for once a step.
|
|
87
|
+
*
|
|
88
|
+
* Left out, nothing is, and a pufferfish with nothing to argue with is a
|
|
89
|
+
* slightly lumpy fish sculling about, which is what one is.
|
|
90
|
+
*/
|
|
91
|
+
about?: () => readonly Presence[]
|
|
92
|
+
count?: number
|
|
93
|
+
/** The ground at x, at a given distance; `seabed.floorAt`. */
|
|
94
|
+
floor: (x: number, depth?: number) => number
|
|
95
|
+
height: number
|
|
96
|
+
seed: number
|
|
97
|
+
/** How far it may move from its last drawing before it recuts, in px. */
|
|
98
|
+
tolerance?: number
|
|
99
|
+
width: number
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* How long one is, in metres.
|
|
104
|
+
*
|
|
105
|
+
* A reef pufferfish, which is the ordinary one and is a hand's length. The
|
|
106
|
+
* giants are a different animal and a different picture.
|
|
107
|
+
*/
|
|
108
|
+
const LENGTH = 0.3
|
|
109
|
+
|
|
110
|
+
/** How much bigger or smaller than that one of them is. */
|
|
111
|
+
const ODD = 0.22
|
|
112
|
+
|
|
113
|
+
/** How much of its drawn size is its distance. The floor's own share. */
|
|
114
|
+
const DEPTH_SIZE = 0.55
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* How far back they go, and how far off the sand they hang.
|
|
118
|
+
*
|
|
119
|
+
* Near the bottom without being on it. A pufferfish works the floor and the
|
|
120
|
+
* lower reef, and one drawn out in the open water is a balloon somebody let go
|
|
121
|
+
* of. The height off the sand is in its own body widths, so a small one keeps
|
|
122
|
+
* closer to the ground than a large one, which is what they do.
|
|
123
|
+
*/
|
|
124
|
+
const DEPTH_FAR = 0.3
|
|
125
|
+
|
|
126
|
+
const DEPTH_NEAR = 0.95
|
|
127
|
+
|
|
128
|
+
const OVER_LEAST = 0.6
|
|
129
|
+
|
|
130
|
+
const OVER_SPAN = 3.4
|
|
131
|
+
|
|
132
|
+
/** How fast it sculls, in its own widths a second, and how far off that one
|
|
133
|
+
* of them is. Slow. Two fins the size of a thumbnail on a body like a fist. */
|
|
134
|
+
const SCULL = 0.42
|
|
135
|
+
|
|
136
|
+
const DAWDLE = 0.45
|
|
137
|
+
|
|
138
|
+
/** How long it holds a heading, in seconds. It has nowhere to be. */
|
|
139
|
+
const HOLD_LEAST = 4
|
|
140
|
+
|
|
141
|
+
const HOLD_SPAN = 11
|
|
142
|
+
|
|
143
|
+
/** How far past the side it goes before it comes back on the other. */
|
|
144
|
+
const MARGIN = 1.5
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* How much bigger than a pufferfish a thing has to be before it is minded, and
|
|
148
|
+
* how far off it is minded from as a share of its own size.
|
|
149
|
+
*
|
|
150
|
+
* `walkers.ts`, `nemos.ts` and `eels.ts` weigh the water by these. The rule is
|
|
151
|
+
* shared and the answer is not, which is the point of having four animals read
|
|
152
|
+
* one register.
|
|
153
|
+
*/
|
|
154
|
+
const MINDS = 1.7
|
|
155
|
+
|
|
156
|
+
const NOTICE = 2.6
|
|
157
|
+
|
|
158
|
+
/** How much of an alarm something with no menace about it can still cause. */
|
|
159
|
+
const BULK = 0.25
|
|
160
|
+
|
|
161
|
+
/** How little a thing has to worry it before it is not worth the water. */
|
|
162
|
+
const MINDS_AT = 0.16
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* How fast it swells and how fast it settles, in shares of full a second.
|
|
166
|
+
*
|
|
167
|
+
* Far apart, and the far apart is the animal. Swallowing enough water to double
|
|
168
|
+
* is a second's work and getting rid of it again is not, so what a pufferfish
|
|
169
|
+
* looks like for a while after something went past is a pufferfish that has not
|
|
170
|
+
* finished being frightened.
|
|
171
|
+
*/
|
|
172
|
+
const SWELL = 1.6
|
|
173
|
+
|
|
174
|
+
const SETTLE = 0.1
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* How much wider it gets, and how much rounder.
|
|
178
|
+
*
|
|
179
|
+
* The width is the smaller half of it. What says inflated is that the animal
|
|
180
|
+
* has stopped being a fish shape: a deflated one is a long oval and a swollen
|
|
181
|
+
* one is a circle, and the eye reads the roundness before it reads the size.
|
|
182
|
+
*/
|
|
183
|
+
const GROWTH = 0.55
|
|
184
|
+
|
|
185
|
+
/** How long a body is against its width, at rest and full. */
|
|
186
|
+
const LONG_REST = 1.5
|
|
187
|
+
|
|
188
|
+
const LONG_FULL = 1.06
|
|
189
|
+
|
|
190
|
+
/** How finely the outline is cut. Enough that a circle has no corners in it. */
|
|
191
|
+
const RIM = 30
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The tail, as a share of the body's own width: how long at rest, how much of
|
|
195
|
+
* that survives being swollen, and how far it fans.
|
|
196
|
+
*
|
|
197
|
+
* Nearly none of it survives. A tail on a sphere is a flap, and the flap is
|
|
198
|
+
* what makes the sphere read as an animal rather than as a mine.
|
|
199
|
+
*/
|
|
200
|
+
const TAIL = 0.26
|
|
201
|
+
|
|
202
|
+
const TAIL_KEPT = 0.34
|
|
203
|
+
|
|
204
|
+
const TAIL_FAN = 0.5
|
|
205
|
+
|
|
206
|
+
/** How much of the rim the tail joins across, either side of the back. */
|
|
207
|
+
const TAIL_ROOT = 0.05
|
|
208
|
+
|
|
209
|
+
/** How fast the tail works, and how far it sweeps, in radians. */
|
|
210
|
+
const BEAT_SLOWEST = 0.5
|
|
211
|
+
|
|
212
|
+
const BEAT_FASTEST = 0.9
|
|
213
|
+
|
|
214
|
+
const SWEEP = 0.42
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* How many spines it carries, how long one is against the body, and how much
|
|
218
|
+
* of the swell has to happen before any of them show.
|
|
219
|
+
*
|
|
220
|
+
* They lie flat until the skin is stretched, which is a fact about the animal
|
|
221
|
+
* and is also the thing that makes the drawing work: spines out on a resting
|
|
222
|
+
* fish would be a hedgehog.
|
|
223
|
+
*/
|
|
224
|
+
const SPINES = 16
|
|
225
|
+
|
|
226
|
+
const SPINE_LONG = 0.19
|
|
227
|
+
|
|
228
|
+
const SPINE_FROM = 0.25
|
|
229
|
+
|
|
230
|
+
/** How much of the gap between two spines the base of one takes. */
|
|
231
|
+
const SPINE_SEAT = 0.3
|
|
232
|
+
|
|
233
|
+
/** Where the eye sits along the body and how far off the middle, and how big
|
|
234
|
+
* it is. A pufferfish is mostly eye, which is why it reads as a face. */
|
|
235
|
+
const EYE_ALONG = 0.62
|
|
236
|
+
|
|
237
|
+
const EYE_UP = 0.22
|
|
238
|
+
|
|
239
|
+
/** How far it drifts before it is worth redrawing, in px, by default. */
|
|
240
|
+
const TOLERANCE = 0.3
|
|
241
|
+
|
|
242
|
+
const MIN_SPAN = 1
|
|
243
|
+
|
|
244
|
+
/** What it was when it was last drawn. */
|
|
245
|
+
interface Drawn {
|
|
246
|
+
swell: number
|
|
247
|
+
wag: number
|
|
248
|
+
x: number
|
|
249
|
+
y: number
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Everything about one that is not in the drawing. */
|
|
253
|
+
interface Habit {
|
|
254
|
+
/** How fast the tail works. */
|
|
255
|
+
readonly beat: number
|
|
256
|
+
/** Where in that beat it is. */
|
|
257
|
+
clock: number
|
|
258
|
+
drawn: Drawn | null
|
|
259
|
+
/** Radians. Where it is going, which is nowhere in particular. */
|
|
260
|
+
heading: number
|
|
261
|
+
/** Seconds left on the heading it is holding. */
|
|
262
|
+
hold: number
|
|
263
|
+
/** How high off the sand it hangs, in its own widths. */
|
|
264
|
+
readonly over: number
|
|
265
|
+
/** How fast it sculls, as a share of the ordinary. */
|
|
266
|
+
readonly pace: number
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function createPuffers(options: PuffersOptions): Puffers {
|
|
270
|
+
const random = makeRandom(options.seed ^ 0x71c3)
|
|
271
|
+
|
|
272
|
+
let width = Math.max(MIN_SPAN, options.width)
|
|
273
|
+
let height = Math.max(MIN_SPAN, options.height)
|
|
274
|
+
let floor = options.floor
|
|
275
|
+
|
|
276
|
+
const tolerance = Math.max(0, options.tolerance ?? TOLERANCE)
|
|
277
|
+
|
|
278
|
+
const puffers: Puffer[] = []
|
|
279
|
+
const habits: Habit[] = []
|
|
280
|
+
|
|
281
|
+
function fill(): void {
|
|
282
|
+
puffers.length = 0
|
|
283
|
+
habits.length = 0
|
|
284
|
+
|
|
285
|
+
const many = Math.max(0, Math.floor(options.count ?? 0))
|
|
286
|
+
for (let made = 0; made < many; made++) {
|
|
287
|
+
const depth = DEPTH_FAR + random() * (DEPTH_NEAR - DEPTH_FAR)
|
|
288
|
+
const across = drawnAt(LENGTH, BED_OFF) * width
|
|
289
|
+
const size = across * (1 - DEPTH_SIZE + DEPTH_SIZE * depth) * (1 + (random() - 0.5) * 2 * ODD)
|
|
290
|
+
const x = random() * width
|
|
291
|
+
|
|
292
|
+
puffers.push({
|
|
293
|
+
body: [],
|
|
294
|
+
cut: 0,
|
|
295
|
+
depth,
|
|
296
|
+
eye: { x, y: 0 },
|
|
297
|
+
facing: random() < 0.5 ? -1 : 1,
|
|
298
|
+
size,
|
|
299
|
+
spines: [],
|
|
300
|
+
swell: 0,
|
|
301
|
+
x,
|
|
302
|
+
y: 0,
|
|
303
|
+
})
|
|
304
|
+
habits.push({
|
|
305
|
+
beat: BEAT_SLOWEST + random() * (BEAT_FASTEST - BEAT_SLOWEST),
|
|
306
|
+
clock: random() * Math.PI * 2,
|
|
307
|
+
drawn: null,
|
|
308
|
+
heading: 0,
|
|
309
|
+
hold: random() * HOLD_SPAN,
|
|
310
|
+
over: OVER_LEAST + random() * OVER_SPAN,
|
|
311
|
+
pace: 1 + (random() - 0.5) * 2 * DAWDLE,
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (let at = 0; at < puffers.length; at++) sit(at)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Put it where it hangs over the sand it is above. */
|
|
319
|
+
function sit(at: number): void {
|
|
320
|
+
const one = puffers[at]
|
|
321
|
+
const habit = habits[at]
|
|
322
|
+
if (!one || !habit) return
|
|
323
|
+
|
|
324
|
+
one.y = Math.min(height, floor(one.x, one.depth) - one.size * habit.over)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* How much the water worries it, right now.
|
|
329
|
+
*
|
|
330
|
+
* The same rule the rest of the sea reads, and a different answer. What comes
|
|
331
|
+
* out of this is not where to go: it is how much water to swallow.
|
|
332
|
+
*/
|
|
333
|
+
function minded(one: Puffer, water: readonly Presence[]): number {
|
|
334
|
+
let worst = 0
|
|
335
|
+
|
|
336
|
+
for (const thing of water) {
|
|
337
|
+
if (thing.size < one.size * MINDS) continue
|
|
338
|
+
|
|
339
|
+
const reach = thing.size * NOTICE
|
|
340
|
+
const away = Math.hypot(one.x - thing.x, one.y - thing.y)
|
|
341
|
+
if (away >= reach) continue
|
|
342
|
+
|
|
343
|
+
const near = (1 - away / reach) * abreast(one.depth, thing.depth)
|
|
344
|
+
worst = Math.max(worst, near * (BULK + (thing.menace ?? 0) * (1 - BULK)))
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return worst < MINDS_AT ? 0 : worst
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The outline, from an oval at rest to a circle full.
|
|
352
|
+
*
|
|
353
|
+
* One shape rather than two blended, because two drawings crossfaded is what
|
|
354
|
+
* a sprite sheet does and it shows: the halfway frame of a crossfade is two
|
|
355
|
+
* animals at once. What changes here is how long the body is against how wide
|
|
356
|
+
* it is, and every point on the outline is on the one animal the whole way
|
|
357
|
+
* through.
|
|
358
|
+
*/
|
|
359
|
+
function cut(at: number): void {
|
|
360
|
+
const one = puffers[at]
|
|
361
|
+
const habit = habits[at]
|
|
362
|
+
if (!one || !habit) return
|
|
363
|
+
|
|
364
|
+
const wide = (one.size / 2) * (1 + one.swell * GROWTH)
|
|
365
|
+
const long = wide * (LONG_REST + (LONG_FULL - LONG_REST) * one.swell)
|
|
366
|
+
|
|
367
|
+
const body: Point[] = []
|
|
368
|
+
for (let step = 0; step < RIM; step++) {
|
|
369
|
+
const turn = (step / RIM) * Math.PI * 2
|
|
370
|
+
body.push({
|
|
371
|
+
x: one.x + Math.cos(turn) * long * one.facing,
|
|
372
|
+
y: one.y + Math.sin(turn) * wide,
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// The tail, hung off the back and swept by its own clock. It is drawn as
|
|
377
|
+
// part of the outline rather than beside it, so a swollen fish has one
|
|
378
|
+
// silhouette instead of a ball with a leaf behind it.
|
|
379
|
+
const wag = Math.sin(habit.clock) * SWEEP
|
|
380
|
+
const stub = one.size * TAIL * (TAIL_KEPT + (1 - TAIL_KEPT) * (1 - one.swell))
|
|
381
|
+
const from = { x: one.x - long * one.facing, y: one.y }
|
|
382
|
+
const tip = {
|
|
383
|
+
x: from.x - Math.cos(wag) * stub * one.facing,
|
|
384
|
+
y: from.y + Math.sin(wag) * stub,
|
|
385
|
+
}
|
|
386
|
+
const fan = stub * TAIL_FAN
|
|
387
|
+
|
|
388
|
+
// Spliced at the back of the rim rather than at the seam, and across a few
|
|
389
|
+
// points of it rather than between two. The seam is where the outline
|
|
390
|
+
// wraps, which is the front of the animal, and a tail let in there is a
|
|
391
|
+
// wedge driven through the middle of the fish; let in between two adjacent
|
|
392
|
+
// points it is a wedge hung off one, which is a leaf on a string. What a
|
|
393
|
+
// tail actually joins is a stretch of the back.
|
|
394
|
+
const back = Math.round(RIM / 2)
|
|
395
|
+
const root = Math.max(1, Math.round(RIM * TAIL_ROOT))
|
|
396
|
+
one.body = [
|
|
397
|
+
...body.slice(0, back - root),
|
|
398
|
+
{ x: tip.x, y: tip.y + fan },
|
|
399
|
+
{ x: tip.x, y: tip.y - fan },
|
|
400
|
+
...body.slice(back + root),
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
one.eye = {
|
|
404
|
+
x: one.x + long * EYE_ALONG * one.facing,
|
|
405
|
+
y: one.y - wide * EYE_UP,
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Triangles rather than lines, because a spine has a point on it and a
|
|
409
|
+
// stroke with a round cap has a thumb. Each one sits on a chord of the rim
|
|
410
|
+
// and leaves from the middle of it, so the base is the width the body can
|
|
411
|
+
// actually spare and the taper is the rim's own curvature.
|
|
412
|
+
one.spines = []
|
|
413
|
+
if (one.swell > SPINE_FROM) {
|
|
414
|
+
const out = ((one.swell - SPINE_FROM) / (1 - SPINE_FROM)) * one.size * SPINE_LONG
|
|
415
|
+
const seat = (Math.PI * 2 * SPINE_SEAT) / SPINES
|
|
416
|
+
for (let step = 0; step < SPINES; step++) {
|
|
417
|
+
const turn = (step / SPINES) * Math.PI * 2
|
|
418
|
+
const on = (off: number) => ({
|
|
419
|
+
x: one.x + Math.cos(turn + off) * long * one.facing,
|
|
420
|
+
y: one.y + Math.sin(turn + off) * wide,
|
|
421
|
+
})
|
|
422
|
+
one.spines.push([
|
|
423
|
+
on(-seat),
|
|
424
|
+
{
|
|
425
|
+
x: one.x + Math.cos(turn) * (long + out) * one.facing,
|
|
426
|
+
y: one.y + Math.sin(turn) * (wide + out),
|
|
427
|
+
},
|
|
428
|
+
on(seat),
|
|
429
|
+
])
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
habit.drawn = { swell: one.swell, wag, x: one.x, y: one.y }
|
|
434
|
+
one.cut++
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** How far the drawing has drifted from what is on screen. */
|
|
438
|
+
function stir(at: number): number {
|
|
439
|
+
const one = puffers[at]
|
|
440
|
+
const habit = habits[at]
|
|
441
|
+
if (!one || !habit || !habit.drawn) return Number.POSITIVE_INFINITY
|
|
442
|
+
|
|
443
|
+
const was = habit.drawn
|
|
444
|
+
const wag = Math.sin(habit.clock) * SWEEP
|
|
445
|
+
return (
|
|
446
|
+
Math.hypot(one.x - was.x, one.y - was.y) +
|
|
447
|
+
Math.abs(one.swell - was.swell) * one.size +
|
|
448
|
+
Math.abs(wag - was.wag) * one.size * TAIL
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
fill()
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
puffers,
|
|
456
|
+
|
|
457
|
+
resize(next: number, tall: number, ground: (x: number, depth?: number) => number): void {
|
|
458
|
+
width = Math.max(MIN_SPAN, next)
|
|
459
|
+
height = Math.max(MIN_SPAN, tall)
|
|
460
|
+
floor = ground
|
|
461
|
+
fill()
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
step(seconds: number): void {
|
|
465
|
+
const water = options.about?.() ?? []
|
|
466
|
+
|
|
467
|
+
for (let at = 0; at < puffers.length; at++) {
|
|
468
|
+
const one = puffers[at]
|
|
469
|
+
const habit = habits[at]
|
|
470
|
+
if (!one || !habit) continue
|
|
471
|
+
|
|
472
|
+
const worries = minded(one, water)
|
|
473
|
+
|
|
474
|
+
// The whole animal, in one line. It swells towards what the water
|
|
475
|
+
// deserves and settles back at a fraction of the rate, and it never
|
|
476
|
+
// steers on either: where it was going is where it is still going.
|
|
477
|
+
const rate = (worries > one.swell ? SWELL : SETTLE) * seconds
|
|
478
|
+
one.swell += Math.max(-rate, Math.min(rate, worries - one.swell))
|
|
479
|
+
|
|
480
|
+
habit.clock += seconds * habit.beat * Math.PI * 2
|
|
481
|
+
|
|
482
|
+
habit.hold -= seconds
|
|
483
|
+
if (habit.hold <= 0) {
|
|
484
|
+
habit.hold = HOLD_LEAST + random() * HOLD_SPAN
|
|
485
|
+
// A shallow pitch either side of level and never a turn, the reason
|
|
486
|
+
// `shoal.ts` gives: a drawn animal faces one way, and one that
|
|
487
|
+
// mirrored would turn inside out between two frames.
|
|
488
|
+
habit.heading = (random() - 0.5) * 0.6
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Slower the more swollen it is, because a ball is a worse shape to
|
|
492
|
+
// push through water than a fish is, and a pufferfish that kept its
|
|
493
|
+
// speed while doubling would be a pufferfish nothing had happened to.
|
|
494
|
+
const pace = SCULL * habit.pace * one.size * (1 - one.swell * 0.7)
|
|
495
|
+
one.x += Math.cos(habit.heading) * pace * seconds * one.facing
|
|
496
|
+
one.y += Math.sin(habit.heading) * pace * seconds
|
|
497
|
+
|
|
498
|
+
const edge = one.size * MARGIN
|
|
499
|
+
if (one.x < -edge) one.x += width + edge * 2
|
|
500
|
+
if (one.x > width + edge) one.x -= width + edge * 2
|
|
501
|
+
|
|
502
|
+
const over = floor(one.x, one.depth) - one.size * habit.over
|
|
503
|
+
one.y += (over - one.y) * Math.min(1, seconds)
|
|
504
|
+
|
|
505
|
+
if (stir(at) < tolerance) continue
|
|
506
|
+
cut(at)
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
}
|
|
510
|
+
}
|