@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.
@@ -0,0 +1,809 @@
1
+ /**
2
+ * A whole sea, from the seed it grew from to the block of numbers a renderer
3
+ * reads.
4
+ *
5
+ * The simulations decide what is in the water and where it is; `pen.ts` says
6
+ * what one drawing looks like; the files beside it turn one kind of thing into
7
+ * drawings. This is what holds all of that together and hands out a frame.
8
+ *
9
+ * Nothing here draws. What comes back from `over` is a Float32Array with every
10
+ * drawing in the frame written into it, and what a renderer does with that is
11
+ * its own business: the site's `render/card.ts` cuts it into triangles for a graphics card,
12
+ * its `render/flat.ts` walks it onto a canvas, and the desktop's own renderer reads the
13
+ * same block out of V8.
14
+ *
15
+ * ## Why it is a factory
16
+ *
17
+ * Because a page has more than one sea on it. A hero has a band of water behind
18
+ * its words, a design system page has a specimen of every animal, and each of
19
+ * them is its own box with its own seed and its own day. A module holding one
20
+ * scene in module state can serve one screen, which is what a wallpaper is and
21
+ * what a page is not.
22
+ *
23
+ * ## Two ways to hand over the bed
24
+ *
25
+ * `bed: "marks"` paints the ground and every plant into the frame with
26
+ * everything else, which is what a renderer without a vertex shader wants.
27
+ * `bed: "wire"` leaves them out of the frame and hands them over instead
28
+ * through `layout` and `publish`: the shape once, and after that two numbers a
29
+ * plant, for a renderer that would rather bend them on the card.
30
+ */
31
+ import { createBiome } from "../biome.ts"
32
+ import { createCaustics } from "../caustics.ts"
33
+ import { type Kind, manyIn } from "../census.ts"
34
+ import { createCephalopods } from "../cephalopods.ts"
35
+ import { createClouds } from "../cloud.ts"
36
+ import { createCrags } from "../crags.ts"
37
+ import { createDrift } from "../drift.ts"
38
+ import { createEels } from "../eels.ts"
39
+ import { createFlora, type Plant } from "../flora.ts"
40
+ import { createJellies, KINDS as JELLY_KINDS } from "../jellies.ts"
41
+ import { createNemos } from "../nemos.ts"
42
+ import { createPassers } from "../passers.ts"
43
+ import { thriving } from "../plenty.ts"
44
+ import { createPuffers } from "../puffers.ts"
45
+ import { createRays } from "../rays.ts"
46
+ import { createReef } from "../reef.ts"
47
+ import { createRelics } from "../relics.ts"
48
+ import { createSeabed } from "../seabed.ts"
49
+ import { createShoal, daySeed, KINDS as FISH_KINDS, wild } from "../shoal.ts"
50
+ import { createSwarm } from "../swarm.ts"
51
+ import { createVisitors } from "../visitors.ts"
52
+ import { createWalkers } from "../walkers.ts"
53
+ import { paintCaustics, paintFlora, paintGround, paintWater } from "./bed.ts"
54
+ import {
55
+ paintEels,
56
+ paintInklings,
57
+ paintJellies,
58
+ paintNemos,
59
+ paintPassers,
60
+ paintPuffers,
61
+ paintShoal,
62
+ paintSwarm,
63
+ paintVisitors,
64
+ paintWalkers,
65
+ } from "./life.ts"
66
+ import { paintRays, paintSnow } from "./light.ts"
67
+ import { Pen } from "./pen.ts"
68
+ import { paintCrags, paintHeads, paintRelics, paintStones } from "./rock.ts"
69
+ import { type Caster, paintShade } from "./shade.ts"
70
+ import { type Asked, paintSky } from "./sky.ts"
71
+
72
+ /** How thickly the bed is sown, per thousand px of width. */
73
+ const PER_K = {
74
+ anemones: 34,
75
+ cliffs: 5.5,
76
+ corals: 46,
77
+ fans: 19,
78
+ grasses: 124,
79
+ kelps: 24,
80
+ stones: 40,
81
+ } as const
82
+
83
+ /**
84
+ * The most of each the bed will ever hold.
85
+ *
86
+ * High enough that a rich day is drawn rather than clipped, and no higher. What
87
+ * a given renderer can actually afford is its own business and it says so with
88
+ * `thin`, since the ceiling that suits a wallpaper on a desktop card is not the
89
+ * one that suits a band of water behind a paragraph.
90
+ */
91
+ const MOST = {
92
+ anemones: 200,
93
+ cliffs: 16,
94
+ corals: 500,
95
+ fans: 220,
96
+ grasses: 1400,
97
+ kelps: 280,
98
+ stones: 108,
99
+ } as const
100
+
101
+ const LEAST = 2
102
+ const REEF_HEADS = 34
103
+ const RANGES = 6
104
+
105
+ /**
106
+ * How far apart two openings of the same water can be, in seconds.
107
+ *
108
+ * Past this the scene is a scene anybody has seen, so there is nothing to be
109
+ * gained by winding further and a wind is not free.
110
+ */
111
+ const CYCLE = 150
112
+
113
+ /** Sand, a hill, a mound and a cliff, on the wire. */
114
+ const GROUND = { cliff: 2, hill: 1, mound: 3, sand: 0 } as const
115
+
116
+ /** What a plant's kind comes to on the wire. */
117
+ const WIRE_KINDS: Record<string, number> = { anemone: 2, coral: 4, fan: 3, grass: 1, kelp: 0 }
118
+
119
+ /**
120
+ * The floors and ceilings on what the water is allowed to hold.
121
+ *
122
+ * How many of each there are is `census.ts`'s answer, and these are what a
123
+ * frame will actually carry: a floor so a sliver of a box still has a sea in
124
+ * it, and a ceiling so a bloom is a busy water rather than a dropped frame.
125
+ * `night` is the share of a shoal that is still out after dark.
126
+ */
127
+ const FISH = { least: 6, most: 52, night: 0.55 }
128
+ const MOTES = { least: 40, most: 280, per: 12_500 }
129
+ const CRAWLERS = { mostCrabs: 14, mostStarfish: 14 }
130
+ const COLONY = { fewest: 10, most: 120 }
131
+ const PUFFERS = { most: 3 }
132
+ const JELLIES = { most: 20 }
133
+ const SHAFTS = 5
134
+ const CLOUDS = 4
135
+ const VENTS = 3
136
+ const SPECKS = 3000
137
+ const MOST_INKLINGS = 4
138
+
139
+ /** How much of its own length a fish swims in a second. */
140
+ const CRUISE = 0.8
141
+
142
+ /** How far the water is wound on before anybody sees it, in steps of a tenth. */
143
+ const WIND_STEP = 1 / 10
144
+
145
+ /**
146
+ * How much the light has to move before the shoal is told a new number.
147
+ *
148
+ * Dawn takes minutes and the count is worked towards over them, so this is only
149
+ * how often the question is asked.
150
+ */
151
+ const DAWN_STEP = 0.01
152
+
153
+ /**
154
+ * How far the frame wanders, as a share of the box's short side, and how far it
155
+ * rolls, in radians.
156
+ *
157
+ * Nobody holds a camera still. A picture that does not move at all is a picture
158
+ * on a tripod, and a tripod is the thing this scene is trying not to be.
159
+ */
160
+ const SWAY_REACH = 0.005
161
+ const SWAY_ROLL = 0.0022
162
+
163
+ /**
164
+ * How many numbers one frame of this water is given room for.
165
+ *
166
+ * A busy sea is a few thousand drawings and a drawing is a few dozen numbers,
167
+ * so this is several times over what one has ever come to. It is a float array
168
+ * allocated once that lives as long as the sea does, which is why the default
169
+ * is a page's answer rather than a wallpaper's: a screen wants the room, and a
170
+ * page with a dozen small seas on it wants the megabytes back.
171
+ */
172
+ const ROOM = 1 << 20
173
+
174
+ /**
175
+ * A coral's drawing, as numbers.
176
+ *
177
+ * The corals are the one thing in the bed that arrives as SVG, and they are
178
+ * also the one thing that never moves, so each drawing is read once and handed
179
+ * over as coordinates from then on. Every twig in `flora.ts` is a move and one
180
+ * curve, which is the whole of the grammar this needs.
181
+ */
182
+ const carved = new Map<string, number[]>()
183
+
184
+ function twigOf(d: string): number[] {
185
+ const held = carved.get(d)
186
+ if (held) return held
187
+
188
+ const numbers = d.match(/-?\d+(\.\d+)?/g)
189
+ const cut = numbers ? numbers.map(Number) : []
190
+ carved.set(d, cut)
191
+ return cut
192
+ }
193
+
194
+ export interface SeaOptions {
195
+ /**
196
+ * Where the ground and the plants are handed over.
197
+ *
198
+ * `marks` puts them in the frame with everything else. `wire` leaves them out
199
+ * of it and answers `layout` and `publish` instead.
200
+ */
201
+ bed?: "marks" | "wire"
202
+ /**
203
+ * Wind the rare things on with everything else.
204
+ *
205
+ * A passer keeps an appointment rather than a stopwatch and a shark is out
206
+ * once in an evening, so a still taken the way a scene opens one is a still of
207
+ * empty water. A harness comparing two renderers needs a sea with something
208
+ * in it; a page does not, and this is off for one.
209
+ */
210
+ eager?: boolean
211
+ height: number
212
+ /**
213
+ * An hour asked for rather than read off the clock; see `sky.ts`.
214
+ */
215
+ hour?: Asked | null
216
+ /**
217
+ * A block to write frames into, rather than one of this sea's own.
218
+ *
219
+ * For a host that has to know where the block is before there is a sea to
220
+ * hold it. The desktop's renderer reads the address out of V8 once, as the
221
+ * bundle finishes loading, and borrows it from then on; a sea that allocated
222
+ * its own would be writing somewhere the host is not reading. `room` is
223
+ * ignored when this is given, since the block brought is the room there is.
224
+ */
225
+ into?: Float32Array
226
+ /**
227
+ * How far a thing may move from its last drawing before it is drawn again, in
228
+ * px.
229
+ */
230
+ /**
231
+ * How many numbers a frame of this water is given room for.
232
+ *
233
+ * Raise it for a whole screen. A frame that runs out ends on a whole drawing
234
+ * rather than halfway through one, so what a block too small costs is the
235
+ * last few things in the picture and never a picture that cannot be read.
236
+ */
237
+ room?: number
238
+ /**
239
+ * Which water this is. The calendar day by default, which is what makes
240
+ * tomorrow somewhere else.
241
+ */
242
+ seed?: number
243
+ /**
244
+ * How much of the bed this renderer will carry, as a share of the ceilings
245
+ * above.
246
+ *
247
+ * One number rather than a table, because what varies between renderers is
248
+ * how much of a frame they have and not which plant they like. At 1 the bed
249
+ * is as thick as a good day makes it.
250
+ */
251
+ thin?: number
252
+ tolerance?: number
253
+ width: number
254
+ }
255
+
256
+ export interface Sea {
257
+ /** How far the box is across, as the sea last understood it. */
258
+ readonly box: { height: number; width: number }
259
+ /** The block every frame is written into. Read it, do not keep it. */
260
+ readonly geometry: Float32Array
261
+ /** The bed, once, for a renderer bending it on the card. */
262
+ layout(): number
263
+ /** Everything that moves, this frame. Returns how many numbers were written. */
264
+ over(): number
265
+ /** Where every plant's sway has got to, for a renderer bending it on the card. */
266
+ publish(): number
267
+ resize(width: number, height: number): void
268
+ /** The seed this water grew from. */
269
+ readonly seed: number
270
+ step(seconds: number): void
271
+ /** Carry the water on by a stretch of time, as cheaply as it can be done. */
272
+ wind(seconds: number): void
273
+ }
274
+
275
+ function spread(perThousand: number, most: number, width: number, thin: number): number {
276
+ return Math.max(
277
+ LEAST,
278
+ Math.min(Math.round(most * thin), Math.round((width * perThousand) / 1000)),
279
+ )
280
+ }
281
+
282
+ /** The same, for anything that grows, which is as thick as the day decided. */
283
+ function lush(perThousand: number, most: number, width: number, day: number, thin: number): number {
284
+ return spread(perThousand * day * thin, most, width, thin)
285
+ }
286
+
287
+ /**
288
+ * Which sea today is.
289
+ *
290
+ * The local calendar day, stirred. A day is the unit because both of the
291
+ * obvious ones are wrong: one fixed seed is the same sea forever, and a seed off
292
+ * the clock is a new seabed on every visit.
293
+ */
294
+ export function today(): number {
295
+ return daySeed()
296
+ }
297
+
298
+ export function createSea(options: SeaOptions): Sea {
299
+ const wired = options.bed === "wire"
300
+ const eager = options.eager ?? false
301
+ const thin = Math.max(0.05, options.thin ?? 1)
302
+ const tolerance = options.tolerance ?? 0
303
+ const seed = options.seed ?? today()
304
+ const geometry = options.into ?? new Float32Array(Math.max(1 << 14, options.room ?? ROOM))
305
+
306
+ let box = { height: options.height, width: options.width }
307
+ let at = 0
308
+ let held = 0
309
+ let turn = 0
310
+ let daylight = 1
311
+ let aimed = -1
312
+
313
+ // Everything that might put something on the sand this frame, gathered into
314
+ // one array that is emptied and filled rather than made again: this is the
315
+ // one list in the scene that is as long as the shoal, and a fresh one of those
316
+ // every frame is a new allocation the size of the whole water sixty times a
317
+ // second.
318
+ const shade: Caster[] = []
319
+
320
+ const put = (value: number) => {
321
+ geometry[at++] = value
322
+ }
323
+
324
+ const water = createBiome()
325
+
326
+ const seabed = createSeabed({
327
+ cliffs: spread(PER_K.cliffs, MOST.cliffs, box.width, thin),
328
+ height: box.height,
329
+ ranges: RANGES,
330
+ seed,
331
+ stones: spread(PER_K.stones, MOST.stones, box.width, thin),
332
+ width: box.width,
333
+ })
334
+
335
+ const reef = createReef({
336
+ floor: seabed.floorAt,
337
+ heads: Math.max(LEAST, Math.round(REEF_HEADS * thin)),
338
+ height: box.height,
339
+ seed,
340
+ tolerance,
341
+ width: box.width,
342
+ })
343
+
344
+ const day = thriving(seed)
345
+
346
+ const alive = (kinds: readonly Kind[], least: number, most: number): number =>
347
+ Math.max(least, Math.min(most, Math.round(manyIn(kinds, box.width, box.height, seed))))
348
+
349
+ // Dug before the bed is sown, because the bed asks it where not to grow. The
350
+ // colony reads the water and nothing reads the colony: a garden eel is
351
+ // smaller than anything here that would care, and what a bed of them is to
352
+ // the rest of this sea is sand.
353
+ const eels = createEels({
354
+ about: water.about,
355
+ count: alive(["eel"], COLONY.fewest, COLONY.most),
356
+ floor: seabed.floorAt,
357
+ height: box.height,
358
+ seed,
359
+ tolerance,
360
+ width: box.width,
361
+ })
362
+
363
+ const flora = createFlora({
364
+ about: water.about,
365
+ anemones: lush(PER_K.anemones, MOST.anemones, box.width, day, thin),
366
+ bare: eels.bare,
367
+ corals: lush(PER_K.corals, MOST.corals, box.width, day, thin),
368
+ // A renderer bending the bed on the card takes every plant but the crown as
369
+ // a shape at rest, and the crown is the honest exception: it is a heading
370
+ // that keeps turning rather than a shape with a bend put through it.
371
+ cutting: wired ? ["anemone"] : undefined,
372
+ fans: lush(PER_K.fans, MOST.fans, box.width, day, thin),
373
+ floor: seabed.floorAt,
374
+ grasses: lush(PER_K.grasses, MOST.grasses, box.width, day, thin),
375
+ height: box.height,
376
+ kelps: lush(PER_K.kelps, MOST.kelps, box.width, day, thin),
377
+ seed,
378
+ tolerance,
379
+ width: box.width,
380
+ })
381
+
382
+ const fishCount = (): number => {
383
+ const full = alive(FISH_KINDS, FISH.least, FISH.most)
384
+ const hour = FISH.night + (1 - FISH.night) * daylight
385
+ return Math.max(FISH.least, Math.round(full * hour))
386
+ }
387
+
388
+ const moteCount = (): number =>
389
+ Math.max(MOTES.least, Math.min(MOTES.most, Math.round((box.width * box.height) / MOTES.per)))
390
+
391
+ const walkers = createWalkers({
392
+ about: water.about,
393
+ crabs: alive(["crab"], LEAST, CRAWLERS.mostCrabs),
394
+ floor: seabed.floorAt,
395
+ seed,
396
+ starfish: alive(["starfish"], LEAST, CRAWLERS.mostStarfish),
397
+ tolerance,
398
+ width: box.width,
399
+ })
400
+
401
+ // It reads the register and never enters it. Something the size of a fist
402
+ // that has stopped running is not a thing anything else in this water has to
403
+ // make a decision about, which is the whole of what being a pufferfish buys.
404
+ const puffers = createPuffers({
405
+ about: water.about,
406
+ count: alive(["puffer"], 0, PUFFERS.most),
407
+ floor: seabed.floorAt,
408
+ height: box.height,
409
+ seed,
410
+ tolerance,
411
+ width: box.width,
412
+ })
413
+
414
+ const shoal = createShoal({
415
+ count: fishCount(),
416
+ cruise: CRUISE,
417
+ height: box.height,
418
+ seed,
419
+ species: wild(seed),
420
+ width: box.width,
421
+ })
422
+
423
+ const inklings = createCephalopods({
424
+ floor: seabed.floorAt,
425
+ height: box.height,
426
+ octopuses: alive(["octopus"], 1, MOST_INKLINGS),
427
+ seed,
428
+ squids: alive(["squid"], 1, MOST_INKLINGS),
429
+ width: box.width,
430
+ })
431
+
432
+ // The cephalopods, entered from out here rather than from inside their own
433
+ // file. An octopus is measured across its arms rather than its head, which is
434
+ // twice the size it is drawn at and is the honest number: what a crab sees
435
+ // coming over a stone is the reach.
436
+ water.enter(() => {
437
+ const about = []
438
+ for (const pus of inklings.octopuses) {
439
+ if (pus.lift > 0) {
440
+ about.push({ depth: pus.depth, menace: 0.55, size: pus.size * 2, x: pus.x, y: pus.y })
441
+ }
442
+ }
443
+ for (const squid of inklings.squids) {
444
+ about.push({ depth: squid.depth, menace: 0.3, size: squid.size, x: squid.x, y: squid.y })
445
+ }
446
+ return about
447
+ })
448
+
449
+ const jellies = createJellies({
450
+ count: alive(JELLY_KINDS, 1, JELLIES.most),
451
+ floor: seabed.floorAt,
452
+ height: box.height,
453
+ seed,
454
+ water,
455
+ width: box.width,
456
+ })
457
+
458
+ const wreckage = createRelics({
459
+ floor: seabed.floorAt,
460
+ height: box.height,
461
+ seed,
462
+ width: box.width,
463
+ })
464
+ const passers = createPassers({ eager, height: box.height, seed, width: box.width })
465
+ const visitors = createVisitors({ eager, height: box.height, seed, water, width: box.width })
466
+ const flock = createSwarm({
467
+ count: Math.round(SPECKS * thin),
468
+ eager,
469
+ height: box.height,
470
+ seed,
471
+ water,
472
+ width: box.width,
473
+ })
474
+ const nemos = createNemos({ about: water.about, reef, seed })
475
+ const caustics = createCaustics({ seed })
476
+ const crags = createCrags({ floor: seabed.floorAt, height: box.height, seed, width: box.width })
477
+ const drift = createDrift({
478
+ floor: seabed.floorAt,
479
+ height: box.height,
480
+ motes: Math.round(moteCount() * thin),
481
+ seed,
482
+ tolerance,
483
+ vents: VENTS,
484
+ width: box.width,
485
+ })
486
+ const light = createRays({ count: SHAFTS, height: box.height, seed, width: box.width })
487
+ const clouds = createClouds({ count: CLOUDS, height: box.height, seed, width: box.width })
488
+
489
+ /** Whatever the water is most frightened of this frame. */
490
+ const felt = () => {
491
+ const above = passers.startle
492
+ const among = visitors.startle
493
+ if (!above) return among
494
+ if (!among) return above
495
+ return among.force > above.force ? among : above
496
+ }
497
+
498
+ const putPoints = (points: readonly { x: number; y: number }[]) => {
499
+ put(points.length)
500
+ for (const point of points) {
501
+ put(point.x)
502
+ put(point.y)
503
+ }
504
+ }
505
+
506
+ const putTwigs = (plant: Plant) => {
507
+ put(plant.twigs.length)
508
+ for (const twig of plant.twigs) {
509
+ const cut = twigOf(twig.d)
510
+ put(twig.width)
511
+ put(cut.length)
512
+ for (const n of cut) put(n)
513
+ }
514
+ }
515
+
516
+ /** What each anemone was last handed over at, so a still one is not sent
517
+ * again. A plant says when it has recut itself, and nothing else does. */
518
+ const handed: number[] = []
519
+
520
+ return {
521
+ get box() {
522
+ return box
523
+ },
524
+
525
+ geometry,
526
+
527
+ layout(): number {
528
+ at = 0
529
+ put(3)
530
+ put(box.width)
531
+ put(box.height)
532
+ put(1 + seabed.ranges.length + 1 + seabed.cliffs.length)
533
+ put(flora.plants.length)
534
+
535
+ put(GROUND.sand)
536
+ put(1)
537
+ putPoints(seabed.ridge)
538
+
539
+ for (const band of seabed.ranges) {
540
+ put(GROUND.hill)
541
+ put(band.depth)
542
+ putPoints(band.ridge)
543
+ }
544
+
545
+ put(GROUND.mound)
546
+ put(reef.depth)
547
+ putPoints(reef.crest)
548
+
549
+ for (const cliff of seabed.cliffs) {
550
+ put(GROUND.cliff)
551
+ put(cliff.depth)
552
+ putPoints(cliff.ridge)
553
+ }
554
+
555
+ for (let p = 0; p < flora.plants.length; p++) {
556
+ const plant = flora.plants[p]
557
+ if (!plant) continue
558
+ put(WIRE_KINDS[plant.kind] ?? 0)
559
+ put(plant.depth)
560
+ put(plant.girth)
561
+ put(plant.scale)
562
+ put(plant.x)
563
+ put(plant.y)
564
+
565
+ if (plant.kind === "coral") {
566
+ putTwigs(plant)
567
+ continue
568
+ }
569
+ if (plant.kind === "anemone") continue
570
+
571
+ const frame = flora.madeOf(p)
572
+ if (!frame) {
573
+ put(0)
574
+ put(0)
575
+ continue
576
+ }
577
+
578
+ put(frame.limbs.length)
579
+ for (const limb of frame.limbs) {
580
+ put(limb.beat)
581
+ put(limb.give)
582
+ put(limb.own)
583
+ put(limb.seat)
584
+ put(limb.shift)
585
+ put(limb.slant)
586
+ put(limb.span)
587
+ put(limb.steps)
588
+ put(limb.stem)
589
+ }
590
+
591
+ put(frame.leaves.length)
592
+ for (const leaf of frame.leaves) {
593
+ put(leaf.limb)
594
+ put(leaf.seat)
595
+ putPoints(leaf.shape)
596
+ }
597
+ }
598
+
599
+ return at
600
+ },
601
+
602
+ /**
603
+ * A frame of the water.
604
+ *
605
+ * ```
606
+ * swayX swayY tilt overscan turn daylight
607
+ * count
608
+ * drawing: form tone weight shade alpha lane width fall fadeTop fadeSpan
609
+ * thin soft n (m (x y)*m)*n
610
+ * ```
611
+ */
612
+ over(): number {
613
+ at = 0
614
+ const unit = Math.min(box.width, box.height) * SWAY_REACH
615
+
616
+ // Two sines an octave and a bit apart on each axis, which is the cheapest
617
+ // thing that does not read as a pendulum: one alone is a frame swinging,
618
+ // and two whose periods do not divide each other never come back to the
619
+ // same place twice in anything like a minute.
620
+ put(unit * (0.62 * Math.sin(held * 0.11) + 0.38 * Math.sin(held * 0.29 + 1.7)))
621
+ put(unit * (0.62 * Math.sin(held * 0.13 + 2.4) + 0.38 * Math.sin(held * 0.23 + 0.6)))
622
+ put(SWAY_ROLL * Math.sin(held * 0.09 + 1.1))
623
+
624
+ // How much bigger than its box the scene is drawn, so that a frame which
625
+ // has wandered is never a frame with the page showing along one edge. The
626
+ // margin covers the roll as well as the wander: a corner is swung by the
627
+ // angle times its distance from the middle, which is further than the
628
+ // middle goes.
629
+ const reach = unit + (Math.abs(SWAY_ROLL) * Math.hypot(box.width, box.height)) / 2
630
+ put(1 + (2 * reach) / Math.max(1, Math.min(box.width, box.height)))
631
+ put(turn)
632
+
633
+ const lit = at
634
+ put(daylight)
635
+
636
+ const pen = new Pen(geometry, at)
637
+ if (!wired) paintWater(pen, box)
638
+
639
+ const sky = paintSky(pen, box, clouds, options.hour ?? null)
640
+ daylight = sky.daylight
641
+ geometry[lit] = daylight
642
+
643
+ // The shoal follows the light. Reef fish shelter after dark and the water
644
+ // over the sand thins out, so the count is a share of the day's, and the
645
+ // shoal is told the number rather than made to be it: it works towards it
646
+ // from the edges over the minutes dawn takes anyway, because a fish
647
+ // appearing in the middle of the picture is the one thing the water
648
+ // promises never to do.
649
+ if (Math.abs(daylight - aimed) > DAWN_STEP) {
650
+ aimed = daylight
651
+ shoal.hold(fishCount())
652
+ }
653
+
654
+ if (!wired) {
655
+ paintGround(pen, seabed, reef, box)
656
+ paintFlora(pen, flora)
657
+ }
658
+
659
+ paintCrags(pen, crags, box)
660
+ paintStones(pen, seabed)
661
+ paintHeads(pen, reef)
662
+ paintRelics(pen, wreckage)
663
+ // Over the ground and under everything standing on it, which is where
664
+ // light that came through the surface has got to.
665
+ paintCaustics(pen, seabed, caustics, box, daylight)
666
+
667
+ // Under everything that swims and over everything that does not, which is
668
+ // the one place in the order where a shadow on the ground is true. Built
669
+ // here rather than asked of each painter, because what a thing puts on the
670
+ // sand is the same question whatever the thing is.
671
+ shade.length = 0
672
+ for (const one of shoal.fish) shade.push(one)
673
+ for (const one of jellies.jellies) shade.push(one)
674
+ for (const one of puffers.puffers) shade.push(one)
675
+ for (const one of walkers.walkers) shade.push(one)
676
+ for (const one of visitors.crossing) shade.push(one)
677
+ paintShade(pen, shade, seabed.floorAt, box.height)
678
+
679
+ paintRays(pen, light, daylight)
680
+ paintSnow(pen, drift)
681
+ paintShoal(pen, shoal)
682
+ paintNemos(pen, nemos)
683
+ paintSwarm(pen, flock)
684
+ paintInklings(pen, inklings)
685
+ paintJellies(pen, jellies, daylight)
686
+ paintEels(pen, eels)
687
+ paintPuffers(pen, puffers)
688
+ paintWalkers(pen, walkers)
689
+ paintVisitors(pen, visitors)
690
+ paintPassers(pen, passers)
691
+
692
+ at = pen.close()
693
+ return at
694
+ },
695
+
696
+ publish(): number {
697
+ at = 0
698
+ put(flora.plants.length)
699
+
700
+ for (let p = 0; p < flora.plants.length; p++) {
701
+ const plant = flora.plants[p]
702
+ const swing = flora.swinging[p]
703
+ if (!plant) continue
704
+ put(swing ? swing.amp : 0)
705
+ put(swing ? swing.own : 0)
706
+
707
+ if (plant.kind !== "anemone" || handed[p] === plant.cut) {
708
+ put(0)
709
+ continue
710
+ }
711
+
712
+ handed[p] = plant.cut
713
+ put(1)
714
+ putPoints(plant.points)
715
+ put(plant.blades.length)
716
+ for (const blade of plant.blades) putPoints(blade)
717
+ }
718
+
719
+ return at
720
+ },
721
+
722
+ resize(width: number, height: number): void {
723
+ box = { height, width }
724
+ seabed.resize(width, height)
725
+ reef.resize(width, height, seabed.floorAt)
726
+ eels.resize(width, height, seabed.floorAt)
727
+ puffers.resize(width, height, seabed.floorAt)
728
+ flora.resize(width, height, seabed.floorAt)
729
+ handed.length = 0
730
+ },
731
+
732
+ seed,
733
+
734
+ step(seconds: number): void {
735
+ // The passers and the visitors go first, so the fish answer the hull and
736
+ // the shark where they are this frame rather than where they were last
737
+ // one. Everything else here is indifferent to the order.
738
+ passers.step(seconds)
739
+ visitors.step(seconds)
740
+ flock.step(seconds, felt())
741
+ shoal.step(seconds, null, felt())
742
+ drift.step(seconds)
743
+ caustics.step(seconds)
744
+ light.step(seconds)
745
+ clouds.step(seconds)
746
+ flora.step(seconds)
747
+ inklings.step(seconds, passers.startle)
748
+ jellies.step(seconds, felt())
749
+ walkers.step(seconds)
750
+ eels.step(seconds)
751
+ puffers.step(seconds)
752
+ reef.step(seconds)
753
+ nemos.step(seconds)
754
+ wreckage.step(seconds)
755
+
756
+ held += seconds
757
+ turn = (turn + 1) % 512
758
+ },
759
+
760
+ /**
761
+ * Carry the whole water forward by a stretch of time, as cheaply as it can
762
+ * be done.
763
+ *
764
+ * Everything that swims has to be swum: where a fish is after a minute is
765
+ * the minute it spent getting there. The bed is the exception, because a
766
+ * plant has no memory. The passers are the other exception, and for the
767
+ * opposite reason: they keep appointments rather than a stopwatch, so
768
+ * winding them here would spend the day's boat on the two minutes of water
769
+ * that exist to be skipped.
770
+ */
771
+ wind(seconds: number): void {
772
+ const steps = Math.round(Math.max(0, seconds) / WIND_STEP)
773
+
774
+ for (let i = 0; i < steps; i++) {
775
+ if (eager) passers.step(WIND_STEP)
776
+ visitors.step(WIND_STEP)
777
+ flock.step(WIND_STEP, felt())
778
+ shoal.step(WIND_STEP, null, felt())
779
+ drift.step(WIND_STEP)
780
+ light.step(WIND_STEP)
781
+ clouds.step(WIND_STEP)
782
+ inklings.step(WIND_STEP, eager ? passers.startle : null)
783
+ jellies.step(WIND_STEP, felt())
784
+ walkers.step(WIND_STEP)
785
+ eels.step(WIND_STEP)
786
+ puffers.step(WIND_STEP)
787
+ nemos.step(WIND_STEP)
788
+ wreckage.step(WIND_STEP)
789
+ }
790
+
791
+ flora.wind(steps * WIND_STEP)
792
+ reef.wind(steps * WIND_STEP)
793
+ held += steps * WIND_STEP
794
+ },
795
+ }
796
+ }
797
+
798
+ /**
799
+ * How far to wind a sea that is opening now, in seconds.
800
+ *
801
+ * The wall clock wrapped at `CYCLE`, on top of the settling every scene owes
802
+ * itself. Nothing is written down and nothing is read back: a clock is a clock
803
+ * on every machine, so two boxes opening together open on the same moment of
804
+ * the same water for free, and one opening an hour later does not open on the
805
+ * same picture.
806
+ */
807
+ export function opening(settle: number): number {
808
+ return settle + (Math.floor(Date.now() / 1000) % CYCLE)
809
+ }