// The digital rain's arithmetic, apart from the canvas: the glyph cell, how a // column starts and steps, which glyph a column draws, and the translucent // fade colour that leaves each glyph's trail. Plain functions, with the random // source passed in, so they can be tested. export const GLYPHS = "アイウエオカキクケコサシスセソタチツテト0123456789:=*+-<>¦"; /** The glyph cell size, in px. */ export const CELL = 16; /** How far above the top a column may start, in cells. */ const MAX_HEAD_START = 40; /** The chance, once a column has fallen past the bottom, that it restarts at * the top on a given step. */ const RESTART_ABOVE = 0.975; /** The fade trail's alpha. */ const FADE_ALPHA = 0.09; /** Whether the rain may move. The app's own motion setting (`data-motion` on * the document, from the settings menu) wins over the OS preference in either * direction: `reduce` stops it, `allow` lets it run on a reduce-motion OS, and * anything else follows the OS. */ export function rainAllowed( dataMotion: string | undefined, osReduces: boolean, ): boolean { if (dataMotion === "reduce") return false; if (dataMotion === "allow") return true; return !osReduces; } /** How many columns fill a canvas this wide. */ export function columnsFor(width: number): number { return Math.max(1, Math.ceil(width / CELL)); } /** Each column's starting row: up to `MAX_HEAD_START` cells above the top, so * the columns do not all start falling together. */ export function startDrops(cols: number, rand: () => number): number[] { return Array.from({ length: cols }, () => Math.floor(rand() * -MAX_HEAD_START), ); } /** One step: every column moves down a row, and one that has fallen past the * bottom of a canvas `height` tall goes back to the top on a small chance. */ export function stepDrops( drops: number[], height: number, rand: () => number, ): number[] { return drops.map((row) => { const restarted = row * CELL > height && rand() > RESTART_ABOVE ? 0 : row; return restarted + 1; }); } /** The glyph a column draws this step. */ export function glyphFor(rand: () => number): string { return GLYPHS[Math.floor(rand() * GLYPHS.length)] ?? "0"; } /** The fade colour for a computed background colour: the same channels at a low * alpha. They are read out numerically, so the trail fades whatever spacing * `rgb()` or `rgba()` comes back in; a colour with no channels gets the * default background's, so the trail never fills solid. */ export function fadeColorOf(background: string): string { const m = background.match(/(\d+)[^\d]+(\d+)[^\d]+(\d+)/); const [r, g, b] = m ? [m[1], m[2], m[3]] : [30, 30, 46]; return `rgba(${r}, ${g}, ${b}, ${FADE_ALPHA})`; }