@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/LICENSE +661 -0
- package/README.md +52 -2
- package/package.json +35 -3
- package/src/biome.ts +157 -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/frame.ts +204 -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/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/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"
|