import React, { useEffect, useRef } from "react"; const CHAR_WIDTH = 10; // canvas pixels const CHAR_HEIGHT = 12; // canvas pixels const ROWS = 50; const COLS = 50; const TRAIL_COUNT = 50; const ALPHABET = (() => { let alphabet = []; // 0 - 9 for (let charCode = "0".charCodeAt(0); charCode <= "9".charCodeAt(0); ++charCode) { alphabet.push(String.fromCharCode(charCode)); } // A - Z for (let charCode = "A".charCodeAt(0); charCode <= "Z".charCodeAt(0); ++charCode) { alphabet.push(String.fromCharCode(charCode)); } // https://en.wikipedia.org/wiki/Half-width_kana for (let codePoint = 0xFF71; codePoint <= 0xFF9D; ++codePoint) { alphabet.push(String.fromCodePoint(codePoint)); } return alphabet; })(); // TODO add more const MEMES = [ "ABFAHRT", "BALLERN", "NO SLEEP", "ZOCKEN ALTER", "MEGA KRASSE LAN", "アヤヤアヤヤ", // Ayaya Ayaya "オマエハモウシンテイル", // Omae wa mou shindeiru ] const NORMAL_STYLE = "#020"; const MEME_STYLE = "#050"; const HEAD_STYLE = "#555"; /** * Likelihood of a meme to appear after each character. * * 0.1 means there is one meme approximately every 10 chars. */ const MEME_PROBABILITY = 0.02; type Trail = { /** Column in which the trail falls. Should be between 0 and `COLS` */ col: number, /** Row where the head (lowest character) of the trail is. May be negative or above `ROWS`. */ head: number, /** Number of characters to display above, including the head */ length: number, /** * Vertical segments of text to show in the trail body * * The sum of each `chars.length` should be at least `ROWS`, but may be greater. */ content: { chars: string, fillStyle: string, // TODO is there a better type for this? }[], } function randomTrail(): Trail { let content = [] let current = ""; let totalLength = 0; while (totalLength < ROWS) { if (Math.random() < MEME_PROBABILITY) { // Commit the current non-meme chars if (current.length > 0) { content.push({ chars: current, fillStyle: NORMAL_STYLE, }); current = ""; } // Pick and add a random meme const meme = choice(MEMES); // If the meme has spaces, replace them with random characters // This also places a random character after the meme, ensuring // that two memes don't come immediately after each other for (let chunk of meme.split(" ")) { content.push({ chars: chunk, fillStyle: MEME_STYLE, }); content.push({ chars: choice(ALPHABET), fillStyle: NORMAL_STYLE, }); } totalLength += meme.length + 1; } else { // No meme, just add one random character current += choice(ALPHABET); totalLength += 1; } } if (current.length > 0) { content.push({ chars: current, fillStyle: NORMAL_STYLE, }); } return { col: randRange(0, COLS), head: randRange(-1 * COLS, 0), // spawn above the screen initially length: randRange(5, 20), content: content }; } function randRange(from: number, to: number) { return from + Math.floor((to - from + 1) * Math.random()); } function choice(candidates: T[]): T { return candidates[randRange(0, candidates.length - 1)]; } const MatrixBackground = () => { const canvasRef: React.MutableRefObject = useRef(null); useEffect(() => { const canvas = canvasRef.current if (canvas === null) return; const ctx = canvas.getContext("2d"); if (ctx === null) throw new Error('getContext("2d") returned null!'); ctx.font = '10pt monospace'; const width = canvas.width; const height = canvas.height; const trails: Trail[] = new Array(TRAIL_COUNT).fill(0).map((_) => randomTrail()); const matrixEffect = () => { ctx.fillStyle = "#000"; ctx.fillRect(0, 0, width, height); // draw all trails for (let trail of trails) { const x = trail.col * CHAR_WIDTH; let idx = 0; // TODO this can probably be optimized outer: for (let item of trail.content) { for (let char of item.chars) { // stop drawing characters after reaching the head if (idx > trail.head) break outer; // skip characters behind the trail end if (idx < trail.head - trail.length) { idx += 1; continue; } // draw the character, overwriting any that are already there const y = idx * CHAR_HEIGHT; ctx.fillStyle = "#000"; ctx.fillRect(x, y - CHAR_HEIGHT, CHAR_WIDTH, CHAR_HEIGHT); ctx.fillStyle = (idx === trail.head ? HEAD_STYLE : item.fillStyle); ctx.fillText(char, x, y); idx += 1; } } } // update all trails for (let i in trails) { trails[i].head += 1; if (trails[i].head - trails[i].length > ROWS) { // trail is completely off-screen, generate a new one trails[i] = randomTrail(); trails[i].head = 0; } } } const interval = setInterval(matrixEffect, 100); return () => { clearInterval(interval); } }, [canvasRef]); return ( ) }; export default MatrixBackground;