Enter matrix
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Kai Vogelgesang 2022-10-11 13:49:15 +02:00
parent 1046ff09e0
commit ac757fd322
Signed by: kai
GPG Key ID: 0A95D3B6E62C0879
4 changed files with 226 additions and 20 deletions

211
src/MatrixBackground.tsx Normal file
View File

@ -0,0 +1,211 @@
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",
"NOSLEEP",
"アヤヤアヤヤ", // 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);
content.push({
chars: meme,
fillStyle: MEME_STYLE,
});
totalLength += meme.length;
} 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
ctx.fillStyle = (idx === trail.head ? HEAD_STYLE : item.fillStyle);
const y = idx * CHAR_HEIGHT;
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();
}
}
}
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;

View File

@ -20,7 +20,6 @@
} }
.App-header { .App-header {
background-color: #282c34;
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -1,27 +1,23 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import logo from './logo.svg';
import './PartyPage.css'; import './PartyPage.css';
import { PartyContext } from './PartyContext'; import { PartyContext } from './PartyContext';
import MatrixBackground from './MatrixBackground';
const myDear = {
"m": "lieber",
"f": "liebe",
"d": "liebes",
};
export const PartyPage: React.FC = () => { export const PartyPage: React.FC = () => {
const partyContext = useContext(PartyContext); const partyContext = useContext(PartyContext);
return <div className="App" > return <div className="App">
<header className="App-header" > <MatrixBackground />
<img src={logo} className="App-logo" alt="logo" /> <header className="App-header">
<p> <span>Hallo {myDear[partyContext.self.grammatical_gender]} {partyContext.self.name},</span>
Edit <code> src/PartyPage.tsx </code> and save to reload. <span>komm doch zur Party!</span>
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
<span>Hello {partyContext.self.name}</span>
</header> </header>
</div> </div>
}; };

View File

@ -1,7 +1,7 @@
import { APIEndPoint, PartyStatus, SelfStatus } from "./PartyContext"; import { APIEndPoint, PartyStatus, SelfStatus } from "./PartyContext";
export const parseURI = (uri: string): APIEndPoint | undefined => { export const parseURI = (uri: string): APIEndPoint | undefined => {
const x = uri.match(/https?:\/\/(?<partyName>.+)\.party\.leafbla\.de\/(?<token>.+)/); const x = uri.match(/https?:\/\/(?<partyName>.+)\.(?<host>.+)\/(?<token>.+)/);
if (x === null || x.groups === undefined) return; if (x === null || x.groups === undefined) return;
const partyName = x.groups["partyName"]; const partyName = x.groups["partyName"];
const token = x.groups["token"]; const token = x.groups["token"];
@ -9,7 +9,7 @@ export const parseURI = (uri: string): APIEndPoint | undefined => {
return { partyName, token }; return { partyName, token };
}; };
const apiUrl = (apiEndPoint : APIEndPoint): string => { const apiUrl = (apiEndPoint: APIEndPoint): string => {
return `https://party.leafbla.de/api/${apiEndPoint.partyName}/${apiEndPoint.token}`; return `https://party.leafbla.de/api/${apiEndPoint.partyName}/${apiEndPoint.token}`;
}; };