@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/crags.ts ADDED
@@ -0,0 +1,974 @@
1
+ /**
2
+ * What you are looking out from, and what is standing in the distance.
3
+ *
4
+ * Rock near the edges of the picture, and now and then an island. Kept free of
5
+ * the DOM the way the rest of the ornament is, and static the way `seabed.ts`
6
+ * is: nothing here moves, so there is no `step`. Rock that swayed would be a
7
+ * landslide.
8
+ *
9
+ * ## Why a seascape needs somewhere to be
10
+ *
11
+ * The bed gave the water a bottom and the distance gave it a depth, and what is
12
+ * still missing is a place. Open water with animals in it is a picture of
13
+ * animals; the same water with a wall at the edge of the frame is a picture of
14
+ * being somewhere, and the difference costs one silhouette.
15
+ *
16
+ * It works because of what near rock does to everything else. A shape a metre
17
+ * from your mask is black, hard-edged and out of focus, and it is the only
18
+ * thing in the scene that can be. Put one at the edge and every other distance
19
+ * in the picture is measured against it, so the murk becomes far rather than
20
+ * merely dim. It is the same trick a landscape painter uses with a tree in the
21
+ * foreground, and it is the difference between looking at water and being in
22
+ * it.
23
+ *
24
+ * ## Most days there is none
25
+ *
26
+ * The framing is drawn from the seed, which on a desktop is the day's, so a
27
+ * machine gets one framing today and a different one tomorrow and every screen
28
+ * on it agrees. Open water is much the commonest, and it has to be: a cave you
29
+ * are in every day is a room, and the whole worth of this is that some mornings
30
+ * the water has a shape to it that it did not have yesterday.
31
+ *
32
+ * The four are one thing at different strengths. A shoulder is one rock at one
33
+ * edge, which reads as swimming past something. Hills are low ground close by
34
+ * at both edges, which reads as a channel. A cave is both walls and a roof, and
35
+ * it is the only one that says you are inside anything.
36
+ *
37
+ * ## The island is the other end of the same thought
38
+ *
39
+ * The ground in `seabed.ts` climbs as it goes back and stops at the horizon
40
+ * because that is as far as a seabed goes. An island is what happens when it
41
+ * does not stop: it carries on up until it is out of the water, and what the
42
+ * water gets is a shore. It also gives the sun something to go behind, which is
43
+ * the one thing in this scene that has never been occluded by anything.
44
+ *
45
+ * All of one is its outline, so all of the work is there, and the outline it
46
+ * used to have was an arc. One curve of one width, taller than the box every
47
+ * time, which put its sides in the water at the angle a sea cliff stands at and
48
+ * its summit somewhere above the ceiling: a wall across the back of the picture
49
+ * with a smooth edge, the same wall every day it turned up. Ground reads as
50
+ * ground for reasons that are all about that edge, and there are three of them.
51
+ *
52
+ * * **It has a size, and the size is rolled.** The pitch of a flank is rolled
53
+ * as an angle rather than falling out of a width, so nothing leaves the
54
+ * water at seventy degrees by accident, and the two sides are stretched
55
+ * apart because an island with matching flanks is a cone. See
56
+ * `ISLE_PITCH_LEAST`.
57
+ * * **It has shoulders.** A summit with lesser masses standing on its flanks,
58
+ * taken together by whichever is higher, so the run from the water to the
59
+ * top has a saddle in it and sometimes an outlier standing off the shore.
60
+ * See `SPURS_LEAST`.
61
+ * * **It has weather on it.** Octaves of the field folded at nought, which is
62
+ * the shape water leaves in a hill: edges along the ridges and curves along
63
+ * the valleys. Added in the island's own units rather than scaled by the
64
+ * height it stands at, because scaled it dies at the waterline and leaves
65
+ * the shore the one clean tangent in the picture. See `ISLE_RELIEF`.
66
+ *
67
+ * ## Rock is not a black rectangle
68
+ *
69
+ * Four things stop it being one, and the first two are about the edge.
70
+ *
71
+ * * **It is bedded.** Two octaves of noise give a lumpy edge, and a lumpy
72
+ * edge is a cloud. What rock has that a cloud has not is flats and steps,
73
+ * because it breaks along its beds, so another field is quantised into
74
+ * shelves and mixed in. See `LEDGE`.
75
+ * * **It has masses of its own.** Noise is rough everywhere by the same
76
+ * amount, which is a coastline. A wall has a few buttresses and long quiet
77
+ * stretches between them, and that is `OUTCROPS_LEAST`.
78
+ *
79
+ * The other two are about what the rock is and what is on it.
80
+ *
81
+ * * **There is rock behind the rock.** Every side is two masses at two
82
+ * distances, because a single silhouette has nothing to be in front of and
83
+ * the eye reads it as a border drawn on the picture rather than as a place.
84
+ * * **It is alive.** `perches` are where something grows, and they come in
85
+ * colonies rather than scattered, because nothing on a reef is evenly
86
+ * spaced: growth goes where growth already is. A face is bare for a stretch
87
+ * and then crowded, and it is the crowded parts that the eye reads the
88
+ * rock's own shape off. Bare rock at this distance is a shape somebody
89
+ * forgot to finish.
90
+ */
91
+
92
+ import { gathered, type Twig } from "./flora.ts"
93
+ import { makeNoise2, makeRandom, stir } from "./perlin.ts"
94
+ import type { Point } from "./seabed.ts"
95
+
96
+ export type Framing = "cave" | "hills" | "open" | "shoulder"
97
+
98
+ /**
99
+ * What a thing growing on rock is.
100
+ *
101
+ * Not a species, a silhouette. At the size these are drawn the only question
102
+ * the eye can answer is what shape it is against the rock, and four shapes is
103
+ * as many as a wall can carry before they stop reading as different things.
104
+ * A fan stands square across the current, a frond hangs and moves, a sponge is
105
+ * a lump with a body, and a tuft is a scruff of short lines.
106
+ *
107
+ * The lump is the important one, for the reason `flora.ts` gives about its
108
+ * boulder coral: everything else here is a line, and a wall of lines is a wall
109
+ * of hairs however many are on it.
110
+ */
111
+ export type Growth = "fan" | "frond" | "sponge" | "tuft"
112
+
113
+ /** Somewhere on a face of rock where something is growing. */
114
+ export interface Perch {
115
+ readonly kind: Growth
116
+ /** Which way it grows, in radians. Out of the rock, whatever the rock does. */
117
+ readonly lean: number
118
+ /** How big, as a multiplier a renderer scales its own drawing by. */
119
+ readonly size: number
120
+ readonly x: number
121
+ readonly y: number
122
+ }
123
+
124
+ export interface Crag {
125
+ /** How far forward it stands. Near, which is why it is nearly black. */
126
+ readonly depth: number
127
+ /** Which side of the box it is anchored to, so a renderer can close it off. */
128
+ readonly edge: "left" | "right" | "top"
129
+ /** Its face, from one end to the other. The edge behind it is the renderer's. */
130
+ readonly outline: readonly Point[]
131
+ readonly perches: readonly Perch[]
132
+ }
133
+
134
+ /** Ground that did not stop at the horizon. */
135
+ export interface Isle {
136
+ readonly depth: number
137
+ /** Its outline, left to right, running off the top of the box in the middle. */
138
+ readonly outline: readonly Point[]
139
+ }
140
+
141
+ export interface Crags {
142
+ readonly framing: Framing
143
+ readonly isle: Isle | null
144
+ resize(width: number, height: number, floor: (x: number, depth?: number) => number): void
145
+ readonly rocks: readonly Crag[]
146
+ }
147
+
148
+ export interface CragsOptions {
149
+ /** The ground at x at a distance; `seabed.floorAt`. An island stands on it. */
150
+ floor: (x: number, depth?: number) => number
151
+ /** Force a framing rather than drawing one. For previews and for tests. */
152
+ framing?: Framing
153
+ height: number
154
+ /** Force the island on or off, the same way. */
155
+ isle?: boolean
156
+ seed: number
157
+ width: number
158
+ }
159
+
160
+ /**
161
+ * How likely each framing is, against the others.
162
+ *
163
+ * Weighted hard towards nothing at all. Everything in this file is a large dark
164
+ * shape across part of the picture, which is a strong thing to do to a
165
+ * background and not a thing to do most days; the shoulder is the mild one and
166
+ * gets most of what is left. A cave on one morning in ten is a morning somebody
167
+ * notices.
168
+ */
169
+ const FRAMINGS: Readonly<Record<Framing, number>> = {
170
+ cave: 1,
171
+ hills: 2,
172
+ open: 6,
173
+ shoulder: 3,
174
+ }
175
+
176
+ /** How likely an island is on a day that has room for one; see `isleAllowed`. */
177
+ const ISLE_ODDS = 0.22
178
+
179
+ /**
180
+ * How far into the picture each framing's rock comes, as shares of the width.
181
+ *
182
+ * All of them leave most of the frame open, which is the discipline the whole
183
+ * file needs. Rock taking half the picture is a picture of rock, and what is
184
+ * wanted is a picture of water with rock at the edge of it. The cave is not the
185
+ * widest of the three for that reason: it already has a roof, and a mouth with
186
+ * both is closed enough.
187
+ */
188
+ const REACHES: Readonly<Record<Framing, number>> = {
189
+ cave: 0.12,
190
+ hills: 0.15,
191
+ open: 0,
192
+ shoulder: 0.12,
193
+ }
194
+
195
+ /**
196
+ * How far off straight out of the box a face may point the things on it.
197
+ *
198
+ * A perch reads the slope under it, which is the whole point of them: growth
199
+ * that ignored the rock would say the rock is a backdrop. A face steep enough
200
+ * turns square-to-the-face into square-to-the-box, though, and a little past it
201
+ * is a fan growing back into the wall it is standing on. Held short of a right
202
+ * angle, a steep face still lays its growth hard along itself and none of it
203
+ * disappears.
204
+ */
205
+ const LEAN_MOST = Math.PI * 0.44
206
+
207
+ /** How wide a hill is at the foot of the box, whatever height it starts at. */
208
+ const HILL_PEAK = 1.22
209
+
210
+ /** How far down a cave's roof hangs, as a share of the height. */
211
+ const ROOF_DROP = 0.1
212
+
213
+ /** How far past the ends of the box an outline runs, so no edge shows a seam. */
214
+ const OVERHANG = 40
215
+
216
+ /** How finely a face is cut. Fine enough that the noise in it is rock. */
217
+ const FACE_STEPS = 64
218
+
219
+ /**
220
+ * How much of a face is its own noise, and how many features fit along it.
221
+ *
222
+ * Most of it. A wall whose edge is a smooth curve is a shape rather than a
223
+ * rock, and the whole job of the near silhouette is to be the one hard, broken,
224
+ * unarguable thing in a picture where everything else is soft.
225
+ */
226
+ const ROUGH = 0.55
227
+
228
+ const ROUGH_CELLS = 3.4
229
+
230
+ /** A second, finer roughness, for the bites a smooth wall never has. */
231
+ const BITE = 0.18
232
+
233
+ const BITE_CELLS = 11
234
+
235
+ /**
236
+ * How much of a face is terraced, into how many shelves, and how broadly.
237
+ *
238
+ * The third octave, and it is not noise. Noise of any two frequencies gives a
239
+ * lumpy edge, and a lumpy edge is a cloud: what rock has that a cloud does not
240
+ * is flats and steps, because it is bedded and it breaks along its beds. So one
241
+ * slow field is quantised into shelves and mixed in, and what comes out has
242
+ * ledges an eye can rest on and drops between them.
243
+ */
244
+ const LEDGE = 0.26
245
+
246
+ const LEDGE_CELLS = 1.5
247
+
248
+ const LEDGE_STEPS = 4
249
+
250
+ /**
251
+ * How many lumps stand out of a face, and how big.
252
+ *
253
+ * Buttresses and boulders. The octaves make an edge that is rough everywhere by
254
+ * the same amount, which is a coastline; what a wall actually has is a few
255
+ * masses of its own and long quiet stretches between them. These are those
256
+ * masses, and they are what stops the two sides of a cave being one edge drawn
257
+ * twice.
258
+ */
259
+ const OUTCROPS_LEAST = 2
260
+
261
+ const OUTCROPS_SPAN = 4
262
+
263
+ const OUTCROP_SPAN_LEAST = 0.08
264
+
265
+ const OUTCROP_SPAN_SPAN = 0.2
266
+
267
+ const OUTCROP_RISE_LEAST = 0.15
268
+
269
+ const OUTCROP_RISE_SPAN = 0.34
270
+
271
+ /**
272
+ * How many masses stand on one side of the picture, and how far back the
273
+ * furthest of them is.
274
+ *
275
+ * A side is not one wall. A single silhouette, however broken, is a cut-out:
276
+ * there is nothing for it to be in front of, so the eye reads it as a border
277
+ * drawn on the picture rather than as a place. Put a paler mass behind it and
278
+ * the near one becomes rock with more rock behind it, which is what anybody who
279
+ * has swum along a wall has actually seen.
280
+ *
281
+ * How many is a roll, like everything else here, and the two sides roll
282
+ * separately. One layer is a bank you are passing; four is a canyon with its
283
+ * own distance in it, and the day that turns up four is a different day. A
284
+ * fixed number would be the same wall every time with the same amount of
285
+ * depth in it, which is the thing this file exists not to be.
286
+ */
287
+ const LAYERS_LEAST = 1
288
+
289
+ const LAYERS_MOST = 4
290
+
291
+ const FURTHEST = 0.28
292
+
293
+ /** How much broader the furthest mass of a side is than the nearest. */
294
+ const BACKING_WIDER = 1.3
295
+
296
+ /** How far forward the near rock stands, and how far apart two pieces of it are. */
297
+ const NEAR = 1
298
+
299
+ const BEHIND = 0.86
300
+
301
+ /**
302
+ * How the four kinds of growth are shared out.
303
+ *
304
+ * Tufts are the commonest because they are the cheapest thing to look at and
305
+ * what most rock is actually furred with. Fans are next, because a fan is the
306
+ * one that reads as a reef from any distance at all.
307
+ */
308
+ const GROWTHS: Readonly<Record<Growth, number>> = {
309
+ fan: 3,
310
+ frond: 3,
311
+ sponge: 2,
312
+ tuft: 4,
313
+ }
314
+
315
+ /**
316
+ * What each of the four is, as strokes in a drawing that grows up from nothing.
317
+ *
318
+ * Up, in a space about thirty across, which is the coral's arrangement in
319
+ * `flora.ts` and is that way so a renderer can hand these to whatever already
320
+ * draws that. A perch says which way it is pointing and how big it is; the
321
+ * drawing says nothing about either, so the same fan is a fan on a roof and on
322
+ * a wall and on the underside of an overhang.
323
+ *
324
+ * The widths are what tells them apart as much as the paths do. A sponge is two
325
+ * strokes so fat they close into a lump, and the fan is thin enough that what
326
+ * reads is the lattice and not the branches.
327
+ */
328
+ export const SPRIGS: Readonly<Record<Growth, readonly Twig[]>> = {
329
+ fan: gathered([
330
+ { d: "M0 0 C1 -6 0 -10 -1 -15", width: 2.6 },
331
+ { d: "M-1 -15 C-5 -19 -9 -21 -13 -27", width: 1.8 },
332
+ { d: "M-1 -15 C-1 -21 -1 -25 -2 -31", width: 1.8 },
333
+ { d: "M-1 -15 C3 -19 7 -21 10 -26", width: 1.8 },
334
+ { d: "M-6 -19 C-9 -23 -12 -24 -16 -24", width: 1.2 },
335
+ { d: "M4 -18 C7 -22 11 -23 14 -22", width: 1.2 },
336
+ { d: "M-2 -22 C-4 -26 -5 -29 -6 -33", width: 1.2 },
337
+ { d: "M0 -22 C2 -26 4 -29 6 -32", width: 1.2 },
338
+ ]),
339
+ frond: gathered([
340
+ { d: "M0 0 C4 -8 3 -16 6 -25 C7 -29 6 -31 4 -33", width: 2.2 },
341
+ { d: "M0 0 C-3 -7 -4 -14 -3 -21 C-3 -25 -4 -27 -6 -29", width: 1.8 },
342
+ { d: "M1 -6 C-4 -10 -7 -15 -8 -21", width: 1.4 },
343
+ ]),
344
+ sponge: gathered([
345
+ { d: "M-3 -4 C-4 -9 -3 -12 -2 -14", width: 9 },
346
+ { d: "M4 -3 C5 -7 5 -10 4 -12", width: 7 },
347
+ ]),
348
+ tuft: gathered([
349
+ { d: "M0 0 L-8 -11", width: 1.6 },
350
+ { d: "M0 0 L-4 -15", width: 1.6 },
351
+ { d: "M0 0 L1 -17", width: 1.6 },
352
+ { d: "M0 0 L6 -14", width: 1.6 },
353
+ { d: "M0 0 L10 -9", width: 1.6 },
354
+ ]),
355
+ }
356
+
357
+ /**
358
+ * How many colonies stand on a face, per thousand px of it, and how many things
359
+ * are in one.
360
+ *
361
+ * Colonies rather than a scatter, and this is the whole of what makes a wall
362
+ * look inhabited rather than stippled. Nothing on a reef is evenly spaced:
363
+ * growth goes where growth already is, so a face is bare for a stretch and then
364
+ * crowded, and the crowded parts are what the eye reads the rock's own shape
365
+ * off. Evenly sown, the same count reads as a texture map.
366
+ */
367
+ const COLONIES_PER_K = 11
368
+
369
+ const COLONY_LEAST = 3
370
+
371
+ const COLONY_MOST = 9
372
+
373
+ /** How far a colony spreads along the face, as a share of the whole face. */
374
+ const COLONY_SPREAD = 0.055
375
+
376
+ /**
377
+ * The ends of a growth's size, and how far it may lean off the rock's normal.
378
+ *
379
+ * A wide range on purpose. Everything the same size is a pattern, and the small
380
+ * ones are what fill in between the large ones so the face is busy rather than
381
+ * dotted.
382
+ */
383
+ const PERCH_SMALLEST = 0.35
384
+
385
+ const PERCH_LARGEST = 1.9
386
+
387
+ const PERCH_WANDER = 0.6
388
+
389
+ /** How far back an island stands, which is the furthest thing there is. */
390
+ const ISLE_DEEP = 0.04
391
+
392
+ /**
393
+ * How high an island stands above the ground it grew out of, as a share of the
394
+ * box, and how the roll is weighted.
395
+ *
396
+ * It used to be taller than the box every time, so the summit was always out of
397
+ * the frame and what the picture held was a flank crossing it corner to corner.
398
+ * That is one island, drawn at one size, and the eye reads it as a curtain
399
+ * somebody hung at the back rather than as land. Most of them are small enough
400
+ * to see the top of now, which is the thing that makes an island an island, and
401
+ * the roll is weighted towards the low end so the one that walks out of the top
402
+ * of the box is the exception it was meant to be.
403
+ */
404
+ const ISLE_RISE_LEAST = 0.1
405
+
406
+ const ISLE_RISE_SPAN = 0.78
407
+
408
+ const ISLE_RISE_BIAS = 2.4
409
+
410
+ /**
411
+ * The steepest pitch a flank takes, in radians, how unevenly the two sides
412
+ * share it, and how far an island may reach whatever it says.
413
+ *
414
+ * Rolled as an angle and turned into a width, rather than rolled as a width.
415
+ * The complaint the old island answered to is exactly an angle: its sides left
416
+ * the water at seventy degrees and nothing but a sea cliff does that, and the
417
+ * only way to be sure of the angle is to name it. What is named is the steepest
418
+ * part of the flank, which is the middle of it; the shore and the summit are
419
+ * gentler than this by the shape of the curve.
420
+ *
421
+ * The two sides are stretched apart on purpose. An island with matching flanks
422
+ * is a cone, and a cone standing in water is a hat. What weather leaves is one
423
+ * long side and one short one.
424
+ */
425
+ const ISLE_PITCH_LEAST = 0.42
426
+
427
+ const ISLE_PITCH_SPAN = 0.4
428
+
429
+ const ISLE_LOPSIDED = 2
430
+
431
+ const ISLE_WIDEST = 0.6
432
+
433
+ /**
434
+ * How a flank bends between the summit and the shore.
435
+ *
436
+ * A raised cosine is one curve and every island cut from it is the same island.
437
+ * The exponent is what makes them different ground: below one the flank leaves
438
+ * the water steeply and lies over towards the top, which is a headland; above
439
+ * one it comes out of the water as a long apron and steepens behind it, which
440
+ * is a cone. Rolled per side, so a single island can be both, which is what an
441
+ * island with the weather on one side of it is.
442
+ */
443
+ const ISLE_TOE_LEAST = 0.7
444
+
445
+ const ISLE_TOE_SPAN = 1.1
446
+
447
+ /**
448
+ * How many lesser masses stand on an island, and where along the flank.
449
+ *
450
+ * The point of them is that a mountain has shoulders. One curve, however bent,
451
+ * runs from the water to the top without anything happening on the way, and the
452
+ * eye reads an unbroken run as a drawn line however long it is. Each of these is
453
+ * the same shape at a fraction of the size, laid on the slope or out past the
454
+ * foot of it, and taken together with the summit by whichever stands higher. On
455
+ * the slope one is a shoulder with a saddle above it; past the foot it clears
456
+ * the water on its own and the island has an outlier.
457
+ */
458
+ const SPURS_LEAST = 2
459
+
460
+ const SPURS_SPAN = 3
461
+
462
+ const SPUR_OUT_LEAST = 0.4
463
+
464
+ const SPUR_OUT_SPAN = 0.8
465
+
466
+ const SPUR_REACH_LEAST = 0.12
467
+
468
+ const SPUR_REACH_SPAN = 0.28
469
+
470
+ const SPUR_RISE_LEAST = 0.1
471
+
472
+ const SPUR_RISE_SPAN = 0.36
473
+
474
+ /**
475
+ * How much ground an island's own relief takes off and puts on, over how many
476
+ * features of its width, and how the octaves of it fall away.
477
+ *
478
+ * Ridged rather than plain, which is the whole difference between hills and
479
+ * weather. Plain noise is as round at the top as it is at the bottom and what
480
+ * it lays over a slope is dents; ground is the other way up, because water
481
+ * takes the valleys out and leaves the ridges standing, so it comes to edges
482
+ * along the tops and lies in curves along the bottoms. Folded to the same
483
+ * shape at each octave, an island gets spurs, the spurs get gullies, and the
484
+ * gullies get notches, which is the only way an outline says how big the thing
485
+ * it belongs to is.
486
+ *
487
+ * Added to the land in the island's own units rather than scaled by how high
488
+ * the land already is, so it is still there at the waterline. Scaled, it would
489
+ * die exactly where the land meets the water and leave the shore the one clean
490
+ * tangent in the picture. It fades over the last of the rise all the same, so
491
+ * the ends of an island lie down on the ground it stands on instead of stepping
492
+ * off it.
493
+ *
494
+ * How much of it there is never falls below a share of the box, because weather
495
+ * works at its own size rather than at the island's. A notch is the notch that
496
+ * water and frost cut, and it is the same notch on a mountain and on a stack;
497
+ * taken as a share of the summit it would vanish off the small ones, and a
498
+ * small island is the one whose whole outline anybody can see at once.
499
+ */
500
+ const ISLE_RELIEF = 0.34
501
+
502
+ const ISLE_RELIEF_LEAST = 0.065
503
+
504
+ const ISLE_RELIEF_CELLS = 2.6
505
+
506
+ const ISLE_RELIEF_OCTAVES = 5
507
+
508
+ const ISLE_RELIEF_FALL = 0.54
509
+
510
+ const ISLE_RELIEF_STEP = 2.3
511
+
512
+ const ISLE_RELIEF_FOOT = 0.16
513
+
514
+ /**
515
+ * How finely an island's skyline is cut.
516
+ *
517
+ * Finer than it was, and for the same reason the far ranges are: an island runs
518
+ * a long way across a wide screen, and a skyline cut that broadly is a coast
519
+ * made of chords. It carries the relief as well, and the finest octave of that
520
+ * is worth nothing if the outline steps past it.
521
+ */
522
+ const ISLE_STEPS = 320
523
+
524
+ const MIN_SPAN = 1
525
+
526
+ export function createCrags(options: CragsOptions): Crags {
527
+ // Stirred rather than taken as it comes, which nothing else in the ornament
528
+ // bothers with and this file cannot do without. `makeRandom` is an xorshift
529
+ // and a small state takes a few draws to come apart, so counting seeds hand
530
+ // back near-identical opening numbers. Everywhere else that costs nothing,
531
+ // because the first draw is a position among many. Here the first draw is the
532
+ // framing, and the whole of a day's water hangs off it: seeds 1 to 400 came
533
+ // back a shoulder four hundred times.
534
+ const random = makeRandom(stir(options.seed ^ 0x3ca9) | 0)
535
+ const rough = makeNoise2(options.seed ^ 0x9a11)
536
+ const bite = makeNoise2(options.seed ^ 0x0b17)
537
+ const bedding = makeNoise2(options.seed ^ 0x7e33)
538
+ const carve = makeNoise2(options.seed ^ 0xa17e)
539
+
540
+ let width = Math.max(MIN_SPAN, options.width)
541
+ let height = Math.max(MIN_SPAN, options.height)
542
+ let floor = options.floor
543
+
544
+ /** Which framing today is, drawn once and kept through a resize. */
545
+ const framing = options.framing ?? draw()
546
+
547
+ /** Whether today has an island, on its own roll; see `isleAllowed`. */
548
+ const hasIsle = options.isle ?? (isleAllowed(framing) && random() < ISLE_ODDS)
549
+
550
+ function draw(): Framing {
551
+ const kinds = Object.keys(FRAMINGS) as Framing[]
552
+ let total = 0
553
+ for (const kind of kinds) total += FRAMINGS[kind]
554
+
555
+ let at = random() * total
556
+ for (const kind of kinds) {
557
+ at -= FRAMINGS[kind]
558
+ if (at <= 0) return kind
559
+ }
560
+
561
+ return "open"
562
+ }
563
+
564
+ /** A lump standing out of a face: where along it, how far, and how wide. */
565
+ interface Outcrop {
566
+ readonly at: number
567
+ readonly rise: number
568
+ readonly span: number
569
+ }
570
+
571
+ /** A face's own lumps, drawn once so a wall keeps them. */
572
+ function lumps(): Outcrop[] {
573
+ const count = OUTCROPS_LEAST + Math.floor(random() * (OUTCROPS_SPAN + 1))
574
+
575
+ return Array.from({ length: count }, () => ({
576
+ at: random(),
577
+ rise: OUTCROP_RISE_LEAST + random() * OUTCROP_RISE_SPAN,
578
+ span: OUTCROP_SPAN_LEAST + random() * OUTCROP_SPAN_SPAN,
579
+ }))
580
+ }
581
+
582
+ /**
583
+ * How far out the rock stands at one point along its own face, 0 to 1.
584
+ *
585
+ * A shape, three octaves and whatever lumps this face happens to have. The
586
+ * shape is what makes a wall a wall rather than a ridge; the octaves are what
587
+ * stop the shape being drawn; the shelves are what make it rock rather than
588
+ * cloud; and the lumps are what stop the two sides of a cave being one edge
589
+ * drawn twice.
590
+ */
591
+ function face(
592
+ along: number,
593
+ lane: number,
594
+ shape: (at: number) => number,
595
+ outcrops: readonly Outcrop[],
596
+ ): number {
597
+ const swell = rough(along * ROUGH_CELLS, lane)
598
+ const nibble = bite(along * BITE_CELLS, lane + 3.3)
599
+ const shelf = Math.round(bedding(along * LEDGE_CELLS, lane + 8.1) * LEDGE_STEPS) / LEDGE_STEPS
600
+ const grain = 1 + swell * ROUGH + nibble * BITE + shelf * LEDGE
601
+
602
+ let stood = 0
603
+ for (const lump of outcrops) {
604
+ const t = (along - (lump.at - lump.span / 2)) / lump.span
605
+ if (t <= 0 || t >= 1) continue
606
+ stood = Math.max(stood, lump.rise * ((1 - Math.cos(t * Math.PI * 2)) / 2))
607
+ }
608
+
609
+ return Math.max(0, shape(along) * grain + stood)
610
+ }
611
+
612
+ /**
613
+ * How wide a wall is along its own height, which is the whole of the framing.
614
+ *
615
+ * A cave's walls swell at the top and pinch a little below the middle, which
616
+ * is what a swim-through does: you are looking out of a gap rather than
617
+ * between two posts. Hills do the opposite and are ground, so they are
618
+ * nothing at the top and everything at the bottom. A shoulder is one rock
619
+ * going past and is broadest where it happens to be.
620
+ *
621
+ * `sway`, between -1 and 1, is the one thing a wall gets to argue with. It
622
+ * moves where the shape swells to without changing how far it swells, so two
623
+ * masses in a stack pinch at different heights and their edges cross. Walls
624
+ * that all pinched in the same place would be the same wall drawn at three
625
+ * weights, which reads as a shadow under a cut-out rather than as distance.
626
+ */
627
+ function profile(at: number, sway: number): number {
628
+ if (framing === "hills") {
629
+ const foot = 0.44 + sway * 0.1
630
+ return Math.max(0, (at - foot) / (1 - foot)) ** (1.5 + sway * 0.4) * HILL_PEAK
631
+ }
632
+
633
+ if (framing === "cave") return 0.62 + 0.38 * Math.cos(at * Math.PI * 2 - 0.6 + sway * 1.2)
634
+
635
+ return 0.55 + 0.45 * Math.sin(at * Math.PI * 0.9 + 0.5 + sway * 1.4)
636
+ }
637
+
638
+ /**
639
+ * Where something is growing on a face, and which way.
640
+ *
641
+ * The lean is read off the face's own slope rather than set square to the
642
+ * box, so a fan on an overhang hangs off the overhang. That is the whole
643
+ * point of them: growth that ignored the rock it is on would say the rock is
644
+ * a backdrop.
645
+ */
646
+ /** Which kind of thing this one is, drawn on `GROWTHS`. */
647
+ function sort(): Growth {
648
+ const kinds = Object.keys(GROWTHS) as Growth[]
649
+ let total = 0
650
+ for (const kind of kinds) total += GROWTHS[kind]
651
+
652
+ let at = random() * total
653
+ for (const kind of kinds) {
654
+ at -= GROWTHS[kind]
655
+ if (at <= 0) return kind
656
+ }
657
+
658
+ return "tuft"
659
+ }
660
+
661
+ /** One thing growing at a station of a face, square to the face there. */
662
+ function root(outline: readonly Point[], at: number, out: number, away: number): Perch | null {
663
+ const steps = outline.length
664
+ const on = Math.max(1, Math.min(steps - 2, at))
665
+ const here = outline[on]
666
+ const back = outline[on - 1]
667
+ const next = outline[on + 1]
668
+ if (!here || !back || !next) return null
669
+
670
+ const runX = next.x - back.x
671
+ const runY = next.y - back.y
672
+ const square = Math.atan2(-runX * out, runY * out) + (random() - 0.5) * PERCH_WANDER * 2
673
+ const off = Math.atan2(Math.sin(square - away), Math.cos(square - away))
674
+
675
+ return {
676
+ kind: sort(),
677
+ lean: away + Math.max(-LEAN_MOST, Math.min(LEAN_MOST, off)),
678
+ size: PERCH_SMALLEST + random() ** 1.6 * (PERCH_LARGEST - PERCH_SMALLEST),
679
+ x: here.x,
680
+ y: here.y,
681
+ }
682
+ }
683
+
684
+ function settle(outline: readonly Point[], out: number, away: number, share: number): Perch[] {
685
+ const steps = outline.length
686
+ if (steps < 3 || share <= 0) return []
687
+
688
+ let along = 0
689
+ for (let at = 1; at < steps; at++) {
690
+ const here = outline[at]
691
+ const back = outline[at - 1]
692
+ if (here && back) along += Math.hypot(here.x - back.x, here.y - back.y)
693
+ }
694
+
695
+ const colonies = Math.round((along * COLONIES_PER_K * share) / 1000)
696
+ const spread = Math.max(1, Math.round(steps * COLONY_SPREAD))
697
+ const perches: Perch[] = []
698
+
699
+ for (let made = 0; made < colonies; made++) {
700
+ const middle = Math.floor(random() * steps)
701
+ const many = COLONY_LEAST + Math.floor(random() * (COLONY_MOST - COLONY_LEAST + 1))
702
+
703
+ for (let one = 0; one < many; one++) {
704
+ const grown = root(outline, middle + Math.round((random() * 2 - 1) * spread), out, away)
705
+ if (grown) perches.push(grown)
706
+ }
707
+ }
708
+
709
+ return perches
710
+ }
711
+
712
+ /**
713
+ * One wall, top to bottom, standing off `side` of the box.
714
+ *
715
+ * `near` is where in its own stack it stands, 0 at the back and 1 at the
716
+ * front, and it decides how much lives on it. Only the front of a stack is
717
+ * properly furred: a fan on the mass behind would be a mark of one pixel, and
718
+ * a far shape with texture on it stops being the thing the near one is read
719
+ * against.
720
+ */
721
+ function wall(side: -1 | 1, reach: number, lane: number, depth: number, near: number): Crag {
722
+ const outcrops = lumps()
723
+ const outline: Point[] = []
724
+ const sway = random() * 2 - 1
725
+ const shape = (at: number): number => profile(at, sway)
726
+
727
+ for (let at = 0; at <= FACE_STEPS; at++) {
728
+ const along = at / FACE_STEPS
729
+ const y = -OVERHANG + along * (height + OVERHANG * 2)
730
+ const out = face(along, lane, shape, outcrops) * reach * width
731
+
732
+ outline.push({ x: side < 0 ? out : width - out, y })
733
+ }
734
+
735
+ return {
736
+ depth,
737
+ edge: side < 0 ? "left" : "right",
738
+ outline,
739
+ perches: settle(outline, side < 0 ? 1 : -1, side < 0 ? 0 : Math.PI, near ** 3),
740
+ }
741
+ }
742
+
743
+ /** The roof of a cave, left to right, hanging off the top of the box. */
744
+ function roof(lane: number): Crag {
745
+ const outcrops = lumps()
746
+ const outline: Point[] = []
747
+ const shape = (at: number): number => 0.4 + 0.6 * Math.cos((at - 0.5) * Math.PI * 2) ** 2
748
+
749
+ for (let at = 0; at <= FACE_STEPS; at++) {
750
+ const along = at / FACE_STEPS
751
+ const x = -OVERHANG + along * (width + OVERHANG * 2)
752
+
753
+ outline.push({ x, y: face(along, lane, shape, outcrops) * ROOF_DROP * height })
754
+ }
755
+
756
+ return { depth: NEAR, edge: "top", outline, perches: settle(outline, -1, Math.PI / 2, 1) }
757
+ }
758
+
759
+ /** One side of a mass of land: how far it reaches, and how it bends. */
760
+ interface Slope {
761
+ readonly reach: number
762
+ readonly toe: number
763
+ }
764
+
765
+ /** Land standing above the water: where its top is, how high, and its sides. */
766
+ interface Mass {
767
+ readonly left: Slope
768
+ readonly middle: number
769
+ readonly right: Slope
770
+ readonly rise: number
771
+ }
772
+
773
+ const slope = (reach: number): Slope => ({
774
+ reach: Math.min(width * ISLE_WIDEST, reach),
775
+ toe: ISLE_TOE_LEAST + random() * ISLE_TOE_SPAN,
776
+ })
777
+
778
+ /**
779
+ * The summit, and the lesser ground standing on its flanks.
780
+ *
781
+ * The summit is rolled first because everything else is a share of it: a spur
782
+ * is a fraction of its height laid a fraction of the way down one of its
783
+ * sides, so a low wide island gets low wide shoulders and a steep one gets
784
+ * crags. Rolled on their own they would be the same furniture on every
785
+ * island, which is half of why every island used to look like the last one.
786
+ */
787
+ function land(): Mass[] {
788
+ const middle = width * (0.15 + random() * 0.7)
789
+ const rise = height * (ISLE_RISE_LEAST + random() ** ISLE_RISE_BIAS * ISLE_RISE_SPAN)
790
+ const pitch = ISLE_PITCH_LEAST + random() * ISLE_PITCH_SPAN
791
+ const spread = ((Math.PI / 2) * rise) / Math.tan(pitch)
792
+ const stretch = 1 + random() * (ISLE_LOPSIDED - 1)
793
+ const longer = random() < 0.5 ? -1 : 1
794
+
795
+ const peak: Mass = {
796
+ left: slope(spread * (longer < 0 ? stretch : 1)),
797
+ middle,
798
+ right: slope(spread * (longer < 0 ? 1 : stretch)),
799
+ rise,
800
+ }
801
+
802
+ const many = SPURS_LEAST + Math.floor(random() * (SPURS_SPAN + 1))
803
+
804
+ const spurs = Array.from({ length: many }, () => {
805
+ const out = random() < 0.5 ? -1 : 1
806
+ const side = out < 0 ? peak.left : peak.right
807
+ const span = side.reach * (SPUR_REACH_LEAST + random() * SPUR_REACH_SPAN)
808
+
809
+ return {
810
+ left: slope(span),
811
+ middle: middle + out * side.reach * (SPUR_OUT_LEAST + random() * SPUR_OUT_SPAN),
812
+ right: slope(span * (0.6 + random() * 0.8)),
813
+ rise: rise * (SPUR_RISE_LEAST + random() * SPUR_RISE_SPAN),
814
+ }
815
+ })
816
+
817
+ return [peak, ...spurs]
818
+ }
819
+
820
+ /** How high one mass of land stands at x, and nothing past its own feet. */
821
+ function stands(x: number, hill: Mass): number {
822
+ const side = x < hill.middle ? hill.left : hill.right
823
+ const along = Math.abs(x - hill.middle) / side.reach
824
+ if (along >= 1) return 0
825
+
826
+ return hill.rise * ((1 + Math.cos(along * Math.PI)) / 2) ** side.toe
827
+ }
828
+
829
+ /**
830
+ * An island's own ground at a point along it, between roughly -0.5 and 0.5.
831
+ *
832
+ * Octaves of the field folded at nought, which turns each of them upside
833
+ * down: a plain octave is as round at the top as at the bottom, and folded it
834
+ * comes to an edge along the tops and lies in curves along the bottoms, which
835
+ * is what water leaves when it takes the valleys out of a hill. Read in the
836
+ * island's own width rather than the box's, so a small island is rough at its
837
+ * own size instead of wearing a large one's weather.
838
+ */
839
+ function relief(along: number): number {
840
+ let sum = 0
841
+ let weight = 0
842
+ let strength = 1
843
+ let cells = ISLE_RELIEF_CELLS
844
+
845
+ for (let octave = 0; octave < ISLE_RELIEF_OCTAVES; octave++) {
846
+ sum += strength * (1 - Math.abs(carve(along * cells, octave * 3.7)))
847
+ weight += strength
848
+ strength *= ISLE_RELIEF_FALL
849
+ cells *= ISLE_RELIEF_STEP
850
+ }
851
+
852
+ return sum / weight - 0.5
853
+ }
854
+
855
+ /**
856
+ * An island, standing on the furthest ground there is.
857
+ *
858
+ * Ground the way `seabed.ts` raises a summit, and what it does that the far
859
+ * ranges do not is stand near enough to be read as a place rather than as a
860
+ * line at the back. It gives the sun something to go behind, which is the one
861
+ * thing in this scene that has never been occluded by anything.
862
+ *
863
+ * The whole of it is the outline, so the outline is where the work is. A
864
+ * summit and its shoulders say how big it is; the pitch of the flanks says
865
+ * what kind of coast it has; and the relief laid along them says it is ground
866
+ * rather than a curve, which is the thing a single arc out of the water can
867
+ * never say however well it is drawn. Whichever of them stands highest at an
868
+ * x is what the water has in front of it there.
869
+ */
870
+ function raise(): Isle {
871
+ const masses = land()
872
+ const peak = masses[0]
873
+ const outline: Point[] = []
874
+ if (!peak) return { depth: ISLE_DEEP, outline }
875
+
876
+ const own = (peak.left.reach + peak.right.reach) / 2
877
+ const weather = Math.max(peak.rise * ISLE_RELIEF, height * ISLE_RELIEF_LEAST)
878
+
879
+ for (let at = 0; at <= ISLE_STEPS; at++) {
880
+ const x = -OVERHANG + (at / ISLE_STEPS) * (width + OVERHANG * 2)
881
+
882
+ let lift = 0
883
+ for (const hill of masses) lift = Math.max(lift, stands(x, hill))
884
+
885
+ const ashore = Math.min(1, lift / (peak.rise * ISLE_RELIEF_FOOT))
886
+ const rough = weather * relief((x - peak.middle) / own) * ashore
887
+
888
+ outline.push({ x, y: floor(x, ISLE_DEEP) - Math.max(0, lift + rough) })
889
+ }
890
+
891
+ return { depth: ISLE_DEEP, outline }
892
+ }
893
+
894
+ /**
895
+ * One side of the picture, as however many masses today gave it.
896
+ *
897
+ * Furthest first, because a renderer draws them in the order it is given and
898
+ * the near one has to be the one that occludes. Each is broader than the one
899
+ * in front of it and each reads its own lane of the field, so a stack is a
900
+ * set of different rocks at different distances rather than one rock drawn
901
+ * three times at three weights.
902
+ */
903
+ function side(which: -1 | 1, reach: number): Crag[] {
904
+ const layers = LAYERS_LEAST + Math.floor(random() * (LAYERS_MOST - LAYERS_LEAST + 1))
905
+ const front = which < 0 ? NEAR : BEHIND
906
+ const stack: Crag[] = []
907
+
908
+ for (let at = 0; at < layers; at++) {
909
+ const near = layers < 2 ? 1 : at / (layers - 1)
910
+
911
+ stack.push(
912
+ wall(
913
+ which,
914
+ reach * (1 + (1 - near) * (BACKING_WIDER - 1)),
915
+ random() * 7,
916
+ FURTHEST + (front - FURTHEST) * near,
917
+ near,
918
+ ),
919
+ )
920
+ }
921
+
922
+ return stack
923
+ }
924
+
925
+ function build(): Crag[] {
926
+ const reach = REACHES[framing]
927
+ if (reach <= 0) return []
928
+
929
+ if (framing === "shoulder") return side(random() < 0.5 ? -1 : 1, reach)
930
+
931
+ const rocks = [...side(-1, reach), ...side(1, reach * (0.7 + random() * 0.5))]
932
+ if (framing === "cave") rocks.push(roof(random() * 7))
933
+
934
+ return rocks
935
+ }
936
+
937
+ let rocks = build()
938
+ let isle = hasIsle ? raise() : null
939
+
940
+ return {
941
+ framing,
942
+ get isle() {
943
+ return isle
944
+ },
945
+
946
+ /**
947
+ * A resized box is rebuilt rather than scaled, for the reason `seabed.ts`
948
+ * gives: everything here is a pure function of the seed and the box, and
949
+ * rock that stretched with a window would be the one thing on screen
950
+ * admitting it is a drawing. The framing itself is kept, because it is the
951
+ * day's rather than the box's.
952
+ */
953
+ resize(nextWidth, nextHeight, nextFloor) {
954
+ width = Math.max(MIN_SPAN, nextWidth)
955
+ height = Math.max(MIN_SPAN, nextHeight)
956
+ floor = nextFloor
957
+ rocks = build()
958
+ isle = hasIsle ? raise() : null
959
+ },
960
+
961
+ get rocks() {
962
+ return rocks
963
+ },
964
+ }
965
+ }
966
+
967
+ /**
968
+ * Whether a day's framing leaves room for an island.
969
+ *
970
+ * Not from inside a cave. An island is a thing in the far distance and a cave
971
+ * is a thing a hand's width from your mask, and a picture holding both is a
972
+ * picture that cannot decide where it is standing.
973
+ */
974
+ const isleAllowed = (framing: Framing): boolean => framing === "hills" || framing === "open"