aboutsummaryrefslogtreecommitdiff
path: root/aero.js
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-26 00:33:22 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-26 00:33:22 -0700
commitb2827329a32d3fe627a62bd4ff4191b2a3e4407f (patch)
tree2d5ef329e579340d1e6b64b179a8d723c1da9b54 /aero.js
parent1c9ad19a5ff2e3c1463ba34ee5d707b93ebf8960 (diff)
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 <noreply@anthropic.com>
Diffstat (limited to 'aero.js')
-rw-r--r--aero.js179
1 files changed, 179 insertions, 0 deletions
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 = `<svg viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice" style="width:100%;height:100%;">
+ <defs>
+ <radialGradient id="cg" cx="50%" cy="40%" r="60%">
+ <stop offset="0%" stop-color="white" stop-opacity="0.95"/>
+ <stop offset="55%" stop-color="white" stop-opacity="0.6"/>
+ <stop offset="100%" stop-color="white" stop-opacity="0"/>
+ </radialGradient>
+ </defs>
+ <g fill="url(#cg)">
+ <ellipse cx="180" cy="200" rx="260" ry="50"/>
+ <ellipse cx="900" cy="120" rx="320" ry="55"/>
+ <ellipse cx="1300" cy="320" rx="220" ry="42"/>
+ <ellipse cx="500" cy="640" rx="380" ry="60" opacity="0.7"/>
+ <ellipse cx="1150" cy="720" rx="260" ry="48" opacity="0.7"/>
+ </g>
+ </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 => `<div class="d">${d}</div>`).join('');
+ return `<div class="counter"><div class="digits">${digits}</div><div class="label">${label}</div></div>`;
+}
+
+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 `<div style="display:flex;gap:12px;align-items:center">
+ <div style="width:${sz}px;height:${sz}px;border-radius:10px;background:${bgStyle};box-shadow:inset 0 1px 0 rgba(255,255,255,0.5),0 3px 8px rgba(0,0,0,0.2);position:relative;overflow:hidden">
+ <div style="position:absolute;inset:0;background:linear-gradient(to bottom,rgba(255,255,255,0.4),transparent 40%)"></div>
+ </div>
+ <div style="flex:1;min-width:0">
+ <div style="font-size:10px;color:oklch(40% 0.10 240);text-transform:uppercase;letter-spacing:1px">${header}</div>
+ <div style="font-size:${compact?13:15}px;font-weight:700;color:oklch(25% 0.05 240)">${title}</div>
+ <div style="font-size:${compact?11:13}px;color:oklch(35% 0.04 240)">${artist}${album ? ' — ' + album : ''}</div>
+ <div class="eq" style="display:flex;gap:2px;align-items:flex-end;height:12px;margin-top:4px">
+ ${[0,1,2,3,4].map(i => `<div data-i="${i}" style="width:3px;height:6px;border-radius:1px;background:linear-gradient(to top,oklch(60% 0.15 240),oklch(80% 0.14 60));transition:height 280ms"></div>`).join('')}
+ </div>
+ </div>
+ </div>`;
+}
+
+function animateEq(root) {
+ setInterval(() => {
+ root.querySelectorAll('.eq div').forEach(d => {
+ d.style.height = (3 + Math.random() * 10) + 'px';
+ });
+ }, 280);
+}
+
+function musicToggleHTML() {
+ return `<div class="music-toggle" style="display:inline-flex;align-items:center;gap:8px;padding:6px 12px 6px 6px;border-radius:999px;background:linear-gradient(to bottom,rgba(255,255,255,0.78),rgba(200,225,255,0.55));border:1px solid rgba(255,255,255,0.85);box-shadow:inset 0 1px 0 rgba(255,255,255,0.9),0 3px 10px rgba(80,130,180,0.25);font-family:'Segoe UI',Tahoma,sans-serif;font-size:12px;color:oklch(30% 0.05 240)">
+ <button class="mt-btn no-drag" style="width:26px;height:26px;border-radius:50%;border:1px solid oklch(45% 0.13 240);background:radial-gradient(circle at 30% 25%,white,oklch(82% 0.12 220) 60%,oklch(50% 0.13 240));color:white;cursor:pointer;font-size:11px;padding:0;box-shadow:inset 0 1px 0 rgba(255,255,255,0.9)">▶</button>
+ <span class="mt-label" style="min-width:120px">music off</span>
+ </div>`;
+}
+
+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 };