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 = `
${earned ? escHtml(def.icon) : '\uD83D\uDD12'}
${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; }
}