@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/passers.ts
ADDED
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What goes past overhead, what is looking for you, and what is passing
|
|
3
|
+
* through.
|
|
4
|
+
*
|
|
5
|
+
* A boat crossing the surface, sonar, and a submarine. All three are rare on a
|
|
6
|
+
* scale nothing else in the ornament is: each happens at most once in a day,
|
|
7
|
+
* and the submarine only on about one day in seven. Kept free of the DOM the
|
|
8
|
+
* way the rest is.
|
|
9
|
+
*
|
|
10
|
+
* ## Why a thing that almost never happens is worth the code
|
|
11
|
+
*
|
|
12
|
+
* Because the alternative is a loop, and a loop is what turns a background into
|
|
13
|
+
* wallpaper. Everything else here is on a cycle short enough to see the whole
|
|
14
|
+
* of: the fish come round, the kelp sways, the bubbles burst and rest, and
|
|
15
|
+
* after a minute you have watched the entire vocabulary. A scene that still has
|
|
16
|
+
* something in it you have not seen is a scene you have not finished.
|
|
17
|
+
*
|
|
18
|
+
* It also gives the water a surface and a beyond. A hull passing over the top
|
|
19
|
+
* of the picture says the water is under something; a ping says somebody is out
|
|
20
|
+
* there; a submarine says they came looking. None of that can be said by
|
|
21
|
+
* anything swimming.
|
|
22
|
+
*
|
|
23
|
+
* ## Rare in the day, not rare in the minute
|
|
24
|
+
*
|
|
25
|
+
* The first version counted seconds: a boat every eight minutes or so of the
|
|
26
|
+
* scene's own running time. That is the wrong clock for the surface this ended
|
|
27
|
+
* up on. A desktop advances its water only while the wallpaper is uncovered, so
|
|
28
|
+
* the eight minutes are eight minutes of glimpses, and a boat that is rare per
|
|
29
|
+
* minute watched turns out to be a boat you see most times you clear a
|
|
30
|
+
* workspace. Rarity anybody can feel is rarity in the day.
|
|
31
|
+
*
|
|
32
|
+
* So each kind keeps an appointment instead. The day's plan is drawn from the
|
|
33
|
+
* seed and the date, which makes it the same plan on every screen of a machine
|
|
34
|
+
* and a different one tomorrow: whether this kind happens at all today, and the
|
|
35
|
+
* moment it is due. Once the clock is past that moment the passer is owed, and
|
|
36
|
+
* it crosses the next time there is somebody to see it, after a stretch of
|
|
37
|
+
* watched time so that it never arrives on the instant a window closes. Then it
|
|
38
|
+
* is done until tomorrow.
|
|
39
|
+
*
|
|
40
|
+
* Which means a passer waits rather than being missed. A machine that was off
|
|
41
|
+
* at four in the morning gets its boat when it wakes up, and a machine nobody
|
|
42
|
+
* looks at all day gets one crossing when they finally do, not the nine it
|
|
43
|
+
* would have been owed. The hole in it is honest and stated here: nothing is
|
|
44
|
+
* written to disk, so restarting the shell forgets what has already crossed and
|
|
45
|
+
* a day with a restart in it can hold a second boat.
|
|
46
|
+
*
|
|
47
|
+
* ## All three are one shape
|
|
48
|
+
*
|
|
49
|
+
* A passer is a thing that is not here, then is here, then is not. They differ
|
|
50
|
+
* in how likely they are, how long they take, where in the column they sit, and
|
|
51
|
+
* what a renderer makes of them, so they share `Passer` and everything else
|
|
52
|
+
* lives in a table. Adding another kind means another row.
|
|
53
|
+
*
|
|
54
|
+
* ## A passer has to leave a mark on the water
|
|
55
|
+
*
|
|
56
|
+
* The first boat did not. It was a silhouette sliding across the ceiling with
|
|
57
|
+
* two straight lines trailing off the stern, and the two lines were supposed to
|
|
58
|
+
* be a wake. They read as a beam: a hard pair of rays opening into the picture
|
|
59
|
+
* off the back of a hull is a searchlight, or the sonar the ping is meant to
|
|
60
|
+
* be, and the eye is quite sure it is being pointed at. Nothing else about the
|
|
61
|
+
* boat said the water had noticed it at all.
|
|
62
|
+
*
|
|
63
|
+
* So a hull now leaves `wake`, which is churn: puffs of white water let go at
|
|
64
|
+
* the stern that swell, open into the V a hull actually leaves, thin, and go.
|
|
65
|
+
* They belong to the water rather than to the boat, which is the whole point of
|
|
66
|
+
* them. They stay where they were made while the hull goes on, and they are
|
|
67
|
+
* still dying out after it has left the picture, so what somebody sees is a
|
|
68
|
+
* boat that pushed the water aside rather than a boat drawn on top of it.
|
|
69
|
+
*
|
|
70
|
+
* And a passer says what it is doing to the fish, in `startle`. A hull
|
|
71
|
+
* overhead and a ping going through are both things a shoal reacts to, and
|
|
72
|
+
* `shoal.ts` takes one straight: the fish under the boat dive and scatter, and
|
|
73
|
+
* a ping empties the water in front of it. That is the difference between a
|
|
74
|
+
* scene with a boat in it and a scene a boat is happening to.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
import { makeRandom, stir } from "./perlin.ts"
|
|
78
|
+
import { drawnAt } from "./sizes.ts"
|
|
79
|
+
|
|
80
|
+
export type PassKind = "boat" | "sonar" | "submarine"
|
|
81
|
+
|
|
82
|
+
export interface Passer {
|
|
83
|
+
/** How far through its crossing, 0 to 1. */
|
|
84
|
+
along: number
|
|
85
|
+
/** Which way it is going: 1 rightward, -1 left. Meaningless for sonar. */
|
|
86
|
+
readonly facing: number
|
|
87
|
+
readonly kind: PassKind
|
|
88
|
+
/** Its size in px: hull length for a boat, the ring's reach for sonar. */
|
|
89
|
+
readonly scale: number
|
|
90
|
+
/** How loud it is now, 0 to 1, faded in and out at the ends. */
|
|
91
|
+
weight: number
|
|
92
|
+
/** Where it is, in the box's own units. */
|
|
93
|
+
x: number
|
|
94
|
+
y: number
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* One puff of churned water, left behind by whatever made it.
|
|
99
|
+
*
|
|
100
|
+
* It has no course of its own. Where it was made is where it stays, and all it
|
|
101
|
+
* does from there is swell, drift off the line it was made on, and thin out.
|
|
102
|
+
* The two arms of a wake are one of these let go alternately to either side.
|
|
103
|
+
*/
|
|
104
|
+
export interface Froth {
|
|
105
|
+
/** How far through its life it is, 0 new and 1 gone. */
|
|
106
|
+
age: number
|
|
107
|
+
r: number
|
|
108
|
+
/**
|
|
109
|
+
* The size of the thing that made it, in px.
|
|
110
|
+
*
|
|
111
|
+
* Kept so the puff can go on swelling and opening at its own rate after its
|
|
112
|
+
* boat has left the picture. A renderer has no use for it.
|
|
113
|
+
*/
|
|
114
|
+
readonly scale: number
|
|
115
|
+
/**
|
|
116
|
+
* Which way it drifts off the line it was made on, and how fast.
|
|
117
|
+
*
|
|
118
|
+
* The sign is the arm of the wake it belongs to. The size of it is what
|
|
119
|
+
* keeps the two arms from being two rows of dots: every puff opens at its
|
|
120
|
+
* own rate, so the churn spreads into a band that is thickest at the arms
|
|
121
|
+
* and has water in it everywhere, which is what a wake is.
|
|
122
|
+
*/
|
|
123
|
+
readonly side: number
|
|
124
|
+
x: number
|
|
125
|
+
y: number
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* What the water is being told to do right now, for anything living in it.
|
|
130
|
+
*
|
|
131
|
+
* A push out from a point, felt to `reach` and no further. `shoal.ts` takes it
|
|
132
|
+
* as a thing to get away from, which is why the numbers are in px and a share
|
|
133
|
+
* rather than in anything to do with boats: a fish has no opinion about what
|
|
134
|
+
* kind of passer it is under.
|
|
135
|
+
*/
|
|
136
|
+
export interface Startle {
|
|
137
|
+
/** How hard it is felt at the middle of it, 0 to 1. */
|
|
138
|
+
force: number
|
|
139
|
+
/** How far out it is felt at all, in px. */
|
|
140
|
+
reach: number
|
|
141
|
+
x: number
|
|
142
|
+
y: number
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface Passers {
|
|
146
|
+
readonly passing: readonly Passer[]
|
|
147
|
+
resize(width: number, height: number): void
|
|
148
|
+
/** Where the water is being disturbed now, or null while it is still. */
|
|
149
|
+
readonly startle: Startle | null
|
|
150
|
+
/**
|
|
151
|
+
* `seconds` is watched time, which is what a crossing is made of, and `now`
|
|
152
|
+
* is the wall clock, which is what the day's plan is read against. They are
|
|
153
|
+
* two different clocks on a desktop and telling them apart is the whole of
|
|
154
|
+
* the timing here: a covered wallpaper stops the first and not the second.
|
|
155
|
+
*/
|
|
156
|
+
step(seconds: number, now?: Date): void
|
|
157
|
+
/** The churn left behind, oldest first. */
|
|
158
|
+
readonly wake: readonly Froth[]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface PassersOptions {
|
|
162
|
+
/** Ignore the day's plan and keep crossing. See `createPassers`. */
|
|
163
|
+
eager?: boolean
|
|
164
|
+
height: number
|
|
165
|
+
kinds?: readonly PassKind[]
|
|
166
|
+
seed: number
|
|
167
|
+
width: number
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* How far up and out the two hulls are, in metres.
|
|
172
|
+
*
|
|
173
|
+
* A boat is not small, it is far, and writing it down that way is what keeps
|
|
174
|
+
* the next one from being wrong: it is a shape crossing the ceiling rather than
|
|
175
|
+
* a thing you are swimming beside, and the height of the water column is most
|
|
176
|
+
* of why. Drawn as though it were alongside, it filled the top of the frame and
|
|
177
|
+
* the eye went to it and stayed.
|
|
178
|
+
*
|
|
179
|
+
* The submarine is out in the blue instead, which is the only place a hull that
|
|
180
|
+
* long can be and still be a hull rather than a wall. Both go through
|
|
181
|
+
* `sizes.ts` with the animals, so the boat comes out over a turtle and under a
|
|
182
|
+
* shark, and the rare thing is the largest silhouette this water ever draws.
|
|
183
|
+
*/
|
|
184
|
+
const BOAT_OFF = 30
|
|
185
|
+
|
|
186
|
+
const SUB_OFF = 150
|
|
187
|
+
|
|
188
|
+
/** How much of a crossing is spent fading in, and the same again fading out. */
|
|
189
|
+
const FADE = 0.16
|
|
190
|
+
|
|
191
|
+
/** How far down the box a boat's waterline sits, as a share of the height. */
|
|
192
|
+
const WATERLINE = 0.035
|
|
193
|
+
|
|
194
|
+
/** How far past each side a boat starts and finishes, as shares of the width. */
|
|
195
|
+
const OFFING = 0.2
|
|
196
|
+
|
|
197
|
+
/** How far in from a side a ping comes from: never the middle. */
|
|
198
|
+
const PING_EDGE = 0.18
|
|
199
|
+
|
|
200
|
+
interface Habit {
|
|
201
|
+
/**
|
|
202
|
+
* How many days in ten it happens at all.
|
|
203
|
+
*
|
|
204
|
+
* The one number that decides whether something is scenery or a find. A boat
|
|
205
|
+
* and a ping are every day, so the water has a beyond to it as a matter of
|
|
206
|
+
* course; the submarine is not, because a thing that turns up daily is a
|
|
207
|
+
* thing you stop looking for.
|
|
208
|
+
*/
|
|
209
|
+
readonly chance: number
|
|
210
|
+
/** Where it sits in the column, as shares of the height. */
|
|
211
|
+
readonly deepLeast: number
|
|
212
|
+
readonly deepSpan: number
|
|
213
|
+
/**
|
|
214
|
+
* How big it is drawn, as a share of the box's width.
|
|
215
|
+
*
|
|
216
|
+
* A hull says it in metres of hull at metres of distance and lets `sizes.ts`
|
|
217
|
+
* do the sum. Sonar has no length to state: what it is measuring out is how
|
|
218
|
+
* far the ring reaches, which is a share of the box and nothing else.
|
|
219
|
+
*/
|
|
220
|
+
readonly sizeLeast: number
|
|
221
|
+
readonly sizeSpan: number
|
|
222
|
+
/** How long a crossing takes, at least and up to that again. */
|
|
223
|
+
readonly takeLeast: number
|
|
224
|
+
readonly takeSpan: number
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* How likely each is on a given day, how deep it goes, and how long it takes.
|
|
229
|
+
*
|
|
230
|
+
* The boat is the largest thing the scene ever draws and it is a hard
|
|
231
|
+
* silhouette across the top of the picture, so it gets one crossing a day and
|
|
232
|
+
* no more. Sonar is cheaper to look at and does not travel at all, but it is
|
|
233
|
+
* the loudest thing here in the sense that matters: something out there knows
|
|
234
|
+
* you are down here. Once is plenty.
|
|
235
|
+
*
|
|
236
|
+
* The submarine is the rare one, and it is rare in the only way that lasts. It
|
|
237
|
+
* could have been made rare per minute, and then it would be a thing you see
|
|
238
|
+
* every few sittings and stop seeing after a fortnight; on about one day in
|
|
239
|
+
* seven it is a thing somebody mentions.
|
|
240
|
+
*/
|
|
241
|
+
const HABITS: Readonly<Record<PassKind, Habit>> = {
|
|
242
|
+
boat: {
|
|
243
|
+
chance: 1,
|
|
244
|
+
deepLeast: WATERLINE,
|
|
245
|
+
deepSpan: 0,
|
|
246
|
+
sizeLeast: drawnAt(6, BOAT_OFF),
|
|
247
|
+
sizeSpan: drawnAt(11, BOAT_OFF) - drawnAt(6, BOAT_OFF),
|
|
248
|
+
takeLeast: 26,
|
|
249
|
+
takeSpan: 18,
|
|
250
|
+
},
|
|
251
|
+
sonar: {
|
|
252
|
+
chance: 1,
|
|
253
|
+
deepLeast: 0.12,
|
|
254
|
+
deepSpan: 0.43,
|
|
255
|
+
sizeLeast: 0.4,
|
|
256
|
+
sizeSpan: 0.3,
|
|
257
|
+
takeLeast: 4.5,
|
|
258
|
+
takeSpan: 2.5,
|
|
259
|
+
},
|
|
260
|
+
submarine: {
|
|
261
|
+
chance: 0.15,
|
|
262
|
+
deepLeast: 0.17,
|
|
263
|
+
deepSpan: 0.19,
|
|
264
|
+
sizeLeast: drawnAt(55, SUB_OFF),
|
|
265
|
+
sizeSpan: drawnAt(90, SUB_OFF) - drawnAt(55, SUB_OFF),
|
|
266
|
+
takeLeast: 38,
|
|
267
|
+
takeSpan: 22,
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
interface Churn {
|
|
272
|
+
/** How much water it moves, as a share of what a hull at full pelt moves. */
|
|
273
|
+
readonly force: number
|
|
274
|
+
/** Where the churn leaves it, along and across, in units of `scale`. */
|
|
275
|
+
readonly x: number
|
|
276
|
+
readonly y: number
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* What each does to the water it is in.
|
|
281
|
+
*
|
|
282
|
+
* The boat is at the surface with its screws in the top of the water, so it
|
|
283
|
+
* moves the most and it moves it where everything can see. A submarine at depth
|
|
284
|
+
* moves about half as much: it is smaller, it is further off, and a wake that
|
|
285
|
+
* came off it as hard as the boat's would make it the loud thing in the picture
|
|
286
|
+
* when the point of it is that it went by almost unnoticed.
|
|
287
|
+
*
|
|
288
|
+
* Sonar moves none. It is a sound, and the only mark it leaves is on the fish.
|
|
289
|
+
*/
|
|
290
|
+
const CHURN: Readonly<Record<PassKind, Churn>> = {
|
|
291
|
+
boat: { force: 1, x: -1, y: 0.19 },
|
|
292
|
+
sonar: { force: 0, x: 0, y: 0 },
|
|
293
|
+
submarine: { force: 0.5, x: -1.12, y: 0 },
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Seconds between one puff of churn and the next, while something is crossing.
|
|
298
|
+
*
|
|
299
|
+
* Close enough together that the puffs overlap at the stern, which is the
|
|
300
|
+
* difference between foam and beads. Foam is not a collection of things: it is
|
|
301
|
+
* a patch of water that has gone white, and the only way a renderer drawing
|
|
302
|
+
* discs gets a patch is to hand it more discs than there is room for.
|
|
303
|
+
*/
|
|
304
|
+
const FROTH_GAP = 0.04
|
|
305
|
+
|
|
306
|
+
/** How long a puff of churn lasts, in seconds. */
|
|
307
|
+
const FROTH_LIFE = 4.4
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* The most churn the water holds at once, which is the two above and a margin.
|
|
311
|
+
*
|
|
312
|
+
* A ceiling rather than a count, and it exists because a renderer that keeps a
|
|
313
|
+
* pool of shapes needs to know how deep to make the pool. Running out at the
|
|
314
|
+
* young end would leave a gap of clear water between the stern and its own
|
|
315
|
+
* wake, which is a boat that has stopped churning while still under power.
|
|
316
|
+
*/
|
|
317
|
+
export const FROTH_MOST = 112
|
|
318
|
+
|
|
319
|
+
/** How big a puff leaves, and how fast it swells, in units of `scale`. */
|
|
320
|
+
const FROTH_SEED = 0.07
|
|
321
|
+
|
|
322
|
+
const FROTH_SWELL = 0.04
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* How fast the two arms open, in units of `scale` a second.
|
|
326
|
+
*
|
|
327
|
+
* The V of a wake is a fixed angle in the real world, so what opens it here is
|
|
328
|
+
* time rather than distance: a puff a second old sits about this far off the
|
|
329
|
+
* line the hull ran down, whatever the hull was doing. It slows as it ages,
|
|
330
|
+
* because the water it is pushing into is water at rest.
|
|
331
|
+
*/
|
|
332
|
+
const FROTH_OPEN = 0.2
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* How far a hull is felt through the water, in units of its own `scale`, and
|
|
336
|
+
* how hard a ping is felt against a hull.
|
|
337
|
+
*
|
|
338
|
+
* A hull is felt close in and a ping is felt everywhere it has got to, which is
|
|
339
|
+
* the honest difference between a thing displacing water and a sound going
|
|
340
|
+
* through it. The ping is the softer of the two on purpose: a shoal that bolted
|
|
341
|
+
* from a noise would be a shoal that had been hurt, and this one has only been
|
|
342
|
+
* asked a question.
|
|
343
|
+
*/
|
|
344
|
+
const HULL_FELT = 2.6
|
|
345
|
+
|
|
346
|
+
const PING_FELT = 0.55
|
|
347
|
+
|
|
348
|
+
/** Seconds in a day, which is the unit the whole plan is written in. */
|
|
349
|
+
const DAY = 24 * 60 * 60
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* How long an owed passer waits for watched time before it crosses, at least
|
|
353
|
+
* and up to that again.
|
|
354
|
+
*
|
|
355
|
+
* Not zero, and this is the difference between a scene and a trick. A boat that
|
|
356
|
+
* arrived the instant the last window closed would be a boat that had been
|
|
357
|
+
* waiting for you, and everything else in this water is indifferent to whether
|
|
358
|
+
* anybody is there. A minute or two of ordinary sea first, and it is weather.
|
|
359
|
+
*/
|
|
360
|
+
const WAIT_LEAST = 35
|
|
361
|
+
|
|
362
|
+
const WAIT_SPAN = 130
|
|
363
|
+
|
|
364
|
+
/** Watched seconds between the end of one crossing and the start of the next. */
|
|
365
|
+
const APART = 90
|
|
366
|
+
|
|
367
|
+
const MIN_SPAN = 1
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The passers of a piece of water.
|
|
371
|
+
*
|
|
372
|
+
* `eager` throws the day's plan away and crosses one after another, and it
|
|
373
|
+
* exists for previews and for tests. Nothing should pass it in earnest: a boat
|
|
374
|
+
* you get for turning the screen on is a boat on a schedule, and the whole
|
|
375
|
+
* claim of this file is that there is no schedule.
|
|
376
|
+
*/
|
|
377
|
+
export function createPassers(options: PassersOptions): Passers {
|
|
378
|
+
const random = makeRandom(options.seed ^ 0x30b7)
|
|
379
|
+
const kinds = options.kinds ?? (["boat", "sonar", "submarine"] as const)
|
|
380
|
+
const eager = options.eager ?? false
|
|
381
|
+
|
|
382
|
+
let width = Math.max(MIN_SPAN, options.width)
|
|
383
|
+
let height = Math.max(MIN_SPAN, options.height)
|
|
384
|
+
|
|
385
|
+
const passing: Passer[] = []
|
|
386
|
+
const takes = new Map<PassKind, number>()
|
|
387
|
+
|
|
388
|
+
/** The churn in the water, and which arm of the wake the next puff is on. */
|
|
389
|
+
const wake: Froth[] = []
|
|
390
|
+
let since = 0
|
|
391
|
+
let arm = 1
|
|
392
|
+
|
|
393
|
+
/** What the water is being told to do this tick; see `Startle`. */
|
|
394
|
+
let felt: Startle | null = null
|
|
395
|
+
|
|
396
|
+
/** The day the plan below was drawn for, as `daySeed` counts a day. */
|
|
397
|
+
let planned = 0
|
|
398
|
+
|
|
399
|
+
/** When each kind is due today, in seconds into the day, or null for not today. */
|
|
400
|
+
const due = new Map<PassKind, number | null>()
|
|
401
|
+
|
|
402
|
+
/** How much watched time each still owes before it crosses; see `WAIT_LEAST`. */
|
|
403
|
+
const waits = new Map<PassKind, number>()
|
|
404
|
+
|
|
405
|
+
/** What has already crossed today, and how long since the last one did. */
|
|
406
|
+
const gone = new Set<PassKind>()
|
|
407
|
+
let apart = APART
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Draw a day's plan.
|
|
411
|
+
*
|
|
412
|
+
* A pure function of the seed and the date, on its own generator, so every
|
|
413
|
+
* screen of a machine agrees about what today holds without any of them
|
|
414
|
+
* saying so, and tomorrow is a different day rather than the next few numbers
|
|
415
|
+
* of this one. Both rolls are spent whatever the first one says, because a
|
|
416
|
+
* plan whose shape depended on its own outcome would put the kinds in a
|
|
417
|
+
* queue: a day without a submarine would hand the boat the submarine's hour.
|
|
418
|
+
*/
|
|
419
|
+
function plan(day: number): void {
|
|
420
|
+
planned = day
|
|
421
|
+
gone.clear()
|
|
422
|
+
|
|
423
|
+
const dice = makeRandom(stir(options.seed ^ day ^ 0x51ed) | 0)
|
|
424
|
+
|
|
425
|
+
for (const kind of kinds) {
|
|
426
|
+
const happens = dice() < HABITS[kind].chance
|
|
427
|
+
const at = Math.floor(dice() * DAY)
|
|
428
|
+
due.set(kind, happens ? at : null)
|
|
429
|
+
waits.set(kind, WAIT_LEAST + dice() * WAIT_SPAN)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function launch(kind: PassKind): void {
|
|
434
|
+
const habit = HABITS[kind]
|
|
435
|
+
const facing = random() < 0.5 ? -1 : 1
|
|
436
|
+
const scale = width * (habit.sizeLeast + random() * habit.sizeSpan)
|
|
437
|
+
const deep = habit.deepLeast + random() * habit.deepSpan
|
|
438
|
+
|
|
439
|
+
takes.set(kind, habit.takeLeast + random() * habit.takeSpan)
|
|
440
|
+
gone.add(kind)
|
|
441
|
+
|
|
442
|
+
// Owing a puff from the first tick, so a hull is churning by the time the
|
|
443
|
+
// frame has it rather than a tenth of a second later.
|
|
444
|
+
since = FROTH_GAP
|
|
445
|
+
|
|
446
|
+
// A ping never comes from the middle of the water, because a ring centred
|
|
447
|
+
// in the picture is a target reticle rather than something out there. It
|
|
448
|
+
// comes from a side, high up, which is where anything looking for you is.
|
|
449
|
+
// The two that travel start off the picture entirely; see `OFFING`.
|
|
450
|
+
const side = kind === "sonar" ? (facing > 0 ? 1 - PING_EDGE : PING_EDGE) : 0
|
|
451
|
+
|
|
452
|
+
passing.push({
|
|
453
|
+
along: 0,
|
|
454
|
+
facing,
|
|
455
|
+
kind,
|
|
456
|
+
scale,
|
|
457
|
+
weight: 0,
|
|
458
|
+
x: width * side,
|
|
459
|
+
y: height * deep,
|
|
460
|
+
})
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Which kind, if any, is owed a crossing right now.
|
|
465
|
+
*
|
|
466
|
+
* One at a time and a gap between them, because two of these at once is a
|
|
467
|
+
* busy sea and this is a scene whose whole argument is that almost nothing
|
|
468
|
+
* happens in it. Everything owed keeps standing until it gets its turn.
|
|
469
|
+
*/
|
|
470
|
+
function owed(dt: number, clock: number): PassKind | null {
|
|
471
|
+
if (passing.length > 0 || apart > 0) return null
|
|
472
|
+
|
|
473
|
+
// The one that has been waiting longest, rather than the first in the list.
|
|
474
|
+
// Order in a list is not a reason to go first, and the kind that would
|
|
475
|
+
// always lose that argument is the submarine: it is owed on one day in
|
|
476
|
+
// seven, and on those days it would queue behind the two that are owed
|
|
477
|
+
// every day.
|
|
478
|
+
let next: PassKind | null = null
|
|
479
|
+
let oldest = Number.POSITIVE_INFINITY
|
|
480
|
+
|
|
481
|
+
for (const kind of kinds) {
|
|
482
|
+
if (gone.has(kind)) continue
|
|
483
|
+
|
|
484
|
+
const at = due.get(kind)
|
|
485
|
+
if (at == null || clock < at || at >= oldest) continue
|
|
486
|
+
|
|
487
|
+
oldest = at
|
|
488
|
+
next = kind
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (!next) return null
|
|
492
|
+
|
|
493
|
+
const left = (waits.get(next) ?? 0) - dt
|
|
494
|
+
waits.set(next, left)
|
|
495
|
+
return left <= 0 ? next : null
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Let go of a puff of churn at the stern, if one is due.
|
|
500
|
+
*
|
|
501
|
+
* Alternate arms rather than one either side at every beat. Two puffs born at
|
|
502
|
+
* the same instant on the same line are a mirror, and a mirrored wake is a
|
|
503
|
+
* drawing of a wake; taking turns puts every puff on its own clock and the
|
|
504
|
+
* two arms come out of step the way water is. How far off its arm each one
|
|
505
|
+
* goes is its own business; see `Froth.side`.
|
|
506
|
+
*/
|
|
507
|
+
function shed(one: Passer, churn: Churn, dt: number): void {
|
|
508
|
+
since += dt
|
|
509
|
+
if (since < FROTH_GAP) return
|
|
510
|
+
|
|
511
|
+
since = 0
|
|
512
|
+
arm = -arm
|
|
513
|
+
|
|
514
|
+
if (wake.length >= FROTH_MOST) wake.shift()
|
|
515
|
+
|
|
516
|
+
wake.push({
|
|
517
|
+
age: 0,
|
|
518
|
+
r: one.scale * churn.force * FROTH_SEED * (0.6 + random() * 0.8),
|
|
519
|
+
scale: one.scale * churn.force,
|
|
520
|
+
side: arm * (0.3 + random() * 0.7),
|
|
521
|
+
x: one.x + one.facing * one.scale * churn.x + (random() - 0.5) * one.scale * 0.09,
|
|
522
|
+
y: one.y + one.scale * churn.y + (random() - 0.5) * one.scale * 0.05,
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* How hard a ping is being felt, from the front of it.
|
|
528
|
+
*
|
|
529
|
+
* The first ring is the one out in front, so it is the one the fish meet.
|
|
530
|
+
* The rings behind it are a sound arriving twice more and the water has
|
|
531
|
+
* already been told what it needed to know.
|
|
532
|
+
*/
|
|
533
|
+
function heard(one: Passer): Startle | null {
|
|
534
|
+
const front = ringAt(one.along, PING_RINGS[0] ?? 0)
|
|
535
|
+
if (!front) return null
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
force: PING_FELT * front.weight * one.weight,
|
|
539
|
+
reach: front.reach * one.scale,
|
|
540
|
+
x: one.x,
|
|
541
|
+
y: one.y,
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** Carry the churn on: swell, open away from the line, thin, and go. */
|
|
546
|
+
function settle(dt: number): void {
|
|
547
|
+
for (let at = wake.length - 1; at >= 0; at--) {
|
|
548
|
+
const puff = wake[at]
|
|
549
|
+
if (!puff) continue
|
|
550
|
+
|
|
551
|
+
puff.age += dt / FROTH_LIFE
|
|
552
|
+
if (puff.age >= 1) {
|
|
553
|
+
wake.splice(at, 1)
|
|
554
|
+
continue
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
puff.r += puff.scale * FROTH_SWELL * dt
|
|
558
|
+
puff.y += puff.side * puff.scale * FROTH_OPEN * (1 - puff.age) * dt
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return {
|
|
563
|
+
passing,
|
|
564
|
+
|
|
565
|
+
resize(nextWidth, nextHeight) {
|
|
566
|
+
width = Math.max(MIN_SPAN, nextWidth)
|
|
567
|
+
height = Math.max(MIN_SPAN, nextHeight)
|
|
568
|
+
// Whatever was crossing is dropped rather than rescaled. A boat that
|
|
569
|
+
// changed length halfway over would be worse than no boat, and a window
|
|
570
|
+
// being dragged is the one moment nobody is watching the water anyway.
|
|
571
|
+
// It stays spent for the day: half a boat is the day's boat.
|
|
572
|
+
passing.length = 0
|
|
573
|
+
wake.length = 0
|
|
574
|
+
felt = null
|
|
575
|
+
},
|
|
576
|
+
|
|
577
|
+
get startle() {
|
|
578
|
+
return felt
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
step(seconds, now = new Date()) {
|
|
582
|
+
const dt = Math.min(Math.max(seconds, 0), 0.1)
|
|
583
|
+
const day = now.getFullYear() * 10000 + (now.getMonth() + 1) * 100 + now.getDate()
|
|
584
|
+
if (day !== planned) plan(day)
|
|
585
|
+
|
|
586
|
+
apart = Math.max(0, apart - dt)
|
|
587
|
+
|
|
588
|
+
if (eager) {
|
|
589
|
+
if (passing.length === 0) {
|
|
590
|
+
gone.clear()
|
|
591
|
+
launch(kinds[Math.floor(random() * kinds.length)] ?? "boat")
|
|
592
|
+
}
|
|
593
|
+
} else {
|
|
594
|
+
const next = owed(dt, now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds())
|
|
595
|
+
if (next) launch(next)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
for (let at = passing.length - 1; at >= 0; at--) {
|
|
599
|
+
const one = passing[at]
|
|
600
|
+
if (!one) continue
|
|
601
|
+
|
|
602
|
+
one.along += dt / Math.max(takes.get(one.kind) ?? 1, 0.001)
|
|
603
|
+
if (one.along >= 1) {
|
|
604
|
+
passing.splice(at, 1)
|
|
605
|
+
apart = APART
|
|
606
|
+
continue
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// In at one end and out at the other, so nothing ever appears. A hull
|
|
610
|
+
// that popped into being at the edge of the picture would say the
|
|
611
|
+
// picture has an edge, which is the one thing the water must not admit.
|
|
612
|
+
one.weight = Math.min(1, one.along / FADE, (1 - one.along) / FADE)
|
|
613
|
+
|
|
614
|
+
if (one.kind === "sonar") continue
|
|
615
|
+
const from = -OFFING * width
|
|
616
|
+
const to = width * (1 + OFFING)
|
|
617
|
+
one.x = one.facing > 0 ? from + one.along * (to - from) : to - one.along * (to - from)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// What the water is doing about it, after everything has been moved: the
|
|
621
|
+
// churn comes off a stern that is already where this tick left it, and a
|
|
622
|
+
// fish is answering the boat it can see rather than the one from last
|
|
623
|
+
// frame. There is only ever one passer, so the last word is the only one.
|
|
624
|
+
felt = null
|
|
625
|
+
|
|
626
|
+
for (const one of passing) {
|
|
627
|
+
const churn = CHURN[one.kind]
|
|
628
|
+
if (churn.force <= 0) {
|
|
629
|
+
felt = heard(one)
|
|
630
|
+
continue
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
shed(one, churn, dt)
|
|
634
|
+
felt = {
|
|
635
|
+
force: churn.force * one.weight,
|
|
636
|
+
reach: one.scale * HULL_FELT,
|
|
637
|
+
x: one.x,
|
|
638
|
+
y: one.y,
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
settle(dt)
|
|
643
|
+
},
|
|
644
|
+
wake,
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* A boat, in a hull two units long, sitting on its own waterline at the origin.
|
|
650
|
+
*
|
|
651
|
+
* Seen from under it, which is the only view this water has. That means no
|
|
652
|
+
* superstructure and no detail above the line: what there is to draw is a hull
|
|
653
|
+
* from below, and a hull from below is a long shallow curve with a bow at one
|
|
654
|
+
* end. The `+x` end is the bow.
|
|
655
|
+
*
|
|
656
|
+
* Two propellers rather than one, because one on the centreline reads as a
|
|
657
|
+
* rudder. They say the thing is under power, and the wake coming off them says
|
|
658
|
+
* the water knows it; like everything else under a unit here they are a path
|
|
659
|
+
* rather than a shape a renderer would round to whole pixels before scaling it
|
|
660
|
+
* up.
|
|
661
|
+
*/
|
|
662
|
+
export const HULL = "M-1 0 L1 0 L0.82 0.2 Q0.1 0.34 -0.72 0.28 Q-0.94 0.24 -1 0 Z"
|
|
663
|
+
|
|
664
|
+
export const SCREWS =
|
|
665
|
+
"M-0.78 0.15 A0.07 0.07 0 1 1 -0.78 0.29 A0.07 0.07 0 1 1 -0.78 0.15 Z " +
|
|
666
|
+
"M-0.62 0.19 A0.07 0.07 0 1 1 -0.62 0.33 A0.07 0.07 0 1 1 -0.62 0.19 Z"
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* A submarine, two units long, on its own centreline at the origin.
|
|
670
|
+
*
|
|
671
|
+
* Seen from the side, because unlike the boat it is down here in the water with
|
|
672
|
+
* everything else, and a thing at your own depth is a thing you see the flank
|
|
673
|
+
* of. The `+x` end is the bow.
|
|
674
|
+
*
|
|
675
|
+
* One path, in six pieces that overlap: hull, sail, the periscope out of the
|
|
676
|
+
* top of it, the fairwater planes across it, and a fin above and below the
|
|
677
|
+
* stern. It has to be filled with a winding rule rather than an odd-even one,
|
|
678
|
+
* which is the whole reason this note exists: under odd-even the sail would cut
|
|
679
|
+
* a hole out of the hull where it sits on it, and the drawing would come apart
|
|
680
|
+
* exactly where it is meant to join. Every subpath is wound the same way so a
|
|
681
|
+
* winding fill sees one silhouette.
|
|
682
|
+
*
|
|
683
|
+
* The periscope is four hundredths of a unit wide and nobody will ever
|
|
684
|
+
* knowingly see it. It is there because the difference between a shape that
|
|
685
|
+
* reads as a submarine and a shape that reads as a cigar is entirely in what
|
|
686
|
+
* sticks out of it.
|
|
687
|
+
*/
|
|
688
|
+
export const SUBMARINE =
|
|
689
|
+
"M-0.92 -0.15 L0.4 -0.17 Q0.86 -0.16 1 0 Q0.86 0.16 0.4 0.17 L-0.92 0.15 " +
|
|
690
|
+
"Q-1 0.1 -1 0 Q-1 -0.1 -0.92 -0.15 Z " +
|
|
691
|
+
"M0.02 -0.16 L0.08 -0.44 L0.3 -0.44 L0.34 -0.16 Z " +
|
|
692
|
+
"M0.16 -0.44 L0.16 -0.62 L0.2 -0.62 L0.2 -0.44 Z " +
|
|
693
|
+
"M-0.02 -0.38 L0.42 -0.38 L0.42 -0.33 L-0.02 -0.33 Z " +
|
|
694
|
+
"M-0.74 -0.14 L-0.96 -0.4 L-0.84 -0.4 L-0.62 -0.14 Z " +
|
|
695
|
+
"M-0.62 0.14 L-0.84 0.4 L-0.96 0.4 L-0.74 0.14 Z"
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Its screw, as two blades on a shaft astern.
|
|
699
|
+
*
|
|
700
|
+
* Apart from the hull for the same reason the boat's is: it is the one part
|
|
701
|
+
* that says the thing is under power rather than sinking, and a renderer may
|
|
702
|
+
* want to weight it differently or leave it out at a size where it would be a
|
|
703
|
+
* smudge.
|
|
704
|
+
*/
|
|
705
|
+
export const SUB_SCREW =
|
|
706
|
+
"M-1 -0.02 L-1.14 -0.14 L-1.18 -0.06 L-1.04 0 L-1.18 0.06 L-1.14 0.14 L-1 0.02 Z"
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* How many rings a ping puts out, and where in its life each one starts.
|
|
710
|
+
*
|
|
711
|
+
* Three, staggered, which is what a ping sounds like and therefore what it
|
|
712
|
+
* should look like. One ring is a bubble.
|
|
713
|
+
*/
|
|
714
|
+
export const PING_RINGS: readonly number[] = [0, 0.16, 0.32]
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* How far out a ring has got and how loud it still is, at `along` through the
|
|
718
|
+
* whole ping.
|
|
719
|
+
*
|
|
720
|
+
* The reach eases out and the weight falls off with it, which is a wave losing
|
|
721
|
+
* itself in the water rather than a circle being scaled. Returns null before
|
|
722
|
+
* its ring has started or after it has gone, so a renderer can skip it without
|
|
723
|
+
* knowing the arithmetic.
|
|
724
|
+
*/
|
|
725
|
+
export function ringAt(along: number, offset: number): { reach: number; weight: number } | null {
|
|
726
|
+
const t = (along - offset) / (1 - offset)
|
|
727
|
+
if (t <= 0 || t >= 1) return null
|
|
728
|
+
|
|
729
|
+
return { reach: 1 - (1 - t) ** 2, weight: (1 - t) ** 1.6 }
|
|
730
|
+
}
|