@@ -9,6 +9,15 @@ const RGBDR_anim = (() => {
9
9
/** Maximum `Uint32` + 1 */
10
10
POW2_32 = 2 ** 32 //eslint-disable-line no-magic-numbers
11
11
12
+ /** `const isFinite` binding, for purity */
13
+ const not_inf_nan = isFinite
14
+
15
+ /**
16
+ checks if `x` is not finite
17
+ @param {number } x
18
+ */
19
+ const is_inf_nan = x => ! not_inf_nan ( x )
20
+
12
21
/**
13
22
@param {number } n
14
23
*/
@@ -124,45 +133,64 @@ const RGBDR_anim = (() => {
124
133
} ) ( )
125
134
126
135
const Droplet = class {
136
+ /*
137
+ there's no `size` field,
138
+ because I want all droplets to share the same size,
139
+ even when that size changes at runtime.
140
+
141
+ I'm aware of the implications,
142
+ such as trails with old sizes,
143
+ and trail "self-overlay".
144
+ */
127
145
#x
128
146
#y
147
+ /** expiration height */
129
148
#max_y
149
+ /**
150
+ Not a pointer to `settings.colors`,
151
+ because the droplet must hold a consistent color,
152
+ and a pointer could be invalidated at any time.
153
+ */
130
154
#color
131
155
/**
132
- Create with default props .
156
+ Create with default fields .
133
157
Use `init` to set them.
134
158
*/
135
159
constructor ( ) {
136
160
this . #x = this . #y = 0
137
- this . #max_y = 1
138
- this . #color = '777' // visible in light and dark schemes
161
+ this . #max_y = MAX_U8
162
+ // visible in light and dark schemes,
163
+ // for easier debugging
164
+ this . #color = '777'
139
165
}
140
166
141
167
/**
142
- Sets/Resets the props
168
+ Set `x` and `y` coordinates,
169
+ `max_y` is randomly-generated if `gen_max`,
170
+ `color` is randomly-picked from settings.
143
171
@param {number } x finite
144
172
@param {number } y finite
145
- @param {number } max_y finite
146
- @param {string } color 3 hexadecimal nibbles.
147
-
148
- Not a pointer to `settings.colors`,
149
- because the droplet must hold a consistent color,
150
- and a pointer could become invalid at any time.
151
173
*/
152
- init ( x , y , max_y , color ) {
153
- if ( ! isFinite ( x ) || ! isFinite ( y ) )
174
+ init ( x , y , gen_max = false ) {
175
+ if ( is_inf_nan ( x ) || is_inf_nan ( y ) )
154
176
throw new RangeError ( `invalid coords: x=${ x } y=${ y } ` )
155
- if ( ! isFinite ( max_y ) || y > max_y )
156
- throw new RangeError ( `invalid max_y: ${ max_y } ` )
157
- if ( ! / ^ [ a - f \d ] { 3 } $ / gi. test ( color ) )
158
- throw new RangeError ( `invalid color: ${ color } ` )
159
177
160
178
this . #x = x
161
179
this . #y = y
162
- this . #max_y = max_y
163
- this . #color = color
180
+ if ( gen_max ) {
181
+ const h = canv . height
182
+ //eslint-disable-next-line no-magic-numbers
183
+ this . #max_y = rand_U32 ( h * 3 / 4 , h + droplet_abs_size )
184
+ }
185
+ this . #color = rand_pick ( anim . settings . colors )
164
186
return this
165
187
}
188
+ /**
189
+ coords, `max_y` and `color` are random.
190
+ */
191
+ init_auto ( ) {
192
+ return this . init ( rng ( ) * canv . width , rng ( ) * droplet_abs_size , true )
193
+ }
166
194
167
195
get x ( ) { return this . #x }
168
196
get y ( ) { return this . #y }
@@ -178,17 +206,14 @@ const RGBDR_anim = (() => {
178
206
inc_y ( n ) {
179
207
// don't do this at home, kids!
180
208
const y = this . #y += n
181
- if ( ! isFinite ( y ) )
209
+ if ( is_inf_nan ( y ) )
182
210
throw new RangeError ( `${ y } ` )
183
211
return y
184
212
}
185
213
}
186
214
187
- const get_droplet_size = ( ) => anim . settings . droplet_rel_size * Math . max ( canv . width , canv . height )
188
-
189
- // pre-allocate.
190
- // https://en.wikipedia.org/wiki/Object_pool_pattern
191
- const droplet_ls = Array . from ( { length : DROPLET_DENSITY } , ( ) => new Droplet )
215
+ /** absolute pixel size */
216
+ let droplet_abs_size = 1
192
217
193
218
/**
194
219
Set `canv` dimensions to fill the full viewport.
@@ -201,47 +226,99 @@ const RGBDR_anim = (() => {
201
226
const scale = devicePixelRatio
202
227
canv . width = clientWidth * scale >>> 0
203
228
canv . height = clientHeight * scale >>> 0
204
- // is normalization necessary?
205
- //ctx.scale(scale, scale)
206
- ctx . font = `bold ${ get_droplet_size ( ) } px monospace`
229
+ //ctx.scale(scale, scale) // is normalization necessary?
230
+ // should it be W, H, max(W,H), min(W,H), hypot(W,H), or sqrt(W * H)?
231
+ droplet_abs_size = anim . settings . droplet_rel_size * canv . height
232
+ ctx . font = `bold ${ droplet_abs_size } px monospace`
233
+ }
234
+
235
+ /**
236
+ list of auto-generated `Droplet`s
237
+ */
238
+ const droplets_auto =
239
+ // https://en.wikipedia.org/wiki/Object_pool_pattern
240
+ Array . from ( {
241
+ // div2 leaves enough room
242
+ // for the user to spawn new droplets
243
+ length : DROPLET_DENSITY >> 1
244
+ } , ( ) => new Droplet )
245
+ /**
246
+ list of user-spawned `Droplet`s
247
+ @type {Droplet[] }
248
+ */
249
+ const droplets_user = [ ]
250
+
251
+ /**
252
+ fill a character randomly-picked from `charset`, into `ctx`.
253
+
254
+ unlike `fillText`, this is centered.
255
+ @param {number } x
256
+ @param {number } y
257
+ */
258
+ const draw_char = ( x , y ) => {
259
+ /** why does `4` center but not `2`? */
260
+ const CENTERING_FACTOR = 4
261
+ const size = droplet_abs_size / CENTERING_FACTOR
262
+ ctx . fillText ( rand_pick ( anim . settings . charset ) , x - size , y + size )
207
263
}
208
264
209
265
/**
210
266
@param {DOMHighResTimeStamp } now
211
267
*/
212
268
const draw_droplets = now => {
213
- // should it `ceil` instead of `trunc`?
214
- const times = ( now - last_drop ) / Hz_to_ms ( anim . settings . droplet_Hz ) >>> 0
215
- if ( times == 0 )
269
+ /**
270
+ max number of times to step per batch
271
+ */
272
+ const steps =
273
+ // should it `ceil` instead of `trunc`?
274
+ ( now - last_drop ) / Hz_to_ms ( anim . settings . droplet_Hz ) >>> 0
275
+ // guard against hi-FPS and low-speed
276
+ if ( steps == 0 )
216
277
return
217
278
218
- const
219
- { colors, charset } = anim . settings ,
220
- size = get_droplet_size ( )
279
+ const size = droplet_abs_size , h = canv . height
221
280
222
- // according to MDN docs, `forEach` seems to be thread-safe here (I guess)
223
- droplet_ls . forEach ( droplet => {
281
+ // the shallow-copy is needed because of `splice`,
282
+ // and because I'm too lazy to use a classic `for` loop
283
+ // with mutable indices.
284
+ for ( const [ i , d ] of [ ...droplets_user ] . entries ( ) ) {
224
285
// this is outside `for...of`
225
286
// to take advantage of batch-rendering
226
- ctx . fillStyle = '#' + droplet . color
287
+ ctx . fillStyle = '#' + d . color
227
288
228
289
// unlock speed limit to go beyond FPS ⚡
229
- for ( const _ of range ( times ) ) {
230
- ctx . fillText ( rand_pick ( charset ) , droplet . x , droplet . y )
231
-
232
- if ( droplet . y > droplet . max_y ) {
233
- const col = rand_pick ( colors )
234
- droplet . init (
235
- rng ( ) * canv . width ,
236
- rng ( ) * size ,
237
- rand_U32 ( canv . height * 3 / 4 , canv . height + size ) ,
238
- col
239
- )
240
- ctx . fillStyle = '#' + col
290
+ for ( const _ of range ( steps ) ) {
291
+ draw_char ( d . x , d . y )
292
+
293
+ // expire after going out-of-bounds
294
+ if ( d . y > h ) {
295
+ droplets_user . splice ( i , 1 )
296
+ // immediately stop processing `d`
297
+ break
241
298
}
242
- else droplet . inc_y ( size )
299
+ d . inc_y ( size )
243
300
}
244
- } )
301
+ }
302
+ // according to MDN, this is thread-safe
303
+ for ( const d of droplets_auto ) {
304
+ ctx . fillStyle = '#' + d . color
305
+
306
+ for ( const _ of range ( steps ) ) {
307
+ draw_char ( d . x , d . y )
308
+
309
+ if ( d . y > d . max_y ) {
310
+ d . init_auto ( )
311
+ /*
312
+ this isn't necessary,
313
+ but it adds an intentional delay
314
+ before processing the droplet's `init`ed version.
315
+ It also reduces the workload of each batch.
316
+ */ break
317
+ }
318
+ d . inc_y ( size )
319
+ }
320
+ }
321
+
245
322
last_drop = now
246
323
}
247
324
@@ -290,25 +367,38 @@ const RGBDR_anim = (() => {
290
367
}
291
368
292
369
const main = ( ) => {
293
- resize ( ) // not part of anim, and has some latency, so no RAF
294
- const
295
- { dim_factor, colors } = anim . settings ,
296
- size = get_droplet_size ( )
297
-
298
- ctx_fillFull ( dim_factor < 0 ? 'fff' : '000' )
299
-
300
- droplet_ls . forEach ( ( d , i ) => d . init (
301
- i * size , // uniformity
302
- rng ( ) * size , // details ✨
303
- rand_U32 ( canv . height * 3 / 4 , canv . height + size ) ,
304
- colors [ i % colors . length ] // everyone will be used
305
- ) )
370
+ // not part of anim, and has some latency, so no RAF
371
+ resize ( )
372
+ ctx_fillFull ( anim . settings . dim_factor < 0 ? 'fff' : '000' )
373
+ // these don't work as desired
374
+ //ctx.textAlign = 'center'
375
+ //ctx.textBaseline = 'middle'
376
+
377
+ for ( const d of droplets_auto )
378
+ d . init_auto ( )
379
+
306
380
anim . playing = true
307
381
382
+ canv . addEventListener ( 'click' , e => {
383
+ const scale = devicePixelRatio
384
+ droplets_user . push ( ( new Droplet ) . init (
385
+ e . clientX * scale , e . clientY * scale
386
+ ) )
387
+ /*
388
+ Because of batch-rendering,
389
+ the droplet will wait to be drawn
390
+ together with all the other ones.
391
+
392
+ So the user will experience a delay
393
+ if the speed-setting is low
394
+ */
395
+ } )
396
+
308
397
/**
309
398
timeout ID, necessary to debounce `resize`
310
399
@type {undefined|number }
311
400
*/ let tm_ID
401
+ // should this be attached to `body` rather than `window`?
312
402
addEventListener ( 'resize' , ( ) => {
313
403
clearTimeout ( tm_ID )
314
404
tm_ID = setTimeout ( resize , anim . settings . resize_delay_ms )
0 commit comments