From b2827329a32d3fe627a62bd4ff4191b2a3e4407f Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Tue, 26 May 2026 00:33:22 -0700 Subject: redesign: frutiger aero faux-OS desktop replaces the old enter/index pages with a draggable-windows desktop metaphor. wires last.fm now-playing, films.tylerhoang.xyz diary, and a php visitor counter; keeps background music via /mus/mmt.mp3. Co-Authored-By: Claude Opus 4.7 --- aero.js | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 aero.js (limited to 'aero.js') diff --git a/aero.js b/aero.js new file mode 100644 index 0000000..e97e376 --- /dev/null +++ b/aero.js @@ -0,0 +1,179 @@ +// Shared Aero JS: bubbles, sparkle cursor, music toggle, drag windows, 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 hue = 30 + Math.random() * 200; + s.style.background = `radial-gradient(circle, oklch(95% 0.12 ${hue}) 0%, oklch(85% 0.18 ${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))'; + }); +} + +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; +} + +window.Aero = { spawnBubbles, makeClouds, sparkleCursor, makeDraggable, counterHTML, nowPlayingHTML, animateEq, musicToggleHTML, bindMusicToggle, fetchLastFm, fetchFilms, fetchVisitorCount }; -- cgit v1.3-2-g0d8e