// Pre-loaded SVG content cache let blackStoneSvg = null; let whiteStoneSvg = null; // Pre-load SVG content when the script loads async function preloadStoneSvgs() { try { // Fetch black stone SVG const blackResponse = await fetch('/black-stone.svg'); if (blackResponse.ok) { blackStoneSvg = await blackResponse.text(); } else { console.error('Failed to load black stone SVG:', blackResponse.status); } // Fetch white stone SVG const whiteResponse = await fetch('/white-stone.svg'); if (whiteResponse.ok) { whiteStoneSvg = await whiteResponse.text(); } else { console.error('Failed to load white stone SVG:', whiteResponse.status); } } catch (error) { console.error('Error pre-loading stone SVGs:', error); } } // Pre-load the SVGs immediately preloadStoneSvgs(); // Create bouncing stone animation without anime.js function createBouncingStonesAnimation(whiteCount = 20, blackCount = 20) { const gameBoard = document.getElementById('game-board'); if (!gameBoard) return; // Clear any existing animation elements const existingAnimations = gameBoard.querySelectorAll('.animation-stone'); existingAnimations.forEach((stone) => stone.remove()); const boardRect = gameBoard.getBoundingClientRect(); const stones = []; // Create stone elements with pre-loaded SVG content function createStones() { for (let i = 0; i < whiteCount + blackCount; i++) { const stone = document.createElement('div'); stone.className = 'animation-stone'; stone.style.position = 'absolute'; stone.style.width = '30px'; stone.style.height = '30px'; stone.style.pointerEvents = 'none'; stone.style.zIndex = '1000'; stone.style.opacity = '1'; // Set the pre-loaded SVG content as innerHTML stone.innerHTML = i < whiteCount ? whiteStoneSvg : blackStoneSvg; // Make sure the SVG inside fills the container const svg = stone.querySelector('svg'); if (svg) { svg.style.width = '100%'; svg.style.height = '100%'; } // Random starting position within the board const startX = Math.random() * (boardRect.width - 30); const startY = Math.random() * (boardRect.height - 30); stone.style.left = startX + 'px'; stone.style.top = startY + 'px'; gameBoard.appendChild(stone); stones.push({ element: stone, x: startX, y: startY, vx: (Math.random() - 0.5) * 8, // Random velocity between -4 and 4 vy: (Math.random() - 0.5) * 8, }); } } // Create the stones (no need to wait since we're using pre-loaded content) createStones(); // Animation state let animationId; let startTime; let isPlaying = false; // Animation function function animateStones() { stones.forEach((stone) => { // Update position stone.x += stone.vx; stone.y += stone.vy; // Bounce off edges if (stone.x <= 0 || stone.x >= boardRect.width - 30) { stone.vx = -stone.vx; stone.x = Math.max(0, Math.min(boardRect.width - 30, stone.x)); } if (stone.y <= 0 || stone.y >= boardRect.height - 30) { stone.vy = -stone.vy; stone.y = Math.max(0, Math.min(boardRect.height - 30, stone.y)); } // Apply position stone.element.style.left = stone.x + 'px'; stone.element.style.top = stone.y + 'px'; }); } // Fade out function function fadeOut() { const animationStones = document.querySelectorAll('.animation-stone'); const fadeStartTime = performance.now(); const fadeDuration = 500; function fade(currentTime) { const elapsed = currentTime - fadeStartTime; const progress = Math.min(elapsed / fadeDuration, 1); const opacity = 1 - progress; animationStones.forEach((stone) => { stone.style.opacity = opacity; }); if (progress < 1) { requestAnimationFrame(fade); } else { // Remove stones after fade is complete animationStones.forEach((stone) => stone.remove()); } } requestAnimationFrame(fade); } // Main animation loop function animate(currentTime) { if (!startTime) startTime = currentTime; const elapsed = currentTime - startTime; // Wait 15s to fade away if (elapsed < 15000) { animateStones(); animationId = requestAnimationFrame(animate); } else { // Animation complete - start fade out fadeOut(); } } // Animation control object const animationControl = { play: function () { if (!isPlaying) { isPlaying = true; startTime = null; // Reset start time animationId = requestAnimationFrame(animate); } }, pause: function () { if (animationId) { cancelAnimationFrame(animationId); isPlaying = false; } }, stop: function () { if (animationId) { cancelAnimationFrame(animationId); isPlaying = false; } // Remove all stones immediately const animationStones = document.querySelectorAll('.animation-stone'); animationStones.forEach((stone) => stone.remove()); }, }; return animationControl; } const animations = {}; // We use functions so that we have time to pre-load the svgs before trying to create the animations // which isn't actually ideal because what if the animation plays right away, but, it works! animations['black-victory'] = () => { return createBouncingStonesAnimation(0, 40); }; animations['white-victory'] = () => { return createBouncingStonesAnimation(40, 0); }; animations['draw'] = () => { return createBouncingStonesAnimation(20, 20); }; document.addEventListener('htmx:wsAfterMessage', async function (e) { let msg; try { msg = JSON.parse(e.detail.message); } catch (_) { return; } if (msg.type !== 'animation') { return; } const animationFn = animations[msg.animation]; if (animationFn) { animationFn().play(); } else { console.error('Unknown animation:', msg.animation); } });