309 lines
8.7 KiB
TypeScript
309 lines
8.7 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
import { Box, IconButton } from '@mui/material';
|
|
import { Close } from '@mui/icons-material';
|
|
import { useThemeStore } from '../stores/themeStore';
|
|
|
|
interface ScreensaverProps {
|
|
onClose: () => void;
|
|
}
|
|
|
|
// Color palette from ColorPicker for complementary color selection
|
|
const colorPalette = [
|
|
'#673ab7', '#9c27b0', '#e1bee7', // Purples
|
|
'#1976d2', '#03dac6', '#3f51b5', // Blues
|
|
'#2e7d32', '#4caf50', '#009688', // Greens
|
|
'#d32f2f', '#ff5722', '#ff9800', // Reds/Oranges
|
|
'#424242', '#607d8b', '#795548', // Grays
|
|
];
|
|
|
|
// Get complementary colors based on primary color
|
|
function getComplementaryColors(primaryColor: string): string[] {
|
|
const primaryIndex = colorPalette.findIndex(color => color === primaryColor);
|
|
if (primaryIndex === -1) {
|
|
// If primary color is not in palette, use default complementary colors
|
|
return ['#ff5722', '#4caf50']; // Orange and Green
|
|
}
|
|
|
|
// Choose colors that are visually distinct from primary
|
|
const complementaryIndices = [
|
|
(primaryIndex + 6) % colorPalette.length, // Opposite side
|
|
(primaryIndex + 9) % colorPalette.length, // Further opposite
|
|
];
|
|
|
|
return complementaryIndices.map(index => colorPalette[index]);
|
|
}
|
|
|
|
// Create gradient pattern
|
|
function createGradientPattern(
|
|
ctx: CanvasRenderingContext2D,
|
|
width: number,
|
|
height: number,
|
|
colors: string[],
|
|
time: number
|
|
) {
|
|
const gradient1 = ctx.createRadialGradient(
|
|
width * 0.3 + Math.sin(time * 0.001) * 100,
|
|
height * 0.3 + Math.cos(time * 0.001) * 100,
|
|
0,
|
|
width * 0.3 + Math.sin(time * 0.001) * 100,
|
|
height * 0.3 + Math.cos(time * 0.001) * 100,
|
|
width * 0.8
|
|
);
|
|
|
|
gradient1.addColorStop(0, `${colors[0]}80`);
|
|
gradient1.addColorStop(0.5, `${colors[1]}40`);
|
|
gradient1.addColorStop(1, `${colors[2]}20`);
|
|
|
|
ctx.fillStyle = gradient1;
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
const gradient2 = ctx.createRadialGradient(
|
|
width * 0.7 + Math.cos(time * 0.002) * 150,
|
|
height * 0.7 + Math.sin(time * 0.002) * 150,
|
|
0,
|
|
width * 0.7 + Math.cos(time * 0.002) * 150,
|
|
height * 0.7 + Math.sin(time * 0.002) * 150,
|
|
width * 0.6
|
|
);
|
|
|
|
gradient2.addColorStop(0, `${colors[2]}60`);
|
|
gradient2.addColorStop(0.7, `${colors[0]}30`);
|
|
gradient2.addColorStop(1, 'transparent');
|
|
|
|
ctx.fillStyle = gradient2;
|
|
ctx.fillRect(0, 0, width, height);
|
|
}
|
|
|
|
// Draw animated shapes
|
|
function drawShapes(
|
|
ctx: CanvasRenderingContext2D,
|
|
width: number,
|
|
height: number,
|
|
colors: string[],
|
|
time: number
|
|
) {
|
|
const numShapes = 8;
|
|
|
|
for (let i = 0; i < numShapes; i++) {
|
|
const x = width * 0.5 + Math.sin(time * 0.001 + i * 0.8) * (width * 0.3);
|
|
const y = height * 0.5 + Math.cos(time * 0.001 + i * 0.8) * (height * 0.3);
|
|
const size = 50 + Math.sin(time * 0.002 + i) * 30;
|
|
const colorIndex = i % colors.length;
|
|
const opacity = 0.3 + Math.sin(time * 0.003 + i) * 0.2;
|
|
|
|
ctx.save();
|
|
ctx.globalAlpha = opacity;
|
|
ctx.fillStyle = colors[colorIndex];
|
|
|
|
if (i % 3 === 0) {
|
|
// Circles
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, size, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
} else if (i % 3 === 1) {
|
|
// Squares
|
|
ctx.fillRect(x - size/2, y - size/2, size, size);
|
|
} else {
|
|
// Triangles
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, y - size/2);
|
|
ctx.lineTo(x - size/2, y + size/2);
|
|
ctx.lineTo(x + size/2, y + size/2);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
// Draw line art
|
|
function drawLineArt(
|
|
ctx: CanvasRenderingContext2D,
|
|
width: number,
|
|
height: number,
|
|
colors: string[],
|
|
time: number
|
|
) {
|
|
const numLines = 12;
|
|
|
|
for (let i = 0; i < numLines; i++) {
|
|
const x1 = Math.sin(time * 0.0005 + i * 0.5) * width;
|
|
const y1 = Math.cos(time * 0.0005 + i * 0.5) * height;
|
|
const x2 = Math.sin(time * 0.0005 + i * 0.5 + Math.PI) * width;
|
|
const y2 = Math.cos(time * 0.0005 + i * 0.5 + Math.PI) * height;
|
|
|
|
const colorIndex = i % colors.length;
|
|
const opacity = 0.2 + Math.sin(time * 0.001 + i) * 0.1;
|
|
|
|
ctx.save();
|
|
ctx.globalAlpha = opacity;
|
|
ctx.strokeStyle = colors[colorIndex];
|
|
ctx.lineWidth = 2 + Math.sin(time * 0.002 + i) * 1;
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(x1, y1);
|
|
ctx.lineTo(x2, y2);
|
|
ctx.stroke();
|
|
|
|
ctx.restore();
|
|
}
|
|
}
|
|
|
|
export function Screensaver({ onClose }: ScreensaverProps) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
const animationRef = useRef<number>();
|
|
const { primaryColor } = useThemeStore();
|
|
const [lastClearTime, setLastClearTime] = useState(Date.now());
|
|
const [interactionEnabled, setInteractionEnabled] = useState(false);
|
|
|
|
useEffect(() => {
|
|
console.log('Screensaver: useEffect triggered');
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) {
|
|
console.error('Screensaver: Canvas ref is null');
|
|
return;
|
|
}
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) {
|
|
console.error('Screensaver: Could not get 2D context');
|
|
return;
|
|
}
|
|
|
|
console.log('Screensaver: Canvas and context initialized');
|
|
|
|
// Set canvas size
|
|
const resizeCanvas = () => {
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
console.log('Screensaver: Canvas resized to', canvas.width, 'x', canvas.height);
|
|
};
|
|
|
|
resizeCanvas();
|
|
window.addEventListener('resize', resizeCanvas);
|
|
|
|
// Get complementary colors
|
|
const complementaryColors = getComplementaryColors(primaryColor);
|
|
const colors = [primaryColor, ...complementaryColors];
|
|
console.log('Screensaver: Using colors', colors);
|
|
|
|
let startTime = Date.now();
|
|
let clearInterval = 0;
|
|
|
|
const animate = () => {
|
|
const currentTime = Date.now();
|
|
const elapsed = currentTime - startTime;
|
|
|
|
// Clear screen every 3 minutes (180000ms) like Windows 98
|
|
if (currentTime - lastClearTime > 180000) {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
setLastClearTime(currentTime);
|
|
clearInterval = 0;
|
|
}
|
|
|
|
// Clear screen every few seconds for variety
|
|
if (clearInterval > 5000) {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
clearInterval = 0;
|
|
}
|
|
|
|
// Create gradient background
|
|
createGradientPattern(ctx, canvas.width, canvas.height, colors, elapsed);
|
|
|
|
// Draw shapes
|
|
drawShapes(ctx, canvas.width, canvas.height, colors, elapsed);
|
|
|
|
// Draw line art
|
|
drawLineArt(ctx, canvas.width, canvas.height, colors, elapsed);
|
|
|
|
clearInterval += 16; // ~60fps
|
|
animationRef.current = requestAnimationFrame(animate);
|
|
};
|
|
|
|
console.log('Screensaver: Starting animation');
|
|
animate();
|
|
|
|
return () => {
|
|
console.log('Screensaver: Cleaning up');
|
|
window.removeEventListener('resize', resizeCanvas);
|
|
if (animationRef.current) {
|
|
cancelAnimationFrame(animationRef.current);
|
|
}
|
|
};
|
|
}, [primaryColor, lastClearTime]);
|
|
|
|
// Enable interaction detection after a delay to prevent immediate closing
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => {
|
|
console.log('Screensaver: Enabling interaction detection');
|
|
setInteractionEnabled(true);
|
|
}, 1000); // 1 second delay
|
|
|
|
return () => clearTimeout(timer);
|
|
}, []);
|
|
|
|
// Close on any key press or mouse movement (only when interaction is enabled)
|
|
useEffect(() => {
|
|
if (!interactionEnabled) return;
|
|
|
|
const handleInteraction = () => {
|
|
console.log('Screensaver: User interaction detected, closing');
|
|
onClose();
|
|
};
|
|
|
|
window.addEventListener('keydown', handleInteraction);
|
|
window.addEventListener('mousemove', handleInteraction);
|
|
window.addEventListener('click', handleInteraction);
|
|
window.addEventListener('touchstart', handleInteraction);
|
|
|
|
return () => {
|
|
window.removeEventListener('keydown', handleInteraction);
|
|
window.removeEventListener('mousemove', handleInteraction);
|
|
window.removeEventListener('click', handleInteraction);
|
|
window.removeEventListener('touchstart', handleInteraction);
|
|
};
|
|
}, [onClose, interactionEnabled]);
|
|
|
|
console.log('Screensaver: Rendering component');
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
position: 'fixed',
|
|
top: 0,
|
|
left: 0,
|
|
width: '100vw',
|
|
height: '100vh',
|
|
zIndex: 99999,
|
|
bgcolor: 'black',
|
|
display: 'block',
|
|
}}
|
|
>
|
|
<canvas
|
|
ref={canvasRef}
|
|
style={{
|
|
display: 'block',
|
|
width: '100%',
|
|
height: '100%',
|
|
background: 'black',
|
|
}}
|
|
/>
|
|
<IconButton
|
|
onClick={onClose}
|
|
sx={{
|
|
position: 'absolute',
|
|
top: 16,
|
|
right: 16,
|
|
color: 'white',
|
|
bgcolor: 'rgba(0, 0, 0, 0.5)',
|
|
'&:hover': {
|
|
bgcolor: 'rgba(0, 0, 0, 0.7)',
|
|
},
|
|
}}
|
|
>
|
|
<Close />
|
|
</IconButton>
|
|
</Box>
|
|
);
|
|
} |