@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/swarm.ts
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A flock of thousands, once in a while.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of the DOM the way the rest of the ornament is. It is not a shoal
|
|
5
|
+
* and it is not a second `shoal.ts`: the fish in that file are steered one at a
|
|
6
|
+
* time and each of them has an opinion, which is what makes a dozen of them
|
|
7
|
+
* read as animals. Do that to a thousand and two things happen, both bad. It
|
|
8
|
+
* costs a thousand times as much, and it looks wrong, because a bait ball is
|
|
9
|
+
* not a thousand fish deciding. It is one thing with a thousand fish in it.
|
|
10
|
+
*
|
|
11
|
+
* ## What the eye is actually reading
|
|
12
|
+
*
|
|
13
|
+
* Not the fish. At the size a fish in a flock is drawn, nobody can follow one,
|
|
14
|
+
* and anybody watching one of these is reading three things instead:
|
|
15
|
+
*
|
|
16
|
+
* * **The shape.** A ball, a ceiling, a ribbon. It is the shape that says how
|
|
17
|
+
* many there are, and it is the shape that turns.
|
|
18
|
+
* * **The grain.** Fish at slightly different distances, so the mass has a
|
|
19
|
+
* front and a back rather than being a stipple. This is most of why marks
|
|
20
|
+
* that could be counted read as thousands: the eye gives up counting the
|
|
21
|
+
* moment they stop lying on one plane.
|
|
22
|
+
* * **What happens when something goes through it.** A flock opens and closes
|
|
23
|
+
* again. That is the whole reason a flock is worth having in a scene that
|
|
24
|
+
* already has fish in it.
|
|
25
|
+
*
|
|
26
|
+
* So a fish here has no opinions at all. It has a place in the flock's own
|
|
27
|
+
* frame, it holds it, and everything it appears to decide is the flock turning
|
|
28
|
+
* or something pushing it aside. `HOMED` is what brings it back.
|
|
29
|
+
*
|
|
30
|
+
* ## How many marks the mass is made of
|
|
31
|
+
*
|
|
32
|
+
* As many as it takes to read as packed, which is more than it used to be. The
|
|
33
|
+
* count was a few hundred while the reason for it was the frame: a renderer
|
|
34
|
+
* draws one mark per fish, and a desktop background does not get to spend a
|
|
35
|
+
* frame on scenery. Timed in the wgpu renderer, three thousand specks cost
|
|
36
|
+
* about a millisecond of a twenty-three millisecond frame, so the frame was
|
|
37
|
+
* never the thing holding the number down, and a few hundred marks spread over
|
|
38
|
+
* a mass tens of metres across is not a flock. It is a sprinkle with a shape.
|
|
39
|
+
*
|
|
40
|
+
* So the count follows from the mass instead. Each shape says how tightly it
|
|
41
|
+
* packs and how big it is, and how many fish are in it falls out of the two,
|
|
42
|
+
* which is why a ceiling is thousands of marks and a ball is a fraction of
|
|
43
|
+
* that while being the denser of the two. The pool a renderer hands over is
|
|
44
|
+
* still a ceiling on all of it.
|
|
45
|
+
*
|
|
46
|
+
* ## Why a speck does not hold its drawing
|
|
47
|
+
*
|
|
48
|
+
* Everything else in this water that costs a renderer real money holds its last
|
|
49
|
+
* drawing until it has moved far enough to be worth another one, and the flock
|
|
50
|
+
* is the layer where that buys nothing. Nearly every speck travels past a
|
|
51
|
+
* quarter of a pixel between frames, and most of them past a whole one, and a
|
|
52
|
+
* whole pixel on a mark a few px long is a mark the eye can watch stepping. A
|
|
53
|
+
* flock is the one thing down here that never stops, which is the whole of what
|
|
54
|
+
* it is for.
|
|
55
|
+
*
|
|
56
|
+
* So what a speck wants is not fewer frames of itself. It is fewer of itself in
|
|
57
|
+
* the murk, where nobody could make one out anyway, and that is a different
|
|
58
|
+
* change from this one.
|
|
59
|
+
* `docs/decisions/0027-let-an-element-decide-when-its-drawing-is-stale.md` is
|
|
60
|
+
* the contract this is the exception to, and the exception is written here so
|
|
61
|
+
* that reading it costs less than measuring it again.
|
|
62
|
+
*
|
|
63
|
+
* ## Rarer than anything that swims and commoner than a boat
|
|
64
|
+
*
|
|
65
|
+
* It sits between the two clocks the ornament already keeps. `visitors.ts` is
|
|
66
|
+
* minutes and `passers.ts` is days, and this is a long stretch of minutes: often
|
|
67
|
+
* enough that somebody who keeps a desktop uncovered will meet one, seldom
|
|
68
|
+
* enough that it is the thing they mention.
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
import type { Biome } from "./biome.ts"
|
|
72
|
+
import { makeNoise2, makeRandom } from "./perlin.ts"
|
|
73
|
+
import type { Startle } from "./shoal.ts"
|
|
74
|
+
|
|
75
|
+
export type SwarmShape = "ball" | "ceiling" | "ribbon"
|
|
76
|
+
|
|
77
|
+
/** One fish of a flock, which is a mark rather than an animal. */
|
|
78
|
+
export interface Speck {
|
|
79
|
+
/** 0 is as far off as this water goes, 1 is up against the glass. */
|
|
80
|
+
depth: number
|
|
81
|
+
/** How long it is drawn, in px, with the distance already in it. */
|
|
82
|
+
size: number
|
|
83
|
+
/** Which way it is pointing, in radians. It is going that way. */
|
|
84
|
+
tilt: number
|
|
85
|
+
x: number
|
|
86
|
+
y: number
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface Swarm {
|
|
90
|
+
resize(width: number, height: number, count?: number): void
|
|
91
|
+
/** What the flock is at the moment, for a renderer that wants to know. */
|
|
92
|
+
readonly shape: SwarmShape
|
|
93
|
+
readonly specks: readonly Speck[]
|
|
94
|
+
/**
|
|
95
|
+
* Advance by `seconds`. `startle` is whatever is going through the flock,
|
|
96
|
+
* which is the only thing here that is not the flock's own business.
|
|
97
|
+
*/
|
|
98
|
+
step(seconds: number, startle?: Startle | null): void
|
|
99
|
+
/** How loud the flock is now, 0 to 1. Nothing at all while there is none. */
|
|
100
|
+
readonly weight: number
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface SwarmOptions {
|
|
104
|
+
/** How many fish. A renderer's ceiling, not a count of anything real. */
|
|
105
|
+
count: number
|
|
106
|
+
/** Come round one after another with no rest. For previews and for tests. */
|
|
107
|
+
eager?: boolean
|
|
108
|
+
height: number
|
|
109
|
+
seed: number
|
|
110
|
+
shapes?: readonly SwarmShape[]
|
|
111
|
+
/**
|
|
112
|
+
* The water this is in, so that whatever lives down there can see it.
|
|
113
|
+
*
|
|
114
|
+
* A flock enters as one thing rather than as several hundred, which is how it
|
|
115
|
+
* is seen. What passes over a crab is a shadow the width of the whole ball;
|
|
116
|
+
* offered a fish at a time it would be hundreds of animals none of them
|
|
117
|
+
* bigger than the crab itself, and nothing on the floor would look up.
|
|
118
|
+
*/
|
|
119
|
+
water?: Biome
|
|
120
|
+
width: number
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* How each shape is laid out, and where it sits in the column.
|
|
125
|
+
*
|
|
126
|
+
* `wide` and `tall` are the flock's own radii as shares of the box's width, and
|
|
127
|
+
* `thick` is the same measure into the screen, which is what gives the mass a
|
|
128
|
+
* front and a back. A ball is nearly as deep as it is wide because it is a
|
|
129
|
+
* ball; a ceiling is broad, shallow and deep, because what it is is the
|
|
130
|
+
* underside of a shoal spread out over you.
|
|
131
|
+
*
|
|
132
|
+
* The ceiling is the one worth the trouble. A ball is a shape in the water with
|
|
133
|
+
* you and a ceiling is a shape you are under, which is the difference between
|
|
134
|
+
* watching a flock and being in one. It seats high and reaches past both edges
|
|
135
|
+
* of the picture on purpose: a ceiling with ends is a cloud.
|
|
136
|
+
*/
|
|
137
|
+
interface Layout {
|
|
138
|
+
/**
|
|
139
|
+
* How wide the mass really is, in metres.
|
|
140
|
+
*
|
|
141
|
+
* What every fish in it is drawn against, and the only place this scene says
|
|
142
|
+
* how big a flock is out loud. A ball is a room across, a ribbon is a street,
|
|
143
|
+
* and a ceiling is the far side of what anybody can see.
|
|
144
|
+
*/
|
|
145
|
+
readonly across: number
|
|
146
|
+
/** How fast the whole mass turns over, in turns a second. */
|
|
147
|
+
readonly churn: number
|
|
148
|
+
/**
|
|
149
|
+
* How many fish stand over a square metre of the face this shape shows.
|
|
150
|
+
*
|
|
151
|
+
* A share of the pool was the wrong statement, because it made the mark
|
|
152
|
+
* count independent of `across`: widen a ceiling from a street to a horizon
|
|
153
|
+
* and the same marks were asked to cover it, so the mass thinned and nothing
|
|
154
|
+
* said so. This is the same fact `census.ts` keeps for the sea, told about
|
|
155
|
+
* one animal in one shape.
|
|
156
|
+
*
|
|
157
|
+
* It is a per-shape number rather than one number in metres, because how
|
|
158
|
+
* tightly fish pack is what separates these shapes. A ball is a knot they
|
|
159
|
+
* have tied against something hunting them and it is the densest thing in
|
|
160
|
+
* this water; a ceiling is the same animals with nothing after them, spread
|
|
161
|
+
* out over you, and it is loose. Packing a ceiling like a ball would be a
|
|
162
|
+
* ball the size of the sky.
|
|
163
|
+
*/
|
|
164
|
+
readonly packed: number
|
|
165
|
+
/** Where in the column it sits, as shares of the height. */
|
|
166
|
+
readonly seatLeast: number
|
|
167
|
+
readonly seatSpan: number
|
|
168
|
+
/** How long it takes to cross, at least and up to that again. */
|
|
169
|
+
readonly takeLeast: number
|
|
170
|
+
readonly takeSpan: number
|
|
171
|
+
readonly tall: number
|
|
172
|
+
readonly thick: number
|
|
173
|
+
readonly wide: number
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const LAYOUTS: Readonly<Record<SwarmShape, Layout>> = {
|
|
177
|
+
ball: {
|
|
178
|
+
across: 8,
|
|
179
|
+
churn: 0.05,
|
|
180
|
+
packed: 20,
|
|
181
|
+
seatLeast: 0.18,
|
|
182
|
+
seatSpan: 0.34,
|
|
183
|
+
takeLeast: 70,
|
|
184
|
+
takeSpan: 40,
|
|
185
|
+
tall: 0.11,
|
|
186
|
+
thick: 0.11,
|
|
187
|
+
wide: 0.12,
|
|
188
|
+
},
|
|
189
|
+
ceiling: {
|
|
190
|
+
across: 45,
|
|
191
|
+
churn: 0.015,
|
|
192
|
+
packed: 11,
|
|
193
|
+
seatLeast: 0.04,
|
|
194
|
+
seatSpan: 0.12,
|
|
195
|
+
takeLeast: 95,
|
|
196
|
+
takeSpan: 50,
|
|
197
|
+
tall: 0.1,
|
|
198
|
+
thick: 0.3,
|
|
199
|
+
wide: 0.75,
|
|
200
|
+
},
|
|
201
|
+
ribbon: {
|
|
202
|
+
across: 22,
|
|
203
|
+
churn: 0.03,
|
|
204
|
+
packed: 13,
|
|
205
|
+
seatLeast: 0.2,
|
|
206
|
+
seatSpan: 0.36,
|
|
207
|
+
takeLeast: 60,
|
|
208
|
+
takeSpan: 34,
|
|
209
|
+
tall: 0.05,
|
|
210
|
+
thick: 0.1,
|
|
211
|
+
wide: 0.36,
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** How far past each side the flock starts and finishes, as shares of width. */
|
|
216
|
+
const OFFING = 0.55
|
|
217
|
+
|
|
218
|
+
/** How much of a crossing is spent fading in, and the same again fading out. */
|
|
219
|
+
const FADE = 0.12
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Watched seconds between one flock leaving and the next arriving.
|
|
223
|
+
*
|
|
224
|
+
* Longer than a visitor's rest by a good deal, and this is the number that
|
|
225
|
+
* decides whether the thing is an event or a feature of the water. Anything
|
|
226
|
+
* that comes round inside a sitting stops being thousands of fish and becomes
|
|
227
|
+
* the fish animation.
|
|
228
|
+
*/
|
|
229
|
+
const REST_LEAST = 420
|
|
230
|
+
|
|
231
|
+
const REST_SPAN = 620
|
|
232
|
+
|
|
233
|
+
/** How much of that rest the water waits before the first flock of a sitting. */
|
|
234
|
+
const OPENING = 0.4
|
|
235
|
+
|
|
236
|
+
/** How far back the flock as a whole swims, and how much of that is its depth. */
|
|
237
|
+
const DEEP_LEAST = 0.3
|
|
238
|
+
|
|
239
|
+
const DEEP_SPAN = 0.5
|
|
240
|
+
|
|
241
|
+
const DEPTH_SIZE = 0.7
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* How long one fish is, in metres, and how much longer or shorter than that
|
|
245
|
+
* any one of them may be.
|
|
246
|
+
*
|
|
247
|
+
* An anchovy, which is what a bait ball is made of. It is the one animal in
|
|
248
|
+
* this ornament that is not sized by `sizes.ts`, and the reason is worth
|
|
249
|
+
* writing down: everything else is a single body somewhere in the water, and a
|
|
250
|
+
* flock is a body made of bodies. The root that keeps a shark from swamping a
|
|
251
|
+
* reef fish would put a fish the size of your hand in a ball the size of a bus,
|
|
252
|
+
* which is a ball of about nine fish.
|
|
253
|
+
*
|
|
254
|
+
* So the flock is the yardstick instead. A fish is drawn at its own length
|
|
255
|
+
* against the mass it is in, which is honest for the same reason a clownfish is
|
|
256
|
+
* measured against its anemone in `nemos.ts`: the two are at one distance, so
|
|
257
|
+
* nothing is being compressed between them.
|
|
258
|
+
*/
|
|
259
|
+
const FISH_LONG = 0.11
|
|
260
|
+
|
|
261
|
+
const FISH_ODD = 0.22
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* How far a fish wanders inside the flock, and how fast.
|
|
265
|
+
*
|
|
266
|
+
* Small, and it has to be. A fish that wanders far is a fish somebody can
|
|
267
|
+
* follow, and a mass whose parts can be followed is not a mass. What this is
|
|
268
|
+
* for is that a flock holding perfectly still inside itself reads as a pattern
|
|
269
|
+
* printed on a moving card.
|
|
270
|
+
*/
|
|
271
|
+
const FIDGET = 0.055
|
|
272
|
+
|
|
273
|
+
const FIDGET_RATE = 0.55
|
|
274
|
+
|
|
275
|
+
/** How many features of the fidget field fit across one flock. */
|
|
276
|
+
const FIDGET_CELLS = 2.6
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* How hard a flock is pushed by something coming through it, and how far the
|
|
280
|
+
* push reaches past what pushed it.
|
|
281
|
+
*
|
|
282
|
+
* A flock is the one thing in this water that answers a disturbance visibly,
|
|
283
|
+
* because it is the only thing with enough parts for the answer to have a
|
|
284
|
+
* shape: the hole a shark leaves is a picture of the shark.
|
|
285
|
+
*
|
|
286
|
+
* What it must not be is a rout. A real bait ball harried by something is torn
|
|
287
|
+
* to pieces, and it is a picture about being eaten; this water is not that. So
|
|
288
|
+
* the push is a dent that the mass closes over rather than a hole through it,
|
|
289
|
+
* which is what `HOMED` is doing on the other side of the same sum.
|
|
290
|
+
*/
|
|
291
|
+
const SHOVE = 0.85
|
|
292
|
+
|
|
293
|
+
const SHOVE_REACH = 1
|
|
294
|
+
|
|
295
|
+
/** How fast a pushed fish comes back to its place, per second. */
|
|
296
|
+
const HOMED = 1.2
|
|
297
|
+
|
|
298
|
+
/** How much of a flock has to have arrived before the water can see it. */
|
|
299
|
+
const SEEN = 0.05
|
|
300
|
+
|
|
301
|
+
/** How far a mark may be turned by what it is doing, at most. */
|
|
302
|
+
const TILT_EASE = 3.5
|
|
303
|
+
|
|
304
|
+
const MIN_SPAN = 1
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* How many marks a shape is made of, off its own size and its own packing.
|
|
308
|
+
*
|
|
309
|
+
* The face it shows, in metres: `across` wide, and as tall as `tall` is against
|
|
310
|
+
* `wide`, since the two are shares of one number and their ratio is the shape's
|
|
311
|
+
* own. What is behind that face is not counted, which is why `packed` is a
|
|
312
|
+
* density on a face rather than through a volume: a ceiling is deeper than a
|
|
313
|
+
* ball and would come out the denser of the two, and it is the looser one.
|
|
314
|
+
*/
|
|
315
|
+
function marks(layout: Layout): number {
|
|
316
|
+
return Math.round(layout.packed * layout.across * layout.across * (layout.tall / layout.wide))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The flock of a piece of water.
|
|
321
|
+
*
|
|
322
|
+
* `eager` throws the rest away and brings one round after another, for the
|
|
323
|
+
* preview harness and for tests. Nothing should pass it in earnest.
|
|
324
|
+
*/
|
|
325
|
+
export function createSwarm(options: SwarmOptions): Swarm {
|
|
326
|
+
const random = makeRandom(options.seed ^ 0x5f0c)
|
|
327
|
+
const field = makeNoise2(options.seed ^ 0x1caa)
|
|
328
|
+
const shapes = options.shapes ?? (["ball", "ceiling", "ribbon"] as const)
|
|
329
|
+
const eager = options.eager ?? false
|
|
330
|
+
|
|
331
|
+
let width = Math.max(MIN_SPAN, options.width)
|
|
332
|
+
let height = Math.max(MIN_SPAN, options.height)
|
|
333
|
+
let count = Math.max(0, Math.round(options.count))
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The pool, and the part of it this flock is using.
|
|
337
|
+
*
|
|
338
|
+
* `specks` holds the same objects rather than copies of them, so a renderer
|
|
339
|
+
* reading it and this file moving them are looking at one set of marks.
|
|
340
|
+
*/
|
|
341
|
+
const pool: Speck[] = []
|
|
342
|
+
|
|
343
|
+
const specks: Speck[] = []
|
|
344
|
+
|
|
345
|
+
/** Where each one lives in the flock's own frame, and how it is pushed off it. */
|
|
346
|
+
const homes: {
|
|
347
|
+
/** How long this one is, before the distance is taken out of it. */
|
|
348
|
+
long: number
|
|
349
|
+
pushX: number
|
|
350
|
+
pushY: number
|
|
351
|
+
u: number
|
|
352
|
+
v: number
|
|
353
|
+
w: number
|
|
354
|
+
}[] = []
|
|
355
|
+
|
|
356
|
+
/** What is crossing: its shape, its course, and how far through it is. */
|
|
357
|
+
let shape: SwarmShape = "ball"
|
|
358
|
+
let along = 1
|
|
359
|
+
let deep = 1
|
|
360
|
+
let facing = 1
|
|
361
|
+
let seat = 0
|
|
362
|
+
let spin = 0
|
|
363
|
+
let take = 1
|
|
364
|
+
let weight = 0
|
|
365
|
+
|
|
366
|
+
/** Watched seconds still owed before the next flock arrives. */
|
|
367
|
+
let rest = (REST_LEAST + random() * REST_SPAN) * OPENING
|
|
368
|
+
|
|
369
|
+
/** The clock the fidget field is read against. */
|
|
370
|
+
let drift = 0
|
|
371
|
+
|
|
372
|
+
// The whole flock as one presence, measured off where its fish actually are
|
|
373
|
+
// rather than off where it was aimed. A ball that something has just gone
|
|
374
|
+
// through is wider than the ball that set off, and how wide it looks is the
|
|
375
|
+
// only thing anything reading this cares about.
|
|
376
|
+
options.water?.enter(() => {
|
|
377
|
+
const first = specks[0]
|
|
378
|
+
if (weight <= SEEN || !first) return []
|
|
379
|
+
|
|
380
|
+
let left = first.x
|
|
381
|
+
let right = left
|
|
382
|
+
let top = first.y
|
|
383
|
+
let bottom = top
|
|
384
|
+
let far = 0
|
|
385
|
+
|
|
386
|
+
for (const one of specks) {
|
|
387
|
+
if (one.x < left) left = one.x
|
|
388
|
+
if (one.x > right) right = one.x
|
|
389
|
+
if (one.y < top) top = one.y
|
|
390
|
+
if (one.y > bottom) bottom = one.y
|
|
391
|
+
far += one.depth
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return [
|
|
395
|
+
{
|
|
396
|
+
depth: far / specks.length,
|
|
397
|
+
size: right - left,
|
|
398
|
+
x: (left + right) / 2,
|
|
399
|
+
y: (top + bottom) / 2,
|
|
400
|
+
},
|
|
401
|
+
]
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* A place in the flock, drawn as a point of a solid rather than of a disc.
|
|
406
|
+
*
|
|
407
|
+
* The cube root is what makes it solid. Drawn straight off three rolls the
|
|
408
|
+
* points bunch in the middle and the mass has a hot core with a haze round
|
|
409
|
+
* it, which is a nebula. Taking the radius to the third root spreads them
|
|
410
|
+
* evenly through the volume, so the flock has a skin and the eye can find its
|
|
411
|
+
* edge, which is the thing it is reading the shape off.
|
|
412
|
+
*/
|
|
413
|
+
function place(): { u: number; v: number; w: number } {
|
|
414
|
+
const u = random() * 2 - 1
|
|
415
|
+
const v = random() * 2 - 1
|
|
416
|
+
const w = random() * 2 - 1
|
|
417
|
+
const reach = Math.hypot(u, v, w) || 1
|
|
418
|
+
const full = random() ** (1 / 3)
|
|
419
|
+
|
|
420
|
+
return { u: (u / reach) * full, v: (v / reach) * full, w: (w / reach) * full }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function fill(): void {
|
|
424
|
+
pool.length = 0
|
|
425
|
+
specks.length = 0
|
|
426
|
+
homes.length = 0
|
|
427
|
+
|
|
428
|
+
for (let made = 0; made < count; made++) {
|
|
429
|
+
homes.push({
|
|
430
|
+
long: 1 - FISH_ODD + random() * FISH_ODD * 2,
|
|
431
|
+
pushX: 0,
|
|
432
|
+
pushY: 0,
|
|
433
|
+
...place(),
|
|
434
|
+
})
|
|
435
|
+
pool.push({ depth: 1, size: 1, tilt: 0, x: 0, y: 0 })
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function arrive(): void {
|
|
440
|
+
shape = shapes[Math.floor(random() * shapes.length)] ?? "ball"
|
|
441
|
+
const layout = LAYOUTS[shape]
|
|
442
|
+
|
|
443
|
+
along = 0
|
|
444
|
+
deep = DEEP_LEAST + random() * DEEP_SPAN
|
|
445
|
+
facing = random() < 0.5 ? -1 : 1
|
|
446
|
+
seat = height * (layout.seatLeast + random() * layout.seatSpan)
|
|
447
|
+
spin = random() * Math.PI * 2
|
|
448
|
+
take = layout.takeLeast + random() * layout.takeSpan
|
|
449
|
+
weight = 0
|
|
450
|
+
|
|
451
|
+
for (const home of homes) {
|
|
452
|
+
home.pushX = 0
|
|
453
|
+
home.pushY = 0
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
specks.length = 0
|
|
457
|
+
const using = Math.min(pool.length, marks(layout))
|
|
458
|
+
for (let at = 0; at < using; at++) {
|
|
459
|
+
const one = pool[at]
|
|
460
|
+
if (one) specks.push(one)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Move every fish to where the flock has it now.
|
|
466
|
+
*
|
|
467
|
+
* One pass, and everything a fish is doing is in it: its place in the mass,
|
|
468
|
+
* the mass turning, its own small fidget, and whatever is shoving it. There
|
|
469
|
+
* is nothing here that reads another fish, which is the whole reason a flock
|
|
470
|
+
* of this size is affordable at all.
|
|
471
|
+
*/
|
|
472
|
+
function carry(dt: number, middle: number, startle: Startle | null): void {
|
|
473
|
+
const layout = LAYOUTS[shape]
|
|
474
|
+
const wide = width * layout.wide
|
|
475
|
+
const tall = width * layout.tall
|
|
476
|
+
const thick = layout.thick
|
|
477
|
+
// What one fish's length comes to, drawn: the mass is so many metres
|
|
478
|
+
// across and so many pixels across, and a fish is its own length of that.
|
|
479
|
+
const mark = ((wide * 2) / layout.across) * FISH_LONG
|
|
480
|
+
const cos = Math.cos(spin)
|
|
481
|
+
const sin = Math.sin(spin)
|
|
482
|
+
const fall = HOMED * dt
|
|
483
|
+
|
|
484
|
+
for (let at = 0; at < specks.length; at++) {
|
|
485
|
+
const home = homes[at]
|
|
486
|
+
const one = specks[at]
|
|
487
|
+
if (!home || !one) continue
|
|
488
|
+
|
|
489
|
+
// The mass turns about its own middle, which is what a bait ball does and
|
|
490
|
+
// the one motion that reads at this size. It is the same turn for every
|
|
491
|
+
// fish in it, so the shape is preserved and the parts are not.
|
|
492
|
+
const u = home.u * cos - home.w * sin
|
|
493
|
+
const w = home.u * sin + home.w * cos
|
|
494
|
+
|
|
495
|
+
const wobbleX = field(home.u * FIDGET_CELLS + drift, home.v * FIDGET_CELLS)
|
|
496
|
+
const wobbleY = field(home.v * FIDGET_CELLS, home.u * FIDGET_CELLS - drift)
|
|
497
|
+
|
|
498
|
+
const wantX = middle + (u + wobbleX * FIDGET) * wide
|
|
499
|
+
const wantY = seat + (home.v + wobbleY * FIDGET) * tall
|
|
500
|
+
|
|
501
|
+
if (startle && startle.force > 0) {
|
|
502
|
+
const offX = wantX + home.pushX - startle.x
|
|
503
|
+
const offY = wantY + home.pushY - startle.y
|
|
504
|
+
const gap = Math.hypot(offX, offY)
|
|
505
|
+
const reach = startle.reach * SHOVE_REACH
|
|
506
|
+
|
|
507
|
+
if (gap < reach && gap > 0) {
|
|
508
|
+
const push = ((reach - gap) / reach) * startle.force * SHOVE * dt * reach
|
|
509
|
+
home.pushX += (offX / gap) * push
|
|
510
|
+
home.pushY += (offY / gap) * push
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
home.pushX -= home.pushX * Math.min(1, fall)
|
|
515
|
+
home.pushY -= home.pushY * Math.min(1, fall)
|
|
516
|
+
|
|
517
|
+
const nextX = wantX + home.pushX
|
|
518
|
+
const nextY = wantY + home.pushY
|
|
519
|
+
|
|
520
|
+
// Pointing along whatever it just did, eased, so a fish that is barely
|
|
521
|
+
// moving does not spin. A mark whose angle is read straight off a frame's
|
|
522
|
+
// worth of travel is a mark that flickers.
|
|
523
|
+
const runX = nextX - one.x
|
|
524
|
+
const runY = nextY - one.y
|
|
525
|
+
if (Math.hypot(runX, runY) > 0.01) {
|
|
526
|
+
const want = Math.atan2(runY, runX)
|
|
527
|
+
const turn = Math.atan2(Math.sin(want - one.tilt), Math.cos(want - one.tilt))
|
|
528
|
+
one.tilt += turn * Math.min(1, TILT_EASE * dt)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
one.depth = Math.max(0.05, Math.min(1, deep + w * thick))
|
|
532
|
+
one.size = home.long * mark * (1 - DEPTH_SIZE + DEPTH_SIZE * one.depth)
|
|
533
|
+
one.x = nextX
|
|
534
|
+
one.y = nextY
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
fill()
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
resize(nextWidth, nextHeight, nextCount) {
|
|
542
|
+
width = Math.max(MIN_SPAN, nextWidth)
|
|
543
|
+
height = Math.max(MIN_SPAN, nextHeight)
|
|
544
|
+
if (nextCount != null) count = Math.max(0, Math.round(nextCount))
|
|
545
|
+
// Whatever was crossing is dropped, the way a passer is. A flock that
|
|
546
|
+
// changed size halfway over would be a flock that had been counted.
|
|
547
|
+
along = 1
|
|
548
|
+
weight = 0
|
|
549
|
+
fill()
|
|
550
|
+
},
|
|
551
|
+
|
|
552
|
+
get shape() {
|
|
553
|
+
return shape
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
specks,
|
|
557
|
+
|
|
558
|
+
step(seconds, startle = null) {
|
|
559
|
+
const dt = Math.min(Math.max(seconds, 0), 0.1)
|
|
560
|
+
drift += FIDGET_RATE * dt
|
|
561
|
+
|
|
562
|
+
if (along >= 1) {
|
|
563
|
+
weight = 0
|
|
564
|
+
rest -= dt
|
|
565
|
+
if (eager || rest <= 0) {
|
|
566
|
+
arrive()
|
|
567
|
+
rest = REST_LEAST + random() * REST_SPAN
|
|
568
|
+
}
|
|
569
|
+
if (along >= 1) return
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
along += dt / Math.max(take, 0.001)
|
|
573
|
+
spin += LAYOUTS[shape].churn * Math.PI * 2 * dt
|
|
574
|
+
weight = Math.max(0, Math.min(1, along / FADE, (1 - along) / FADE))
|
|
575
|
+
|
|
576
|
+
const from = -OFFING * width
|
|
577
|
+
const to = width * (1 + OFFING)
|
|
578
|
+
const at = Math.min(1, along)
|
|
579
|
+
|
|
580
|
+
carry(dt, facing > 0 ? from + at * (to - from) : to - at * (to - from), startle)
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
get weight() {
|
|
584
|
+
return weight
|
|
585
|
+
},
|
|
586
|
+
}
|
|
587
|
+
}
|