diff --git a/src/MatrixBackground.tsx b/src/MatrixBackground.tsx new file mode 100644 index 0000000..1406272 --- /dev/null +++ b/src/MatrixBackground.tsx @@ -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(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 + 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 ( + + ) + +}; + +export default MatrixBackground; \ No newline at end of file diff --git a/src/PartyPage.css b/src/PartyPage.css index 07b1f68..fd62b40 100644 --- a/src/PartyPage.css +++ b/src/PartyPage.css @@ -20,7 +20,6 @@ } .App-header { - background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; diff --git a/src/PartyPage.tsx b/src/PartyPage.tsx index 8ca2536..effc595 100644 --- a/src/PartyPage.tsx +++ b/src/PartyPage.tsx @@ -1,27 +1,23 @@ import React, { useContext } from 'react'; -import logo from './logo.svg'; import './PartyPage.css'; import { PartyContext } from './PartyContext'; +import MatrixBackground from './MatrixBackground'; + +const myDear = { + "m": "lieber", + "f": "liebe", + "d": "liebes", +}; export const PartyPage: React.FC = () => { const partyContext = useContext(PartyContext); - - return
-
- logo -

- Edit src/PartyPage.tsx and save to reload. -

- - Learn React - - Hello {partyContext.self.name} + + return
+ +
+ Hallo {myDear[partyContext.self.grammatical_gender]} {partyContext.self.name}, + komm doch zur Party!
}; \ No newline at end of file diff --git a/src/partyApi.ts b/src/partyApi.ts index e7d87f7..f6456e7 100644 --- a/src/partyApi.ts +++ b/src/partyApi.ts @@ -1,7 +1,7 @@ import { APIEndPoint, PartyStatus, SelfStatus } from "./PartyContext"; export const parseURI = (uri: string): APIEndPoint | undefined => { - const x = uri.match(/https?:\/\/(?.+)\.party\.leafbla\.de\/(?.+)/); + const x = uri.match(/https?:\/\/(?.+)\.(?.+)\/(?.+)/); if (x === null || x.groups === undefined) return; const partyName = x.groups["partyName"]; const token = x.groups["token"]; @@ -9,7 +9,7 @@ export const parseURI = (uri: string): APIEndPoint | undefined => { return { partyName, token }; }; -const apiUrl = (apiEndPoint : APIEndPoint): string => { +const apiUrl = (apiEndPoint: APIEndPoint): string => { return `https://party.leafbla.de/api/${apiEndPoint.partyName}/${apiEndPoint.token}`; };