1// Vibe coded with AI. Heavily tuned.
2(globalThis as any).canvas_2020 = function(canvas: HTMLCanvasElement) {
3 const isStandalone = canvas.getAttribute("data-standalone") === "true";
4 // Rain effect with slanted lines
5 // Configuration interface for the rain effect
6 interface RainConfig {
7 fps: number; // frames per second
8 color: string; // color of rain particles
9 angle: number; // angle in degrees
10 particleDensity: number; // particles per 10000 pixels of canvas area
11 speed: number; // speed of particles (pixels per frame)
12 lineWidth: number; // thickness of rain lines
13 lineLength: number; // length of rain lines
14 }
15
16 // Rain particle interface
17 interface RainParticle {
18 x: number; // x position
19 y: number; // y position
20 }
21
22 // Default configuration
23 const config: RainConfig = {
24 fps: 16,
25 color: isStandalone ? "#00FEFB99" : "#081F24",
26 angle: -18,
27 particleDensity: 1,
28 speed: 400,
29 lineWidth: 8,
30 lineLength: 100,
31 };
32
33 // Get the canvas context
34 const ctx = canvas.getContext("2d");
35 if (!ctx) {
36 console.error("Could not get canvas context");
37 return () => {};
38 }
39
40 // Make canvas transparent
41 if (isStandalone) {
42 canvas.style.backgroundColor = "#0F252B";
43 } else {
44 canvas.style.backgroundColor = "transparent";
45 }
46
47 // Calculate canvas dimensions and update when resized
48 let width = canvas.width;
49 let height = canvas.height;
50 let particles: RainParticle[] = [];
51 let animationFrameId: number;
52 let lastFrameTime = 0;
53 const frameInterval = 1000 / config.fps;
54
55 // Calculate angle in radians
56 const angleRad = (config.angle * Math.PI) / 180;
57
58 // Update canvas dimensions and particle count when resized
59 const updateDimensions = () => {
60 width = canvas.width = canvas.offsetWidth;
61 height = canvas.height = canvas.offsetHeight;
62
63 // Calculate the canvas area in pixels
64 const canvasArea = width * height;
65
66 // Calculate target number of particles based on canvas area
67 const targetParticleCount = Math.floor(
68 (canvasArea / 10000) * config.particleDensity,
69 );
70
71 // Calculate buffer for horizontal offset due to slanted angle
72 const buffer = Math.abs(height * Math.tan(angleRad)) + config.lineLength;
73
74 // Adjust the particles array
75 if (particles.length < targetParticleCount) {
76 // Add more particles if needed
77 for (let i = particles.length; i < targetParticleCount; i++) {
78 particles.push(createParticle(true, buffer));
79 }
80 } else if (particles.length > targetParticleCount) {
81 // Remove excess particles
82 particles = particles.slice(0, targetParticleCount);
83 }
84 };
85
86 // Create a new particle
87 // Added initialDistribution parameter to distribute particles across the entire canvas at startup
88 const createParticle = (
89 initialDistribution = false,
90 buffer: number,
91 ): RainParticle => {
92 // For initial distribution, place particles throughout the canvas
93 // Otherwise start them above the canvas
94 let x = Math.random() * (width + buffer * 2) - buffer;
95 let y;
96
97 if (initialDistribution) {
98 // Distribute across the entire canvas height for initial setup
99 y = Math.random() * (height + config.lineLength * 2) - config.lineLength;
100 } else {
101 // Start new particles from above the canvas with some randomization
102 y = -config.lineLength - (Math.random() * config.lineLength * 20);
103 }
104
105 return {
106 x,
107 y,
108 };
109 };
110
111 // Update particle positions
112 const updateParticles = () => {
113 // Calculate buffer for horizontal offset due to slanted angle
114 const buffer = Math.abs(height * Math.tan(angleRad)) + config.lineLength;
115
116 for (let i = 0; i < particles.length; i++) {
117 const p = particles[i];
118
119 // Update position based on speed and angle
120 p.x += Math.sin(angleRad) * config.speed;
121 p.y += Math.cos(angleRad) * config.speed;
122
123 // Reset particles that go offscreen - only determined by position
124 // Add extra buffer to ensure particles fully exit the visible area before resetting
125 if (
126 p.y > height + config.lineLength
127 || p.x < -buffer
128 || p.x > width + buffer
129 ) {
130 particles[i] = createParticle(false, buffer);
131 }
132 }
133 };
134
135 // Draw particles
136 const drawParticles = () => {
137 // Clear canvas
138 ctx.clearRect(0, 0, width, height);
139
140 // Set drawing properties
141 ctx.strokeStyle = config.color;
142 ctx.lineWidth = config.lineWidth;
143 ctx.lineCap = "square";
144
145 // Draw each rain line
146 ctx.beginPath();
147 for (const p of particles) {
148 // Only draw particles that are either on screen or within a reasonable buffer
149 // This is for performance reasons - we don't need to draw particles far offscreen
150 if (p.y >= -config.lineLength * 2 && p.y <= height + config.lineLength) {
151 const endX = p.x + Math.sin(angleRad) * config.lineLength;
152 const endY = p.y + Math.cos(angleRad) * config.lineLength;
153
154 ctx.moveTo(p.x, p.y);
155 ctx.lineTo(endX, endY);
156 }
157 }
158 ctx.stroke();
159 };
160
161 // Animation loop
162 const animate = (currentTime: number) => {
163 animationFrameId = requestAnimationFrame(animate);
164
165 // Control frame rate
166 if (currentTime - lastFrameTime < frameInterval) {
167 return;
168 }
169
170 lastFrameTime = currentTime;
171
172 updateParticles();
173 drawParticles();
174 };
175
176 // Initialize the animation
177 const init = () => {
178 // Set up resize handler
179 window.addEventListener("resize", updateDimensions);
180
181 // Initial setup
182 updateDimensions();
183
184 // Start animation
185 lastFrameTime = performance.now();
186 animationFrameId = requestAnimationFrame(animate);
187 };
188
189 // Start the animation
190 init();
191
192 // Return cleanup function
193 return () => {
194 window.removeEventListener("resize", updateDimensions);
195 cancelAnimationFrame(animationFrameId);
196 };
197};