Compare commits
	
		
			2 commits
		
	
	
		
			e934e3132f
			...
			44cfcec197
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 44cfcec197 | |||
| 226f5282ea | 
					 2 changed files with 324 additions and 281 deletions
				
			
		| 
						 | 
				
			
			@ -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();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue