2025-10-20 15:09:39 +02:00
|
|
|
import Star from "./Star.js";
|
|
|
|
|
import Meteor from "./Meteor.js";
|
|
|
|
|
|
|
|
|
|
const starInstances = [];
|
|
|
|
|
const meteorInstances = [];
|
2025-10-22 08:47:23 +02:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-10-20 15:09:39 +02:00
|
|
|
|
|
|
|
|
function createStars() {
|
|
|
|
|
const stars = document.getElementById("stars");
|
2025-10-20 15:24:43 +02:00
|
|
|
const count = 400;
|
2025-10-20 15:09:39 +02:00
|
|
|
stars.innerHTML = "";
|
|
|
|
|
starInstances.length = 0;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
|
|
|
const star = new Star(stars);
|
|
|
|
|
starInstances.push(star);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function injectStarCSS() {
|
|
|
|
|
const style = document.createElement("style");
|
|
|
|
|
style.textContent = `
|
|
|
|
|
.star {
|
|
|
|
|
position: absolute;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
animation: twinkle ease-in-out infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes twinkle {
|
|
|
|
|
0%, 100% { opacity: 1; }
|
|
|
|
|
50% { opacity: 0.3; }
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
document.head.appendChild(style);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function trySpawnMeteor() {
|
2025-10-22 08:47:23 +02:00
|
|
|
// Only spawn if tab is visible and not too many meteors
|
|
|
|
|
if (!isTabVisible || meteorInstances.length >= 2) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const spawnChance = 0.15;
|
2025-10-20 15:09:39 +02:00
|
|
|
if (Math.random() < spawnChance) {
|
|
|
|
|
const stars = document.getElementById("stars");
|
|
|
|
|
const newMeteor = new Meteor(stars);
|
|
|
|
|
meteorInstances.push(newMeteor);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-22 08:47:23 +02:00
|
|
|
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
|
2025-10-20 15:09:39 +02:00
|
|
|
starInstances.forEach((star) => star.update());
|
|
|
|
|
|
|
|
|
|
// Update meteors and remove dead ones
|
|
|
|
|
for (let i = meteorInstances.length - 1; i >= 0; i--) {
|
2025-10-22 08:47:23 +02:00
|
|
|
if (!meteorInstances[i].update(cappedDelta)) {
|
2025-10-20 15:09:39 +02:00
|
|
|
meteorInstances.splice(i, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-22 08:47:23 +02:00
|
|
|
animationFrameId = requestAnimationFrame(animate);
|
2025-10-20 15:09:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createMeteorBurst() {
|
2025-10-22 08:47:23 +02:00
|
|
|
// 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++) {
|
2025-10-20 15:09:39 +02:00
|
|
|
setTimeout(() => {
|
2025-10-22 08:47:23 +02:00
|
|
|
if (isTabVisible && meteorInstances.length < 3) {
|
|
|
|
|
const stars = document.getElementById("stars");
|
|
|
|
|
const newMeteor = new Meteor(stars);
|
|
|
|
|
meteorInstances.push(newMeteor);
|
|
|
|
|
}
|
|
|
|
|
}, i * 300);
|
2025-10-20 15:09:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-22 08:47:23 +02:00
|
|
|
let spawnInterval = null;
|
|
|
|
|
let burstInterval = null;
|
|
|
|
|
|
2025-10-20 15:09:39 +02:00
|
|
|
function init() {
|
|
|
|
|
injectStarCSS();
|
|
|
|
|
createStars();
|
2025-10-22 08:47:23 +02:00
|
|
|
lastTime = performance.now();
|
|
|
|
|
animate();
|
|
|
|
|
|
|
|
|
|
// Longer interval for spawning
|
|
|
|
|
spawnInterval = setInterval(() => {
|
|
|
|
|
if (isTabVisible) {
|
|
|
|
|
trySpawnMeteor();
|
|
|
|
|
}
|
|
|
|
|
}, 3000); // Every 3 seconds instead of 2
|
2025-10-20 15:09:39 +02:00
|
|
|
|
2025-10-22 08:47:23 +02:00
|
|
|
// Longer interval for bursts
|
|
|
|
|
burstInterval = setInterval(() => {
|
|
|
|
|
if (isTabVisible) {
|
|
|
|
|
createMeteorBurst();
|
|
|
|
|
}
|
|
|
|
|
}, 20000); // Every 20 seconds instead of 15
|
2025-10-20 15:09:39 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-22 08:47:23 +02:00
|
|
|
// Cleanup function
|
|
|
|
|
function cleanup() {
|
|
|
|
|
if (animationFrameId) {
|
|
|
|
|
cancelAnimationFrame(animationFrameId);
|
|
|
|
|
}
|
|
|
|
|
if (spawnInterval) {
|
|
|
|
|
clearInterval(spawnInterval);
|
|
|
|
|
}
|
|
|
|
|
if (burstInterval) {
|
|
|
|
|
clearInterval(burstInterval);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle page unload
|
|
|
|
|
window.addEventListener("beforeunload", cleanup);
|
|
|
|
|
|
2025-10-20 15:09:39 +02:00
|
|
|
if (document.readyState === "loading") {
|
|
|
|
|
document.addEventListener("DOMContentLoaded", init);
|
|
|
|
|
} else {
|
|
|
|
|
init();
|
|
|
|
|
}
|
2025-10-22 08:47:23 +02:00
|
|
|
|
|
|
|
|
export { cleanup };
|