// Shared Aero JS: bubbles, sparkle cursor, music toggle, drag windows, theme switcher, helpers. function spawnBubbles(host, count = 22) { const f = document.createElement('div'); f.className = 'bubble-field'; for (let i = 0; i < count; i++) { const b = document.createElement('div'); b.className = 'bubble'; const size = 18 + Math.random() * 90; b.style.width = b.style.height = size + 'px'; b.style.left = Math.random() * 100 + '%'; b.style.setProperty('--drift', ((Math.random() - 0.5) * 120) + 'px'); b.style.animationDuration = (14 + Math.random() * 16) + 's'; b.style.animationDelay = (-Math.random() * 20) + 's'; f.appendChild(b); } host.appendChild(f); } function makeClouds(host) { const svg = ` `; const w = document.createElement('div'); w.className = 'clouds'; w.innerHTML = svg; host.appendChild(w); } function sparkleCursor() { let last = 0; window.addEventListener('mousemove', (e) => { const now = performance.now(); if (now - last < 40) return; last = now; const s = document.createElement('div'); s.className = 'sparkle'; s.style.left = e.clientX + 'px'; s.style.top = e.clientY + 'px'; const theme = document.body.dataset.theme; // chrome: iridescent / holographic palette (cyan→magenta→pink) // aero: warm-cool rainbow as before const hue = theme === 'chrome' ? 200 + Math.random() * 160 : 30 + Math.random() * 200; const chroma = theme === 'chrome' ? 0.22 : 0.18; s.style.background = `radial-gradient(circle, oklch(96% 0.10 ${hue}) 0%, oklch(82% ${chroma} ${hue} / 0.6) 40%, transparent 70%)`; document.body.appendChild(s); setTimeout(() => s.remove(), 900); }); } function makeDraggable(el, handle) { let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false; (handle || el).addEventListener('mousedown', (e) => { if (e.target.closest('.no-drag')) return; dragging = true; sx = e.clientX; sy = e.clientY; const r = el.getBoundingClientRect(); const p = el.offsetParent.getBoundingClientRect(); ox = r.left - p.left; oy = r.top - p.top; el.style.zIndex = (++window.__zTop || (window.__zTop = 100)); e.preventDefault(); }); window.addEventListener('mousemove', (e) => { if (!dragging) return; el.style.left = (ox + e.clientX - sx) + 'px'; el.style.top = (oy + e.clientY - sy) + 'px'; }); window.addEventListener('mouseup', () => { dragging = false; }); } function counterHTML(val = 42137, label = "visitors") { const digits = String(val).padStart(7, '0').split('').map(d => `
${d}
`).join(''); return `
${digits}
${label}
`; } function nowPlayingHTML(compact, track) { const sz = compact ? 48 : 72; const hasTrack = track && track.name; const title = hasTrack ? track.name : 'Naima'; const artist = hasTrack ? track.artist : 'John Coltrane'; const album = hasTrack ? (track.album || '') : 'Giant Steps'; let header = '♪ now playing · last.fm'; if (hasTrack && !track.nowplaying) { const now = Date.now(); const ago = Math.floor((now - track.when) / 1000); const mins = Math.floor(ago / 60); header = `♪ last played ${mins}m ago`; } let bgStyle = `linear-gradient(135deg,oklch(75% 0.16 55),oklch(60% 0.18 30) 50%,oklch(45% 0.12 280))`; if (hasTrack && track.art) { const safeArt = track.art.replace(/'/g, "%27").replace(/"/g, "%22"); bgStyle = `url('${safeArt}') center / cover`; } return `
${header}
${title}
${artist}${album ? ' — ' + album : ''}
${[0,1,2,3,4].map(i => `
`).join('')}
`; } function animateEq(root) { setInterval(() => { root.querySelectorAll('.eq div').forEach(d => { d.style.height = (3 + Math.random() * 10) + 'px'; }); }, 280); } function musicToggleHTML() { return `
music off
`; } function bindMusicToggle(root) { const btn = root.querySelector('.mt-btn'); const lbl = root.querySelector('.mt-label'); if (!btn) return; let on = false; btn.addEventListener('click', () => { on = !on; btn.textContent = on ? '❚❚' : '▶'; lbl.textContent = on ? '♪ kero kero bonito — flamingo' : 'music off'; btn.style.background = on ? 'radial-gradient(circle at 30% 25%,white,oklch(75% 0.14 55) 60%,oklch(55% 0.15 35))' : 'radial-gradient(circle at 30% 25%,white,oklch(82% 0.12 220) 60%,oklch(50% 0.13 240))'; }); } // THEME SWITCHER — Aero (Frutiger) ↔ Chrome (Y2K) const THEME_KEY = 'tyler.theme'; const THEMES = ['aero', 'chrome']; function getTheme() { const t = localStorage.getItem(THEME_KEY); return THEMES.includes(t) ? t : 'aero'; } function setTheme(t) { if (!THEMES.includes(t)) t = 'aero'; document.body.setAttribute('data-theme', t); localStorage.setItem(THEME_KEY, t); document.querySelectorAll('.theme-switcher button').forEach(b => { b.classList.toggle('active', b.dataset.theme === t); }); try { window.dispatchEvent(new CustomEvent('themechange', { detail: t })); } catch(_){} } function mountThemeSwitcher(host) { host = host || document.body; const wrap = document.createElement('div'); wrap.className = 'theme-switcher no-drag'; wrap.innerHTML = THEMES.map(t => ` `).join(''); host.appendChild(wrap); wrap.querySelectorAll('button').forEach(b => { b.addEventListener('click', () => setTheme(b.dataset.theme)); }); setTheme(getTheme()); return wrap; } function initTheme() { document.body.setAttribute('data-theme', getTheme()); } // REAL API INTEGRATIONS async function fetchLastFm(user = 'trollshotlol', key = 'e4d5c973811037717f7603f616259cdf', limit = 4) { const url = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${user}&api_key=${key}&format=json&limit=${limit}`; const r = await fetch(url); if (!r.ok) throw new Error('lastfm ' + r.status); const j = await r.json(); const tracks = (j.recenttracks && j.recenttracks.track) || []; return tracks.map(t => ({ name: t.name, artist: t.artist && (t.artist['#text'] || t.artist.name), album: t.album && t.album['#text'], art: (t.image && t.image[t.image.length - 1] && t.image[t.image.length - 1]['#text']) || null, nowplaying: t['@attr'] && t['@attr'].nowplaying === 'true', when: t.date && t.date.uts ? Number(t.date.uts) * 1000 : null, })); } async function fetchFilms() { const r = await fetch('https://films.tylerhoang.xyz/tyler/api/recent'); if (!r.ok) throw new Error('films ' + r.status); return await r.json(); } async function fetchVisitorCount() { const r = await fetch('/counter.php'); if (!r.ok) throw new Error('counter ' + r.status); const j = await r.json(); return j.count; } async function fetchReelMouthFeed(limit = 6) { const r = await fetch(`/podcast.php?limit=${limit}`); if (!r.ok) throw new Error('podcast.php ' + r.status); return await r.json(); } window.Aero = { spawnBubbles, makeClouds, sparkleCursor, makeDraggable, counterHTML, nowPlayingHTML, animateEq, musicToggleHTML, bindMusicToggle, // theme api getTheme, setTheme, mountThemeSwitcher, initTheme, THEMES, // real api integrations fetchLastFm, fetchFilms, fetchVisitorCount, fetchReelMouthFeed, };