From d265acea43791c621884b551d3fbd999057bd3d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Apr 2026 23:48:41 +0000 Subject: [PATCH] feat: badge click-reveal with animated transitions Agent-Logs-Url: https://github.com/nitrocode/token-deathclock/sessions/5b4b30d5-9d87-4907-bc9e-1f8e0cdb8c15 Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- src/js/14-badges.js | 69 ++++++++++++++++++++++++++++++ styles/features.css | 100 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+) diff --git a/src/js/14-badges.js b/src/js/14-badges.js index 57b3102..d327f35 100644 --- a/src/js/14-badges.js +++ b/src/js/14-badges.js @@ -34,6 +34,13 @@ const LS_BADGES_KEY = 'tokenDeathclockBadges'; const LS_VISITS_KEY = 'tokenDeathclockVisits'; + const BADGE_ANIMS = ['flip-x', 'flip-y', 'zoom-spin', 'bounce', 'glitch']; + const ICON_INFO = '\u2139\uFE0F'; // â„šī¸ + const ICON_SEARCH = '\uD83D\uDD0D'; // 🔍 + const T_ANIM_OUT = 310; // ms — must be â‰Ĩ longest out-animation duration + const T_HOLD = 2600; // ms — info stays visible + const T_ANIM_IN = 380; // ms — must be â‰Ĩ longest in-animation duration + let earnedBadges = new Set(); const toastQueue = []; let toastActive = false; @@ -73,6 +80,66 @@ if (hour >= 0 && hour < 4) awardBadge('nocturnal_doomer'); } + function getBadgeRevealText(def) { + const earned = earnedBadges.has(def.id); + if (earned) return def.desc; + if (def.type === 'time') { + const s = def.threshold; + const minutes = Math.round(s / 60); + return s < 60 + ? `Stay on the page for ${s} seconds` + : `Stay on the page for ${minutes} minute${minutes !== 1 ? 's' : ''}`; + } + if (def.type === 'easter') return '\uD83E\uDD2B Keep exploring\u2026'; + return def.desc; + } + + function handleBadgeClick(def, el) { + if (el.dataset.animating === '1') return; + el.dataset.animating = '1'; + + const anim = BADGE_ANIMS[Math.floor(Math.random() * BADGE_ANIMS.length)]; + el.dataset.anim = anim; + + const earned = earnedBadges.has(def.id); + const iconEl = el.querySelector('.badge-icon'); + const nameEl = el.querySelector('.badge-name'); + if (!iconEl || !nameEl) { el.dataset.animating = '0'; return; } + + const origIcon = iconEl.textContent; + const origName = nameEl.textContent; + + // Phase 1 — animate out + el.classList.add('badge-anim-out'); + setTimeout(() => { + // Swap in info content + iconEl.textContent = earned ? ICON_INFO : ICON_SEARCH; + nameEl.textContent = getBadgeRevealText(def); + el.classList.remove('badge-anim-out'); + el.classList.add('badge-showing-info', 'badge-anim-in'); + + setTimeout(() => { + // Phase 2 — animate out again before restoring + el.classList.remove('badge-anim-in'); + el.classList.add('badge-anim-out'); + + setTimeout(() => { + // Restore original content + iconEl.textContent = origIcon; + nameEl.textContent = origName; + el.classList.remove('badge-anim-out', 'badge-showing-info'); + el.classList.add('badge-anim-in'); + + setTimeout(() => { + el.classList.remove('badge-anim-in'); + delete el.dataset.anim; + el.dataset.animating = '0'; + }, T_ANIM_IN); + }, T_ANIM_OUT); + }, T_HOLD); + }, T_ANIM_OUT); + } + function renderBadgesGrid() { const grid = document.getElementById('badges-grid'); if (!grid) return; @@ -90,6 +157,7 @@ div.innerHTML = ` ${escHtml(def.name)}`; + div.addEventListener('click', () => handleBadgeClick(def, div)); grid.appendChild(div); }); } @@ -98,6 +166,7 @@ BADGE_DEFS.forEach((def) => { const el = document.getElementById('badge-' + def.id); if (!el) return; + if (el.dataset.animating === '1') return; // don't interrupt a running animation const earned = earnedBadges.has(def.id); el.className = 'badge-item ' + (earned ? 'earned' : 'locked'); const iconEl = el.querySelector('.badge-icon'); diff --git a/styles/features.css b/styles/features.css index f8771f0..2c28233 100644 --- a/styles/features.css +++ b/styles/features.css @@ -333,7 +333,9 @@ border-radius: 0.75rem; padding: 1rem 0.75rem; text-align: center; + cursor: pointer; transition: transform 0.15s, box-shadow 0.2s; + will-change: transform, opacity; } .badge-item.earned { @@ -366,6 +368,102 @@ } .badge-item.earned .badge-name { color: var(--accent-2); } +/* ---- Badge click-reveal state ---- */ +.badge-item.badge-showing-info { + border-color: var(--accent-3); + background: var(--surface-2); + box-shadow: 0 0 18px rgba(120, 200, 255, 0.22); +} +.badge-item.badge-showing-info .badge-icon { + font-size: 1.3rem; +} +.badge-item.badge-showing-info .badge-name { + font-family: inherit; + font-size: 0.6rem; + color: var(--text); + letter-spacing: 0; + line-height: 1.4; + word-break: break-word; +} +.badge-item.badge-anim-out, +.badge-item.badge-anim-in { + pointer-events: none; +} + +/* ---- Badge animation keyframes ---- */ + +/* flip-x */ +@keyframes bdgFlipXOut { + from { transform: perspective(600px) rotateX(0deg); opacity: 1; } + to { transform: perspective(600px) rotateX(90deg); opacity: 0; } +} +@keyframes bdgFlipXIn { + from { transform: perspective(600px) rotateX(-90deg); opacity: 0; } + to { transform: perspective(600px) rotateX(0deg); opacity: 1; } +} +.badge-item[data-anim="flip-x"].badge-anim-out { animation: bdgFlipXOut 0.3s ease forwards; } +.badge-item[data-anim="flip-x"].badge-anim-in { animation: bdgFlipXIn 0.35s ease forwards; } + +/* flip-y */ +@keyframes bdgFlipYOut { + from { transform: perspective(600px) rotateY(0deg); opacity: 1; } + to { transform: perspective(600px) rotateY(90deg); opacity: 0; } +} +@keyframes bdgFlipYIn { + from { transform: perspective(600px) rotateY(-90deg); opacity: 0; } + to { transform: perspective(600px) rotateY(0deg); opacity: 1; } +} +.badge-item[data-anim="flip-y"].badge-anim-out { animation: bdgFlipYOut 0.3s ease forwards; } +.badge-item[data-anim="flip-y"].badge-anim-in { animation: bdgFlipYIn 0.35s ease forwards; } + +/* zoom-spin */ +@keyframes bdgZoomSpinOut { + from { transform: scale(1) rotate(0deg); opacity: 1; } + to { transform: scale(0) rotate(180deg); opacity: 0; } +} +@keyframes bdgZoomSpinIn { + from { transform: scale(0) rotate(-180deg); opacity: 0; } + 70% { transform: scale(1.12) rotate(5deg); opacity: 1; } + to { transform: scale(1) rotate(0deg); opacity: 1; } +} +.badge-item[data-anim="zoom-spin"].badge-anim-out { animation: bdgZoomSpinOut 0.3s ease-in forwards; } +.badge-item[data-anim="zoom-spin"].badge-anim-in { animation: bdgZoomSpinIn 0.38s ease-out forwards; } + +/* bounce */ +@keyframes bdgBounceOut { + 0% { transform: scale(1); opacity: 1; } + 35% { transform: scale(1.2); opacity: 1; } + 100% { transform: scale(0); opacity: 0; } +} +@keyframes bdgBounceIn { + 0% { transform: scale(0); opacity: 0; } + 55% { transform: scale(1.15); opacity: 1; } + 75% { transform: scale(0.95); } + 100% { transform: scale(1); opacity: 1; } +} +.badge-item[data-anim="bounce"].badge-anim-out { animation: bdgBounceOut 0.3s ease-in forwards; } +.badge-item[data-anim="bounce"].badge-anim-in { animation: bdgBounceIn 0.38s ease-out forwards; } + +/* glitch */ +@keyframes bdgGlitchOut { + 0% { transform: translate(0); opacity: 1; filter: none; } + 20% { transform: translate(-3px, 2px) skewX(-5deg); opacity: 0.8; filter: hue-rotate(90deg); } + 40% { transform: translate( 3px, -1px) skewX( 5deg); opacity: 0.6; filter: hue-rotate(180deg); } + 60% { transform: translate(-2px, 3px); opacity: 0.4; filter: hue-rotate(270deg); } + 80% { transform: translate( 2px, -3px) skewX(-3deg); opacity: 0.2; } + 100% { transform: translate(0); opacity: 0; filter: none; } +} +@keyframes bdgGlitchIn { + 0% { transform: translate( 3px, -2px); opacity: 0; filter: hue-rotate(270deg); } + 20% { transform: translate(-3px, 1px) skewX( 4deg); opacity: 0.4; filter: hue-rotate(180deg); } + 40% { transform: translate( 2px, -3px) skewX(-4deg); opacity: 0.7; filter: hue-rotate(90deg); } + 60% { transform: translate(-1px, 2px); opacity: 0.9; filter: hue-rotate(20deg); } + 80% { transform: translate( 1px, -1px); } + 100% { transform: translate(0); opacity: 1; filter: none; } +} +.badge-item[data-anim="glitch"].badge-anim-out { animation: bdgGlitchOut 0.3s steps(5) forwards; } +.badge-item[data-anim="glitch"].badge-anim-in { animation: bdgGlitchIn 0.35s steps(5) forwards; } + /* ---- Share Doom Panel (floating) ---- */ .share-doom-panel { position: fixed; @@ -579,5 +677,7 @@ .modal-overlay, .receipt-card, .share-doom-options, .ai-ticker-text { transition: none; animation: none; } + .badge-item.badge-anim-out, + .badge-item.badge-anim-in { animation: none; opacity: 1; transform: none; filter: none; } }