lan-2022-10/src/MatrixBackground.tsx
Kai Vogelgesang 316c92ecd0
All checks were successful
continuous-integration/drone/push Build is passing
Fix overlap, Add memes
2022-10-11 15:39:19 +02:00

227 lines
6.4 KiB
TypeScript

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<T>(candidates: T[]): T {
return candidates[randRange(0, candidates.length - 1)];
}
const MatrixBackground = () => {
const canvasRef: React.MutableRefObject<HTMLCanvasElement | null> = 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 (
<canvas
ref={canvasRef}
style={{
background: '#000000',
position: 'fixed',
width: '100%',
height: '100%',
zIndex: -1,
top: '0',
left: '0',
objectFit: 'cover',
imageRendering: 'crisp-edges',
}}
width={COLS * CHAR_WIDTH}
height={ROWS * CHAR_HEIGHT}
/>
)
};
export default MatrixBackground;