matrix rain katakana digital dark hacker cyberpunk code falling
Matrix Rain
matrix-rain.js
/**
* Matrix Rain — falling katakana + digit characters with fade trails.
* Category: backgrounds | Animate: true | Interactive: false | Complexity: medium
*/
export default function matrixRain(ctx, state) {
const CHAR_SIZE = 14;
const TRAIL_LENGTH = 16;
const CHARS =
'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン01';
let columns = [];
function init(width, height) {
const count = Math.ceil(width / CHAR_SIZE);
columns = Array.from({ length: count }, (_, i) => ({
x: i * CHAR_SIZE + CHAR_SIZE / 2,
y: -(Math.random() * height * 1.5),
speed: 0.6 + Math.random() * 1.4,
charOffset: Math.floor(Math.random() * CHARS.length),
}));
}
init(state.width, state.height);
return {
resize({ width, height }) {
init(width, height);
},
frame({ width, height, time, delta, reducedMotion }) {
ctx.clearRect(0, 0, width, height);
if (reducedMotion) {
// Static snapshot — no animation
ctx.font = `${CHAR_SIZE}px monospace`;
for (const col of columns) {
const steps = Math.floor(Math.abs(col.y) / CHAR_SIZE);
for (let i = 0; i < Math.min(steps, TRAIL_LENGTH); i++) {
const alpha = (1 - i / TRAIL_LENGTH) * 0.25;
ctx.fillStyle = `oklch(0.55 0.2 145 / ${alpha})`;
const charIdx = (col.charOffset + i) % CHARS.length;
ctx.fillText(CHARS[charIdx], col.x - CHAR_SIZE / 2, Math.abs(col.y) - i * CHAR_SIZE);
}
}
return;
}
ctx.font = `${CHAR_SIZE}px monospace`;
ctx.textBaseline = 'top';
for (const col of columns) {
const headY = col.y;
// Draw trail from head upward
for (let i = 0; i < TRAIL_LENGTH; i++) {
const y = headY - i * CHAR_SIZE;
if (y < -CHAR_SIZE || y > height) continue;
const charIdx = (col.charOffset + Math.floor(time * col.speed * 8) + i) % CHARS.length;
const char = CHARS[charIdx];
if (i === 0) {
// Head: bright white-green
ctx.fillStyle = 'oklch(0.96 0.1 145)';
} else {
// Trail: fading green
const alpha = (1 - i / TRAIL_LENGTH) * 0.85;
ctx.fillStyle = `oklch(0.55 0.22 145 / ${alpha})`;
}
ctx.fillText(char, col.x - CHAR_SIZE / 2, y);
}
// Advance column
col.y += col.speed * delta * 80;
if (col.y > height + CHAR_SIZE * TRAIL_LENGTH) {
col.y = -(CHAR_SIZE * (3 + Math.random() * 12));
col.speed = 0.6 + Math.random() * 1.4;
col.charOffset = Math.floor(Math.random() * CHARS.length);
}
}
},
destroy() {
columns = [];
},
};
}
embed.html
<canvas id="canvas-effect" style="width:100%;height:400px;display:block" aria-hidden="true"></canvas>
<script>
/**
* mountCanvas — Minimal runtime for Canvas effects.
*
* Handles HiDPI scaling, ResizeObserver, RAF loop, pointer tracking,
* reduced-motion, and cleanup. The effect only implements draw logic.
*
* @param {HTMLCanvasElement} canvas
* @param {(ctx: CanvasRenderingContext2D, state: object) => object} createEffect
* @param {object} [options]
* @returns {() => void} destroy function
*/
function mountCanvas(canvas, createEffect, options = {}) {
const ctx = canvas.getContext('2d', { alpha: true });
if (!ctx) throw new Error('2D context not available');
const opts = {
animate: true,
clear: true,
maxDpr: 2,
pauseWhenHidden: true,
respectReducedMotion: true,
...options,
};
const reducedMotion =
opts.respectReducedMotion && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const state = {
canvas,
ctx,
width: 0,
height: 0,
pixelWidth: 0,
pixelHeight: 0,
dpr: 1,
time: 0,
delta: 0,
frame: 0,
playing: false,
reducedMotion,
pointer: { x: 0, y: 0, inside: false, down: false },
};
let rafId = 0;
let lastTime = 0;
let disposed = false;
function measure() {
const rect = canvas.getBoundingClientRect();
const w = Math.max(1, rect.width);
const h = Math.max(1, rect.height);
const dpr = Math.min(window.devicePixelRatio || 1, opts.maxDpr);
const pw = Math.round(w * dpr);
const ph = Math.round(h * dpr);
if (canvas.width !== pw || canvas.height !== ph) {
canvas.width = pw;
canvas.height = ph;
}
state.width = w;
state.height = h;
state.pixelWidth = pw;
state.pixelHeight = ph;
state.dpr = dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function clear() {
ctx.clearRect(0, 0, state.width, state.height);
}
const effect = createEffect(ctx, state) || {};
function onResize() {
measure();
effect.resize?.(state);
if (!opts.animate || reducedMotion) {
if (opts.clear) clear();
effect.frame?.(state);
}
}
function loop(now) {
if (disposed) return;
if (opts.pauseWhenHidden && document.hidden) {
rafId = requestAnimationFrame(loop);
return;
}
state.delta = lastTime ? (now - lastTime) / 1000 : 0;
state.time = now / 1000;
state.frame += 1;
lastTime = now;
if (opts.clear) clear();
effect.frame?.(state);
rafId = requestAnimationFrame(loop);
}
function onPointerMove(e) {
const rect = canvas.getBoundingClientRect();
state.pointer.x = e.clientX - rect.left;
state.pointer.y = e.clientY - rect.top;
if (!opts.animate || reducedMotion) {
if (opts.clear) clear();
effect.frame?.(state);
}
}
function onPointerEnter() {
state.pointer.inside = true;
}
function onPointerLeave() {
state.pointer.inside = false;
state.pointer.down = false;
}
function onPointerDown() {
state.pointer.down = true;
}
function onPointerUp() {
state.pointer.down = false;
}
const ro = new ResizeObserver(onResize);
ro.observe(canvas);
canvas.addEventListener('pointermove', onPointerMove);
canvas.addEventListener('pointerenter', onPointerEnter);
canvas.addEventListener('pointerleave', onPointerLeave);
canvas.addEventListener('pointerdown', onPointerDown);
window.addEventListener('pointerup', onPointerUp);
measure();
if (opts.animate && !reducedMotion) {
state.playing = true;
rafId = requestAnimationFrame(loop);
} else {
effect.frame?.(state);
}
return function destroy() {
disposed = true;
state.playing = false;
cancelAnimationFrame(rafId);
ro.disconnect();
canvas.removeEventListener('pointermove', onPointerMove);
canvas.removeEventListener('pointerenter', onPointerEnter);
canvas.removeEventListener('pointerleave', onPointerLeave);
canvas.removeEventListener('pointerdown', onPointerDown);
window.removeEventListener('pointerup', onPointerUp);
effect.destroy?.(state);
};
}
/**
* Matrix Rain — falling katakana + digit characters with fade trails.
* Category: backgrounds | Animate: true | Interactive: false | Complexity: medium
*/
const __effect = function matrixRain(ctx, state) {
const CHAR_SIZE = 14;
const TRAIL_LENGTH = 16;
const CHARS =
'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン01';
let columns = [];
function init(width, height) {
const count = Math.ceil(width / CHAR_SIZE);
columns = Array.from({ length: count }, (_, i) => ({
x: i * CHAR_SIZE + CHAR_SIZE / 2,
y: -(Math.random() * height * 1.5),
speed: 0.6 + Math.random() * 1.4,
charOffset: Math.floor(Math.random() * CHARS.length),
}));
}
init(state.width, state.height);
return {
resize({ width, height }) {
init(width, height);
},
frame({ width, height, time, delta, reducedMotion }) {
ctx.clearRect(0, 0, width, height);
if (reducedMotion) {
// Static snapshot — no animation
ctx.font = `${CHAR_SIZE}px monospace`;
for (const col of columns) {
const steps = Math.floor(Math.abs(col.y) / CHAR_SIZE);
for (let i = 0; i < Math.min(steps, TRAIL_LENGTH); i++) {
const alpha = (1 - i / TRAIL_LENGTH) * 0.25;
ctx.fillStyle = `oklch(0.55 0.2 145 / ${alpha})`;
const charIdx = (col.charOffset + i) % CHARS.length;
ctx.fillText(CHARS[charIdx], col.x - CHAR_SIZE / 2, Math.abs(col.y) - i * CHAR_SIZE);
}
}
return;
}
ctx.font = `${CHAR_SIZE}px monospace`;
ctx.textBaseline = 'top';
for (const col of columns) {
const headY = col.y;
// Draw trail from head upward
for (let i = 0; i < TRAIL_LENGTH; i++) {
const y = headY - i * CHAR_SIZE;
if (y < -CHAR_SIZE || y > height) continue;
const charIdx = (col.charOffset + Math.floor(time * col.speed * 8) + i) % CHARS.length;
const char = CHARS[charIdx];
if (i === 0) {
// Head: bright white-green
ctx.fillStyle = 'oklch(0.96 0.1 145)';
} else {
// Trail: fading green
const alpha = (1 - i / TRAIL_LENGTH) * 0.85;
ctx.fillStyle = `oklch(0.55 0.22 145 / ${alpha})`;
}
ctx.fillText(char, col.x - CHAR_SIZE / 2, y);
}
// Advance column
col.y += col.speed * delta * 80;
if (col.y > height + CHAR_SIZE * TRAIL_LENGTH) {
col.y = -(CHAR_SIZE * (3 + Math.random() * 12));
col.speed = 0.6 + Math.random() * 1.4;
col.charOffset = Math.floor(Math.random() * CHARS.length);
}
}
},
destroy() {
columns = [];
},
};
}
const canvas = document.getElementById('canvas-effect');
mountCanvas(canvas, __effect, { animate: true, clear: true });
</script> Details
Animated Reduced Motion aria-hidden
matrixrainkatakanadigitaldarkhackercyberpunkcodefalling
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
| CHAR_SIZE | number | 14 | Font size in px — also controls column width |
| TRAIL_LENGTH | number | 16 | Number of fading characters behind the head |
| AMPLITUDE | number | 1.4 | Max speed multiplier for column fall speed |
Draws a configurable number of character columns, each falling at a randomised speed with a TRAIL_LENGTH-character fade tail rendered per-frame — no persistent canvas state needed. The head character is bright white-green; the trail fades to near-transparent using per-step alpha. Columns reset to a random height above the canvas when they clear the bottom.
At reducedMotion: true a static snapshot replaces the animation — columns freeze in place with reduced opacity.