198 lines
5.8 KiB
TypeScript
198 lines
5.8 KiB
TypeScript
|
// Vibe coded with AI
|
||
|
(globalThis as any).canvas_2020 = function (canvas: HTMLCanvasElement) {
|
||
|
const isStandalone = canvas.getAttribute("data-standalone") === "true";
|
||
|
// Rain effect with slanted lines
|
||
|
// Configuration interface for the rain effect
|
||
|
interface RainConfig {
|
||
|
fps: number; // frames per second
|
||
|
color: string; // color of rain particles
|
||
|
angle: number; // angle in degrees
|
||
|
particleDensity: number; // particles per 10000 pixels of canvas area
|
||
|
speed: number; // speed of particles (pixels per frame)
|
||
|
lineWidth: number; // thickness of rain lines
|
||
|
lineLength: number; // length of rain lines
|
||
|
}
|
||
|
|
||
|
// Rain particle interface
|
||
|
interface RainParticle {
|
||
|
x: number; // x position
|
||
|
y: number; // y position
|
||
|
}
|
||
|
|
||
|
// Default configuration
|
||
|
const config: RainConfig = {
|
||
|
fps: 16,
|
||
|
color: isStandalone ? "#00FEFB99" : "#081F24",
|
||
|
angle: -18,
|
||
|
particleDensity: 1,
|
||
|
speed: 400,
|
||
|
lineWidth: 8,
|
||
|
lineLength: 100,
|
||
|
};
|
||
|
|
||
|
// Get the canvas context
|
||
|
const ctx = canvas.getContext("2d");
|
||
|
if (!ctx) {
|
||
|
console.error("Could not get canvas context");
|
||
|
return () => {};
|
||
|
}
|
||
|
|
||
|
// Make canvas transparent
|
||
|
if (isStandalone) {
|
||
|
canvas.style.backgroundColor = "#0F252B";
|
||
|
} else {
|
||
|
canvas.style.backgroundColor = "transparent";
|
||
|
}
|
||
|
|
||
|
// Calculate canvas dimensions and update when resized
|
||
|
let width = canvas.width;
|
||
|
let height = canvas.height;
|
||
|
let particles: RainParticle[] = [];
|
||
|
let animationFrameId: number;
|
||
|
let lastFrameTime = 0;
|
||
|
const frameInterval = 1000 / config.fps;
|
||
|
|
||
|
// Calculate angle in radians
|
||
|
const angleRad = (config.angle * Math.PI) / 180;
|
||
|
|
||
|
// Update canvas dimensions and particle count when resized
|
||
|
const updateDimensions = () => {
|
||
|
width = canvas.width = canvas.offsetWidth;
|
||
|
height = canvas.height = canvas.offsetHeight;
|
||
|
|
||
|
// Calculate the canvas area in pixels
|
||
|
const canvasArea = width * height;
|
||
|
|
||
|
// Calculate target number of particles based on canvas area
|
||
|
const targetParticleCount = Math.floor(
|
||
|
(canvasArea / 10000) * config.particleDensity,
|
||
|
);
|
||
|
|
||
|
// Calculate buffer for horizontal offset due to slanted angle
|
||
|
const buffer = Math.abs(height * Math.tan(angleRad)) + config.lineLength;
|
||
|
|
||
|
// Adjust the particles array
|
||
|
if (particles.length < targetParticleCount) {
|
||
|
// Add more particles if needed
|
||
|
for (let i = particles.length; i < targetParticleCount; i++) {
|
||
|
particles.push(createParticle(true, buffer));
|
||
|
}
|
||
|
} else if (particles.length > targetParticleCount) {
|
||
|
// Remove excess particles
|
||
|
particles = particles.slice(0, targetParticleCount);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Create a new particle
|
||
|
// Added initialDistribution parameter to distribute particles across the entire canvas at startup
|
||
|
const createParticle = (
|
||
|
initialDistribution = false,
|
||
|
buffer: number,
|
||
|
): RainParticle => {
|
||
|
// For initial distribution, place particles throughout the canvas
|
||
|
// Otherwise start them above the canvas
|
||
|
let x = Math.random() * (width + buffer * 2) - buffer;
|
||
|
let y;
|
||
|
|
||
|
if (initialDistribution) {
|
||
|
// Distribute across the entire canvas height for initial setup
|
||
|
y = Math.random() * (height + config.lineLength * 2) - config.lineLength;
|
||
|
} else {
|
||
|
// Start new particles from above the canvas with some randomization
|
||
|
y = -config.lineLength - (Math.random() * config.lineLength * 20);
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
x,
|
||
|
y,
|
||
|
};
|
||
|
};
|
||
|
|
||
|
// Update particle positions
|
||
|
const updateParticles = () => {
|
||
|
// Calculate buffer for horizontal offset due to slanted angle
|
||
|
const buffer = Math.abs(height * Math.tan(angleRad)) + config.lineLength;
|
||
|
|
||
|
for (let i = 0; i < particles.length; i++) {
|
||
|
const p = particles[i];
|
||
|
|
||
|
// Update position based on speed and angle
|
||
|
p.x += Math.sin(angleRad) * config.speed;
|
||
|
p.y += Math.cos(angleRad) * config.speed;
|
||
|
|
||
|
// Reset particles that go offscreen - only determined by position
|
||
|
// Add extra buffer to ensure particles fully exit the visible area before resetting
|
||
|
if (
|
||
|
p.y > height + config.lineLength ||
|
||
|
p.x < -buffer ||
|
||
|
p.x > width + buffer
|
||
|
) {
|
||
|
particles[i] = createParticle(false, buffer);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Draw particles
|
||
|
const drawParticles = () => {
|
||
|
// Clear canvas
|
||
|
ctx.clearRect(0, 0, width, height);
|
||
|
|
||
|
// Set drawing properties
|
||
|
ctx.strokeStyle = config.color;
|
||
|
ctx.lineWidth = config.lineWidth;
|
||
|
ctx.lineCap = "square";
|
||
|
|
||
|
// Draw each rain line
|
||
|
ctx.beginPath();
|
||
|
for (const p of particles) {
|
||
|
// Only draw particles that are either on screen or within a reasonable buffer
|
||
|
// This is for performance reasons - we don't need to draw particles far offscreen
|
||
|
if (p.y >= -config.lineLength * 2 && p.y <= height + config.lineLength) {
|
||
|
const endX = p.x + Math.sin(angleRad) * config.lineLength;
|
||
|
const endY = p.y + Math.cos(angleRad) * config.lineLength;
|
||
|
|
||
|
ctx.moveTo(p.x, p.y);
|
||
|
ctx.lineTo(endX, endY);
|
||
|
}
|
||
|
}
|
||
|
ctx.stroke();
|
||
|
};
|
||
|
|
||
|
// Animation loop
|
||
|
const animate = (currentTime: number) => {
|
||
|
animationFrameId = requestAnimationFrame(animate);
|
||
|
|
||
|
// Control frame rate
|
||
|
if (currentTime - lastFrameTime < frameInterval) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
lastFrameTime = currentTime;
|
||
|
|
||
|
updateParticles();
|
||
|
drawParticles();
|
||
|
};
|
||
|
|
||
|
// Initialize the animation
|
||
|
const init = () => {
|
||
|
// Set up resize handler
|
||
|
globalThis.addEventListener("resize", updateDimensions);
|
||
|
|
||
|
// Initial setup
|
||
|
updateDimensions();
|
||
|
|
||
|
// Start animation
|
||
|
lastFrameTime = performance.now();
|
||
|
animationFrameId = requestAnimationFrame(animate);
|
||
|
};
|
||
|
|
||
|
// Start the animation
|
||
|
init();
|
||
|
|
||
|
// Return cleanup function
|
||
|
return () => {
|
||
|
globalThis.removeEventListener("resize", updateDimensions);
|
||
|
cancelAnimationFrame(animationFrameId);
|
||
|
};
|
||
|
};
|