/** * Pivoine.art - Main JavaScript * Audio blog with WebGL visualizers */ import { Visualizer } from './visualizer/scene.js'; import { ReactiveLogo } from './logo/reactive-logo.js'; // Audio Manager - Web Audio API wrapper class AudioManager { constructor() { this.audio = document.createElement('audio'); this.audio.crossOrigin = 'anonymous'; this.audioContext = null; this.analyser = null; this.source = null; this.frequencyData = null; this.isInitialized = false; this.tracks = []; this.autoplayEnabled = true; } async fetchTracks() { if (this.tracks.length > 0) return this.tracks; try { const response = await fetch('/tracks/index.json'); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); this.tracks = data.tracks || []; } catch (e) { console.error('Failed to fetch tracks:', e); this.tracks = []; } return this.tracks; } getRandomTrack(excludeUrl = null) { const available = this.tracks.filter((t) => t.audio !== excludeUrl); if (available.length === 0) return null; return available[Math.floor(Math.random() * available.length)]; } async playRandomTrack() { await this.fetchTracks(); const currentUrl = window.Alpine?.store('audio')?.currentTrack?.url; const nextTrack = this.getRandomTrack(currentUrl); if (nextTrack) { // Store pending track info for auto-play on page load sessionStorage.setItem( 'pivoine-autoplay', JSON.stringify({ title: nextTrack.title, url: nextTrack.audio, image: nextTrack.image }) ); // Navigate to the new track page window.location.href = nextTrack.url; } } async init() { if (this.isInitialized) return; try { this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); this.analyser = this.audioContext.createAnalyser(); this.analyser.fftSize = 512; this.analyser.smoothingTimeConstant = 0.8; this.source = this.audioContext.createMediaElementSource(this.audio); this.source.connect(this.analyser); this.analyser.connect(this.audioContext.destination); this.frequencyData = new Uint8Array(this.analyser.frequencyBinCount); this.isInitialized = true; // Update Alpine store on audio events this.audio.addEventListener('timeupdate', () => { if (window.Alpine) { Alpine.store('audio').progress = this.audio.currentTime; Alpine.store('audio').duration = this.audio.duration || 0; } }); this.audio.addEventListener('ended', () => { if (window.Alpine) { Alpine.store('audio').isPlaying = false; } // Auto-play next random track if (this.autoplayEnabled) { this.playRandomTrack(); } }); this.audio.addEventListener('play', () => { if (window.Alpine) { Alpine.store('audio').isPlaying = true; } }); this.audio.addEventListener('pause', () => { if (window.Alpine) { Alpine.store('audio').isPlaying = false; } }); } catch (e) { console.error('Failed to initialize audio context:', e); } } async play(url) { await this.init(); if (this.audioContext?.state === 'suspended') { await this.audioContext.resume(); } if (url && url !== this.audio.src) { this.audio.src = url; } await this.audio.play(); } pause() { this.audio.pause(); } toggle() { if (this.audio.paused) { this.audio.play(); } else { this.audio.pause(); } } seek(time) { this.audio.currentTime = time; } setVolume(v) { this.audio.volume = Math.max(0, Math.min(1, v)); } getFrequencyData() { if (this.analyser) { this.analyser.getByteFrequencyData(this.frequencyData); } return this.frequencyData; } getFrequencyBands() { const data = this.getFrequencyData(); if (!data) return { low: 0, mid: 0, high: 0 }; const len = data.length; const low = this._avg(data, 0, len * 0.1) / 255; const mid = this._avg(data, len * 0.1, len * 0.5) / 255; const high = this._avg(data, len * 0.5, len) / 255; return { low, mid, high }; } _avg(data, start, end) { let sum = 0; for (let i = Math.floor(start); i < Math.floor(end); i++) { sum += data[i]; } return sum / (end - start); } } // Initialize global instances if (!window.__pivoine) { const audioManager = new AudioManager(); window.__pivoine = { audioManager, visualizer: null, logo: null }; // Check for auto-play from shuffle/random track const checkAutoplay = () => { const autoplayData = sessionStorage.getItem('pivoine-autoplay'); if (autoplayData) { sessionStorage.removeItem('pivoine-autoplay'); try { const track = JSON.parse(autoplayData); // Update Alpine store if (window.Alpine) { Alpine.store('audio').currentTrack = track; } // Start playback audioManager.play(track.url); } catch (e) { console.error('Failed to auto-play track:', e); } } }; // Run autoplay check after Alpine is ready document.addEventListener('alpine:initialized', checkAutoplay); // Fallback if Alpine is already initialized if (window.Alpine) { setTimeout(checkAutoplay, 100); } // Initialize WebGL components after DOM is ready const initWebGL = () => { // Main visualizer (fullscreen background) const visualizerCanvas = document.getElementById('visualizer'); if (visualizerCanvas && !window.__pivoine.visualizer) { window.__pivoine.visualizer = new Visualizer(visualizerCanvas, audioManager); } // Logo in header const logoCanvas = document.getElementById('logo-canvas'); if (logoCanvas && !window.__pivoine.logo) { window.__pivoine.logo = new ReactiveLogo(logoCanvas, audioManager); } }; // Initialize on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initWebGL); } else { initWebGL(); } } // Alpine.js initialization document.addEventListener('alpine:init', () => { // Global audio store Alpine.store('audio', { currentTrack: null, isPlaying: false, progress: 0, duration: 0, volume: 0.8 }); // Main app component Alpine.data('app', () => ({ init() { // Restore volume from localStorage const savedVolume = localStorage.getItem('pivoine-volume'); if (savedVolume) { Alpine.store('audio').volume = parseFloat(savedVolume); window.__pivoine.audioManager.setVolume(parseFloat(savedVolume)); } } })); // Player UI component Alpine.data('playerUI', () => ({ togglePlay() { window.__pivoine.audioManager.toggle(); }, seek(time) { window.__pivoine.audioManager.seek(parseFloat(time)); }, setVolume(v) { const volume = parseFloat(v); Alpine.store('audio').volume = volume; window.__pivoine.audioManager.setVolume(volume); localStorage.setItem('pivoine-volume', volume); }, toggleMute() { const store = Alpine.store('audio'); if (store.volume > 0) { this._previousVolume = store.volume; this.setVolume(0); } else { this.setVolume(this._previousVolume || 0.8); } }, formatTime(seconds) { if (!seconds || isNaN(seconds)) return '0:00'; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; }, _previousVolume: 0.8 })); }); // htmx lifecycle hooks document.body.addEventListener('htmx:beforeSwap', () => { // Pause visualizer during page transition for performance window.__pivoine?.visualizer?.pause(); }); document.body.addEventListener('htmx:afterSwap', () => { // Resume visualizer after swap window.__pivoine?.visualizer?.resume(); // Re-initialize scroll animations window.dispatchEvent(new CustomEvent('page:loaded')); }); // Scroll animations with Intersection Observer function initScrollAnimations() { const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('is-visible'); } }); }, { threshold: 0.1, rootMargin: '0px 0px -50px 0px' } ); document.querySelectorAll('.fade-in-up').forEach((el) => { observer.observe(el); }); } // Initialize on page load document.addEventListener('DOMContentLoaded', initScrollAnimations); window.addEventListener('page:loaded', initScrollAnimations); export { AudioManager };