@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/src/walkers.ts ADDED
@@ -0,0 +1,861 @@
1
+ /**
2
+ * What walks on the bottom: crabs and starfish.
3
+ *
4
+ * The bed grows things and now it has animals living on it, kept free of the
5
+ * DOM the way the rest of the ornament is. A plant is rooted and everything it
6
+ * does is the water doing it; these two are the other case. They are on the
7
+ * ground, they go where they decide to go, and the current has no say in
8
+ * either.
9
+ *
10
+ * ## Why they are not in `flora.ts`
11
+ *
12
+ * A thing that walks is not a thing that is rooted, and the two want opposite
13
+ * machinery. A plant is a strand recut from a field every step and it never
14
+ * moves, so nothing about it accumulates. A walker has a position it carries
15
+ * from one step to the next, which is a different kind of object however
16
+ * similar the drawings come out.
17
+ *
18
+ * ## Why the bottom needed them
19
+ *
20
+ * Everything else down there is scenery. Kelp leans, coral holds still, and a
21
+ * bed of both is a place with nothing living in it, which the eye reads as a
22
+ * diagram of a seabed. One animal going about its business at the bottom of the
23
+ * picture is what makes the rest of it a floor rather than a pattern, and it
24
+ * costs a handful of lines.
25
+ *
26
+ * ## A crab
27
+ *
28
+ * It goes sideways, which is the whole of what says crab before anything else
29
+ * about the drawing does. Most of its day it is not going anywhere: it stands,
30
+ * it works at the sand, and then it crosses a stretch quickly and stops again.
31
+ * That is the shape a small animal on an open floor actually has, and it is
32
+ * also what makes the crossings worth watching.
33
+ *
34
+ * The gait runs off distance rather than off the clock. A leg cycle driven by
35
+ * time carries on swinging while the animal stands still, which reads as a
36
+ * crab running on the spot; driven by how far it has gone, the legs stop when
37
+ * it stops and hurry when it hurries, for free.
38
+ *
39
+ * ## A starfish
40
+ *
41
+ * It creeps. The number is not a compromise: a starfish moves about as fast as
42
+ * a minute hand, so what it has instead of a gait is its arms, which reach and
43
+ * shorten on their own clocks. Over a sitting it will have gone somewhere, and
44
+ * nobody will have seen it go.
45
+ *
46
+ * It is drawn squashed, because it is lying on the ground rather than standing
47
+ * on it. The ground climbs the picture as it goes back, so the sand is seen
48
+ * from a shallow angle above, and a star lying flat on it is a star seen from
49
+ * that angle. Drawn round it would be a starfish stuck to the glass.
50
+ *
51
+ * ## Nobody tells a crab anything
52
+ *
53
+ * There is no conductor down here. The water is the place everything is in and
54
+ * the register of what is in it, and an animal reads that register and decides
55
+ * for itself; nothing is fired at it. So a crab is handed `about`, a way of
56
+ * asking what else is out there, and what it does with the answer is its own
57
+ * business. Hand it nothing and it goes about its day.
58
+ *
59
+ * What it does is freeze. A crab on open sand cannot outrun anything worth
60
+ * running from, so its first answer is to stop being a moving thing, and its
61
+ * second, when whatever it is gets close enough, is to bolt for the edge of the
62
+ * picture. That order matters: a crab that bolted at the first shadow would be
63
+ * a crab that has never met one.
64
+ *
65
+ * The two answers are to different questions, which is why the water reports
66
+ * size and menace separately. Anything big enough and near enough will stop a
67
+ * crab dead, turtle or shark, because a thing that has not seen you is a thing
68
+ * you keep it that way. What decides whether it then runs is whether the thing
69
+ * looks like it hunts. Something enormous and harmless directly overhead will
70
+ * still move it, because a crab under a turtle is a crab about to be stood on.
71
+ *
72
+ * A starfish does nothing at all, which is not an omission. Its answer to a
73
+ * predator takes hours, and an animal reacting on a timescale nobody is
74
+ * watching is an animal that does not react.
75
+ *
76
+ * ## The drawings
77
+ *
78
+ * The same contract the octopus keeps, so a renderer that can draw one can draw
79
+ * these: a path in a drawing one unit wide sitting on the origin, plus lines in
80
+ * that same drawing, and `size` is what a unit is worth in px. The origin is
81
+ * the sand under the animal, which is what lets a renderer put it at `x`, `y`
82
+ * and be done.
83
+ */
84
+
85
+ import { abreast, type Presence } from "./biome.ts"
86
+ import { makeRandom } from "./perlin.ts"
87
+ import type { Point } from "./seabed.ts"
88
+ import { BED_OFF, drawnAt } from "./sizes.ts"
89
+
90
+ export type WalkerKind = "crab" | "starfish"
91
+
92
+ export interface Walker {
93
+ /**
94
+ * The body, as a path in a drawing one unit wide sitting on the origin.
95
+ *
96
+ * Recut every step. A carapace has no shape to change, so what moves it is
97
+ * the bob; a starfish is all body, so its arms are the only thing it has.
98
+ */
99
+ body: string
100
+ /**
101
+ * How many times this walker has redrawn itself.
102
+ *
103
+ * The only thing a renderer has to watch. While it holds, `body`, `legs`,
104
+ * `x`, `y` and `facing` are what they already were, so a renderer keeping its
105
+ * own drawing keeps one that is still true and does no work. A crab spends
106
+ * most of its day standing, so most of them hold most of the time.
107
+ */
108
+ cut: number
109
+ /** 0 is as far off as this water goes, 1 is up against the glass. */
110
+ readonly depth: number
111
+ /** Which way it is going. A crab leads with a side; a starfish has no front
112
+ * at all and keeps this only to say which way it is creeping. */
113
+ facing: number
114
+ readonly kind: WalkerKind
115
+ /** The legs and the claws, in the same drawing. Empty for a starfish. */
116
+ legs: Point[][]
117
+ /** px per unit of the drawing. */
118
+ readonly size: number
119
+ x: number
120
+ y: number
121
+ }
122
+
123
+ export interface Walkers {
124
+ /**
125
+ * A box of a new size, and the ground that came with it.
126
+ *
127
+ * The height is taken and not read. Nothing here ever leaves the ground, so
128
+ * the only thing a taller box changes is where the ground is, and that
129
+ * arrives as `floor`. It is in the signature because every other layer's
130
+ * resize has that shape and a renderer resizes all of them in a row.
131
+ */
132
+ resize(width: number, height: number, floor: (x: number, depth?: number) => number): void
133
+ step(seconds: number): void
134
+ readonly walkers: readonly Walker[]
135
+ }
136
+
137
+ export interface WalkersOptions {
138
+ /**
139
+ * What else is in the water, asked for once a step.
140
+ *
141
+ * Left out, nothing is: a floor with no register to read is a floor whose
142
+ * animals have nothing to be frightened of, which is the state a scene with
143
+ * no shark in it is in anyway.
144
+ */
145
+ about?: () => readonly Presence[]
146
+ crabs?: number
147
+ /** The ground at x, at a given distance; `seabed.floorAt`. An animal that
148
+ * asked for the sand at the front while standing at the back would be an
149
+ * animal buried up to its shell in the hillside. */
150
+ floor: (x: number, depth?: number) => number
151
+ seed: number
152
+ starfish?: number
153
+ /**
154
+ * How far a walker may travel from its last drawing before it redraws, in px.
155
+ *
156
+ * `FloraOptions.tolerance` for animals that walk, and the same bargain. A
157
+ * crab standing still is free, one crossing a stretch costs what it always
158
+ * did, and in between the drawing arrives in steps of this size. At nothing
159
+ * every walker redraws on every step, which is how it behaved before.
160
+ */
161
+ tolerance?: number
162
+ width: number
163
+ }
164
+
165
+ /**
166
+ * The ends of a crab's shell across, and of a starfish's span tip to tip, in
167
+ * metres.
168
+ *
169
+ * Real animals off the sand, turned into a share of the box by `sizes.ts` at
170
+ * the distance the floor lies at. Written in px they were a fraction of what
171
+ * they should have been, and the fault could not be seen from inside this file:
172
+ * nine px is a defensible crab right up until a fish the length of your hand
173
+ * swims over it at six times its size. The two of them are near enough one
174
+ * another in the water, so they are near enough one another here.
175
+ *
176
+ * The shell rather than the animal, because the shell is what a drawing here is
177
+ * a unit of. A crab reaches about twice this once its legs and its claws are
178
+ * out, which is where the animal a reader actually sees comes from.
179
+ */
180
+ const CRAB_SMALLEST = 0.06
181
+
182
+ const CRAB_LARGEST = 0.14
183
+
184
+ const STARFISH_SMALLEST = 0.18
185
+
186
+ const STARFISH_LARGEST = 0.34
187
+
188
+ /**
189
+ * How much of a walker's drawn size is its distance.
190
+ *
191
+ * The share the rest of the scene uses. Scaled by depth outright, the far end
192
+ * of the range is a tenth of the near end, and a tenth of a crab is a speck;
193
+ * what distance is for down here is weight rather than size.
194
+ */
195
+ const DEPTH_SIZE = 0.55
196
+
197
+ /**
198
+ * How far back they go.
199
+ *
200
+ * As far as the bed grows, and that is the point of them. The far ground is the
201
+ * part of the picture that reads as bare, and a plant does not fix it: that far
202
+ * back a plant is a few px of a faint line, which the haze takes first. What
203
+ * fixes bare ground is something with a body. A shell and a star are both a
204
+ * shape with an outline, and an outline survives being drawn small and faint.
205
+ */
206
+ const DEPTH_FAR = 0.12
207
+
208
+ const DEPTH_NEAR = 1
209
+
210
+ /**
211
+ * How hard a starfish leans towards the back of the water when it is placed.
212
+ *
213
+ * A crab is mostly legs, which is to say mostly lines, and it belongs wherever
214
+ * it happens to be. A starfish is a disc, and a disc is worth most where a line
215
+ * has stopped reading. It is a lean rather than a rule, since one at the front
216
+ * is the only one anybody sees the arms of.
217
+ */
218
+ const STAR_BACK = 1.7
219
+
220
+ /** How far past the side one goes before it comes back on the other. */
221
+ const MARGIN = 1.4
222
+
223
+ /** How fast a crab crosses, in its own widths a second, and how much faster or
224
+ * slower than that one of them is. */
225
+ const SCUTTLE = 2.6
226
+
227
+ const HASTE = 0.45
228
+
229
+ /**
230
+ * How long a crab stands and how long it goes, in seconds, and how often a stop
231
+ * turns it round.
232
+ *
233
+ * Standing takes most of it. An animal that crossed the picture continuously
234
+ * would be a shuttle, and the stopping is what makes a crossing something that
235
+ * happened rather than something that is always happening.
236
+ */
237
+ const STAND_LEAST = 3.5
238
+
239
+ const STAND_SPAN = 9
240
+
241
+ const GO_LEAST = 0.8
242
+
243
+ const GO_SPAN = 2.6
244
+
245
+ const TURN_ODDS = 0.45
246
+
247
+ /**
248
+ * How much bigger than a crab a thing has to be before it is minded, how far
249
+ * off it is minded from as a share of its own size, and how fast the fright
250
+ * fades once it has gone.
251
+ *
252
+ * The first two are the rule `nemos.ts` uses, because it is the right rule and
253
+ * two animals in one water should be frightened of the same things. Something
254
+ * a crab's own size is another crab.
255
+ */
256
+ const MINDS = 1.6
257
+
258
+ const NOTICE = 3.2
259
+
260
+ const FRIGHT_FADE = 3.5
261
+
262
+ /**
263
+ * How much of a bolt something with no menace about it can still provoke.
264
+ *
265
+ * A quarter. `nemos.ts` weighs its fright the same way and for the same reason:
266
+ * a shape that has never eaten anybody is not nothing when it is on top of you,
267
+ * and a sea where a turtle and a shark were the same event is a sea where
268
+ * nothing that happens means anything.
269
+ */
270
+ const BULK = 0.25
271
+
272
+ /**
273
+ * How frightened a crab has to be before it stops, and before it runs.
274
+ *
275
+ * A long way apart. Freezing is the whole of what a small animal on open ground
276
+ * has, and the bolt is what is left when freezing has plainly not worked.
277
+ */
278
+ const FREEZE_AT = 0.05
279
+
280
+ const BOLT_AT = 0.55
281
+
282
+ /**
283
+ * How near a thing has to be, whatever it looks like, before a crab gets out
284
+ * from under it.
285
+ *
286
+ * Nearly touching, and it is the only reading here that ignores menace
287
+ * entirely. A crab under a turtle is a crab about to be stood on, and nothing
288
+ * about the turtle's intentions makes that better.
289
+ */
290
+ const SQUASH_AT = 0.88
291
+
292
+ /** How much faster than a walk a bolt is. */
293
+ const BOLT = 3.4
294
+
295
+ /** How fast a starfish creeps, in its own spans a second, and how far off that
296
+ * one of them is. A minute hand, and the arithmetic is the same. */
297
+ const CREEP = 0.008
298
+
299
+ const DAWDLE = 0.6
300
+
301
+ /** How long a starfish holds a direction, in seconds. Nothing about this animal
302
+ * is in a hurry, and a starfish that changed its mind often would be a leaf. */
303
+ const HOLD_LEAST = 40
304
+
305
+ const HOLD_SPAN = 90
306
+
307
+ /** How high the shell rides above the sand, and how far it bobs as the legs
308
+ * work. A shell that rode on the ground would be a stone with legs. */
309
+ const RIDE = 0.24
310
+
311
+ const BOB = 0.035
312
+
313
+ /** How many legs a side carries, how far one strides, and how far it lifts. */
314
+ const LEGS = 4
315
+
316
+ /** What the far side's legs keep of the near side's reach, and how far up the
317
+ * picture its feet stand. The ground behind something is drawn higher than the
318
+ * ground it is standing on, and a foot that ignored that is a foot in a hole. */
319
+ const AWAY = 0.66
320
+
321
+ const BEHIND = 0.05
322
+
323
+ const STRIDE = 0.1
324
+
325
+ const LIFT = 0.06
326
+
327
+ /** How far a crab's gait comes round for one width travelled. Two cycles, so a
328
+ * leg is not covering half the animal's own width in a stride. */
329
+ const GAIT = Math.PI * 4
330
+
331
+ /** How many arms a starfish has, how far the web between them comes in, and how
332
+ * finely the outline is cut. */
333
+ /** How far a walker travels before it is worth redrawing, in px, by default. */
334
+ const TOLERANCE = 0.25
335
+
336
+ /**
337
+ * How far a crab's foot travels for a radian of its gait, in the drawing.
338
+ *
339
+ * Read off `crabLegs` rather than guessed at. A foot sits at `swing * STRIDE`
340
+ * across and `-max(0, swing) * LIFT` up, and a sine cannot change faster than
341
+ * its own angle, so no part of a leg outruns this. It is the whole of what
342
+ * changes when a crab works its legs without going anywhere.
343
+ */
344
+ const GAIT_REACH = Math.hypot(STRIDE, LIFT)
345
+
346
+ /**
347
+ * How much of an arm's length is the reach that makes it creep, halved.
348
+ *
349
+ * A starfish is drawn one unit across, so an arm tip sits about half a unit out
350
+ * and travels about half of whatever its length does. Its length is
351
+ * `odd * (1 + sin(clock) * ARM_REACH)`, and each arm keeps its own clock, so
352
+ * what a starfish is capable of in a second is the fastest of them.
353
+ */
354
+ const ARM_TIP = 0.5
355
+
356
+ const ARMS = 5
357
+
358
+ const WEB = 0.44
359
+
360
+ const RIM = 44
361
+
362
+ /** How much of its span an arm may be short or long of the rest, and how far it
363
+ * reaches and pulls back as it goes. No two arms of one animal agree. */
364
+ const ODD_ARM = 0.22
365
+
366
+ const ARM_REACH = 0.12
367
+
368
+ /** How fast an arm works through its own reach, slowest to fastest. */
369
+ const ARM_SLOWEST = 0.12
370
+
371
+ const ARM_FASTEST = 0.34
372
+
373
+ /**
374
+ * How much a starfish is squashed, lying on the sand.
375
+ *
376
+ * Nearly half. It is the one drawing here that is being seen from above rather
377
+ * than from the side, and the ground it is lying on is a slope going away from
378
+ * the reader; this is that slope's share of it. Drawn round it is a starfish
379
+ * stuck to the inside of the glass.
380
+ */
381
+ const FLATTEN = 0.46
382
+
383
+ const MIN_SPAN = 1
384
+
385
+ /**
386
+ * What a crab has made of the water this moment: how alarming the worst thing
387
+ * in it is, how much of that is something that hunts, and where it was.
388
+ */
389
+ interface Seen {
390
+ chased: number
391
+ from: number
392
+ near: number
393
+ }
394
+
395
+ /**
396
+ * The walker as it stood when it was last drawn.
397
+ *
398
+ * Everything the drawing was made from, so how far it has wandered since is
399
+ * arithmetic on it. Against the last drawing rather than the last step, since a
400
+ * crab that shuffled and came back has not moved.
401
+ */
402
+ interface Drawn {
403
+ readonly facing: number
404
+ /** Seconds since the drawing was made, which is all a starfish needs. */
405
+ since: number
406
+ readonly stride: number
407
+ readonly x: number
408
+ readonly y: number
409
+ }
410
+
411
+ /** What one walker is doing, alongside the animal itself. */
412
+ interface Doing {
413
+ /** Where each arm is in its own reach. Empty for a crab. */
414
+ readonly arms: number[]
415
+ /** How far through the current bout it is, in seconds. */
416
+ at: number
417
+ /** How much of the fright is something that looks like it hunts. */
418
+ chased: number
419
+ /** How far this one's fastest arm carries its tip in a second, in the
420
+ * drawing. Nought for a crab, which has no arms and a gait instead. */
421
+ readonly creep: number
422
+ /** How frightened it is, 0 easy and 1 with the thing on top of it. */
423
+ fright: number
424
+ /** Whether it is going anywhere at the moment. */
425
+ going: boolean
426
+ /** How long and how odd each arm is. Empty for a crab. */
427
+ readonly odd: readonly number[]
428
+ /** How fast it goes when it is going, in px a second. */
429
+ readonly pace: number
430
+ /** How far each arm gets through its reach in a second. Empty for a crab. */
431
+ readonly rates: readonly number[]
432
+ /** How long this bout runs, in seconds. */
433
+ span: number
434
+ /** How far through the gait it is, in radians. */
435
+ stride: number
436
+ }
437
+
438
+ /**
439
+ * A crab's shell, in a drawing one unit wide sitting on the origin.
440
+ *
441
+ * Wider than it is tall, which is the half of a crab that is not its legs. The
442
+ * `bob` lifts it as the legs work, and it is small enough that nobody will name
443
+ * it: what it is for is that the shell and the legs are one animal rather than
444
+ * a drawing with lines under it.
445
+ */
446
+ export function crabShell(bob: number): string {
447
+ const at = (-RIDE - 0.22 + Math.sin(bob) * BOB).toFixed(3)
448
+ const up = (-RIDE - 0.46 + Math.sin(bob) * BOB).toFixed(3)
449
+ const low = (-RIDE + 0.02 + Math.sin(bob) * BOB).toFixed(3)
450
+
451
+ return [
452
+ `M-0.5 ${at}`,
453
+ `Q-0.48 ${up} 0 ${up}`,
454
+ `Q0.48 ${up} 0.5 ${at}`,
455
+ `Q0.46 ${low} 0 ${low}`,
456
+ `Q-0.46 ${low} -0.5 ${at}`,
457
+ "Z",
458
+ ].join(" ")
459
+ }
460
+
461
+ /** The shell with the bob out of it, for anything wanting a still one. */
462
+ export const CRAB_SHELL = crabShell(0)
463
+
464
+ /** How a crab is standing: which way it leads, and where it is in its gait. */
465
+ export interface CrabPose {
466
+ readonly facing: number
467
+ readonly stride: number
468
+ }
469
+
470
+ /**
471
+ * The legs and the claws, in the same drawing as the shell.
472
+ *
473
+ * A leg is a hip, a knee above the shell and a foot on the sand, which is the
474
+ * arch that says crab rather than insect. They work in two groups, so half the
475
+ * animal is planted whenever the other half is swinging, and the swing is a
476
+ * step forward and a lift rather than a wave: a foot that rose without going
477
+ * anywhere is a crab treading water.
478
+ *
479
+ * The claws lead, because a crab goes sideways and holds them in front of the
480
+ * side it is going to. They are the one part of it drawn to be recognised at
481
+ * this size.
482
+ */
483
+ export function crabLegs(pose: CrabPose): Point[][] {
484
+ const legs: Point[][] = []
485
+
486
+ for (let side = -1; side <= 1; side += 2) {
487
+ // The four on the far side of the animal, drawn shorter and standing
488
+ // closer in. There is no second weight to draw them at, so what says they
489
+ // are behind the shell is that they are foreshortened, which is what the
490
+ // far side of a standing thing actually does. Which side that is only
491
+ // changes at a stop, for the reason the octopus turns at a stop.
492
+ const away = side === -pose.facing ? AWAY : 1
493
+
494
+ for (let at = 0; at < LEGS; at++) {
495
+ const hip = { x: side * (0.44 - at * 0.05), y: -RIDE - 0.34 + at * 0.08 }
496
+ const seat = (0.5 + at * 0.13) * away
497
+
498
+ // Half of them a stride behind the other half. Every leg swinging at once
499
+ // is a crab hopping, and every leg planted at once is a crab sliding.
500
+ const swing = Math.sin(pose.stride + (at % 2 === 0 ? 0 : Math.PI) + (side < 0 ? Math.PI : 0))
501
+ const foot = {
502
+ x: side * seat + swing * STRIDE * pose.facing,
503
+ y: -Math.max(0, swing) * LIFT * away - (away < 1 ? BEHIND : 0),
504
+ }
505
+
506
+ legs.push([
507
+ hip,
508
+ { x: (hip.x + foot.x) / 2 + side * 0.07, y: (-RIDE - 0.36 - at * 0.02) * away },
509
+ foot,
510
+ ])
511
+ }
512
+ }
513
+
514
+ // Out in front of the side it is going to, and folded rather than straight: a
515
+ // claw held out at arm's length is a crab surrendering.
516
+ for (const up of [0, 1]) {
517
+ const lead = pose.facing
518
+ const rise = -RIDE - 0.1 - up * 0.13
519
+
520
+ legs.push([
521
+ { x: lead * 0.36, y: rise },
522
+ { x: lead * (0.74 + up * 0.05), y: rise + 0.04 },
523
+ { x: lead * (1.02 + up * 0.06), y: rise - 0.06 },
524
+ { x: lead * (0.88 + up * 0.05), y: rise - 0.2 },
525
+ ])
526
+ }
527
+
528
+ return legs
529
+ }
530
+
531
+ /**
532
+ * A starfish, as one closed outline in a drawing one unit across.
533
+ *
534
+ * Arms rather than points. A starfish drawn with a straight edge from tip to
535
+ * tip is a sheriff's badge, and what a real one has is a thick arm coming out
536
+ * of a disc and a deep round web between it and the next. `WEB` is where that
537
+ * web comes in to, and the whole difference between the two drawings is that
538
+ * number.
539
+ *
540
+ * `reach` is each arm's own length, which is where every one of these stops
541
+ * being a stamp of the same shape: an animal whose arms all agreed would be a
542
+ * cut-out, and a real one has one arm short from an argument it lost.
543
+ */
544
+ export function starfishBody(reach: readonly number[]): string {
545
+ const arms = Math.max(1, reach.length)
546
+
547
+ // Seated on the longest arm it currently has, so it sits on the sand rather
548
+ // than through it, the way every other drawing here does. The seat shifts as
549
+ // the arms work, which is a settling nobody will name and is cheaper than
550
+ // deciding what the ground in front of it would have hidden.
551
+ const seat = Math.max(...reach) * 0.5 * FLATTEN
552
+
553
+ const path: string[] = []
554
+ for (let at = 0; at <= RIM; at++) {
555
+ const turn = (at / RIM) * Math.PI * 2
556
+ const lobe = (1 + Math.cos(turn * arms)) / 2
557
+ const arm = reach[Math.round((turn * arms) / (Math.PI * 2)) % arms] ?? 1
558
+ const out = (WEB + (1 - WEB) * lobe ** 0.7) * arm * 0.5
559
+
560
+ const x = (Math.sin(turn) * out).toFixed(3)
561
+ const y = (-seat - Math.cos(turn) * out * FLATTEN).toFixed(3)
562
+ path.push(`${at === 0 ? "M" : "L"}${x} ${y}`)
563
+ }
564
+
565
+ path.push("Z")
566
+ return path.join(" ")
567
+ }
568
+
569
+ export function createWalkers(options: WalkersOptions): Walkers {
570
+ const random = makeRandom(options.seed ^ 0x3c4a)
571
+
572
+ let width = Math.max(MIN_SPAN, options.width)
573
+ let floor = options.floor
574
+
575
+ const walkers: Walker[] = []
576
+ const doings: Doing[] = []
577
+
578
+ const tolerance = Math.max(0, options.tolerance ?? TOLERANCE)
579
+
580
+ /** What each walker was drawn from, or null while it never has been. */
581
+ const held: (Drawn | null)[] = []
582
+
583
+ function born(kind: WalkerKind): void {
584
+ const crab = kind === "crab"
585
+ const back = crab ? random() : random() ** STAR_BACK
586
+ const depth = DEPTH_FAR + back * (DEPTH_NEAR - DEPTH_FAR)
587
+ const across = crab
588
+ ? CRAB_SMALLEST + random() * (CRAB_LARGEST - CRAB_SMALLEST)
589
+ : STARFISH_SMALLEST + random() * (STARFISH_LARGEST - STARFISH_SMALLEST)
590
+ const span = drawnAt(across, BED_OFF) * width
591
+ const size = span * (1 - DEPTH_SIZE + DEPTH_SIZE * depth)
592
+ const x = random() * width
593
+
594
+ const odd: number[] = []
595
+ const arms: number[] = []
596
+ const rates: number[] = []
597
+ if (!crab) {
598
+ for (let at = 0; at < ARMS; at++) {
599
+ odd.push(1 - ODD_ARM + random() * ODD_ARM * 2)
600
+ arms.push(random() * Math.PI * 2)
601
+ rates.push(ARM_SLOWEST + random() * (ARM_FASTEST - ARM_SLOWEST))
602
+ }
603
+ }
604
+
605
+ // Part way through whatever it is doing already. A floor where every animal
606
+ // set off on the same frame is a floor that was switched on.
607
+ const going = crab ? random() < 0.3 : true
608
+ const run = crab ? bout(going) : HOLD_LEAST + random() * HOLD_SPAN
609
+
610
+ let quickest = 0
611
+ for (let at = 0; at < odd.length; at++) {
612
+ quickest = Math.max(quickest, (odd[at] ?? 0) * (rates[at] ?? 0) * ARM_REACH * ARM_TIP)
613
+ }
614
+
615
+ doings.push({
616
+ arms,
617
+ at: random() * run,
618
+ chased: 0,
619
+ creep: quickest,
620
+ fright: 0,
621
+ going,
622
+ odd,
623
+ pace: crab
624
+ ? size * SCUTTLE * (1 - HASTE + random() * HASTE * 2)
625
+ : size * CREEP * (1 - DAWDLE + random() * DAWDLE * 2),
626
+ rates,
627
+ span: run,
628
+ stride: random() * Math.PI * 2,
629
+ })
630
+
631
+ walkers.push({
632
+ body: "",
633
+ cut: 0,
634
+ depth,
635
+ facing: random() < 0.5 ? -1 : 1,
636
+ kind,
637
+ legs: [],
638
+ size,
639
+ x,
640
+ y: floor(x, depth),
641
+ })
642
+
643
+ held.push(null)
644
+ }
645
+
646
+ /** How long a crab stands, or how long it goes. */
647
+ function bout(going: boolean): number {
648
+ return going ? GO_LEAST + random() * GO_SPAN : STAND_LEAST + random() * STAND_SPAN
649
+ }
650
+
651
+ /**
652
+ * One animal, a moment on.
653
+ *
654
+ * A crab's gait is wound by the ground it covered rather than by the time
655
+ * that passed, so its legs stop when it does. That is one line and it is the
656
+ * difference between an animal walking and a sprite playing an animation.
657
+ */
658
+ /**
659
+ * How frightened this one is by what is in the water, right now.
660
+ *
661
+ * Bigger than it is, and near enough, where near is measured against the
662
+ * size of the thing rather than in px: a shark is a shark from further off
663
+ * than a bream is. `nemos.ts` minds the water by the same rule, which is
664
+ * what keeps two animals in one sea frightened of the same things.
665
+ */
666
+ function minded(one: Walker, water: readonly Presence[]): Seen {
667
+ const seen: Seen = { chased: 0, from: one.x, near: 0 }
668
+
669
+ for (const thing of water) {
670
+ if (thing.size < one.size * MINDS) continue
671
+
672
+ const reach = thing.size * NOTICE
673
+ const away = Math.hypot(one.x - thing.x, one.y - thing.y)
674
+ if (away >= reach) continue
675
+
676
+ // Weighed by how nearly abreast the two are, because a screen has two
677
+ // distances and this water has three: a shark cruising past the glass is
678
+ // nowhere near a crab back in the murk, however neatly they line up.
679
+ const near = (1 - away / reach) * abreast(one.depth, thing.depth)
680
+ if (near <= seen.near) continue
681
+
682
+ seen.chased = near * (BULK + (thing.menace ?? 0) * (1 - BULK))
683
+ seen.from = thing.x
684
+ seen.near = near
685
+ }
686
+
687
+ return seen
688
+ }
689
+
690
+ function carry(at: number, seconds: number, water: readonly Presence[]): void {
691
+ const one = walkers[at]
692
+ const doing = doings[at]
693
+ if (!one || !doing) return
694
+
695
+ doing.at += seconds
696
+
697
+ const was = held[at]
698
+ if (was) was.since += seconds
699
+
700
+ if (one.kind === "crab") {
701
+ const seen = minded(one, water)
702
+ doing.fright = Math.max(0, Math.max(doing.fright - seconds / FRIGHT_FADE, seen.near))
703
+ doing.chased = Math.max(0, Math.max(doing.chased - seconds / FRIGHT_FADE, seen.chased))
704
+
705
+ // Stop being a moving thing, and run only once that has plainly not
706
+ // worked. Both answers are taken before the bout it was in gets a say,
707
+ // and the bout is left where it was: what it was doing before the
708
+ // shadow came over is what it goes back to afterwards.
709
+ if (doing.chased > BOLT_AT || doing.fright > SQUASH_AT) {
710
+ one.facing = one.x < seen.from ? -1 : 1
711
+ const gone = doing.pace * BOLT * seconds * one.facing
712
+ one.x += gone
713
+ doing.stride += (Math.abs(gone) / one.size) * GAIT
714
+ settle(one)
715
+ return
716
+ }
717
+
718
+ if (doing.fright > FREEZE_AT) {
719
+ settle(one)
720
+ return
721
+ }
722
+
723
+ if (doing.at >= doing.span) {
724
+ doing.going = !doing.going
725
+ doing.at = 0
726
+ doing.span = bout(doing.going)
727
+ if (!doing.going && random() < TURN_ODDS) one.facing = -one.facing
728
+ }
729
+
730
+ if (doing.going) {
731
+ const gone = doing.pace * seconds * one.facing
732
+ one.x += gone
733
+ doing.stride += (Math.abs(gone) / one.size) * GAIT
734
+ }
735
+ } else {
736
+ if (doing.at >= doing.span) {
737
+ doing.at = 0
738
+ doing.span = HOLD_LEAST + random() * HOLD_SPAN
739
+ one.facing = random() < 0.5 ? -1 : 1
740
+ }
741
+
742
+ one.x += doing.pace * seconds * one.facing
743
+ for (let arm = 0; arm < doing.arms.length; arm++) {
744
+ doing.arms[arm] = (doing.arms[arm] ?? 0) + (doing.rates[arm] ?? 0) * seconds
745
+ }
746
+ }
747
+
748
+ settle(one)
749
+ }
750
+
751
+ /**
752
+ * Off one side and on at the other, and standing on the ground it is over.
753
+ *
754
+ * What the water does with everything else that crosses it. Every path
755
+ * through a step ends here, including the one a frightened crab takes, since
756
+ * a crab that ran off the edge of the picture and stayed there would be a
757
+ * crab that was frightened once.
758
+ */
759
+ function settle(one: Walker): void {
760
+ const past = one.size * MARGIN
761
+ if (one.x < -past) one.x += width + past * 2
762
+ if (one.x > width + past) one.x -= width + past * 2
763
+
764
+ one.y = floor(one.x, one.depth)
765
+ }
766
+
767
+ /**
768
+ * How far this walker can have travelled since it was last drawn.
769
+ *
770
+ * The animal moves as a whole and its legs move under it, and both have to be
771
+ * in here because a renderer holds one drawing for the two of them. The whole
772
+ * is `x` and `y` and is exact. The legs are a bound: a crab's foot cannot
773
+ * outrun `GAIT_REACH` for a radian of gait, and a starfish's arm cannot
774
+ * outrun `creep` for a second of creeping, and neither is a measurement of
775
+ * the drawing because making one would cost the drawing.
776
+ *
777
+ * A crab that turns is redrawn whatever the arithmetic says. Its far legs are
778
+ * the ones that shorten, so a mirrored crab is a different animal from the
779
+ * front and nothing continuous describes the change.
780
+ */
781
+ function stir(at: number): number {
782
+ const one = walkers[at]
783
+ const doing = doings[at]
784
+ const was = held[at]
785
+ if (!one || !doing || !was) return Number.POSITIVE_INFINITY
786
+ if (was.facing !== one.facing) return Number.POSITIVE_INFINITY
787
+
788
+ const worked = GAIT_REACH * Math.abs(doing.stride - was.stride) + doing.creep * was.since
789
+
790
+ return Math.hypot(one.x - was.x, one.y - was.y) + one.size * worked
791
+ }
792
+
793
+ /** Redraw one where it currently stands. */
794
+ function draw(at: number): void {
795
+ const one = walkers[at]
796
+ const doing = doings[at]
797
+ if (!one || !doing) return
798
+
799
+ held[at] = {
800
+ facing: one.facing,
801
+ since: 0,
802
+ stride: doing.stride,
803
+ x: one.x,
804
+ y: one.y,
805
+ }
806
+ one.cut++
807
+
808
+ if (one.kind === "crab") {
809
+ one.body = crabShell(doing.stride)
810
+ one.legs = crabLegs({ facing: one.facing, stride: doing.stride })
811
+ return
812
+ }
813
+
814
+ one.body = starfishBody(
815
+ doing.odd.map((odd, arm) => odd * (1 + Math.sin(doing.arms[arm] ?? 0) * ARM_REACH)),
816
+ )
817
+ }
818
+
819
+ function stock(): void {
820
+ walkers.length = 0
821
+ doings.length = 0
822
+ held.length = 0
823
+
824
+ for (let made = 0; made < Math.max(0, options.crabs ?? 0); made++) born("crab")
825
+ for (let made = 0; made < Math.max(0, options.starfish ?? 0); made++) born("starfish")
826
+ }
827
+
828
+ function advance(seconds: number): void {
829
+ const step = Math.min(Math.max(seconds, 0), 0.1)
830
+ const water = options.about?.() ?? []
831
+
832
+ for (let at = 0; at < walkers.length; at++) {
833
+ carry(at, step, water)
834
+ if (stir(at) >= tolerance) draw(at)
835
+ }
836
+ }
837
+
838
+ stock()
839
+ advance(0)
840
+
841
+ return {
842
+ resize(nextWidth, _height, nextFloor) {
843
+ const scale = Math.max(MIN_SPAN, nextWidth) / width
844
+ width = Math.max(MIN_SPAN, nextWidth)
845
+ floor = nextFloor
846
+
847
+ for (let at = 0; at < walkers.length; at++) {
848
+ const one = walkers[at]
849
+ if (!one) continue
850
+
851
+ one.x *= scale
852
+ one.y = floor(one.x, one.depth)
853
+ draw(at)
854
+ }
855
+ },
856
+
857
+ step: advance,
858
+
859
+ walkers,
860
+ }
861
+ }