@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/rays.ts ADDED
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Light coming down through the surface.
3
+ *
4
+ * The simulation behind the shafts in a seascape, kept free of the DOM the way
5
+ * `shoal.ts` and `drift.ts` are.
6
+ *
7
+ * A shaft is what makes water have a top. Without one the picture is a dark
8
+ * field with animals in it and no reason for the dark to be dark; with one
9
+ * there is a surface somewhere up there, the murk is depth rather than colour,
10
+ * and everything else in the scene acquires a distance from the light. It is
11
+ * the cheapest thing in the whole seascape and it changes the most.
12
+ *
13
+ * ## What a shaft is here
14
+ *
15
+ * A wedge, hung from the surface, leaning slightly. It is not drawn as a beam
16
+ * with edges: it is widest and faintest at the bottom, narrow and strongest at
17
+ * the top, and it fades out before it reaches anything, so it never lands on
18
+ * the seabed and never picks out an object. A shaft that lit something would be
19
+ * a spotlight, and the argument for it stops the moment it starts to look like
20
+ * one.
21
+ *
22
+ * Where it ends is `fade`, and the curve matters more than the length. Light
23
+ * thinning at a steady rate still stops at a rate, and an eye reads that as an
24
+ * edge across the water; what it wants is a shaft already almost gone by the
25
+ * time it runs out.
26
+ *
27
+ * ## Why they breathe
28
+ *
29
+ * Real shafts come from a moving surface, so they wander and they pulse, and
30
+ * the pulse is slow. Each one here holds its own phase and its own rate, which
31
+ * is the same trick `shoal.ts` uses on its fish and for the same reason: a set
32
+ * of shafts brightening together is a light being turned up, and a set that
33
+ * never agrees is a surface with waves on it.
34
+ *
35
+ * The rates are drawn low enough that nothing here is ever the fastest thing on
36
+ * the screen. Snow falls, fish swim, and the light takes its time.
37
+ */
38
+
39
+ import { makeRandom } from "./perlin.ts"
40
+
41
+ export interface Ray {
42
+ /**
43
+ * How bright it is now, as a share of the caller's own ceiling.
44
+ *
45
+ * A share rather than a colour or an alpha, because how dark ornament is
46
+ * allowed to be is the renderer's business and differs between a website that
47
+ * owes a contrast ratio to the words on top of it and a desktop that does
48
+ * not.
49
+ */
50
+ glow: number
51
+ /** How far down it reaches before it is gone, in px. */
52
+ readonly reach: number
53
+ /** Its width where it meets the surface, in px. */
54
+ readonly span: number
55
+ /** How far off vertical it leans, in radians. */
56
+ readonly tilt: number
57
+ /** Where it meets the surface, in px across the box. */
58
+ x: number
59
+ }
60
+
61
+ export interface Rays {
62
+ readonly rays: readonly Ray[]
63
+ resize(width: number, height: number, count?: number): void
64
+ step(seconds: number): void
65
+ }
66
+
67
+ export interface RaysOptions {
68
+ count: number
69
+ height: number
70
+ seed: number
71
+ width: number
72
+ }
73
+
74
+ /**
75
+ * How much wider a shaft is where it fades out than where it enters the water.
76
+ *
77
+ * Exported because it is a fact about the shape rather than a taste, and there
78
+ * is more than one renderer drawing these. A canvas and a scene graph disagreeing
79
+ * about how a shaft opens would be the same light in two different seas.
80
+ *
81
+ * It widens rather than narrows, which is worth saying because a beam drawn
82
+ * from a lamp does the opposite. The light here has already been through a
83
+ * rough surface, so what is spreading it is the water.
84
+ */
85
+ export const SPREAD = 2.4
86
+
87
+ /**
88
+ * What is left of a shaft's brightness partway down it, where `down` is the
89
+ * share of its reach travelled: the surface at 0, the end at 1.
90
+ *
91
+ * Exported alongside `SPREAD`, and for the same reason: how a shaft ends is a
92
+ * fact about the light rather than a taste of whichever renderer is drawing it.
93
+ *
94
+ * The curve leaves the surface flat and arrives flat, so a shaft holds its
95
+ * strength where it has some and is already almost nothing by the time it is
96
+ * nothing. A straight ramp cannot do that, and in water it does worse than it
97
+ * sounds: the sea it is drawn on darkens with depth as well, so the same light
98
+ * stands out further the further it falls, and a shaft thinning at a steady
99
+ * rate is a shaft holding its own all the way down and then stopping at a line.
100
+ */
101
+ export function fade(down: number): number {
102
+ const along = Math.min(Math.max(down, 0), 1)
103
+
104
+ return (1 - along ** 2) ** 2
105
+ }
106
+
107
+ /**
108
+ * How wide a shaft is at the surface, as a share of the box's width.
109
+ *
110
+ * Slim. The first pass drew them wide enough to overlap into slabs, and a slab
111
+ * of tinted water is not a shaft of light: what the eye wants is something
112
+ * narrower than it expects, because the width it remembers belongs to the glow
113
+ * around a shaft rather than to the shaft.
114
+ */
115
+ const SPAN_LEAST = 0.018
116
+
117
+ const SPAN_SPAN = 0.055
118
+
119
+ /**
120
+ * How far down one reaches, as a share of the box's height.
121
+ *
122
+ * Shallow enough that the deepest of them is spent in open water. The ground
123
+ * furthest away sits a little over half way down, so a shaft drawn to the foot
124
+ * of the box is a shaft lying on a hill.
125
+ */
126
+ const REACH_LEAST = 0.34
127
+
128
+ const REACH_SPAN = 0.28
129
+
130
+ /**
131
+ * How far off vertical a shaft may lean, in radians.
132
+ *
133
+ * Shallow, and all of them leaning the same way. Light refracting through a
134
+ * surface arrives from wherever the sun is, so shafts in the same water are
135
+ * near enough parallel; a set fanning out in all directions reads as a stage
136
+ * rather than a sea. The spread that is left is what stops them looking ruled.
137
+ */
138
+ const TILT = 0.16
139
+
140
+ const TILT_SPREAD = 0.07
141
+
142
+ /** The ends of a shaft's own brightness, as shares of the caller's ceiling. */
143
+ const GLOW_LEAST = 0.25
144
+
145
+ const GLOW_MOST = 1
146
+
147
+ /** How fast one breathes, slowest to fastest, in cycles a second. */
148
+ const BREATH_SLOWEST = 0.045
149
+
150
+ const BREATH_FASTEST = 0.11
151
+
152
+ /** How far a shaft wanders along the surface, in px, and how fast. */
153
+ const WANDER = 26
154
+
155
+ const WANDER_RATE = 0.06
156
+
157
+ const MIN_SPAN = 1
158
+
159
+ interface Breath {
160
+ readonly at: number
161
+ readonly rate: number
162
+ readonly seat: number
163
+ }
164
+
165
+ export function createRays(options: RaysOptions): Rays {
166
+ const random = makeRandom(options.seed ^ 0x33b7)
167
+
168
+ let width = Math.max(MIN_SPAN, options.width)
169
+ let height = Math.max(MIN_SPAN, options.height)
170
+ let clock = 0
171
+
172
+ const breaths: Breath[] = []
173
+ const rays: Ray[] = []
174
+
175
+ function born(): Ray {
176
+ breaths.push({
177
+ at: random() * Math.PI * 2,
178
+ rate: BREATH_SLOWEST + random() * (BREATH_FASTEST - BREATH_SLOWEST),
179
+ seat: random() * Math.PI * 2,
180
+ })
181
+
182
+ return {
183
+ glow: GLOW_LEAST,
184
+ reach: height * (REACH_LEAST + random() * REACH_SPAN),
185
+ span: width * (SPAN_LEAST + random() * SPAN_SPAN),
186
+ tilt: TILT + (random() - 0.5) * TILT_SPREAD,
187
+ x: random() * width,
188
+ }
189
+ }
190
+
191
+ for (let made = 0; made < Math.max(0, options.count); made++) rays.push(born())
192
+
193
+ return {
194
+ rays,
195
+
196
+ resize(nextWidth, nextHeight, count) {
197
+ const scaleX = Math.max(MIN_SPAN, nextWidth) / width
198
+ width = Math.max(MIN_SPAN, nextWidth)
199
+ height = Math.max(MIN_SPAN, nextHeight)
200
+
201
+ for (const one of rays) one.x *= scaleX
202
+
203
+ if (count == null) return
204
+ while (rays.length > count) {
205
+ rays.pop()
206
+ breaths.pop()
207
+ }
208
+ while (rays.length < count) rays.push(born())
209
+ },
210
+
211
+ step(seconds) {
212
+ clock += Math.min(Math.max(seconds, 0), 0.1)
213
+
214
+ for (let at = 0; at < rays.length; at++) {
215
+ const one = rays[at]
216
+ const breath = breaths[at]
217
+ if (!one || !breath) continue
218
+
219
+ const swell = (Math.sin(breath.at + clock * breath.rate * Math.PI * 2) + 1) / 2
220
+ one.glow = GLOW_LEAST + swell * (GLOW_MOST - GLOW_LEAST)
221
+ one.x += Math.cos(breath.seat + clock * WANDER_RATE * Math.PI * 2) * WANDER * seconds
222
+ }
223
+ },
224
+ }
225
+ }