Skip to content

Commit 9da35d1

Browse files
author
Ricardo Fernández Serrata
committed
Closes #7
1 parent ac26d90 commit 9da35d1

File tree

4 files changed

+166
-69
lines changed

4 files changed

+166
-69
lines changed

.eslintrc.json

+9-2
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,21 @@
1414
"ignore": [
1515
-1,
1616
0,
17-
1
17+
1,
18+
2
1819
],
1920
"ignoreArrayIndexes": true,
2021
"ignoreDefaultValues": true,
2122
"enforceConst": true
2223
}
2324
],
24-
"no-unused-vars": "off",
25+
"no-unused-vars": [
26+
"warn",
27+
{
28+
"varsIgnorePattern": "^_$",
29+
"argsIgnorePattern": "^_$"
30+
}
31+
],
2532
"no-implicit-globals": [
2633
"error",
2734
{

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "rgb-digital-rain",
3-
"version": "0.32.0",
3+
"version": "0.34.0",
44
"main": "src/main.js",
55
"scripts": {
66
"test": "echo \"Error: no test specified\" && exit 1"
@@ -18,4 +18,4 @@
1818
"devDependencies": {
1919
"eslint": "^8.57.0"
2020
}
21-
}
21+
}

src/main.js

+153-63
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ const RGBDR_anim = (() => {
99
/** Maximum `Uint32` + 1 */
1010
POW2_32 = 2 ** 32 //eslint-disable-line no-magic-numbers
1111

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+
1221
/**
1322
@param {number} n
1423
*/
@@ -124,45 +133,64 @@ const RGBDR_anim = (() => {
124133
})()
125134

126135
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+
*/
127145
#x
128146
#y
147+
/** expiration height */
129148
#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+
*/
130154
#color
131155
/**
132-
Create with default props.
156+
Create with default fields.
133157
Use `init` to set them.
134158
*/
135159
constructor() {
136160
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'
139165
}
140166

141167
/**
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.
143171
@param {number} x finite
144172
@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.
151173
*/
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))
154176
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}`)
159177

160178
this.#x = x
161179
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)
164186
return this
165187
}
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+
}
166194

167195
get x() { return this.#x }
168196
get y() { return this.#y }
@@ -178,17 +206,14 @@ const RGBDR_anim = (() => {
178206
inc_y(n) {
179207
// don't do this at home, kids!
180208
const y = this.#y += n
181-
if (!isFinite(y))
209+
if (is_inf_nan(y))
182210
throw new RangeError(`${y}`)
183211
return y
184212
}
185213
}
186214

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
192217

193218
/**
194219
Set `canv` dimensions to fill the full viewport.
@@ -201,47 +226,99 @@ const RGBDR_anim = (() => {
201226
const scale = devicePixelRatio
202227
canv.width = clientWidth * scale >>> 0
203228
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)
207263
}
208264

209265
/**
210266
@param {DOMHighResTimeStamp} now
211267
*/
212268
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)
216277
return
217278

218-
const
219-
{ colors, charset } = anim.settings,
220-
size = get_droplet_size()
279+
const size = droplet_abs_size, h = canv.height
221280

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()) {
224285
// this is outside `for...of`
225286
// to take advantage of batch-rendering
226-
ctx.fillStyle = '#' + droplet.color
287+
ctx.fillStyle = '#' + d.color
227288

228289
// 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
241298
}
242-
else droplet.inc_y(size)
299+
d.inc_y(size)
243300
}
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+
245322
last_drop = now
246323
}
247324

@@ -290,25 +367,38 @@ const RGBDR_anim = (() => {
290367
}
291368

292369
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+
306380
anim.playing = true
307381

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+
308397
/**
309398
timeout ID, necessary to debounce `resize`
310399
@type {undefined|number}
311400
*/let tm_ID
401+
// should this be attached to `body` rather than `window`?
312402
addEventListener('resize', () => {
313403
clearTimeout(tm_ID)
314404
tm_ID = setTimeout(resize, anim.settings.resize_delay_ms)

0 commit comments

Comments
 (0)