@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/src/cloud.ts ADDED
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Weather over the water, for the light to come through.
3
+ *
4
+ * The simulation behind the clouds in a seascape, kept free of the DOM the way
5
+ * `rays.ts` and `drift.ts` are.
6
+ *
7
+ * ## What a cloud is for here
8
+ *
9
+ * Not scenery. A reader looking up through the column sees the sun or the moon
10
+ * and the light it throws down, and a sky that is the same brightness every
11
+ * hour of every night is a lamp rather than weather. A cloud is the thing that
12
+ * takes some of that away for a while and gives it back: the moon dims as one
13
+ * crosses it, the shaft under it thins, and both come back once it has passed.
14
+ *
15
+ * So it is drawn in the water's own colour rather than a colour of its own.
16
+ * What a renderer paints is the absence of light, which is the only thing a
17
+ * cloud is from under here; a white shape over a night sky would be a cloud
18
+ * seen from a field.
19
+ *
20
+ * ## How they move
21
+ *
22
+ * Every one of them differently, and none of them quickly. A cloud carries a
23
+ * heading of its own and a swell of its own, so what crosses the picture is
24
+ * anything from a mass tracking flat across it to one that barely moves
25
+ * sideways and rises and falls in place, with most of them somewhere between
26
+ * and so going diagonally. Wind is one direction and clouds in it are not, and
27
+ * a sky where everything slides the same way at the same rate is a sky on a
28
+ * conveyor.
29
+ *
30
+ * The sideways drift wraps, since a cloud that has left the picture has nothing
31
+ * to say and the water it left is as good a place as any to come back at. The
32
+ * rise and fall does not: it is a swell about a seat, so nothing ever jumps
33
+ * from one edge to the other where somebody can see it happen.
34
+ *
35
+ * ## What it is not
36
+ *
37
+ * There is no wind here, no front, and nothing evaporates. A cloud is a mass of
38
+ * lobes with a thickness, and the only question it answers is how much of the
39
+ * light gets past it at a point.
40
+ */
41
+
42
+ import { makeRandom } from "./perlin.ts"
43
+
44
+ /** One of the swells a cloud is a mass of, about the cloud's own middle. */
45
+ export interface Lobe {
46
+ /** Across, in px. */
47
+ readonly dx: number
48
+ /** And down, in px. */
49
+ readonly dy: number
50
+ readonly r: number
51
+ }
52
+
53
+ export interface Cloud {
54
+ readonly lobes: readonly Lobe[]
55
+ /** How far it reaches from its middle, in px, which is what a wrap needs. */
56
+ readonly span: number
57
+ /**
58
+ * How much of the light it holds back where it is thickest.
59
+ *
60
+ * A share rather than an alpha, because how dark a renderer is allowed to
61
+ * draw is its own business: a website owes a contrast ratio to the words in
62
+ * front of it and a desktop owes nothing to anybody.
63
+ */
64
+ readonly thick: number
65
+ x: number
66
+ y: number
67
+ }
68
+
69
+ export interface Clouds {
70
+ readonly clouds: readonly Cloud[]
71
+ /**
72
+ * How much cloud is over a point: nothing in the clear, and at its highest
73
+ * under the middle of the thickest mass there is.
74
+ *
75
+ * This is what mutes a body rather than the drawing. A shape painted over the
76
+ * moon dims the moon where the shape is, and the shaft under it goes on
77
+ * arriving at full strength out of a moon that is no longer there.
78
+ */
79
+ cover(x: number, y: number): number
80
+ resize(width: number, height: number, count?: number): void
81
+ step(seconds: number): void
82
+ }
83
+
84
+ export interface CloudsOptions {
85
+ count: number
86
+ height: number
87
+ seed: number
88
+ /**
89
+ * How much of the box is sky, as a share of its height, for a picture whose
90
+ * sky is not the usual band.
91
+ *
92
+ * A round window holds a fraction of what a wall of water does and the light
93
+ * in it sits lower down the box, so the band it wants is deeper. Left out, a
94
+ * cloud stays in the top of the picture, which is where a wide box keeps its
95
+ * light.
96
+ */
97
+ sky?: number
98
+ width: number
99
+ }
100
+
101
+ /**
102
+ * How far down the box a cloud may sit, as a share of the height.
103
+ *
104
+ * The sky is the top of this picture and the light comes from up there, so
105
+ * anything lower is a shadow lying in open water with nothing casting it. The
106
+ * bodies cross well inside this band, which is the point: a cloud has to be
107
+ * able to get in front of one.
108
+ */
109
+ const SKY = 0.3
110
+
111
+ /** How wide one is, least and most, as shares of the box's width. */
112
+ const SPAN_LEAST = 0.14
113
+
114
+ const SPAN_SPAN = 0.3
115
+
116
+ /** How far the lobes wander off the line they sit on, against how wide the
117
+ * mass is, which is what keeps it a cloud rather than a heap. */
118
+ const SQUAT = 0.14
119
+
120
+ /** How much of the light the thinnest and the thickest hold back. */
121
+ const THICK_LEAST = 0.3
122
+
123
+ const THICK_MOST = 0.78
124
+
125
+ /**
126
+ * How fast one travels, least and most, in widths a second.
127
+ *
128
+ * Slow enough that nothing here is ever the fastest thing on the screen, and
129
+ * slow enough that a cloud crossing the moon is an evening rather than a
130
+ * moment. Snow falls, fish swim, and the weather takes longer than either.
131
+ */
132
+ const PACE_LEAST = 0.004
133
+
134
+ const PACE_SPAN = 0.011
135
+
136
+ /**
137
+ * How far off flat a cloud's heading may lean, in radians.
138
+ *
139
+ * The lean is the whole of which way one goes: it splits the pace between
140
+ * along and up, so a mass at nothing is tracking flat across the picture and
141
+ * one at the far end of this is climbing as fast as it travels. A quarter turn
142
+ * is as steep as it gets, since what rises faster than it drifts is weather
143
+ * nobody has seen.
144
+ */
145
+ const LEAN = Math.PI / 4
146
+
147
+ /** How much of the sky one may take up rising and falling, and how long a rise
148
+ * and a fall takes, in cycles a second. */
149
+ const SWELL_MOST = 0.3
150
+
151
+ const SWELL_SLOWEST = 0.006
152
+
153
+ const SWELL_FASTEST = 0.022
154
+
155
+ /**
156
+ * The lobes: how many a mass is made of, and how wide the smallest and the
157
+ * largest are against the mass they are in.
158
+ *
159
+ * Enough of them, and wide enough, that consecutive ones overlap. A row of
160
+ * lobes that do not touch is not a cloud with gaps in it, it is a row of
161
+ * lobes.
162
+ */
163
+ const LOBES_LEAST = 5
164
+
165
+ const LOBES_SPAN = 4
166
+
167
+ const LOBE_LEAST = 0.24
168
+
169
+ const LOBE_SPAN = 0.2
170
+
171
+ const MIN_SPAN = 1
172
+
173
+ /** Where a cloud is going, and how far up and down it goes on the way. */
174
+ interface Drift {
175
+ readonly at: number
176
+ readonly lean: number
177
+ readonly pace: number
178
+ readonly rate: number
179
+ readonly seat: number
180
+ readonly swell: number
181
+ }
182
+
183
+ export function createClouds(options: CloudsOptions): Clouds {
184
+ const random = makeRandom(options.seed ^ 0x5ce7)
185
+ const overhead = options.sky ?? SKY
186
+
187
+ let width = Math.max(MIN_SPAN, options.width)
188
+ let height = Math.max(MIN_SPAN, options.height)
189
+ let clock = 0
190
+
191
+ const drifts: Drift[] = []
192
+ const clouds: Cloud[] = []
193
+
194
+ function shape(span: number): Lobe[] {
195
+ const lobes: Lobe[] = []
196
+ const many = LOBES_LEAST + Math.floor(random() * (LOBES_SPAN + 1))
197
+
198
+ for (let made = 0; made < many; made++) {
199
+ const along = many < 2 ? 0.5 : made / (many - 1)
200
+ const r = span * (LOBE_LEAST + random() * LOBE_SPAN) * 0.5
201
+
202
+ lobes.push({
203
+ dx: (along - 0.5) * span,
204
+ dy: (random() - 0.5) * span * SQUAT,
205
+ r,
206
+ })
207
+ }
208
+
209
+ return lobes
210
+ }
211
+
212
+ /**
213
+ * One of them, made where it will be able to swell without leaving the sky.
214
+ *
215
+ * How far it rises and falls comes out of its heading rather than being drawn
216
+ * on its own: at the top of the swell it is going up as fast as its lean says
217
+ * it should, which is what makes a steep cloud a climbing one and a flat
218
+ * cloud a travelling one. Its seat is then drawn from what is left of the
219
+ * band, so nothing here is ever held against the top of the picture.
220
+ */
221
+ function born(): Cloud {
222
+ const band = height * overhead
223
+ const span = width * (SPAN_LEAST + random() * SPAN_SPAN)
224
+ const lean = (random() - 0.5) * 2 * LEAN
225
+ const pace = width * (PACE_LEAST + random() * PACE_SPAN)
226
+ const rate = SWELL_SLOWEST + random() * (SWELL_FASTEST - SWELL_SLOWEST)
227
+ const swell = Math.min(
228
+ band * SWELL_MOST,
229
+ (Math.abs(Math.sin(lean)) * pace) / (2 * Math.PI * rate),
230
+ )
231
+ const seat = swell + random() * Math.max(0, band - 2 * swell)
232
+
233
+ drifts.push({ at: random() * Math.PI * 2, lean, pace, rate, seat, swell })
234
+
235
+ return {
236
+ lobes: shape(span),
237
+ span,
238
+ thick: THICK_LEAST + random() * (THICK_MOST - THICK_LEAST),
239
+ x: random() * width,
240
+ y: seat,
241
+ }
242
+ }
243
+
244
+ for (let made = 0; made < Math.max(0, options.count); made++) clouds.push(born())
245
+
246
+ function settle(one: Cloud, drift: Drift): void {
247
+ one.y = drift.seat + Math.sin(drift.at + clock * drift.rate * Math.PI * 2) * drift.swell
248
+ }
249
+
250
+ return {
251
+ clouds,
252
+
253
+ cover(x, y) {
254
+ let held = 0
255
+
256
+ for (const one of clouds) {
257
+ if (Math.abs(x - one.x) > one.span) continue
258
+
259
+ let under = 0
260
+ for (const lobe of one.lobes) {
261
+ const from = Math.hypot(x - (one.x + lobe.dx), y - (one.y + lobe.dy)) / lobe.r
262
+ if (from >= 1) continue
263
+ under = Math.max(under, (1 - from * from) ** 2)
264
+ }
265
+
266
+ held = Math.max(held, under * one.thick)
267
+ }
268
+
269
+ return held
270
+ },
271
+
272
+ /**
273
+ * A box of another shape is another sky.
274
+ *
275
+ * Everything about a cloud is a share of the box it is in: how wide it is,
276
+ * how far it travels in a second, how far down it may sit. Carrying the
277
+ * old ones over would be masses of the old box's width drifting at the old
278
+ * box's pace, so they are made again. Nothing here is watched closely
279
+ * enough for that to show.
280
+ */
281
+ resize(nextWidth, nextHeight, count) {
282
+ width = Math.max(MIN_SPAN, nextWidth)
283
+ height = Math.max(MIN_SPAN, nextHeight)
284
+
285
+ const many = count ?? clouds.length
286
+ clouds.length = 0
287
+ drifts.length = 0
288
+ for (let made = 0; made < Math.max(0, many); made++) clouds.push(born())
289
+ },
290
+
291
+ step(seconds) {
292
+ clock += Math.min(Math.max(seconds, 0), 0.1)
293
+
294
+ for (let at = 0; at < clouds.length; at++) {
295
+ const one = clouds[at]
296
+ const drift = drifts[at]
297
+ if (!one || !drift) continue
298
+
299
+ one.x += Math.cos(drift.lean) * drift.pace * seconds
300
+ const over = width + one.span * 2
301
+ if (one.x > width + one.span) one.x -= over
302
+ if (one.x < -one.span) one.x += over
303
+
304
+ settle(one, drift)
305
+ }
306
+ },
307
+ }
308
+ }