test-space / index.html
Lineage11's picture
Update index.html
0c1d486 verified
Raw History Blame Contribute Delete
32.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Live Stream Countdown</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Orbitron:wght@700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script>
// Tailwind CSS configuration for custom colors and fonts
tailwind.config = {
theme: {
extend: {
colors: {
magenta: '#7e0039',
gold: '#f88d0d',
grey: '#666666',
charcoal: '#333333',
teal: '#00a3a5',
warmgrey: '#8a7b71',
green: '#3d7061',
orange: '#bb3900',
eucalypt: '#909a8a',
sandstone: '#ab8a5c'
},
fontFamily: {
sans: ['Inter', 'sans-serif'], // Default sans-serif
display: ['Orbitron', 'sans-serif'], // For countdown digits and titles
}
}
}
}
</script>
<style>
/* Prevent scrollbars caused by absolute/fixed positioned particles */
body {
overflow: hidden;
/* Dynamic Gradient Background */
background: linear-gradient(135deg, #7e0039, #333333, #00a3a5); /* Initial gradient */
background-size: 400% 400%; /* Make gradient larger than viewport */
/* Changed animation duration to 60s for slower change */
animation: gradientAnimation 60s ease infinite; /* Animate gradient */
}
@keyframes gradientAnimation {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* Text glow effect */
.glow-text {
text-shadow: 0 0 10px rgba(248, 141, 13, 0.7);
}
/* Pulse animation for the join button */
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
/* Modal styles for showing/hiding with transitions */
.modal {
transition: all 0.3s ease;
background-color: rgba(0,0,0,0); /* Start transparent overlay */
opacity: 0;
pointer-events: none; /* Don't block clicks when hidden */
}
.modal.active {
opacity: 1;
background-color: rgba(0,0,0,0.8); /* Dark overlay when active */
pointer-events: auto; /* Allow clicks when active */
}
.modal-content {
transform: translateY(-20px); /* Start slightly up */
opacity: 0;
transition: all 0.3s ease;
}
.modal.active .modal-content {
transform: translateY(0); /* Slide into place */
opacity: 1;
}
/* Ripple Effect Styles */
/* Apply to the clickable elements within the countdown */
.countdown-digit {
position: relative; /* Needed for ripple positioning */
overflow: hidden; /* Hide ripple outside the element */
cursor: pointer; /* Indicate interactivity */
transform: translateZ(0); /* Improve performance */
}
.ripple {
position: absolute;
border-radius: 50%;
transform: scale(0);
animation: ripple 0.6s linear;
background-color: rgba(255, 255, 255, 0.5); /* White ripple with transparency */
pointer-events: none; /* Don't interfere with clicks */
}
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
/* Particle Styles */
/* Container to hold particles and position them */
.particles-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none; /* Don't block interaction with content */
z-index: -1; /* Place behind content */
}
/* Individual particle style */
.particle {
position: absolute;
width: 5px;
height: 5px;
background-color: rgba(248, 141, 13, 0.6); /* Gold color with transparency */
border-radius: 50%;
opacity: 0.8;
/* Initial animation (will be overridden by JS for random movement) */
animation: float 15s ease-in-out infinite alternate;
}
/* Add variations for different particles */
.particle:nth-child(2n) {
width: 3px;
height: 3px;
background-color: rgba(0, 163, 165, 0.6); /* Teal color */
animation-duration: 20s;
}
.particle:nth-child(3n) {
width: 7px;
height: 7px;
background-color: rgba(138, 123, 113, 0.6); /* Warmgrey color */
animation-duration: 25s;
}
.particle:nth-child(4n) {
width: 4px;
height: 4px;
background-color: rgba(171, 138, 92, 0.6); /* Sandstone color */
animation-duration: 18s;
}
/* Basic float keyframes (used as a fallback/starting point) */
@keyframes float {
0% { transform: translate(0, 0); }
100% { transform: translate(50px, 50px); } /* Dummy values, JS will set random ones */
}
/* Progress Bar Styles */
.progress-bar-container {
width: 80%; /* Adjust width as needed */
max-width: 600px;
height: 15px;
background-color: rgba(102, 102, 102, 0.3); /* Grey with transparency */
border-radius: 8px;
margin: 20px auto; /* Center the bar */
overflow: hidden; /* Hide overflowing progress */
}
.progress-bar {
height: 100%;
width: 100%; /* Starts full */
background: linear-gradient(to right, #00a3a5, #f88d0d); /* Teal to Gold gradient */
border-radius: 8px;
transition: width 1s linear; /* Smooth transition for the bar */
}
/* Countdown Digit Animation (Subtle Scale) */
.countdown-digit > div { /* Target the inner div with the number */
transition: transform 0.3s ease-out; /* Smooth transition */
}
/* Optional: Add a class in JS to trigger animation on update */
.countdown-digit > div.animate-scale {
transform: scale(1.1);
}
</style>
</head>
<body class="bg-charcoal text-white min-h-screen flex flex-col font-sans">
<div class="particles-container">
<div class="particle" style="top: 10%; left: 20%;"></div>
<div class="particle" style="top: 50%; left: 80%;"></div>
<div class="particle" style="top: 80%; left: 40%;"></div>
<div class="particle" style="top: 30%; left: 60%;"></div>
<div class="particle" style="top: 70%; left: 10%;"></div>
<div class="particle" style="top: 20%; left: 90%;"></div>
<div class="particle" style="top: 60%; left: 30%;"></div>
<div class="particle" style="top: 40%; left: 70%;"></div>
<div class="particle" style="top: 90%; left: 50%;"></div>
<div class="particle" style="top: 5%; left: 5%;"></div>
<div class="particle" style="top: 95%; left: 95%;"></div>
<div class="particle" style="top: 15%; left: 75%;"></div>
<div class="particle" style="top: 85%; left: 25%;"></div>
</div>
<button id="adminBtn" class="fixed top-4 right-4 bg-magenta hover:bg-opacity-80 text-white p-2 rounded-full shadow-lg z-50 transition duration-300 ease-in-out transform hover:scale-110" title="Admin Settings">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
<main class="flex-grow flex flex-col items-center justify-center px-4 text-center relative z-10">
<div class="max-w-4xl w-full">
<h1 class="text-4xl md:text-6xl font-display mb-8 glow-text text-gold">Live Stream Starting Soon</h1>
<div class="bg-opacity-20 bg-magenta backdrop-filter backdrop-blur-lg rounded-xl p-8 mb-12 border border-teal border-opacity-30">
<h2 class="text-2xl md:text-3xl font-display font-semibold mb-6 text-eucalypt">Join us for an exciting event</h2>
<p class="text-lg text-warmgrey mb-8">The stream will begin automatically when the countdown reaches zero.</p>
<p id="preStreamMessage" class="text-xl font-bold text-orange mb-8 hidden">Stream starting in less than 5 minutes!</p>
<div class="flex justify-center mb-8">
<div id="countdown" class="grid grid-cols-4 gap-4 w-full max-w-2xl font-display">
<div class="bg-charcoal bg-opacity-70 rounded-lg p-4 border border-gold border-opacity-30 countdown-digit">
<div id="days" class="text-5xl md:text-7xl font-bold text-gold">00</div>
<div class="text-sm uppercase text-sandstone">Days</div>
</div>
<div class="bg-charcoal bg-opacity-70 rounded-lg p-4 border border-gold border-opacity-30 countdown-digit">
<div id="hours" class="text-5xl md:text-7xl font-bold text-gold">00</div>
<div class="text-sm uppercase text-sandstone">Hours</div>
</div>
<div class="bg-charcoal bg-opacity-70 rounded-lg p-4 border border-gold border-opacity-30 countdown-digit">
<div id="minutes" class="text-5xl md:text-7xl font-bold text-gold">00</div>
<div class="text-sm uppercase text-sandstone">Minutes</div>
</div>
<div class="bg-charcoal bg-opacity-70 rounded-lg p-4 border border-gold border-opacity-30 countdown-digit">
<div id="seconds" class="text-5xl md:text-7xl font-bold text-gold">00</div>
<div class="text-sm uppercase text-sandstone">Seconds</div>
</div>
</div>
</div>
<div class="progress-bar-container">
<div id="progressBar" class="progress-bar"></div>
</div>
<div class="mt-8">
<a href="recent-streams.html" class="inline-block bg-teal hover:bg-opacity-80 text-white font-bold py-3 px-6 rounded-full text-lg transition-all transform hover:scale-105">
View Recent Streams
</a>
</div>
<div id="streamReady" class="hidden pulse">
<a id="streamLink" href="#" target="_blank" class="inline-block bg-orange hover:bg-opacity-90 text-white font-bold py-4 px-8 rounded-full text-xl transition-all transform hover:scale-105">
Join Live Stream Now
</a>
</div>
</div>
<div class="text-grey">
<p class="mt-2 text-sm">Stream will begin on: <span id="streamTimeDisplay" class="font-medium text-teal">Loading...</span> <span id="timezoneDisplay" class="font-medium text-eucalypt text-xs">(Your Local Time)</span></p>
<p id="errorDisplay" class="mt-2 text-sm text-orange font-semibold"></p> </div>
</div>
</main>
<footer class="py-6 text-center text-warmgrey text-sm relative z-10">
<p>© <span id="currentYear"></span> Live Stream Countdown. All rights reserved. | <a href="recent-streams.html" class="text-teal hover:text-eucalypt transition duration-300">Recent Streams</a></p>
</footer>
<div id="adminModal" class="modal fixed inset-0 flex items-center justify-center p-4 z-50">
<div class="modal-content bg-charcoal rounded-xl max-w-md w-full p-6 border border-magenta border-opacity-50 text-center">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-gold">Admin Settings</h3>
<button id="closeModal" class="text-grey hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<p class="mb-4 text-warmgrey text-left">
This countdown is controlled by a Google Sheet. To update the stream date, time, or URL, you need to edit the sheet directly.
</p>
<p class="mb-4 text-orange text-sm text-left font-semibold">
Remember: After editing the sheet, you MUST re-publish it to the web (as CSV) for changes to appear here on the page.
</p>
<a id="editSheetLink" href="https://docs.google.com/spreadsheets/d/1XQ8ZDLZvU9lz4kNu9Zn3SXBIuIwniuLG4pVEfk_5NkU/edit?gid=1249364277#gid=1249364277" target="_blank" class="inline-block w-full bg-teal hover:bg-opacity-80 text-white py-3 rounded font-bold transition duration-300 ease-in-out transform hover:scale-105">
Edit Google Sheet
</a>
<div id="messageArea" class="mt-4 text-center text-sm"></div>
</div>
</div>
<audio id="countdownEndSound" src="https://github.com/selli106/avlink/raw/refs/heads/main/ding-36029.mp3" preload="auto"></audio>
<script>
// --- Configuration ---
// Your Google Sheet Published CSV URL
// (File > Share > Publish to web > Choose sheet & CSV > Publish)
// Make sure the sheet contains rows with 'key,value' like 'countdown_date,YYYY-MM-DDTHH:mm:ssZ' and 'stream_url,your_url_here'
const sheetCsvUrl = "https://docs.google.com/spreadsheets/d/e/2PACX-1vSy-8e3C5trnUBXhMnpHxOmWEqB61875RPY0nCIIGCpw3UnU9SK31dt8iqqYzgeUziBkAWdo5qQ8wnd/pub?output=csv";
// Your Google Sheet Edit URL
// (The standard URL from your browser's address bar when editing)
const googleSheetEditUrl = "https://docs.google.com/spreadsheets/d/1XQ8ZDLZvU9lz4kNu9Zn3SXBIuIwniuLG4pVEfk_5NkU/edit?gid=1249364277#gid=1249364277";
// How often to fetch settings from the sheet (in milliseconds)
const fetchInterval = 60000; // 1 minute
// Threshold for pre-stream message (in milliseconds)
const preStreamThreshold = 5 * 60 * 1000; // 5 minutes
// --- DOM Elements ---
const adminBtn = document.getElementById('adminBtn');
const adminModal = document.getElementById('adminModal');
const closeModal = document.getElementById('closeModal');
const editSheetLink = document.getElementById('editSheetLink');
const messageArea = document.getElementById('messageArea'); // Used in modal for fetch status
const errorDisplay = document.getElementById('errorDisplay'); // Used below countdown for fetch errors
const countdownDisplay = document.getElementById('countdown');
const streamReady = document.getElementById('streamReady');
const streamLink = document.getElementById('streamLink');
const streamTimeDisplay = document.getElementById('streamTimeDisplay');
const timezoneDisplay = document.getElementById('timezoneDisplay');
const currentYear = document.getElementById('currentYear');
const daysElement = document.getElementById('days');
const hoursElement = document.getElementById('hours');
const minutesElement = document.getElementById('minutes');
const secondsElement = document.getElementById('seconds');
// Elements for new features
const preStreamMessageElement = document.getElementById('preStreamMessage');
const countdownEndSound = document.getElementById('countdownEndSound');
const progressBar = document.getElementById('progressBar');
// Elements for effects
const countdownDigits = document.querySelectorAll('.countdown-digit');
const particles = document.querySelectorAll('.particle');
// --- State Variables ---
let countdownInterval;
let currentStreamTargetUrl = '#'; // Default URL
let currentCountdownDateString = null; // Store the date string from the sheet
let initialCountdownDuration = 0; // Store the total duration for the progress bar
// --- Functions ---
// Fetches settings from the Google Sheet CSV
async function fetchSettings(csvUrl) {
try {
// Clear previous errors
errorDisplay.textContent = '';
const response = await fetch(csvUrl);
if (!response.ok) {
// Attempt to read body for more specific error if possible, otherwise use status
const errorBody = await response.text().catch(() => `Status: ${response.status}`);
throw new Error(`HTTP error fetching sheet: ${response.status} - ${errorBody.substring(0, 100)}...`); // Limit error body length
}
const data = await response.text();
const rows = data.split("\n").map(row => row.split(",").map(cell => cell.trim())); // Split rows and cells, trim whitespace
let dateString = null;
let urlString = null;
// Parse rows to find settings
for (const row of rows) {
if (row.length >= 2) { // Ensure row has at least key and value columns
const key = row[0];
const value = row[1];
if (key === "countdown_date") {
dateString = value;
} else if (key === "stream_url") {
urlString = value;
}
}
}
if (dateString && urlString) {
// Only update if both values are found and are different from current settings
if (dateString !== currentCountdownDateString || urlString !== currentStreamTargetUrl) {
currentCountdownDateString = dateString;
currentStreamTargetUrl = urlString;
console.log("Settings fetched successfully:", { date: currentCountdownDateString, url: currentStreamTargetUrl });
// Restart countdown with new settings
startCountdown(currentCountdownDateString, currentStreamTargetUrl);
} else {
console.log("Settings fetched, no changes detected.");
// Continue the existing countdown if no changes
}
} else {
errorDisplay.textContent = 'Error: Settings (countdown_date or stream_url) not found in Google Sheet.';
console.error("Missing required settings (countdown_date or stream_url) in Google Sheet CSV data.");
// Indicate error to user and stop countdown if essential data is missing
countdownDisplay.textContent = "Error loading settings.";
clearInterval(countdownInterval);
streamReady.classList.add('hidden');
preStreamMessageElement.classList.add('hidden'); // Hide pre-stream message
progressBar.style.width = '0%'; // Reset progress bar
}
} catch (error) {
errorDisplay.textContent = 'Error fetching settings. Check sheet URL and publish status.';
console.error("Failed to fetch settings from Google Sheet:", error);
// Indicate error to user and stop countdown on fetch error
countdownDisplay.textContent = "Error fetching settings.";
clearInterval(countdownInterval);
streamReady.classList.add('hidden');
preStreamMessageElement.classList.add('hidden'); // Hide pre-stream message
progressBar.style.width = '0%'; // Reset progress bar
}
}
// Starts or updates the countdown timer
function startCountdown(dateString, streamTargetUrl) {
// Clear any existing interval before starting a new one
clearInterval(countdownInterval);
// --- Debugging Log ---
console.log("Parsing date string:", dateString);
const targetDate = new Date(dateString);
console.log("Parsed Date object:", targetDate);
const targetTimestamp = targetDate.getTime();
console.log("Target Timestamp (ms):", targetTimestamp);
// --- End Debugging Log ---
// Check if the parsed date is valid
if (isNaN(targetTimestamp)) {
errorDisplay.textContent = `Error: Invalid date format from sheet: ${dateString}`;
console.error("Invalid date string received from Google Sheet:", dateString);
countdownDisplay.textContent = "Error loading settings.";
clearInterval(countdownInterval);
streamReady.classList.add('hidden');
preStreamMessageElement.classList.add('hidden'); // Hide pre-stream message
progressBar.style.width = '0%'; // Reset progress bar
streamTimeDisplay.textContent = "Invalid Date";
timezoneDisplay.textContent = "";
return; // Stop execution if date is invalid
}
// Calculate the initial duration for the progress bar
const now = new Date().getTime();
initialCountdownDuration = targetTimestamp - now;
// Prevent negative duration if date is in the past
if (initialCountdownDuration < 0) {
initialCountdownDuration = 0;
}
// Update display immediately
updateCountdown(targetTimestamp, streamTargetUrl);
// Set interval to update every second
countdownInterval = setInterval(() => {
updateCountdown(targetTimestamp, streamTargetUrl);
}, 1000);
// Update the display below the countdown with the target time
const options = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short' // Show the user's local timezone abbreviation
};
// Use the Date object created from the string for consistent display
// Note: This displays the target moment converted to the *user's* local time zone.
try {
streamTimeDisplay.textContent = targetDate.toLocaleString(undefined, options); // Use the parsed Date object
timezoneDisplay.textContent = "(Your Local Time)"; // Indicate this is local time
} catch (e) {
// Handle potential invalid date string parsing errors (less likely now with isNaN check)
streamTimeDisplay.textContent = `Error formatting date: ${dateString}`;
timezoneDisplay.textContent = "";
console.error("Error formatting date string:", dateString, e);
}
// Ensure the countdown display is visible if the date is in the future
if (targetTimestamp > new Date().getTime()) {
countdownDisplay.classList.remove('hidden'); // Make sure grid is visible
// Ensure individual elements are visible if the parent was hidden
if(daysElement) daysElement.parentElement.classList.remove('hidden');
if(hoursElement) hoursElement.parentElement.classList.remove('hidden');
if(minutesElement) minutesElement.parentElement.classList.remove('hidden');
if(secondsElement) secondsElement.parentElement.classList.remove('hidden');
streamReady.classList.add('hidden');
}
}
// Updates the countdown display and handles pre-stream message/progress bar
function updateCountdown(targetTimestamp, streamTargetUrl) { // Accepts timestamp now
const now = new Date().getTime();
const distance = targetTimestamp - now;
// --- Progress Bar Update ---
let progressPercentage = 100; // Start full
if (initialCountdownDuration > 0) {
progressPercentage = Math.max(0, (distance / initialCountdownDuration) * 100);
}
progressBar.style.width = `${progressPercentage}%`;
if (distance < 0) {
// Stream time has passed
clearInterval(countdownInterval);
countdownDisplay.classList.add('hidden'); // Hide the grid container
streamReady.classList.remove('hidden'); // Show the join button
preStreamMessageElement.classList.add('hidden'); // Hide pre-stream message
// Ensure stream link is correctly set one last time
streamLink.href = streamTargetUrl;
streamLink.target = "_blank"; // Ensure it opens in a new tab
streamTimeDisplay.textContent = "Stream should be live now!";
timezoneDisplay.textContent = ""; // Clear timezone note
errorDisplay.textContent = ""; // Clear any lingering error messages
// Play sound effect (if loaded and allowed by browser)
if (countdownEndSound) {
countdownEndSound.play().catch(e => console.warn("Audio playback blocked:", e));
}
// Optional: Automatically redirect after a delay or on user interaction
// For automatic redirect, use: window.open(streamTargetUrl, "_blank");
// Added target="_blank" above to the link itself which is usually sufficient.
return;
}
// --- Pre-stream Message ---
if (distance <= preStreamThreshold) {
preStreamMessageElement.classList.remove('hidden');
} else {
preStreamMessageElement.classList.add('hidden');
}
// Calculate time units
const days = Math.floor(distance / (1000 * 60 * 60 * 24));
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
// Update display elements, using ?.textContent for safety just in case
// Add a temporary class to trigger animation
const updateDigit = (element, value) => {
const stringValue = value.toString().padStart(2, '0');
if (element && element.textContent !== stringValue) {
element.textContent = stringValue;
// Trigger animation class
element.classList.add('animate-scale');
// Remove class after animation duration
setTimeout(() => {
element.classList.remove('animate-scale');
}, 300); // Match CSS transition duration
} else if (element) {
element.textContent = stringValue; // Update even if value is same, just in case
}
};
updateDigit(daysElement, days);
updateDigit(hoursElement, hours);
updateDigit(minutesElement, minutes);
updateDigit(secondsElement, seconds);
// Ensure countdown elements are visible if they were hidden after countdown ended previously
countdownDisplay.classList.remove('hidden');
streamReady.classList.add('hidden');
// Also make sure the individual number divs are not hidden if you added display:none to them
if(daysElement && daysElement.parentElement.classList.contains('hidden')) daysElement.parentElement.classList.remove('hidden');
if(hoursElement && hoursElement.parentElement.classList.contains('hidden')) hoursElement.parentElement.classList.remove('hidden');
if(minutesElement && minutesElement.parentElement.classList.contains('hidden')) minutesElement.parentElement.classList.remove('hidden');
if(secondsElement && secondsElement.parentElement.classList.contains('hidden')) secondsElement.parentElement.classList.remove('hidden');
}
// --- Effects Functions ---
// Create ripple effect on countdown digits
function createRipple(event) {
const element = event.currentTarget;
const circle = document.createElement('span');
const diameter = Math.max(element.clientWidth, element.clientHeight);
const radius = diameter / 2;
circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - element.getBoundingClientRect().left - radius}px`;
circle.style.top = `${event.clientY - element.getBoundingClientRect().top - radius}px`;
circle.classList.add('ripple');
// Remove any existing ripple before adding a new one
const ripple = element.getElementsByClassName('ripple')[0];
if (ripple) {
ripple.remove();
}
element.appendChild(circle);
}
// Animate floating particles
function animateParticles() {
// Check if particles exist before trying to animate
if (particles.length === 0) return;
particles.forEach(particle => {
// Generate random target position within a reasonable range
// Adjust the multiplier (e.g., 200, 300) to control particle movement range
const x = (Math.random() - 0.5) * 200; // Random value between -100 and 100
const y = (Math.random() - 0.5) * 200; // Random value between -100 and 100
const duration = 15 + Math.random() * 15; // Random duration between 15s and 30s
// Apply the transition
particle.style.transition = `transform ${duration}s ease-in-out`;
particle.style.transform = `translate(${x}px, ${y}px)`;
});
// Schedule the next animation cycle
// Use a timeout slightly longer
</script>
</body>
</html>