Compare commits
2 Commits
fcfcf28eab
...
e3ea4772d6
| Author | SHA1 | Date | |
|---|---|---|---|
| e3ea4772d6 | |||
| 8acde1f9ad |
@@ -0,0 +1,8 @@
|
|||||||
|
______ _
|
||||||
|
| ____| | |
|
||||||
|
| |__ _ __ __ _ _ __ | | __
|
||||||
|
| __| '__/ _` | '_ \| |/ /
|
||||||
|
| | | | | (_| | | | | <
|
||||||
|
|_| |_| \__,_|_| |_|_|\_\
|
||||||
|
|
||||||
|
was here. sorry about the server.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
___ ___
|
||||||
|
/ __| / __|
|
||||||
|
| (_ | | (_ |
|
||||||
|
\___| \___|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
_____
|
||||||
|
/ \
|
||||||
|
| () () |
|
||||||
|
\ ^ /
|
||||||
|
|||||
|
||||||
|
|||||
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/\
|
||||||
|
//\\
|
||||||
|
// \\
|
||||||
|
// \\
|
||||||
|
|\ /|
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
| | | |
|
||||||
|
__ | | | | __
|
||||||
|
/ \____| | | |____/ \
|
||||||
|
) ___/ ) ( \___ (
|
||||||
|
\__/ \__/ \__/ \__/
|
||||||
|
\____/
|
||||||
|
) (
|
||||||
|
/ /\ \
|
||||||
|
) )( (
|
||||||
|
| || |
|
||||||
|
| || |
|
||||||
|
| || |
|
||||||
|
| || |
|
||||||
|
| )( |
|
||||||
|
) \/ (
|
||||||
|
\____/
|
||||||
|
(____)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { LineColor } from '../components/BootSequence';
|
import type { LineColor } from '../components/BootSequence';
|
||||||
|
import { BOOT_SCREENS } from '../components/MatrixRain';
|
||||||
|
|
||||||
export interface OutputLine {
|
export interface OutputLine {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -101,6 +102,7 @@ export const handlers: Record<string, CommandHandler> = {
|
|||||||
green(' ping <target> - Ping a target'),
|
green(' ping <target> - Ping a target'),
|
||||||
green(' rm <file> - Remove a file'),
|
green(' rm <file> - Remove a file'),
|
||||||
green(' sudo <cmd> - Run as root'),
|
green(' sudo <cmd> - Run as root'),
|
||||||
|
green(' ts - Check TeamSpeak server status'),
|
||||||
green(' reboot - Reboot the server'),
|
green(' reboot - Reboot the server'),
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -211,6 +213,30 @@ export const handlers: Record<string, CommandHandler> = {
|
|||||||
return [red(`rm: cannot remove '${args[args.length - 1]}': Permission denied`)];
|
return [red(`rm: cannot remove '${args[args.length - 1]}': Permission denied`)];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
theme: (args) => {
|
||||||
|
if (args.length === 0) {
|
||||||
|
const current = localStorage.getItem('stift15-bootscreen') || 'logo';
|
||||||
|
return [
|
||||||
|
amber('Boot screen themes:'),
|
||||||
|
...BOOT_SCREENS.map((s) =>
|
||||||
|
green(` ${s === current ? '▸ ' : ' '}${s}`)
|
||||||
|
),
|
||||||
|
dim(''),
|
||||||
|
dim(`Usage: theme <name>`),
|
||||||
|
dim('Run "reboot" to see the new boot screen.'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const name = args[0].toLowerCase();
|
||||||
|
if (!BOOT_SCREENS.includes(name)) {
|
||||||
|
return [red(`theme: '${name}' not found. Available: ${BOOT_SCREENS.join(', ')}`)];
|
||||||
|
}
|
||||||
|
localStorage.setItem('stift15-bootscreen', name);
|
||||||
|
return [
|
||||||
|
green(`Boot screen set to '${name}'.`),
|
||||||
|
dim('Run "reboot" to see it.'),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
sudo: (args) => {
|
sudo: (args) => {
|
||||||
if (args.length === 0) return [red('sudo: missing command')];
|
if (args.length === 0) return [red('sudo: missing command')];
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -91,12 +91,23 @@ export function Bricked() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{visibleCount >= LINES.length && (
|
{visibleCount >= LINES.length && (
|
||||||
<div className="text-term-dim whitespace-pre leading-[1.4] mt-2">
|
<>
|
||||||
<span
|
<div className="text-term-dim whitespace-pre leading-[1.4] mt-2">
|
||||||
className="inline-block w-[0.6em] h-[1em] bg-term-dim ml-0.5 align-middle"
|
<span
|
||||||
style={{ animation: 'blink 1s step-end infinite' }}
|
className="inline-block w-[0.6em] h-[1em] bg-term-dim ml-0.5 align-middle"
|
||||||
/>
|
style={{ animation: 'blink 1s step-end infinite' }}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="mt-6 px-4 py-2 border border-term-dim text-term-dim font-mono text-sm hover:text-term-green hover:border-term-green transition-colors cursor-pointer bg-transparent"
|
||||||
|
onClick={() => {
|
||||||
|
localStorage.removeItem('stift15-bricked');
|
||||||
|
setUnbricked(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
[ F ]
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { handlers, type OutputLine } from '../commands/handlers';
|
import { handlers, type OutputLine } from "../commands/handlers";
|
||||||
|
|
||||||
const PROMPT = 'stift15@server:~$ ';
|
const PROMPT = "stift15@server:~$ ";
|
||||||
|
|
||||||
const WELCOME: OutputLine[] = [
|
const WELCOME: OutputLine[] = [
|
||||||
{ text: '> Connection restored.', color: 'green' },
|
{ text: "> Connection restored.", color: "green" },
|
||||||
{ text: '> Welcome to Stift15 Terminal v1.0', color: 'green' },
|
{ text: "> Welcome to Stift15 Terminal v1.0", color: "green" },
|
||||||
{ text: '> Type \'help\' for available commands.', color: 'dim' },
|
{ text: "> Type 'help' for available commands.", color: "dim" },
|
||||||
{ text: '', color: 'dim' },
|
{ text: "", color: "dim" },
|
||||||
];
|
];
|
||||||
|
|
||||||
interface CommandPromptProps {
|
interface CommandPromptProps {
|
||||||
@@ -17,14 +17,14 @@ interface CommandPromptProps {
|
|||||||
|
|
||||||
export function CommandPrompt({ onReboot, onBrick }: CommandPromptProps) {
|
export function CommandPrompt({ onReboot, onBrick }: CommandPromptProps) {
|
||||||
const [outputLines, setOutputLines] = useState<OutputLine[]>(WELCOME);
|
const [outputLines, setOutputLines] = useState<OutputLine[]>(WELCOME);
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState("");
|
||||||
const [history, setHistory] = useState<string[]>([]);
|
const [history, setHistory] = useState<string[]>([]);
|
||||||
const [historyIndex, setHistoryIndex] = useState(-1);
|
const [historyIndex, setHistoryIndex] = useState(-1);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const bottomRef = useRef<HTMLDivElement>(null);
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const scrollToBottom = useCallback(() => {
|
const scrollToBottom = useCallback(() => {
|
||||||
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(scrollToBottom, [outputLines.length, scrollToBottom]);
|
useEffect(scrollToBottom, [outputLines.length, scrollToBottom]);
|
||||||
@@ -40,7 +40,7 @@ export function CommandPrompt({ onReboot, onBrick }: CommandPromptProps) {
|
|||||||
|
|
||||||
const executeCommand = useCallback(
|
const executeCommand = useCallback(
|
||||||
(cmdLine: string) => {
|
(cmdLine: string) => {
|
||||||
const echoLine: OutputLine = { text: PROMPT + cmdLine, color: 'green' };
|
const echoLine: OutputLine = { text: PROMPT + cmdLine, color: "green" };
|
||||||
const trimmed = cmdLine.trim();
|
const trimmed = cmdLine.trim();
|
||||||
|
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
@@ -52,22 +52,39 @@ export function CommandPrompt({ onReboot, onBrick }: CommandPromptProps) {
|
|||||||
const cmd = parts[0].toLowerCase();
|
const cmd = parts[0].toLowerCase();
|
||||||
const args = parts.slice(1);
|
const args = parts.slice(1);
|
||||||
|
|
||||||
if (cmd === 'clear') {
|
if (cmd === "clear") {
|
||||||
setOutputLines([]);
|
setOutputLines([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd === 'rm' && trimmed.includes('-rf') && trimmed.includes('/')) {
|
if (cmd === "rm" && trimmed.includes("-rf") && trimmed.includes("/")) {
|
||||||
setOutputLines((prev) => [...prev, echoLine]);
|
setOutputLines((prev) => [...prev, echoLine]);
|
||||||
setTimeout(() => onBrick?.(), 300);
|
setTimeout(() => onBrick?.(), 300);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd === 'reboot') {
|
if (cmd === "ts" || cmd === "teamspeak") {
|
||||||
|
const msg = encodeURIComponent("Frank, warum ist der ts down?");
|
||||||
|
window.open(`https://wa.me/?text=${msg}`, "_blank");
|
||||||
setOutputLines((prev) => [
|
setOutputLines((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
echoLine,
|
echoLine,
|
||||||
{ text: 'Rebooting server...', color: 'amber' },
|
{ text: "Checking TeamSpeak server stift15.de...", color: "amber" },
|
||||||
|
{ text: "", color: "dim" },
|
||||||
|
{ text: " ✗ NOT IMPLEMENTED", color: "red" },
|
||||||
|
{ text: " Opening WhatsApp to yell at Frank...", color: "dim" },
|
||||||
|
{ text: "", color: "dim" },
|
||||||
|
]);
|
||||||
|
setHistory((prev) => [...prev, trimmed]);
|
||||||
|
setHistoryIndex(-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmd === "reboot") {
|
||||||
|
setOutputLines((prev) => [
|
||||||
|
...prev,
|
||||||
|
echoLine,
|
||||||
|
{ text: "Rebooting server...", color: "amber" },
|
||||||
]);
|
]);
|
||||||
setTimeout(() => onReboot?.(), 800);
|
setTimeout(() => onReboot?.(), 800);
|
||||||
return;
|
return;
|
||||||
@@ -76,48 +93,61 @@ export function CommandPrompt({ onReboot, onBrick }: CommandPromptProps) {
|
|||||||
const handler = handlers[cmd];
|
const handler = handlers[cmd];
|
||||||
const result = handler
|
const result = handler
|
||||||
? handler(args)
|
? handler(args)
|
||||||
: [{ text: `${cmd}: command not found. Type 'help' for available commands.`, color: 'red' as const }];
|
: [
|
||||||
|
{
|
||||||
|
text: `${cmd}: command not found. Type 'help' for available commands.`,
|
||||||
|
color: "red" as const,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
setOutputLines((prev) => [...prev, echoLine, ...result, { text: '', color: 'dim' }]);
|
setOutputLines((prev) => [
|
||||||
|
...prev,
|
||||||
|
echoLine,
|
||||||
|
...result,
|
||||||
|
{ text: "", color: "dim" },
|
||||||
|
]);
|
||||||
setHistory((prev) => [...prev, trimmed]);
|
setHistory((prev) => [...prev, trimmed]);
|
||||||
setHistoryIndex(-1);
|
setHistoryIndex(-1);
|
||||||
},
|
},
|
||||||
[]
|
[onBrick, onReboot],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === "Enter") {
|
||||||
executeCommand(input);
|
executeCommand(input);
|
||||||
setInput('');
|
setInput("");
|
||||||
setHistoryIndex(-1);
|
setHistoryIndex(-1);
|
||||||
} else if (e.key === 'ArrowUp') {
|
} else if (e.key === "ArrowUp") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (history.length === 0) return;
|
if (history.length === 0) return;
|
||||||
const newIndex = historyIndex === -1 ? history.length - 1 : Math.max(0, historyIndex - 1);
|
const newIndex =
|
||||||
|
historyIndex === -1
|
||||||
|
? history.length - 1
|
||||||
|
: Math.max(0, historyIndex - 1);
|
||||||
setHistoryIndex(newIndex);
|
setHistoryIndex(newIndex);
|
||||||
setInput(history[newIndex]);
|
setInput(history[newIndex]);
|
||||||
} else if (e.key === 'ArrowDown') {
|
} else if (e.key === "ArrowDown") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (historyIndex === -1) return;
|
if (historyIndex === -1) return;
|
||||||
const newIndex = historyIndex + 1;
|
const newIndex = historyIndex + 1;
|
||||||
if (newIndex >= history.length) {
|
if (newIndex >= history.length) {
|
||||||
setHistoryIndex(-1);
|
setHistoryIndex(-1);
|
||||||
setInput('');
|
setInput("");
|
||||||
} else {
|
} else {
|
||||||
setHistoryIndex(newIndex);
|
setHistoryIndex(newIndex);
|
||||||
setInput(history[newIndex]);
|
setInput(history[newIndex]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[input, executeCommand, history, historyIndex]
|
[input, executeCommand, history, historyIndex],
|
||||||
);
|
);
|
||||||
|
|
||||||
const colorClass: Record<string, string> = {
|
const colorClass: Record<string, string> = {
|
||||||
green: 'text-term-green',
|
green: "text-term-green",
|
||||||
amber: 'text-term-amber',
|
amber: "text-term-amber",
|
||||||
red: 'text-term-red',
|
red: "text-term-red",
|
||||||
dim: 'text-term-dim',
|
dim: "text-term-dim",
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -130,7 +160,7 @@ export function CommandPrompt({ onReboot, onBrick }: CommandPromptProps) {
|
|||||||
key={i}
|
key={i}
|
||||||
className={`${colorClass[line.color]} whitespace-pre leading-[1.4]`}
|
className={`${colorClass[line.color]} whitespace-pre leading-[1.4]`}
|
||||||
>
|
>
|
||||||
{line.text || '\u00A0'}
|
{line.text || "\u00A0"}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -140,7 +170,7 @@ export function CommandPrompt({ onReboot, onBrick }: CommandPromptProps) {
|
|||||||
<span>{input}</span>
|
<span>{input}</span>
|
||||||
<span
|
<span
|
||||||
className="inline-block w-[0.6em] h-[1em] bg-term-green ml-0.5"
|
className="inline-block w-[0.6em] h-[1em] bg-term-green ml-0.5"
|
||||||
style={{ animation: 'blink 1s step-end infinite' }}
|
style={{ animation: "blink 1s step-end infinite" }}
|
||||||
/>
|
/>
|
||||||
{/* Hidden native input for reliable focus and mobile keyboard support */}
|
{/* Hidden native input for reliable focus and mobile keyboard support */}
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
import { useEffect, useRef, useState, useCallback } from "react";
|
||||||
import { loadAsciiArt } from '../utils/loadAsciiArt';
|
import { loadAsciiArt } from "../utils/loadAsciiArt";
|
||||||
|
|
||||||
interface MatrixRainProps {
|
interface MatrixRainProps {
|
||||||
onComplete?: () => void;
|
onComplete?: () => void;
|
||||||
@@ -11,11 +11,14 @@ interface Column {
|
|||||||
chars: string[];
|
chars: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const BOOT_SCREENS = ["logo", "skull", "sword", "frank", "gg"];
|
||||||
|
|
||||||
const FONT_SIZE = 14;
|
const FONT_SIZE = 14;
|
||||||
const CHAR_WIDTH = FONT_SIZE * 0.6;
|
const CHAR_WIDTH = FONT_SIZE * 0.6;
|
||||||
const CHAR_HEIGHT = FONT_SIZE * 1.2;
|
const CHAR_HEIGHT = FONT_SIZE * 1.2;
|
||||||
const FADE_ALPHA = 0.05;
|
const FADE_ALPHA = 0.05;
|
||||||
const RAIN_CHARS = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789';
|
const RAIN_CHARS =
|
||||||
|
"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789";
|
||||||
|
|
||||||
function randomChar(): string {
|
function randomChar(): string {
|
||||||
return RAIN_CHARS[Math.floor(Math.random() * RAIN_CHARS.length)];
|
return RAIN_CHARS[Math.floor(Math.random() * RAIN_CHARS.length)];
|
||||||
@@ -35,12 +38,14 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
|||||||
const completedRef = useRef(false);
|
const completedRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAsciiArt('/ascii/logo.txt').then(setArtLines);
|
const stored = localStorage.getItem("stift15-bootscreen");
|
||||||
|
const artFile = stored && BOOT_SCREENS.includes(stored) ? stored : "logo";
|
||||||
|
loadAsciiArt(`/ascii/${artFile}.txt`).then(setArtLines);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const startAnimation = useCallback(
|
const startAnimation = useCallback(
|
||||||
(canvas: HTMLCanvasElement, art: string[]) => {
|
(canvas: HTMLCanvasElement, art: string[]) => {
|
||||||
const ctx = canvas.getContext('2d')!;
|
const ctx = canvas.getContext("2d")!;
|
||||||
let animId: number;
|
let animId: number;
|
||||||
|
|
||||||
function resize() {
|
function resize() {
|
||||||
@@ -67,7 +72,7 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
|||||||
artGrid[r] = [];
|
artGrid[r] = [];
|
||||||
for (let c = 0; c < artWidth; c++) {
|
for (let c = 0; c < artWidth; c++) {
|
||||||
const ch = art[r]?.[c];
|
const ch = art[r]?.[c];
|
||||||
if (ch && ch !== ' ') {
|
if (ch && ch !== " ") {
|
||||||
artGrid[r][c] = ch;
|
artGrid[r][c] = ch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,10 +80,10 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
|||||||
|
|
||||||
// Track which art cells have been revealed and when (for glow decay)
|
// Track which art cells have been revealed and when (for glow decay)
|
||||||
const revealed: boolean[][] = Array.from({ length: artHeight }, () =>
|
const revealed: boolean[][] = Array.from({ length: artHeight }, () =>
|
||||||
Array(artWidth).fill(false)
|
Array(artWidth).fill(false),
|
||||||
);
|
);
|
||||||
const revealTimeMap: number[][] = Array.from({ length: artHeight }, () =>
|
const revealTimeMap: number[][] = Array.from({ length: artHeight }, () =>
|
||||||
Array(artWidth).fill(0)
|
Array(artWidth).fill(0),
|
||||||
);
|
);
|
||||||
const GLOW_DURATION = 1200; // ms for glow to fade out
|
const GLOW_DURATION = 1200; // ms for glow to fade out
|
||||||
let totalArtChars = 0;
|
let totalArtChars = 0;
|
||||||
@@ -92,11 +97,11 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
|||||||
|
|
||||||
// Initialize columns
|
// Initialize columns
|
||||||
const columns: Column[] = Array.from({ length: totalCols }, () =>
|
const columns: Column[] = Array.from({ length: totalCols }, () =>
|
||||||
createColumn(totalRows)
|
createColumn(totalRows),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Clear to black initially
|
// Clear to black initially
|
||||||
ctx.fillStyle = '#0a0a0a';
|
ctx.fillStyle = "#0a0a0a";
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
@@ -105,7 +110,7 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
|||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
ctx.font = `${FONT_SIZE}px "IBM Plex Mono", "Fira Code", "Courier New", monospace`;
|
ctx.font = `${FONT_SIZE}px "IBM Plex Mono", "Fira Code", "Courier New", monospace`;
|
||||||
ctx.textBaseline = 'top';
|
ctx.textBaseline = "top";
|
||||||
|
|
||||||
for (let col = 0; col < totalCols; col++) {
|
for (let col = 0; col < totalCols; col++) {
|
||||||
const column = columns[col];
|
const column = columns[col];
|
||||||
@@ -143,7 +148,7 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
|||||||
|
|
||||||
if (t === 0) {
|
if (t === 0) {
|
||||||
// Head character — bright white-green
|
// Head character — bright white-green
|
||||||
ctx.fillStyle = '#ccffcc';
|
ctx.fillStyle = "#ccffcc";
|
||||||
} else {
|
} else {
|
||||||
// Trail fades from bright to dim
|
// Trail fades from bright to dim
|
||||||
const brightness = Math.max(0, 1 - t / trailLen);
|
const brightness = Math.max(0, 1 - t / trailLen);
|
||||||
@@ -156,11 +161,7 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
|||||||
column.chars[row % column.chars.length] = randomChar();
|
column.chars[row % column.chars.length] = randomChar();
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.fillText(
|
ctx.fillText(column.chars[row % column.chars.length], x, y);
|
||||||
column.chars[row % column.chars.length],
|
|
||||||
x,
|
|
||||||
y
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset column when it falls off screen
|
// Reset column when it falls off screen
|
||||||
@@ -188,9 +189,9 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
|||||||
ctx.fillStyle = `rgb(${white + 51}, 255, ${white + 51})`;
|
ctx.fillStyle = `rgb(${white + 51}, 255, ${white + 51})`;
|
||||||
} else {
|
} else {
|
||||||
// Settled: normal bright green, no shadow
|
// Settled: normal bright green, no shadow
|
||||||
ctx.shadowColor = 'transparent';
|
ctx.shadowColor = "transparent";
|
||||||
ctx.shadowBlur = 0;
|
ctx.shadowBlur = 0;
|
||||||
ctx.fillStyle = '#33ff33';
|
ctx.fillStyle = "#33ff33";
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.fillText(artGrid[r][c]!, x, y);
|
ctx.fillText(artGrid[r][c]!, x, y);
|
||||||
@@ -198,7 +199,7 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Reset shadow state for next frame's rain drawing
|
// Reset shadow state for next frame's rain drawing
|
||||||
ctx.shadowColor = 'transparent';
|
ctx.shadowColor = "transparent";
|
||||||
ctx.shadowBlur = 0;
|
ctx.shadowBlur = 0;
|
||||||
|
|
||||||
// Check completion
|
// Check completion
|
||||||
@@ -223,7 +224,7 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
|||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[onComplete]
|
[onComplete],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -231,10 +232,5 @@ export function MatrixRain({ onComplete }: MatrixRainProps) {
|
|||||||
return startAnimation(canvasRef.current, artLines);
|
return startAnimation(canvasRef.current, artLines);
|
||||||
}, [artLines, startAnimation]);
|
}, [artLines, startAnimation]);
|
||||||
|
|
||||||
return (
|
return <canvas ref={canvasRef} className="absolute inset-0 z-0" />;
|
||||||
<canvas
|
|
||||||
ref={canvasRef}
|
|
||||||
className="absolute inset-0 z-0"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user