diff options
Diffstat (limited to 'aero.js')
| -rw-r--r-- | aero.js | 179 |
1 files changed, 179 insertions, 0 deletions
@@ -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 }; |
