diff --git a/src/js/animation/Meteor.js b/src/js/animation/Meteor.js index 1387e54..a141e80 100644 --- a/src/js/animation/Meteor.js +++ b/src/js/animation/Meteor.js @@ -1,272 +1,309 @@ 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(); - // 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; + // 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"); } + 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(); } - init() { - // 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; - `; - this.svg.setAttribute("width", this.containerRect.width); - this.svg.setAttribute("height", this.containerRect.height); - - // 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"); - - // 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" }, + 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]; + } - 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); + 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% + ) + `; + 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)"; + + // 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"); + + // 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)"; + + 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; + } }); - 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(); + if (Math.random() < 0.3) { + this.addTrailParticle(); } - // Update SVG path with smooth curves - if (this.trailPoints.length >= 2) { - let pathData = `M ${this.trailPoints[0].x} ${this.trailPoints[0].y}`; + // 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; - // 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}`; - } + 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})`; - // 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); + if (p.life <= 0) { + p.element.remove(); + this.particles.splice(i, 1); } } - } - 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.life <= 0 || - (this.hasBeenOnScreen && this.isOffScreen() && this.life < 0.3) + this.x < -50 || + this.x > this.containerRect.width + 50 || + this.y < -50 || + this.y > this.containerRect.height + 50 || + this.life <= 0 ) { + // 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.svg && this.svg.parentNode) { - this.svg.remove(); + if (this.life > 0.3) { + this.createExplosion(); } - if (this.head && this.head.parentNode) { - this.head.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(); } } } diff --git a/src/js/animation/starBackground.js b/src/js/animation/starBackground.js index 83611a0..42b0d0d 100644 --- a/src/js/animation/starBackground.js +++ b/src/js/animation/starBackground.js @@ -3,32 +3,6 @@ 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"); @@ -60,12 +34,7 @@ function injectStarCSS() { } function trySpawnMeteor() { - // Only spawn if tab is visible and not too many meteors - if (!isTabVisible || meteorInstances.length >= 2) { - return; - } - - const spawnChance = 0.15; + const spawnChance = 0.12; if (Math.random() < spawnChance) { const stars = document.getElementById("stars"); const newMeteor = new Meteor(stars); @@ -73,89 +42,40 @@ function trySpawnMeteor() { } } -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 +function animateStars() { starInstances.forEach((star) => star.update()); // Update meteors and remove dead ones for (let i = meteorInstances.length - 1; i >= 0; i--) { - if (!meteorInstances[i].update(cappedDelta)) { + if (!meteorInstances[i].update()) { meteorInstances.splice(i, 1); } } - animationFrameId = requestAnimationFrame(animate); + requestAnimationFrame(animateStars); } function createMeteorBurst() { - // 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++) { + for (let i = 0; i < 3; i++) { setTimeout(() => { - if (isTabVisible && meteorInstances.length < 3) { - const stars = document.getElementById("stars"); - const newMeteor = new Meteor(stars); - meteorInstances.push(newMeteor); - } - }, i * 300); + const stars = document.getElementById("stars"); + const newMeteor = new Meteor(stars); + meteorInstances.push(newMeteor); + }, i * 200); } } -let spawnInterval = null; -let burstInterval = null; - function init() { injectStarCSS(); createStars(); - lastTime = performance.now(); - animate(); + animateStars(); - // 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 + setInterval(trySpawnMeteor, 2000); + setInterval(createMeteorBurst, 15000); } -// 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 };