pages/src/js/animation/Meteor.js

311 lines
8.9 KiB
JavaScript

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");
}
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;
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%
)
`;
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;
}
});
if (Math.random() < 0.3) {
this.addTrailParticle();
}
// 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;
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})`;
if (p.life <= 0) {
p.element.remove();
this.particles.splice(i, 1);
}
}
if (
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.life > 0.3) {
this.createExplosion();
}
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();
}
}
}
export default Meteor;