@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/seabed.ts
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bottom: what the water sits on, and what is standing further out.
|
|
3
|
+
*
|
|
4
|
+
* The ground layer of a seascape, kept free of the DOM the way the rest of the
|
|
5
|
+
* ornament is. Nothing here moves, so there is no `step`. A floor that drifted
|
|
6
|
+
* would be an earthquake.
|
|
7
|
+
*
|
|
8
|
+
* ## Why a seascape needs one
|
|
9
|
+
*
|
|
10
|
+
* Fish, snow and light give water a volume but no bottom, and water with no
|
|
11
|
+
* bottom has no scale in it: a picture of animals at unknown distances. Put a
|
|
12
|
+
* floor in and everything above it acquires a height. It is also the thing
|
|
13
|
+
* every other layer needs to exist at all, since kelp has to be rooted, a vent
|
|
14
|
+
* has to come out of something, and a wreck has to be lying on it.
|
|
15
|
+
*
|
|
16
|
+
* ## The floor
|
|
17
|
+
*
|
|
18
|
+
* A profile sampled across the box, not a straight line and not a jagged one.
|
|
19
|
+
* Two octaves of the same Perlin field the fish read: a long swell that puts
|
|
20
|
+
* the whole bed high at one end, and a shorter one for the lumps. Anything more
|
|
21
|
+
* and it reads as a mountain range seen from a helicopter, which is a landscape
|
|
22
|
+
* rather than a seabed.
|
|
23
|
+
*
|
|
24
|
+
* `floorAt` is the interface the rest of the scene actually uses, and it is
|
|
25
|
+
* exact rather than a lookup into `ridge`: a stone or a kelp plant sitting a
|
|
26
|
+
* pixel above the sand looks like it is hovering, and that is the sort of
|
|
27
|
+
* mistake that is invisible in a still and obvious in motion.
|
|
28
|
+
*
|
|
29
|
+
* ## The floor has a second axis, and it is distance
|
|
30
|
+
*
|
|
31
|
+
* `floorAt(x, depth)` is the ground under something standing that far back. The
|
|
32
|
+
* whole bed used to be one line, which meant every rooted thing in the water
|
|
33
|
+
* stood on the same line: a hundred plants of graded sizes all growing out of
|
|
34
|
+
* one edge, which reads as a stage rather than as a place. Depth is a picture's
|
|
35
|
+
* first subject and a single ridge cannot carry it.
|
|
36
|
+
*
|
|
37
|
+
* So the ground climbs the box as it goes back, its swells grow broad, and it
|
|
38
|
+
* reads a different lane of the same field at each distance, which is what
|
|
39
|
+
* keeps the far ground a different shape rather than the near one lifted. Where
|
|
40
|
+
* a far hill and a near one overlap is where the eye reads the gap between
|
|
41
|
+
* them, and the dip between two hills is a valley for free.
|
|
42
|
+
*
|
|
43
|
+
* `ranges` are that ground cut at a handful of distances, which is what a
|
|
44
|
+
* renderer draws. They are ground rather than scenery: things root to them, and
|
|
45
|
+
* a plant at a given depth is standing on `floorAt` at that same depth.
|
|
46
|
+
*
|
|
47
|
+
* ## What is standing further out
|
|
48
|
+
*
|
|
49
|
+
* Cliffs, in the murk behind all of it. They are not terrain the scene uses;
|
|
50
|
+
* nothing roots to them and nothing collides with them. They exist because a
|
|
51
|
+
* silhouette at the back of the water is the cheapest possible statement that
|
|
52
|
+
* the water continues past the edge of the picture, and without one the scene
|
|
53
|
+
* is a tank.
|
|
54
|
+
*
|
|
55
|
+
* They stand on the ground at their own distance, which is the furthest ground
|
|
56
|
+
* there is, and they rise well above it: a cliff that stopped at the same
|
|
57
|
+
* height as what it stands on would read as more ground.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
import { makeNoise2, makeRandom } from "./perlin.ts"
|
|
61
|
+
|
|
62
|
+
export interface Point {
|
|
63
|
+
readonly x: number
|
|
64
|
+
readonly y: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface Stone {
|
|
68
|
+
/** How far back it lies, which is its weight and most of its size. */
|
|
69
|
+
readonly depth: number
|
|
70
|
+
/** How far it is turned, in radians. Stones are not upright. */
|
|
71
|
+
readonly lean: number
|
|
72
|
+
/** How wide and how tall, in px. */
|
|
73
|
+
readonly rise: number
|
|
74
|
+
readonly span: number
|
|
75
|
+
readonly x: number
|
|
76
|
+
readonly y: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface Cliff {
|
|
80
|
+
/** How far back it stands, which is all its weight. */
|
|
81
|
+
readonly depth: number
|
|
82
|
+
/** Its skyline, left to right, running past both edges of the box. */
|
|
83
|
+
readonly ridge: readonly Point[]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The ground at one distance, from the furthest to just behind the sand. */
|
|
87
|
+
export interface Range {
|
|
88
|
+
readonly depth: number
|
|
89
|
+
/** Its outline, left to right along the top. */
|
|
90
|
+
readonly ridge: readonly Point[]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface Seabed {
|
|
94
|
+
readonly cliffs: readonly Cliff[]
|
|
95
|
+
/**
|
|
96
|
+
* The y of the ground at x, for anything that has to sit on it.
|
|
97
|
+
*
|
|
98
|
+
* `depth` is how far forward the thing standing there is, 1 at the front of
|
|
99
|
+
* the picture and towards 0 in the murk. It defaults to the front, so a
|
|
100
|
+
* caller with nothing to say about distance gets the sand it always got.
|
|
101
|
+
*/
|
|
102
|
+
floorAt(x: number, depth?: number): number
|
|
103
|
+
readonly ranges: readonly Range[]
|
|
104
|
+
resize(width: number, height: number): void
|
|
105
|
+
/** The sand's own outline, left to right. */
|
|
106
|
+
readonly ridge: readonly Point[]
|
|
107
|
+
readonly stones: readonly Stone[]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface SeabedOptions {
|
|
111
|
+
cliffs?: number
|
|
112
|
+
height: number
|
|
113
|
+
ranges?: number
|
|
114
|
+
seed: number
|
|
115
|
+
stones?: number
|
|
116
|
+
width: number
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* How much of the box is left under the nearest ground there is, and how far
|
|
121
|
+
* that ground rolls.
|
|
122
|
+
*
|
|
123
|
+
* Almost none, and that is the whole of it. It used to be a tenth, which read
|
|
124
|
+
* as a shelf: the ground at the front of the picture stopped a tenth of the way
|
|
125
|
+
* up the box and the bottom of the screen was the face of it, seen from above,
|
|
126
|
+
* with nothing on it. Nothing could be on it either, since the nearest a rooted
|
|
127
|
+
* thing goes is this line, so every plant, stone and crab in the scene stood
|
|
128
|
+
* along one edge with bare sand below them.
|
|
129
|
+
*
|
|
130
|
+
* Bringing it to the floor of the box is what gives the front of the picture
|
|
131
|
+
* somewhere to be. The near end of every distance in the scene now lands at the
|
|
132
|
+
* bottom edge, so the bed spreads down to it and the nearest plants are cut off
|
|
133
|
+
* by it, which is what being close to something looks like. The ground is no
|
|
134
|
+
* less shallow for it: what closes the bottom of the scene is the stack of
|
|
135
|
+
* bands behind this one, each shut under the box, and they are where they were.
|
|
136
|
+
*/
|
|
137
|
+
const FLOOR_SEAT = 0.01
|
|
138
|
+
|
|
139
|
+
const FLOOR_ROLL = 0.085
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The same two, for the furthest ground the water has.
|
|
143
|
+
*
|
|
144
|
+
* It sits a third of the way up the box, which is as far back as a seascape can
|
|
145
|
+
* go before the ground is level with the fish and the picture has no sky in it.
|
|
146
|
+
* It rolls harder than the sand does because what is up there is hills: the
|
|
147
|
+
* near bed is a floor with lumps on it, and the far one is country.
|
|
148
|
+
*/
|
|
149
|
+
const HORIZON_SEAT = 0.36
|
|
150
|
+
|
|
151
|
+
const HORIZON_ROLL = 0.12
|
|
152
|
+
|
|
153
|
+
/** How many features of the long swell and the short one fit across the box. */
|
|
154
|
+
const SWELL_CELLS = 1.3
|
|
155
|
+
|
|
156
|
+
const LUMP_CELLS = 5.5
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The same two at the horizon, where everything is broader.
|
|
160
|
+
*
|
|
161
|
+
* Distance takes the detail out of ground long before it takes the shape, which
|
|
162
|
+
* is why a far hill is a silhouette and a near one is a slope with rubble on
|
|
163
|
+
* it. Fewer features across the same box is the whole of that.
|
|
164
|
+
*/
|
|
165
|
+
const HORIZON_SWELL_CELLS = 0.8
|
|
166
|
+
|
|
167
|
+
const HORIZON_LUMP_CELLS = 2.1
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* How fast the ground climbs the box as it goes back.
|
|
171
|
+
*
|
|
172
|
+
* Distance on a flat plane is not linear on the screen and never has been: the
|
|
173
|
+
* ground a step in front of your foot takes a third of the picture and the
|
|
174
|
+
* ground a mile off takes a hair, and everything between the two is squeezed
|
|
175
|
+
* towards the horizon. Held to a straight line, bands of ground come out evenly
|
|
176
|
+
* spaced, and evenly spaced lines across a picture are a contour map.
|
|
177
|
+
*
|
|
178
|
+
* An exponent rather than a proper projection, because there is no camera here
|
|
179
|
+
* and nothing to be right about: what it has to do is bunch at the back and
|
|
180
|
+
* open out at the front, which is the whole of what the eye is reading.
|
|
181
|
+
*/
|
|
182
|
+
const DEPTH_FALL = 1.6
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* How far the field is walked as the ground goes back.
|
|
186
|
+
*
|
|
187
|
+
* Distance is the second axis of one Perlin field, so this is how much of it a
|
|
188
|
+
* screen's worth of distance crosses. Too little and the far hills are the near
|
|
189
|
+
* lumps drawn higher, which is the one thing that gives a flat picture away;
|
|
190
|
+
* this is enough that no two distances share a shape.
|
|
191
|
+
*/
|
|
192
|
+
const DISTANCE_LANES = 3.1
|
|
193
|
+
|
|
194
|
+
/** How much of the roll belongs to the lumps rather than the swell. */
|
|
195
|
+
const LUMP_SHARE = 0.34
|
|
196
|
+
|
|
197
|
+
/** How finely the sand's outline is cut. */
|
|
198
|
+
const RIDGE_STEPS = 96
|
|
199
|
+
|
|
200
|
+
/** How far past each side the outline runs, so no edge shows a seam. */
|
|
201
|
+
const OVERHANG = 40
|
|
202
|
+
|
|
203
|
+
/** The ends of a stone's width in px, and how tall one is against that. */
|
|
204
|
+
const STONE_SMALLEST = 9
|
|
205
|
+
|
|
206
|
+
const STONE_LARGEST = 38
|
|
207
|
+
|
|
208
|
+
const STONE_SQUAT = 0.62
|
|
209
|
+
|
|
210
|
+
/** How far a stone may be turned, in radians. */
|
|
211
|
+
const STONE_LEAN = 0.5
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* How far back stones lie, and how much of a stone's drawn size is that.
|
|
215
|
+
*
|
|
216
|
+
* They go back further than a plant does, because scree is what a slope is made
|
|
217
|
+
* of and a slope with rubble only along its foot is a slope somebody swept.
|
|
218
|
+
* Nothing like the whole of the size, though: a stone at a tenth of its size is
|
|
219
|
+
* a grain of sand, and a beach of those is a texture rather than a distance.
|
|
220
|
+
*/
|
|
221
|
+
const STONE_FAR = 0.3
|
|
222
|
+
|
|
223
|
+
const STONE_NEAR = 1
|
|
224
|
+
|
|
225
|
+
const STONE_SHRINK = 0.55
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* How far back the ground is cut, from the furthest band to the nearest.
|
|
229
|
+
*
|
|
230
|
+
* The near end stops well short of the sand, because two ridges a hand apart
|
|
231
|
+
* read as one ridge that has been drawn twice. What is wanted is a clear gap
|
|
232
|
+
* between what you are standing on and what is behind it.
|
|
233
|
+
*/
|
|
234
|
+
const RANGE_FAR = 0.08
|
|
235
|
+
|
|
236
|
+
const RANGE_NEAR = 0.75
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* How high a cliff stands above the ground it is on, as a share of the box, and
|
|
240
|
+
* how wide.
|
|
241
|
+
*
|
|
242
|
+
* Wide, because a cliff is being read at a distance where the only information
|
|
243
|
+
* is its outline. A small one is a rock, and a rock at the back of the water is
|
|
244
|
+
* a stone somebody has drawn badly. Not as tall as it once was: it used to
|
|
245
|
+
* stand on the sand at the front of the picture and had a whole box to climb,
|
|
246
|
+
* and it stands on the horizon now, where the water above it is all there is
|
|
247
|
+
* left.
|
|
248
|
+
*
|
|
249
|
+
* Both come off one roll of the dice, so a summit that is high is broad and one
|
|
250
|
+
* that is low is narrow. Rolled apart, the tall end of one meets the narrow end
|
|
251
|
+
* of the other every so often, and what that draws is a tooth: sides at sixty
|
|
252
|
+
* degrees coming to a point, which no mountain has and nothing standing in
|
|
253
|
+
* water keeps.
|
|
254
|
+
*/
|
|
255
|
+
const CLIFF_RISE_LEAST = 0.06
|
|
256
|
+
|
|
257
|
+
const CLIFF_RISE_SPAN = 0.16
|
|
258
|
+
|
|
259
|
+
const CLIFF_SPAN_LEAST = 0.12
|
|
260
|
+
|
|
261
|
+
const CLIFF_SPAN_SPAN = 0.26
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* How finely a range's skyline is cut.
|
|
265
|
+
*
|
|
266
|
+
* Finer than it was. It used to be one hill a few hundred px wide and fourteen
|
|
267
|
+
* steps was plenty for that; it is the whole width of the box and then some
|
|
268
|
+
* now, and fourteen steps across a wall would be a summit with three sides.
|
|
269
|
+
*
|
|
270
|
+
* Finer again, because the skyline carries what grows on it, and a fringe cut
|
|
271
|
+
* at thirteen px a step is a row of thumbs rather than a scruff. Points are the
|
|
272
|
+
* cheap half of the bill down here: a range is one shape however many of them
|
|
273
|
+
* its outline holds.
|
|
274
|
+
*/
|
|
275
|
+
const CLIFF_STEPS = 280
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* How far the growth on a skyline stands off it, as a share of the box, how
|
|
279
|
+
* many stretches of it there are across the width, and how much of a range has
|
|
280
|
+
* nothing on it at all.
|
|
281
|
+
*
|
|
282
|
+
* Bare rock at this distance is a shape somebody forgot to finish, which is the
|
|
283
|
+
* argument `crags.ts` makes about the near wall and is no less true of the far
|
|
284
|
+
* one. Nothing up there can be told apart: a hill at the back of the water is
|
|
285
|
+
* an outline, so what a plant on it amounts to is that the outline stops being
|
|
286
|
+
* smooth. Colonies rather than an even fur, for the reason a near face is
|
|
287
|
+
* crowded in places and bare in others, and the same reason a summit under a
|
|
288
|
+
* lawn is a hill somebody has drawn hair on.
|
|
289
|
+
*/
|
|
290
|
+
const FRINGE_RISE = 0.038
|
|
291
|
+
|
|
292
|
+
const FRINGE_PATCHES = 3.4
|
|
293
|
+
|
|
294
|
+
const FRINGE_BARE = 0.12
|
|
295
|
+
|
|
296
|
+
/** How far back the ranges stand. Nearly the colour of the water, and no nearer. */
|
|
297
|
+
const CLIFF_DEEP = 0.05
|
|
298
|
+
|
|
299
|
+
const CLIFF_NEAR = 0.16
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* How many ranges the summits are shared out between.
|
|
303
|
+
*
|
|
304
|
+
* Each is a skyline of its own at its own distance, so what the eye gets is one
|
|
305
|
+
* range standing behind another rather than a row of hills at one remove. Three,
|
|
306
|
+
* because two is a pair and four at these weights is two the reader can find and
|
|
307
|
+
* two they cannot.
|
|
308
|
+
*/
|
|
309
|
+
const CLIFF_RANGES = 3
|
|
310
|
+
|
|
311
|
+
/** How far apart two stands of growth on a skyline are, in px. */
|
|
312
|
+
const STRAND_SPAN = 26
|
|
313
|
+
|
|
314
|
+
const MIN_SPAN = 1
|
|
315
|
+
|
|
316
|
+
/** Where between two numbers a distance falls, 0 at the back and 1 at the front. */
|
|
317
|
+
const mix = (far: number, near: number, at: number): number => far + (near - far) * at
|
|
318
|
+
|
|
319
|
+
export function createSeabed(options: SeabedOptions): Seabed {
|
|
320
|
+
const colonies = makeNoise2(options.seed ^ 0xc010)
|
|
321
|
+
const sprigs = makeNoise2(options.seed ^ 0x5197)
|
|
322
|
+
const swell = makeNoise2(options.seed ^ 0x5eab)
|
|
323
|
+
const lumps = makeNoise2(options.seed ^ 0x10c5)
|
|
324
|
+
const random = makeRandom(options.seed ^ 0x2d0e)
|
|
325
|
+
|
|
326
|
+
let width = Math.max(MIN_SPAN, options.width)
|
|
327
|
+
let height = Math.max(MIN_SPAN, options.height)
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The ground at x, as far back as `depth` says.
|
|
331
|
+
*
|
|
332
|
+
* Everything a distance changes about ground is here and nowhere else, which
|
|
333
|
+
* is what lets a plant, a stone and a whole cut band all agree about where
|
|
334
|
+
* the bottom is at their own distance. It climbs, it broadens, it flattens
|
|
335
|
+
* its detail, and it reads its own lane of the field so that it is a
|
|
336
|
+
* different piece of country rather than the same one drawn higher.
|
|
337
|
+
*/
|
|
338
|
+
function floorAt(x: number, depth = 1): number {
|
|
339
|
+
const near = Math.max(0, Math.min(1, depth))
|
|
340
|
+
const lane = (1 - near) * DISTANCE_LANES
|
|
341
|
+
const up = near ** DEPTH_FALL
|
|
342
|
+
|
|
343
|
+
const seat = height * (1 - mix(HORIZON_SEAT, FLOOR_SEAT, up))
|
|
344
|
+
const roll = height * mix(HORIZON_ROLL, FLOOR_ROLL, up)
|
|
345
|
+
const long = swell((x / width) * mix(HORIZON_SWELL_CELLS, SWELL_CELLS, near), lane)
|
|
346
|
+
const short = lumps((x / width) * mix(HORIZON_LUMP_CELLS, LUMP_CELLS, near), lane + 4.7)
|
|
347
|
+
|
|
348
|
+
return seat - (long * (1 - LUMP_SHARE) + short * LUMP_SHARE) * roll
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function cutRidge(depth: number): Point[] {
|
|
352
|
+
const points: Point[] = []
|
|
353
|
+
for (let at = 0; at <= RIDGE_STEPS; at++) {
|
|
354
|
+
const x = -OVERHANG + (at / RIDGE_STEPS) * (width + OVERHANG * 2)
|
|
355
|
+
points.push({ x, y: floorAt(x, depth) })
|
|
356
|
+
}
|
|
357
|
+
return points
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* The ground cut at each distance, furthest first.
|
|
362
|
+
*
|
|
363
|
+
* Spread evenly rather than rolled, because bands are read as a stack: two
|
|
364
|
+
* that landed at the same distance would be one band with a seam in it, and
|
|
365
|
+
* the whole of what they are for is the gap between them.
|
|
366
|
+
*/
|
|
367
|
+
function cutRanges(count: number): Range[] {
|
|
368
|
+
const bands: Range[] = []
|
|
369
|
+
for (let at = 0; at < count; at++) {
|
|
370
|
+
const depth =
|
|
371
|
+
count < 2 ? RANGE_FAR : RANGE_FAR + (at / (count - 1)) * (RANGE_NEAR - RANGE_FAR)
|
|
372
|
+
bands.push({ depth, ridge: cutRidge(depth) })
|
|
373
|
+
}
|
|
374
|
+
return bands
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* How far something growing at x stands off the skyline there.
|
|
379
|
+
*
|
|
380
|
+
* Every other point of the outline and none of the ones between, which is
|
|
381
|
+
* what makes this read as growth rather than as more rock: a strand is a
|
|
382
|
+
* spike with a gap beside it, and a smooth field at any frequency is a lump
|
|
383
|
+
* however small it is drawn. What varies is how tall each one is and whether
|
|
384
|
+
* there is anything there at all, and both are read off the water rather than
|
|
385
|
+
* rolled, so a range's fringe is a fact about that range and comes back the
|
|
386
|
+
* same on a resize.
|
|
387
|
+
*/
|
|
388
|
+
function fringe(x: number, depth: number, at: number): number {
|
|
389
|
+
if (at % 2 === 0) return 0
|
|
390
|
+
|
|
391
|
+
const patch = colonies((x / width) * FRINGE_PATCHES, depth * 7.3)
|
|
392
|
+
if (patch < FRINGE_BARE) return 0
|
|
393
|
+
|
|
394
|
+
const thick = (patch - FRINGE_BARE) / (1 - FRINGE_BARE)
|
|
395
|
+
const stand = Math.abs(sprigs(x / STRAND_SPAN, depth * 11.7))
|
|
396
|
+
|
|
397
|
+
return height * FRINGE_RISE * thick * (0.4 + stand * 0.6)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* One cliff, as a lump of its own.
|
|
402
|
+
*
|
|
403
|
+
* A summit is a half-cosine rather than noise, so it has a peak and two
|
|
404
|
+
* shoulders instead of a row of equal bumps, and the noise is what stops the
|
|
405
|
+
* two shoulders matching. That much is the only thing between one of these and
|
|
406
|
+
* a hill drawn by a child.
|
|
407
|
+
*
|
|
408
|
+
* What is returned is a whole skyline and not one hill. Each used to be its
|
|
409
|
+
* own little polygon dropped on the bed, and a shape that stops somewhere in
|
|
410
|
+
* the middle of the picture has ends, which is a problem no amount of colour
|
|
411
|
+
* fixes: shut under the box, the way a band of ground is, it grew two vertical
|
|
412
|
+
* walls running the height of the picture and a mountain came to a straight
|
|
413
|
+
* edge and fell off it. Shut along its own foot instead, the ends taper to a
|
|
414
|
+
* knife and lie on the ground in front like a crack in it.
|
|
415
|
+
*
|
|
416
|
+
* A range has no ends. This runs past both edges of the box exactly as the
|
|
417
|
+
* ground does, its summits are the highest of however many domes are standing
|
|
418
|
+
* at that x, and what closes it is off the side of the picture where nobody
|
|
419
|
+
* is looking. Between the summits it is simply the ground at its own distance,
|
|
420
|
+
* which is what the land between two mountains is.
|
|
421
|
+
*/
|
|
422
|
+
function raise(depth: number, peaks: number): Cliff {
|
|
423
|
+
const domes = Array.from({ length: Math.max(1, peaks) }, () => {
|
|
424
|
+
const bulk = random()
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
middle: random() * width,
|
|
428
|
+
rise: height * (CLIFF_RISE_LEAST + bulk * CLIFF_RISE_SPAN),
|
|
429
|
+
rough: makeNoise2((random() * 0xffff) | 0),
|
|
430
|
+
span: width * (CLIFF_SPAN_LEAST + bulk * CLIFF_SPAN_SPAN),
|
|
431
|
+
}
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
const ridge: Point[] = []
|
|
435
|
+
for (let at = 0; at <= CLIFF_STEPS; at++) {
|
|
436
|
+
const x = -OVERHANG + (at / CLIFF_STEPS) * (width + OVERHANG * 2)
|
|
437
|
+
|
|
438
|
+
let lift = 0
|
|
439
|
+
for (const dome of domes) {
|
|
440
|
+
const t = (x - (dome.middle - dome.span / 2)) / dome.span
|
|
441
|
+
if (t <= 0 || t >= 1) continue
|
|
442
|
+
const swell = (1 - Math.cos(t * Math.PI * 2)) / 2
|
|
443
|
+
lift = Math.max(lift, dome.rise * swell * (1 + dome.rough(t * 3.1, 0) * 0.34))
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
ridge.push({ x, y: floorAt(x, depth) - lift - fringe(x, depth, at) })
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return { depth, ridge }
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function settle(): Stone {
|
|
453
|
+
const x = random() * width
|
|
454
|
+
const depth = STONE_FAR + random() * (STONE_NEAR - STONE_FAR)
|
|
455
|
+
const shrink = 1 - STONE_SHRINK + STONE_SHRINK * depth
|
|
456
|
+
const span = (STONE_SMALLEST + random() ** 1.5 * (STONE_LARGEST - STONE_SMALLEST)) * shrink
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
depth,
|
|
460
|
+
lean: (random() - 0.5) * STONE_LEAN * 2,
|
|
461
|
+
rise: span * STONE_SQUAT * (0.7 + random() * 0.6),
|
|
462
|
+
span,
|
|
463
|
+
x,
|
|
464
|
+
y: floorAt(x, depth),
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let ridge = cutRidge(1)
|
|
469
|
+
let ranges = cutRanges(Math.max(0, options.ranges ?? 0))
|
|
470
|
+
/**
|
|
471
|
+
* The ranges, furthest first, with the summits shared out between them.
|
|
472
|
+
*
|
|
473
|
+
* `cliffs` is a count of summits rather than of ranges, which is what a caller
|
|
474
|
+
* counting against the width of its box is actually asking about: a wider
|
|
475
|
+
* screen wants more peaks along the same skylines, not more skylines.
|
|
476
|
+
*/
|
|
477
|
+
function skylines(): Cliff[] {
|
|
478
|
+
const summits = Math.max(0, options.cliffs ?? 0)
|
|
479
|
+
if (summits === 0) return []
|
|
480
|
+
|
|
481
|
+
return Array.from({ length: CLIFF_RANGES }, (_, at) => {
|
|
482
|
+
const step = CLIFF_RANGES > 1 ? at / (CLIFF_RANGES - 1) : 0
|
|
483
|
+
const share =
|
|
484
|
+
Math.round((summits * (at + 1)) / CLIFF_RANGES) - Math.round((summits * at) / CLIFF_RANGES)
|
|
485
|
+
|
|
486
|
+
return raise(CLIFF_DEEP + step * (CLIFF_NEAR - CLIFF_DEEP), share)
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let cliffs = skylines()
|
|
491
|
+
let stones = Array.from({ length: Math.max(0, options.stones ?? 0) }, settle)
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
get cliffs() {
|
|
495
|
+
return cliffs
|
|
496
|
+
},
|
|
497
|
+
floorAt,
|
|
498
|
+
get ranges() {
|
|
499
|
+
return ranges
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* A resized box is rebuilt rather than scaled.
|
|
504
|
+
*
|
|
505
|
+
* Everything here is a pure function of the seed and the box, so rebuilding
|
|
506
|
+
* gives the same bed the same box would have had from the start. Scaling
|
|
507
|
+
* would stretch the sand's own grain, and a seabed that got wider when a
|
|
508
|
+
* window did would be the one thing on screen admitting it is a drawing.
|
|
509
|
+
*/
|
|
510
|
+
resize(nextWidth, nextHeight) {
|
|
511
|
+
width = Math.max(MIN_SPAN, nextWidth)
|
|
512
|
+
height = Math.max(MIN_SPAN, nextHeight)
|
|
513
|
+
ridge = cutRidge(1)
|
|
514
|
+
ranges = cutRanges(ranges.length)
|
|
515
|
+
cliffs = skylines()
|
|
516
|
+
stones = stones.map(settle)
|
|
517
|
+
},
|
|
518
|
+
get ridge() {
|
|
519
|
+
return ridge
|
|
520
|
+
},
|
|
521
|
+
get stones() {
|
|
522
|
+
return stones
|
|
523
|
+
},
|
|
524
|
+
}
|
|
525
|
+
}
|