sitegen/src/file-viewer/scripts/canvas_2020.client.ts
clover caruso 310170fc98 i accidentally deleted the repo, but recovered it. i'll start committing
it was weird. i pressed delete on a subfolder, i think one of the
pages.off folders that i was using. and then, suddenly, nvim on windows
7 decided to delete every file in the directory. they weren't shred off
the space time continuum, but just marked deleted. i had to pay $80 to
get access to a software that could see them. bleh!

just seeing all my work, a little over a week, was pretty heart
shattering. but i remembered that long ago, a close friend said i could
call them whenever i was feeling sad. i finally took them up on that
offer. the first time i've ever called someone for emotional support.
but it's ok. i got it back. and the site framework is better than ever.

i'm gonna commit and push more often. the repo is private anyways.
2025-06-06 23:38:02 -07:00

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