/* WOAH! — Iteration 2 extras: CustomCursor, MusicPlayer, VideoPlayer, GalleryV2 */ const { useState: u2State, useEffect: u2Effect, useRef: u2Ref, useCallback: u2CB } = React; /* ============================================================ CACHE BUSTING — change this number when you update any asset to force browsers to re-download images/audio. How to use: - TopMarquee dynamic nav-height + hero padding 8rem? Bump v from 52 to 53. - Replaced okami-night.mp3? Bump again. - Just save & re-upload — visitors will see the new version. ============================================================ */ window.ASSET_VERSION = '118'; window.v = (path) => `${path}?v=${window.ASSET_VERSION}`; const v = window.v; /* ---------- Custom Cursor ---------- */ const CustomCursor = () => { const dotRef = u2Ref(null); const haloRef = u2Ref(null); const trailLayerRef = u2Ref(null); const stateRef = u2Ref({ x: -100, y: -100, hx: -100, hy: -100, lastTrail: 0, hovering: false, color: '#00F0FF' }); const rafRef = u2Ref(0); u2Effect(() => { if (window.matchMedia('(hover: none)').matches) return; const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; document.body.classList.add('cursor-on'); const onMove = (e) => { stateRef.current.x = e.clientX; stateRef.current.y = e.clientY; }; const onDown = (e) => { const ring = document.createElement('div'); ring.className = 'cur-pulse'; ring.style.left = e.clientX + 'px'; ring.style.top = e.clientY + 'px'; document.body.appendChild(ring); setTimeout(() => ring.remove(), 500); }; const onOver = (e) => { const t = e.target; const interactive = t.closest && t.closest('a,button,input,textarea,[role=button],[data-cursor]'); stateRef.current.hovering = !!interactive; if (interactive) { const c = interactive.getAttribute('data-cursor-color') || '#00F0FF'; stateRef.current.color = c; } else { stateRef.current.color = '#00F0FF'; } }; window.addEventListener('mousemove', onMove, { passive: true }); window.addEventListener('mousedown', onDown); window.addEventListener('mouseover', onOver); const tick = (t) => { const s = stateRef.current; s.hx += (s.x - s.hx) * 0.18; s.hy += (s.y - s.hy) * 0.18; if (dotRef.current) { dotRef.current.style.transform = `translate3d(${s.x}px, ${s.y}px, 0) translate(-50%,-50%) scale(${s.hovering ? 1.9 : 1})`; dotRef.current.style.background = s.color; dotRef.current.style.boxShadow = `0 0 14px ${s.color}, 0 0 28px ${s.color}`; } if (haloRef.current) { haloRef.current.style.transform = `translate3d(${s.hx}px, ${s.hy}px, 0) translate(-50%,-50%) scale(${s.hovering ? 1.7 : 1})`; haloRef.current.style.borderColor = s.color + '66'; haloRef.current.style.boxShadow = `0 0 28px ${s.color}55, inset 0 0 14px ${s.color}33`; } if (!reduced && trailLayerRef.current && t - s.lastTrail > 32) { s.lastTrail = t; const dot = document.createElement('span'); dot.className = 'cur-trail'; dot.style.left = s.x + 'px'; dot.style.top = s.y + 'px'; dot.style.background = s.color; trailLayerRef.current.appendChild(dot); setTimeout(() => dot.remove(), 600); } rafRef.current = requestAnimationFrame(tick); }; rafRef.current = requestAnimationFrame(tick); return () => { cancelAnimationFrame(rafRef.current); window.removeEventListener('mousemove', onMove); window.removeEventListener('mousedown', onDown); window.removeEventListener('mouseover', onOver); document.body.classList.remove('cursor-on'); }; }, []); return ReactDOM.createPortal( <>
>, document.body ); }; /* ---------- Music Player (in nav) ---------- */ const MusicPlayer = () => { const [playing, setPlaying] = u2State(false); const [muted, setMuted] = u2State(false); const [volume, setVolume] = u2State(0.2); // 0–1 (default 20%) const [src] = u2State(v('assets/okami-night.mp3')); const [trackName] = u2State('Ōkami Night'); const [trackJp] = u2State('狼の夜'); const [time, setTime] = u2State(0); const [bars, setBars] = u2State(() => Array.from({length:22}, ()=>0.3)); const audioRef = u2Ref(null); const barTimerRef = u2Ref(0); u2Effect(()=>{ if (playing) { barTimerRef.current = setInterval(()=>{ setBars(prev => prev.map((_,i)=> 0.25 + Math.random()*0.75 )); }, 110); } else { clearInterval(barTimerRef.current); setBars(prev => prev.map((_,i)=> 0.18 + Math.sin((Date.now()/600)+i)*0.05 )); } return ()=> clearInterval(barTimerRef.current); }, [playing]); // Apply volume to the audio element whenever it changes u2Effect(()=>{ if (audioRef.current) audioRef.current.volume = volume; }, [volume]); // hardcoded track const toggle = () => { if (!audioRef.current || !src) return; if (playing) audioRef.current.pause(); else audioRef.current.play().catch(()=>{}); }; const fmt = (t)=> { const m = Math.floor(t/60), s = Math.floor(t%60); return `${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; }; // Toggle mute on click; unmuting restores prior volume on the audio element const toggleMute = () => { setMuted(m => { const next = !m; if (audioRef.current) audioRef.current.muted = next; return next; }); }; // Volume display: muted always shows 0 const displayVol = muted ? 0 : volume; // Pick icon variant based on level const volIcon = muted || volume === 0 ? 'volumeOff' : volume < 0.5 ? 'volumeLow' : 'volume'; return (