From 226f5282ea0e676045df503295a492d20c07b5fe Mon Sep 17 00:00:00 2001 From: sageTheDM Date: Wed, 22 Oct 2025 08:47:23 +0200 Subject: [PATCH] Meteors have been updated to be smoother and better looking --- src/js/animation/Meteor.js | 501 +++++++++++++---------------- src/js/animation/starBackground.js | 104 +++++- 2 files changed, 324 insertions(+), 281 deletions(-) diff --git a/src/js/animation/Meteor.js b/src/js/animation/Meteor.js index a141e80..1387e54 100644 --- a/src/js/animation/Meteor.js +++ b/src/js/animation/Meteor.js @@ -1,309 +1,272 @@ class Meteor { constructor(container) { - this.element = document.createElement("div"); - this.element.style.position = "absolute"; this.container = container; - this.speed = Math.random() * 3 + 4; - this.life = 1.0; - this.particles = []; - - this.colorType = Math.floor(Math.random() * 4); - this.colors = this.getMeteorColors(); - this.containerRect = container.getBoundingClientRect(); - // Start from random edge with PIXEL-based positioning - const edge = Math.floor(Math.random() * 4); - switch (edge) { - case 0: - // Top - this.x = Math.random() * this.containerRect.width; - this.y = -20; - case 1: - // Right - this.x = this.containerRect.width + 20; - this.y = Math.random() * this.containerRect.height; - case 2: - // Bottom - this.x = Math.random() * this.containerRect.width; - this.y = this.containerRect.height + 20; - case 3: - // Left - this.x = -20; - this.y = Math.random() * this.containerRect.height; - default: - console.log("Error creating Meteor direction"); + // Smooth movement settings + this.speed = Math.random() * 1.5 + 2; + this.life = 1.0; + this.fadeSpeed = 0.002; + + // Enhanced color schemes for vibrant streaks + this.colorSchemes = [ + // Classic golden-white meteor + { + core: "#ffffff", + mid: "#ffeb99", + outer: "#ff9933", + }, + // Blue-white ice meteor + { + core: "#ffffff", + mid: "#aaddff", + outer: "#4488ff", + }, + // Purple cosmic meteor + { + core: "#ffffff", + mid: "#ddaaff", + outer: "#aa44ff", + }, + // Green-white aurora meteor + { + core: "#ffffff", + mid: "#aaffcc", + outer: "#44ff88", + }, + ]; + + this.colors = + this.colorSchemes[Math.floor(Math.random() * this.colorSchemes.length)]; + + // Start positions + const startSide = Math.random(); + if (startSide < 0.7) { + this.x = Math.random() * this.containerRect.width; + this.y = -100; + this.angle = Math.PI / 2 + (Math.random() - 0.5) * 0.6; + } else if (startSide < 0.85) { + this.x = -100; + this.y = Math.random() * this.containerRect.height * 0.3; + this.angle = Math.PI / 4 + Math.random() * 0.4; + } else { + this.x = this.containerRect.width + 100; + this.y = Math.random() * this.containerRect.height * 0.3; + this.angle = (Math.PI * 3) / 4 + Math.random() * 0.4; } - const centerX = this.containerRect.width / 2 + (Math.random() - 0.5) * 200; - const centerY = this.containerRect.height / 2 + (Math.random() - 0.5) * 200; - this.angle = - Math.atan2(centerY - this.y, centerX - this.x) + - (Math.random() - 0.5) * 0.8; this.dx = Math.cos(this.angle) * this.speed; this.dy = Math.sin(this.angle) * this.speed; + // Trail settings + this.trailPoints = []; + this.maxTrailLength = 25; + + this.hasBeenOnScreen = false; + this.init(); } - getMeteorColors() { - const colorSchemes = [ - { - head: "rgba(255, 255, 255, 1)", - core: "rgba(255, 230, 150, 0.9)", - mid: "rgba(255, 180, 80, 0.7)", - outer: "rgba(255, 100, 50, 0.5)", - glow: "rgba(255, 200, 100, 0.9)", - }, - { - head: "rgba(255, 255, 255, 1)", - core: "rgba(180, 220, 255, 0.9)", - mid: "rgba(100, 180, 255, 0.7)", - outer: "rgba(50, 100, 255, 0.5)", - glow: "rgba(100, 180, 255, 0.9)", - }, - { - head: "rgba(255, 255, 255, 1)", - core: "rgba(220, 180, 255, 0.9)", - mid: "rgba(200, 100, 255, 0.7)", - outer: "rgba(180, 50, 255, 0.5)", - glow: "rgba(200, 150, 255, 0.9)", - }, - { - head: "rgba(255, 255, 255, 1)", - core: "rgba(180, 255, 200, 0.9)", - mid: "rgba(80, 255, 150, 0.7)", - outer: "rgba(50, 200, 100, 0.5)", - glow: "rgba(150, 255, 180, 0.9)", - }, - ]; - return colorSchemes[this.colorType]; - } - init() { - const size = Math.random() * 4 + 10; - const colors = this.colors; - - const head = document.createElement("div"); - head.style.width = `${size}px`; - head.style.height = `${size}px`; - head.style.borderRadius = "50%"; - head.style.background = ` - radial-gradient( - circle at 30% 30%, - ${colors.head} 0%, - ${colors.core} 30%, - ${colors.mid} 60%, - ${colors.outer} 100% - ) + // Create SVG for smooth gradient trail + this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + this.svg.style.cssText = ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + overflow: visible; + z-index: 9; `; - head.style.boxShadow = ` - 0 0 ${size * 4}px ${colors.glow}, - 0 0 ${size * 2}px rgba(255, 255, 255, 0.8), - inset 0 0 ${size * 0.5}px rgba(255, 255, 255, 0.9) - `; - head.style.position = "absolute"; - head.style.left = "50%"; - head.style.top = "50%"; - head.style.transform = "translate(-50%, -50%)"; - head.style.zIndex = "3"; - head.style.filter = "blur(0.5px)"; + this.svg.setAttribute("width", this.containerRect.width); + this.svg.setAttribute("height", this.containerRect.height); - // Create multi-layer tail - this.createTailLayer(size * 3, size * 1.2, colors.core, 1, "1px"); - this.createTailLayer(size * 5, size * 2, colors.mid, 0.6, "2px"); - this.createTailLayer(size * 8, size * 3, colors.outer, 0.3, "3px"); + // Create gradient for trail + const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); + const gradient = document.createElementNS( + "http://www.w3.org/2000/svg", + "linearGradient" + ); + this.gradientId = `meteor-gradient-${Date.now()}-${Math.random() + .toString(36) + .substr(2, 9)}`; + gradient.setAttribute("id", this.gradientId); + gradient.setAttribute("gradientUnits", "userSpaceOnUse"); - // Assemble meteor - this.element.appendChild(head); - this.element.style.left = `${this.x}px`; - this.element.style.top = `${this.y}px`; - this.element.style.zIndex = "10"; - this.element.style.filter = "blur(0.5px)"; + // Gradient stops for smooth colored fade + const stops = [ + { offset: "0%", color: this.colors.core, opacity: "1" }, + { offset: "30%", color: this.colors.mid, opacity: "0.8" }, + { offset: "60%", color: this.colors.outer, opacity: "0.5" }, + { offset: "100%", color: this.colors.outer, opacity: "0" }, + ]; - this.head = head; - this.size = size; - - this.container.appendChild(this.element); - - this.createTrailParticles(); - } - - createTailLayer(length, width, color, opacity, blur) { - const tail = document.createElement("div"); - const tailAngle = Math.atan2(this.dy, this.dx) + Math.PI; - - tail.style.width = `${length}px`; - tail.style.height = `${width}px`; - tail.style.position = "absolute"; - tail.style.left = "50%"; - tail.style.top = "50%"; - tail.style.transformOrigin = "left center"; - tail.style.transform = `translate(0, -50%) rotate(${tailAngle}rad)`; - tail.style.background = `linear-gradient(to right, - ${color} 0%, - ${color.replace(")", ", 0.8)").replace("rgba", "rgba")} 20%, - ${color.replace(")", ", 0.4)").replace("rgba", "rgba")} 50%, - ${color.replace(")", ", 0.1)").replace("rgba", "rgba")} 80%, - transparent 100%)`; - tail.style.opacity = opacity; - tail.style.filter = `blur(${blur})`; - tail.style.pointerEvents = "none"; - tail.style.zIndex = "2"; - tail.style.borderRadius = "0 50% 50% 0"; - - this.element.appendChild(tail); - this.tail = tail; - } - - createTrailParticles() { - for (let i = 0; i < 5; i++) { - this.addTrailParticle(); - } - } - - addTrailParticle() { - const particle = document.createElement("div"); - const size = Math.random() * 2 + 1; - const colors = this.colors; - - particle.style.width = `${size}px`; - particle.style.height = `${size}px`; - particle.style.borderRadius = "50%"; - particle.style.background = colors.mid; - particle.style.position = "absolute"; - particle.style.left = `${this.x}px`; - particle.style.top = `${this.y}px`; - particle.style.boxShadow = `0 0 ${size * 3}px ${colors.glow}`; - particle.style.opacity = "0.7"; - particle.style.zIndex = "1"; - particle.style.pointerEvents = "none"; - - this.container.appendChild(particle); - - this.particles.push({ - element: particle, - life: 1.0, - x: this.x, - y: this.y, - vx: (Math.random() - 0.5) * 0.5, - vy: (Math.random() - 0.5) * 0.5, - size: size, - }); - } - - update() { - this.x += this.dx; - this.y += this.dy; - this.life -= 0.005; - - this.element.style.left = `${this.x}px`; - this.element.style.top = `${this.y}px`; - this.element.style.opacity = this.life; - - // Update tail direction - const tailAngle = Math.atan2(this.dy, this.dx) + Math.PI; - const tails = this.element.querySelectorAll("div:not(.particle)"); - tails.forEach((tail) => { - if (tail !== this.head) { - tail.style.transform = `translate(0, -50%) rotate(${tailAngle}rad)`; - tail.style.opacity = this.life; - } + stops.forEach((stop) => { + const stopEl = document.createElementNS( + "http://www.w3.org/2000/svg", + "stop" + ); + stopEl.setAttribute("offset", stop.offset); + stopEl.setAttribute("stop-color", stop.color); + stopEl.setAttribute("stop-opacity", stop.opacity); + gradient.appendChild(stopEl); }); - if (Math.random() < 0.3) { - this.addTrailParticle(); + defs.appendChild(gradient); + this.svg.appendChild(defs); + + // Create path for trail + this.trailPath = document.createElementNS( + "http://www.w3.org/2000/svg", + "path" + ); + this.trailPath.setAttribute("fill", "none"); + this.trailPath.setAttribute("stroke", `url(#${this.gradientId})`); + this.trailPath.setAttribute("stroke-linecap", "round"); + this.trailPath.setAttribute("stroke-linejoin", "round"); + + const headSize = Math.random() * 2 + 2.5; + this.headSize = headSize; + this.trailPath.setAttribute("stroke-width", headSize * 2); + this.trailPath.style.filter = `blur(${headSize * 0.4}px)`; + + this.svg.appendChild(this.trailPath); + this.container.appendChild(this.svg); + + // Meteor head - use absolute positioning like the SVG + this.head = document.createElement("div"); + this.head.style.cssText = ` + position: absolute; + width: ${headSize}px; + height: ${headSize}px; + border-radius: 50%; + background: ${this.colors.core}; + box-shadow: 0 0 ${headSize * 3}px ${this.colors.mid}, + 0 0 ${headSize * 5}px ${this.colors.outer}; + pointer-events: none; + z-index: 10; + transform: translate(-50%, -50%); + `; + + this.container.appendChild(this.head); + + // Initialize trail + for (let i = 0; i < 3; i++) { + this.trailPoints.push({ x: this.x, y: this.y }); + } + } + + isOnScreen() { + const buffer = 50; + return ( + this.x > -buffer && + this.x < this.containerRect.width + buffer && + this.y > -buffer && + this.y < this.containerRect.height + buffer + ); + } + + isOffScreen() { + const buffer = 200; + return ( + this.x < -buffer || + this.x > this.containerRect.width + buffer || + this.y < -buffer || + this.y > this.containerRect.height + buffer + ); + } + + updateTrail() { + // Add current position to trail + this.trailPoints.unshift({ x: this.x, y: this.y }); + + // Limit trail length + if (this.trailPoints.length > this.maxTrailLength) { + this.trailPoints.pop(); } - // Update existing particles with pixel positioning - for (let i = this.particles.length - 1; i >= 0; i--) { - const p = this.particles[i]; - p.life -= 0.02; - p.x += p.vx; - p.y += p.vy; + // Update SVG path with smooth curves + if (this.trailPoints.length >= 2) { + let pathData = `M ${this.trailPoints[0].x} ${this.trailPoints[0].y}`; - p.element.style.left = `${p.x}px`; - p.element.style.top = `${p.y}px`; - p.element.style.opacity = p.life; - p.element.style.transform = `scale(${p.life})`; + // Use smooth curves + for (let i = 1; i < this.trailPoints.length - 1; i++) { + const curr = this.trailPoints[i]; + const next = this.trailPoints[i + 1]; + const midX = (curr.x + next.x) / 2; + const midY = (curr.y + next.y) / 2; + pathData += ` Q ${curr.x} ${curr.y} ${midX} ${midY}`; + } - if (p.life <= 0) { - p.element.remove(); - this.particles.splice(i, 1); + // Final point + if (this.trailPoints.length > 1) { + const last = this.trailPoints[this.trailPoints.length - 1]; + pathData += ` L ${last.x} ${last.y}`; + } + + this.trailPath.setAttribute("d", pathData); + + // Update gradient position for directional effect + const gradient = document.getElementById(this.gradientId); + if (gradient) { + gradient.setAttribute("x1", this.x); + gradient.setAttribute("y1", this.y); + const lastPoint = this.trailPoints[this.trailPoints.length - 1]; + gradient.setAttribute("x2", lastPoint.x); + gradient.setAttribute("y2", lastPoint.y); } } + } + update(deltaTime) { + // Use deltaTime for consistent movement + const dt = Math.min(deltaTime / 16.67, 2); + + // Update position + this.x += this.dx * dt; + this.y += this.dy * dt; + + // Track screen presence + if (!this.hasBeenOnScreen && this.isOnScreen()) { + this.hasBeenOnScreen = true; + } + + // Fade when off screen + if (this.hasBeenOnScreen && this.isOffScreen()) { + this.life -= this.fadeSpeed * 3 * dt; + } + + // Update head position using left/top (same coordinate system as SVG) + this.head.style.left = `${this.x}px`; + this.head.style.top = `${this.y}px`; + this.head.style.opacity = this.life; + + // Update SVG trail opacity + this.svg.style.opacity = this.life; + + // Update trail + this.updateTrail(); + + // Check if should be removed if ( - this.x < -50 || - this.x > this.containerRect.width + 50 || - this.y < -50 || - this.y > this.containerRect.height + 50 || - this.life <= 0 + this.life <= 0 || + (this.hasBeenOnScreen && this.isOffScreen() && this.life < 0.3) ) { - // Fade out particles - this.particles.forEach((p) => { - p.element.style.transition = "opacity 0.5s"; - p.element.style.opacity = "0"; - setTimeout(() => p.element.remove(), 500); - }); this.remove(); return false; } + return true; } remove() { - if (this.life > 0.3) { - this.createExplosion(); + if (this.svg && this.svg.parentNode) { + this.svg.remove(); } - - setTimeout(() => { - this.element.remove(); - }, 300); - } - - createExplosion() { - for (let i = 0; i < 8; i++) { - const particle = document.createElement("div"); - const size = Math.random() * 3 + 2; - const colors = this.colors; - - particle.style.width = `${size}px`; - particle.style.height = `${size}px`; - particle.style.borderRadius = "50%"; - particle.style.background = colors.core; - particle.style.position = "absolute"; - particle.style.left = `${this.x}px`; - particle.style.top = `${this.y}px`; - particle.style.boxShadow = `0 0 ${size * 4}px ${colors.glow}`; - particle.style.opacity = "0.8"; - particle.style.zIndex = "2"; - - this.container.appendChild(particle); - - // Animate explosion - const angle = (i / 8) * Math.PI * 2; - const distance = Math.random() * 20 + 10; - const duration = Math.random() * 500 + 500; - - particle.animate( - [ - { - transform: "translate(0, 0) scale(1)", - opacity: 0.8, - }, - { - transform: `translate(${Math.cos(angle) * distance}px, ${ - Math.sin(angle) * distance - }px) scale(0.1)`, - opacity: 0, - }, - ], - { - duration: duration, - easing: "cubic-bezier(0.4, 0, 0.2, 1)", - } - ).onfinish = () => particle.remove(); + if (this.head && this.head.parentNode) { + this.head.remove(); } } } diff --git a/src/js/animation/starBackground.js b/src/js/animation/starBackground.js index 42b0d0d..83611a0 100644 --- a/src/js/animation/starBackground.js +++ b/src/js/animation/starBackground.js @@ -3,6 +3,32 @@ import Meteor from "./Meteor.js"; const starInstances = []; const meteorInstances = []; +let lastTime = performance.now(); +let isTabVisible = true; +let animationFrameId = null; + +// Track tab visibility to prevent spawning when hidden +document.addEventListener("visibilitychange", () => { + isTabVisible = !document.hidden; + + // If tab becomes visible after being hidden, clean up excess meteors + if (isTabVisible && meteorInstances.length > 3) { + // Remove excess meteors + const excess = meteorInstances.length - 3; + for (let i = 0; i < excess; i++) { + if (meteorInstances[i]) { + meteorInstances[i].remove(); + } + } + meteorInstances.splice(0, excess); + } + + // Resume animation loop if it stopped + if (isTabVisible && !animationFrameId) { + lastTime = performance.now(); + animate(); + } +}); function createStars() { const stars = document.getElementById("stars"); @@ -34,7 +60,12 @@ function injectStarCSS() { } function trySpawnMeteor() { - const spawnChance = 0.12; + // Only spawn if tab is visible and not too many meteors + if (!isTabVisible || meteorInstances.length >= 2) { + return; + } + + const spawnChance = 0.15; if (Math.random() < spawnChance) { const stars = document.getElementById("stars"); const newMeteor = new Meteor(stars); @@ -42,40 +73,89 @@ function trySpawnMeteor() { } } -function animateStars() { +function animate() { + const currentTime = performance.now(); + const deltaTime = currentTime - lastTime; + lastTime = currentTime; + + // Cap deltaTime to prevent huge jumps when tab is hidden + const cappedDelta = Math.min(deltaTime, 100); + + // Update stars with delta time starInstances.forEach((star) => star.update()); // Update meteors and remove dead ones for (let i = meteorInstances.length - 1; i >= 0; i--) { - if (!meteorInstances[i].update()) { + if (!meteorInstances[i].update(cappedDelta)) { meteorInstances.splice(i, 1); } } - requestAnimationFrame(animateStars); + animationFrameId = requestAnimationFrame(animate); } function createMeteorBurst() { - for (let i = 0; i < 3; i++) { + // Only burst if tab is visible + if (!isTabVisible) return; + + // Limit burst size + const burstSize = Math.min(2, 3 - meteorInstances.length); + + for (let i = 0; i < burstSize; i++) { setTimeout(() => { - const stars = document.getElementById("stars"); - const newMeteor = new Meteor(stars); - meteorInstances.push(newMeteor); - }, i * 200); + if (isTabVisible && meteorInstances.length < 3) { + const stars = document.getElementById("stars"); + const newMeteor = new Meteor(stars); + meteorInstances.push(newMeteor); + } + }, i * 300); } } +let spawnInterval = null; +let burstInterval = null; + function init() { injectStarCSS(); createStars(); - animateStars(); + lastTime = performance.now(); + animate(); - setInterval(trySpawnMeteor, 2000); - setInterval(createMeteorBurst, 15000); + // Longer interval for spawning + spawnInterval = setInterval(() => { + if (isTabVisible) { + trySpawnMeteor(); + } + }, 3000); // Every 3 seconds instead of 2 + + // Longer interval for bursts + burstInterval = setInterval(() => { + if (isTabVisible) { + createMeteorBurst(); + } + }, 20000); // Every 20 seconds instead of 15 } +// Cleanup function +function cleanup() { + if (animationFrameId) { + cancelAnimationFrame(animationFrameId); + } + if (spawnInterval) { + clearInterval(spawnInterval); + } + if (burstInterval) { + clearInterval(burstInterval); + } +} + +// Handle page unload +window.addEventListener("beforeunload", cleanup); + if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } + +export { cleanup }; -- 2.39.5