// 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 ``;
}
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,
};