Meteors have been updated to be smoother and better looking
This commit is contained in:
parent
6e98a0c133
commit
226f5282ea
2 changed files with 324 additions and 281 deletions
|
|
@ -1,309 +1,272 @@
|
||||||
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();
|
||||||
|
|
||||||
// Start from random edge with PIXEL-based positioning
|
// Smooth movement settings
|
||||||
const edge = Math.floor(Math.random() * 4);
|
this.speed = Math.random() * 1.5 + 2;
|
||||||
switch (edge) {
|
this.life = 1.0;
|
||||||
case 0:
|
this.fadeSpeed = 0.002;
|
||||||
// Top
|
|
||||||
this.x = Math.random() * this.containerRect.width;
|
// Enhanced color schemes for vibrant streaks
|
||||||
this.y = -20;
|
this.colorSchemes = [
|
||||||
case 1:
|
// Classic golden-white meteor
|
||||||
// Right
|
{
|
||||||
this.x = this.containerRect.width + 20;
|
core: "#ffffff",
|
||||||
this.y = Math.random() * this.containerRect.height;
|
mid: "#ffeb99",
|
||||||
case 2:
|
outer: "#ff9933",
|
||||||
// Bottom
|
},
|
||||||
this.x = Math.random() * this.containerRect.width;
|
// Blue-white ice meteor
|
||||||
this.y = this.containerRect.height + 20;
|
{
|
||||||
case 3:
|
core: "#ffffff",
|
||||||
// Left
|
mid: "#aaddff",
|
||||||
this.x = -20;
|
outer: "#4488ff",
|
||||||
this.y = Math.random() * this.containerRect.height;
|
},
|
||||||
default:
|
// Purple cosmic meteor
|
||||||
console.log("Error creating Meteor direction");
|
{
|
||||||
|
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.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();
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
init() {
|
||||||
const size = Math.random() * 4 + 10;
|
// Create SVG for smooth gradient trail
|
||||||
const colors = this.colors;
|
this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
this.svg.style.cssText = `
|
||||||
const head = document.createElement("div");
|
position: absolute;
|
||||||
head.style.width = `${size}px`;
|
top: 0;
|
||||||
head.style.height = `${size}px`;
|
left: 0;
|
||||||
head.style.borderRadius = "50%";
|
width: 100%;
|
||||||
head.style.background = `
|
height: 100%;
|
||||||
radial-gradient(
|
pointer-events: none;
|
||||||
circle at 30% 30%,
|
overflow: visible;
|
||||||
${colors.head} 0%,
|
z-index: 9;
|
||||||
${colors.core} 30%,
|
|
||||||
${colors.mid} 60%,
|
|
||||||
${colors.outer} 100%
|
|
||||||
)
|
|
||||||
`;
|
`;
|
||||||
head.style.boxShadow = `
|
this.svg.setAttribute("width", this.containerRect.width);
|
||||||
0 0 ${size * 4}px ${colors.glow},
|
this.svg.setAttribute("height", this.containerRect.height);
|
||||||
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
|
// Create gradient for trail
|
||||||
this.createTailLayer(size * 3, size * 1.2, colors.core, 1, "1px");
|
const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
||||||
this.createTailLayer(size * 5, size * 2, colors.mid, 0.6, "2px");
|
const gradient = document.createElementNS(
|
||||||
this.createTailLayer(size * 8, size * 3, colors.outer, 0.3, "3px");
|
"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
|
// Gradient stops for smooth colored fade
|
||||||
this.element.appendChild(head);
|
const stops = [
|
||||||
this.element.style.left = `${this.x}px`;
|
{ offset: "0%", color: this.colors.core, opacity: "1" },
|
||||||
this.element.style.top = `${this.y}px`;
|
{ offset: "30%", color: this.colors.mid, opacity: "0.8" },
|
||||||
this.element.style.zIndex = "10";
|
{ offset: "60%", color: this.colors.outer, opacity: "0.5" },
|
||||||
this.element.style.filter = "blur(0.5px)";
|
{ offset: "100%", color: this.colors.outer, opacity: "0" },
|
||||||
|
];
|
||||||
|
|
||||||
this.head = head;
|
stops.forEach((stop) => {
|
||||||
this.size = size;
|
const stopEl = document.createElementNS(
|
||||||
|
"http://www.w3.org/2000/svg",
|
||||||
this.container.appendChild(this.element);
|
"stop"
|
||||||
|
);
|
||||||
this.createTrailParticles();
|
stopEl.setAttribute("offset", stop.offset);
|
||||||
}
|
stopEl.setAttribute("stop-color", stop.color);
|
||||||
|
stopEl.setAttribute("stop-opacity", stop.opacity);
|
||||||
createTailLayer(length, width, color, opacity, blur) {
|
gradient.appendChild(stopEl);
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Math.random() < 0.3) {
|
defs.appendChild(gradient);
|
||||||
this.addTrailParticle();
|
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
|
// Update SVG path with smooth curves
|
||||||
for (let i = this.particles.length - 1; i >= 0; i--) {
|
if (this.trailPoints.length >= 2) {
|
||||||
const p = this.particles[i];
|
let pathData = `M ${this.trailPoints[0].x} ${this.trailPoints[0].y}`;
|
||||||
p.life -= 0.02;
|
|
||||||
p.x += p.vx;
|
|
||||||
p.y += p.vy;
|
|
||||||
|
|
||||||
p.element.style.left = `${p.x}px`;
|
// Use smooth curves
|
||||||
p.element.style.top = `${p.y}px`;
|
for (let i = 1; i < this.trailPoints.length - 1; i++) {
|
||||||
p.element.style.opacity = p.life;
|
const curr = this.trailPoints[i];
|
||||||
p.element.style.transform = `scale(${p.life})`;
|
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) {
|
// Final point
|
||||||
p.element.remove();
|
if (this.trailPoints.length > 1) {
|
||||||
this.particles.splice(i, 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.x < -50 ||
|
this.life <= 0 ||
|
||||||
this.x > this.containerRect.width + 50 ||
|
(this.hasBeenOnScreen && this.isOffScreen() && this.life < 0.3)
|
||||||
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.life > 0.3) {
|
if (this.svg && this.svg.parentNode) {
|
||||||
this.createExplosion();
|
this.svg.remove();
|
||||||
}
|
}
|
||||||
|
if (this.head && this.head.parentNode) {
|
||||||
setTimeout(() => {
|
this.head.remove();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,32 @@ 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");
|
||||||
|
|
@ -34,7 +60,12 @@ function injectStarCSS() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function trySpawnMeteor() {
|
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) {
|
if (Math.random() < spawnChance) {
|
||||||
const stars = document.getElementById("stars");
|
const stars = document.getElementById("stars");
|
||||||
const newMeteor = new Meteor(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());
|
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()) {
|
if (!meteorInstances[i].update(cappedDelta)) {
|
||||||
meteorInstances.splice(i, 1);
|
meteorInstances.splice(i, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(animateStars);
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMeteorBurst() {
|
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(() => {
|
setTimeout(() => {
|
||||||
const stars = document.getElementById("stars");
|
if (isTabVisible && meteorInstances.length < 3) {
|
||||||
const newMeteor = new Meteor(stars);
|
const stars = document.getElementById("stars");
|
||||||
meteorInstances.push(newMeteor);
|
const newMeteor = new Meteor(stars);
|
||||||
}, i * 200);
|
meteorInstances.push(newMeteor);
|
||||||
|
}
|
||||||
|
}, i * 300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let spawnInterval = null;
|
||||||
|
let burstInterval = null;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
injectStarCSS();
|
injectStarCSS();
|
||||||
createStars();
|
createStars();
|
||||||
animateStars();
|
lastTime = performance.now();
|
||||||
|
animate();
|
||||||
|
|
||||||
setInterval(trySpawnMeteor, 2000);
|
// Longer interval for spawning
|
||||||
setInterval(createMeteorBurst, 15000);
|
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") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", init);
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
} else {
|
} else {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { cleanup };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue