Compare commits

..

No commits in common. "44cfcec197bf7c9bfc8c22c3d2c670a67d5a997c" and "e934e3132fb44ca6524d9ecc4f854674b6f9ea4a" have entirely different histories.

2 changed files with 284 additions and 327 deletions

View file

@ -1,272 +1,309 @@
class Meteor { class Meteor {
constructor(container) { constructor(container) {
this.element = document.createElement("div");
this.element.style.position = "absolute";
this.container = container; 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(); this.containerRect = container.getBoundingClientRect();
// Smooth movement settings // Start from random edge with PIXEL-based positioning
this.speed = Math.random() * 1.5 + 2; const edge = Math.floor(Math.random() * 4);
this.life = 1.0; switch (edge) {
this.fadeSpeed = 0.002; case 0:
// Top
// 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.x = Math.random() * this.containerRect.width;
this.y = -100; this.y = -20;
this.angle = Math.PI / 2 + (Math.random() - 0.5) * 0.6; case 1:
} else if (startSide < 0.85) { // Right
this.x = -100; this.x = this.containerRect.width + 20;
this.y = Math.random() * this.containerRect.height * 0.3; this.y = Math.random() * this.containerRect.height;
this.angle = Math.PI / 4 + Math.random() * 0.4; case 2:
} else { // Bottom
this.x = this.containerRect.width + 100; this.x = Math.random() * this.containerRect.width;
this.y = Math.random() * this.containerRect.height * 0.3; this.y = this.containerRect.height + 20;
this.angle = (Math.PI * 3) / 4 + Math.random() * 0.4; 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.dx = Math.cos(this.angle) * this.speed;
this.dy = Math.sin(this.angle) * this.speed; this.dy = Math.sin(this.angle) * this.speed;
// Trail settings
this.trailPoints = [];
this.maxTrailLength = 25;
this.hasBeenOnScreen = false;
this.init(); this.init();
} }
init() { getMeteorColors() {
// Create SVG for smooth gradient trail const colorSchemes = [
this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); {
this.svg.style.cssText = ` head: "rgba(255, 255, 255, 1)",
position: absolute; core: "rgba(255, 230, 150, 0.9)",
top: 0; mid: "rgba(255, 180, 80, 0.7)",
left: 0; outer: "rgba(255, 100, 50, 0.5)",
width: 100%; glow: "rgba(255, 200, 100, 0.9)",
height: 100%; },
pointer-events: none; {
overflow: visible; head: "rgba(255, 255, 255, 1)",
z-index: 9; core: "rgba(180, 220, 255, 0.9)",
`; mid: "rgba(100, 180, 255, 0.7)",
this.svg.setAttribute("width", this.containerRect.width); outer: "rgba(50, 100, 255, 0.5)",
this.svg.setAttribute("height", this.containerRect.height); glow: "rgba(100, 180, 255, 0.9)",
},
// Create gradient for trail {
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); head: "rgba(255, 255, 255, 1)",
const gradient = document.createElementNS( core: "rgba(220, 180, 255, 0.9)",
"http://www.w3.org/2000/svg", mid: "rgba(200, 100, 255, 0.7)",
"linearGradient" outer: "rgba(180, 50, 255, 0.5)",
); glow: "rgba(200, 150, 255, 0.9)",
this.gradientId = `meteor-gradient-${Date.now()}-${Math.random() },
.toString(36) {
.substr(2, 9)}`; head: "rgba(255, 255, 255, 1)",
gradient.setAttribute("id", this.gradientId); core: "rgba(180, 255, 200, 0.9)",
gradient.setAttribute("gradientUnits", "userSpaceOnUse"); mid: "rgba(80, 255, 150, 0.7)",
outer: "rgba(50, 200, 100, 0.5)",
// Gradient stops for smooth colored fade glow: "rgba(150, 255, 180, 0.9)",
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" },
]; ];
return colorSchemes[this.colorType];
}
stops.forEach((stop) => { init() {
const stopEl = document.createElementNS( const size = Math.random() * 4 + 10;
"http://www.w3.org/2000/svg", const colors = this.colors;
"stop"
); const head = document.createElement("div");
stopEl.setAttribute("offset", stop.offset); head.style.width = `${size}px`;
stopEl.setAttribute("stop-color", stop.color); head.style.height = `${size}px`;
stopEl.setAttribute("stop-opacity", stop.opacity); head.style.borderRadius = "50%";
gradient.appendChild(stopEl); 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); if (Math.random() < 0.3) {
this.svg.appendChild(defs); this.addTrailParticle();
}
// Create path for trail // Update existing particles with pixel positioning
this.trailPath = document.createElementNS( for (let i = this.particles.length - 1; i >= 0; i--) {
"http://www.w3.org/2000/svg", const p = this.particles[i];
"path" p.life -= 0.02;
); p.x += p.vx;
this.trailPath.setAttribute("fill", "none"); p.y += p.vy;
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; p.element.style.left = `${p.x}px`;
this.headSize = headSize; p.element.style.top = `${p.y}px`;
this.trailPath.setAttribute("stroke-width", headSize * 2); p.element.style.opacity = p.life;
this.trailPath.style.filter = `blur(${headSize * 0.4}px)`; p.element.style.transform = `scale(${p.life})`;
this.svg.appendChild(this.trailPath); if (p.life <= 0) {
this.container.appendChild(this.svg); p.element.remove();
this.particles.splice(i, 1);
// 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 SVG path with smooth curves
if (this.trailPoints.length >= 2) {
let pathData = `M ${this.trailPoints[0].x} ${this.trailPoints[0].y}`;
// 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}`;
}
// 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 ( if (
this.life <= 0 || this.x < -50 ||
(this.hasBeenOnScreen && this.isOffScreen() && this.life < 0.3) 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(); this.remove();
return false; return false;
} }
return true; return true;
} }
remove() { remove() {
if (this.svg && this.svg.parentNode) { if (this.life > 0.3) {
this.svg.remove(); 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();
} }
} }
} }

View file

@ -3,32 +3,6 @@ import Meteor from "./Meteor.js";
const starInstances = []; const starInstances = [];
const meteorInstances = []; 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() { function createStars() {
const stars = document.getElementById("stars"); const stars = document.getElementById("stars");
@ -60,12 +34,7 @@ function injectStarCSS() {
} }
function trySpawnMeteor() { function trySpawnMeteor() {
// Only spawn if tab is visible and not too many meteors const spawnChance = 0.12;
if (!isTabVisible || meteorInstances.length >= 2) {
return;
}
const spawnChance = 0.15;
if (Math.random() < spawnChance) { if (Math.random() < spawnChance) {
const stars = document.getElementById("stars"); const stars = document.getElementById("stars");
const newMeteor = new Meteor(stars); const newMeteor = new Meteor(stars);
@ -73,89 +42,40 @@ function trySpawnMeteor() {
} }
} }
function animate() { function animateStars() {
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()); starInstances.forEach((star) => star.update());
// Update meteors and remove dead ones // Update meteors and remove dead ones
for (let i = meteorInstances.length - 1; i >= 0; i--) { for (let i = meteorInstances.length - 1; i >= 0; i--) {
if (!meteorInstances[i].update(cappedDelta)) { if (!meteorInstances[i].update()) {
meteorInstances.splice(i, 1); meteorInstances.splice(i, 1);
} }
} }
animationFrameId = requestAnimationFrame(animate); requestAnimationFrame(animateStars);
} }
function createMeteorBurst() { function createMeteorBurst() {
// Only burst if tab is visible for (let i = 0; i < 3; i++) {
if (!isTabVisible) return;
// Limit burst size
const burstSize = Math.min(2, 3 - meteorInstances.length);
for (let i = 0; i < burstSize; i++) {
setTimeout(() => { setTimeout(() => {
if (isTabVisible && meteorInstances.length < 3) {
const stars = document.getElementById("stars"); const stars = document.getElementById("stars");
const newMeteor = new Meteor(stars); const newMeteor = new Meteor(stars);
meteorInstances.push(newMeteor); meteorInstances.push(newMeteor);
} }, i * 200);
}, i * 300);
} }
} }
let spawnInterval = null;
let burstInterval = null;
function init() { function init() {
injectStarCSS(); injectStarCSS();
createStars(); createStars();
lastTime = performance.now(); animateStars();
animate();
// Longer interval for spawning setInterval(trySpawnMeteor, 2000);
spawnInterval = setInterval(() => { setInterval(createMeteorBurst, 15000);
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") { if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init); document.addEventListener("DOMContentLoaded", init);
} else { } else {
init(); init();
} }
export { cleanup };