// 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); }; };