From 2c67db261624f1f46961897fc7bea04553940081 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Apr 2026 16:32:49 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20add=20float-up=20+N=20pops=20to=20a?= =?UTF-8?q?ll=20counters=20and=20=C3=9710=E2=81=BF=20exponent=20annotation?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/nitrocode/token-deathclock/sessions/7149edd6-90cc-4ef9-95e2-f12c11bbadd6 Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- src/js/00-state.js | 25 +++++++++++--- src/js/02-counter.js | 61 ++++++++++++++++++++++++++++------- styles/counter-milestones.css | 21 ++++++++++++ 3 files changed, 92 insertions(+), 15 deletions(-) diff --git a/src/js/00-state.js b/src/js/00-state.js index f1c6ba0..1b0e9bf 100644 --- a/src/js/00-state.js +++ b/src/js/00-state.js @@ -80,10 +80,27 @@ return BASE_TOKENS + (TOKENS_PER_SECOND / continuousGrowthRate) * (Math.exp(continuousGrowthRate * elapsed) - 1); } + // Map each power-of-ten threshold to a Unicode superscript suffix. + // Used by appendExp() and numFmt() to annotate large numbers so viewers + // can instantly see the order of magnitude without googling "quadrillion". + const EXP_SUFFIXES = [ + { threshold: 1e18, suffix: ' ×10¹⁸' }, + { threshold: 1e15, suffix: ' ×10¹⁵' }, + { threshold: 1e12, suffix: ' ×10¹²' }, + { threshold: 1e9, suffix: ' ×10⁹' }, + ]; + + // Append a ×10ⁿ exponent annotation to a formatted string for numbers ≥ 10⁹. + // Returns the string unchanged for smaller numbers. + function appendExp(n, text) { + const entry = EXP_SUFFIXES.find(e => n >= e.threshold); + return entry ? text + entry.suffix : text; + } + function numFmt(n) { - // Compact formatting for the big live counter - if (n >= 1e15) return (n / 1e15).toFixed(3) + ' Quadrillion'; - if (n >= 1e12) return (n / 1e12).toFixed(3) + ' Trillion'; - return formatTokenCount(n); + // Compact formatting for the big live counter, with ×10ⁿ annotation + if (n >= 1e15) return (n / 1e15).toFixed(3) + ' Quadrillion' + ' ×10¹⁵'; + if (n >= 1e12) return (n / 1e12).toFixed(3) + ' Trillion' + ' ×10¹²'; + return appendExp(n, formatTokenCount(n)); } diff --git a/src/js/02-counter.js b/src/js/02-counter.js index dbcc1c0..8841e66 100644 --- a/src/js/02-counter.js +++ b/src/js/02-counter.js @@ -3,22 +3,25 @@ // cloning and reversing on every animation frame in updateCounters). const REVERSED_RATE_SCHEDULE = [...RATE_SCHEDULE].reverse(); - let _lastTokenPop = 0; + // Per-counter throttle timestamps for the floating +N pop animations. + let _lastTokenPop = 0; // total counter + let _lastSessionPop = 0; // session counter + let _lastStatPop = 0; // impact stats + let _lastRatePop = 0; // rate counter (slower cadence) - function spawnTokenPop(ratePerSec) { - const totalEl = document.getElementById('totalCounter'); - if (!totalEl) return; - const container = totalEl.closest('.counter-box'); + // Spawn a floating "+N" element inside `container` that floats up and fades out. + // `cssClass` is appended to 'token-pop ' so colour variants can be applied via CSS. + function spawnPop(container, text, cssClass) { if (!container) return; const el = document.createElement('div'); - el.className = 'token-pop'; + el.className = 'token-pop' + (cssClass ? ' ' + cssClass : ''); el.setAttribute('aria-hidden', 'true'); // Slight random horizontal spread so successive pops don't overlap perfectly. // 42–58 % keeps the pop centred over the number while adding visible variety. const POP_LEFT_BASE = 42; // leftmost starting position (%) const POP_LEFT_SPREAD = 16; // random spread width (%) el.style.left = (POP_LEFT_BASE + Math.random() * POP_LEFT_SPREAD) + '%'; - el.textContent = '+' + formatTokenCountShort(ratePerSec); + el.textContent = text; container.appendChild(el); // Clean up the element when the animation ends or is cancelled. // A fallback timeout handles cases where neither event fires (e.g. hidden tab). @@ -48,14 +51,14 @@ const rateEventEl = document.getElementById('rateEvent'); if (totalEl) totalEl.textContent = numFmt(tokens); - if (sessionEl) sessionEl.textContent = formatTokenCount(sessionTokens); + if (sessionEl) sessionEl.textContent = appendExp(sessionTokens, formatTokenCount(sessionTokens)); if (sessionTimeEl) { const m = Math.floor(elapsed / 60); const s = elapsed % 60; const suffix = firstArrivalTime !== pageLoadTime ? 'since first visit' : 'on page'; sessionTimeEl.textContent = m > 0 ? `${m}m ${s}s ${suffix}` : `${s}s ${suffix}`; } - if (rateEl) rateEl.textContent = formatTokenCount(currentRate); + if (rateEl) rateEl.textContent = appendExp(currentRate, formatTokenCount(currentRate)); if (rateEventEl) { // Beyond BASE_DATE the rate is growing — reflect that in the subtitle const baseMs = new Date(BASE_DATE_ISO).getTime(); @@ -69,10 +72,29 @@ } } - // Spawn a floating "+N" pop on the total counter once per second + // Floating "+N" pops — spawned once per second (rate counter: once per minute) if (now - _lastTokenPop >= 1000) { _lastTokenPop = now; - spawnTokenPop(currentRate); + const totalBox = totalEl && totalEl.closest('.counter-box'); + spawnPop(totalBox, '+' + formatTokenCountShort(currentRate)); + } + + if (now - _lastSessionPop >= 1000) { + _lastSessionPop = now; + const sessionBox = sessionEl && sessionEl.closest('.counter-box'); + spawnPop(sessionBox, '+' + formatTokenCountShort(currentRate), 'token-pop--session'); + } + + // Rate counter: spawn a pop every 60 s showing how much the rate grew that minute. + // (The rate grows ~30 %/yr; a per-minute delta is the smallest visible non-zero unit.) + if (now - _lastRatePop >= 60000) { + _lastRatePop = now; + const rateOneMinAgo = getDynamicRate(new Date(now - 60000)); + const rateDelta = Math.round(currentRate - rateOneMinAgo); + if (rateDelta > 0) { + const rateBox = rateEl && rateEl.closest('.counter-box'); + spawnPop(rateBox, '+' + formatTokenCountShort(rateDelta) + '/s', 'token-pop--rate'); + } } // Impact stats @@ -82,6 +104,23 @@ setStatText('statWater', formatTokenCountShort(impact.waterL)); setStatText('statTrees', formatTokenCountShort(impact.treesEquivalent)); + // Floating pops for impact stats — per-second deltas based on current rate + if (now - _lastStatPop >= 1000) { + _lastStatPop = now; + const impactPerSec = calculateEnvironmentalImpact(currentRate); + [ + { id: 'statKwh', val: impactPerSec.kWh }, + { id: 'statCo2', val: impactPerSec.co2Kg }, + { id: 'statWater', val: impactPerSec.waterL }, + { id: 'statTrees', val: impactPerSec.treesEquivalent }, + ].forEach(({ id, val }) => { + if (val < 0.5) return; // skip if the per-second increase rounds to zero + const statEl = document.getElementById(id); + const statBox = statEl && statEl.closest('.impact-stat'); + spawnPop(statBox, '+' + formatTokenCountShort(Math.round(val)), 'token-pop--stat'); + }); + } + // Update doomsday clock (PRD 1) updateDoomsdayClock(tokens); diff --git a/styles/counter-milestones.css b/styles/counter-milestones.css index 451b4c7..c48061a 100644 --- a/styles/counter-milestones.css +++ b/styles/counter-milestones.css @@ -76,6 +76,25 @@ z-index: 20; } +/* Session counter pop — orange */ +.token-pop--session { + color: var(--accent-2); + text-shadow: 0 0 8px rgba(255,136,0,0.5); +} + +/* Rate counter pop — green */ +.token-pop--rate { + color: var(--accent-3); + text-shadow: 0 0 8px rgba(0,204,119,0.5); +} + +/* Impact stat pop — teal/cyan */ +.token-pop--stat { + color: #4ecdc4; + text-shadow: 0 0 8px rgba(78,205,196,0.5); + font-size: 0.7rem; +} + @keyframes token-pop-float { 0% { opacity: 0; transform: translateX(-50%) translateY(0); } 15% { opacity: 1; } @@ -96,6 +115,8 @@ border-radius: 0.5rem; padding: 1rem; text-align: center; + position: relative; + overflow: visible; } .impact-stat .stat-icon { font-size: 1.8rem; display: block; margin-bottom: 0.4rem; } From 90d9781b810a44bab98d96fa558a0889c74f5140 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Apr 2026 16:34:08 +0000 Subject: [PATCH 2/2] refactor: extract MIN_STAT_POP_THRESHOLD constant per code review Agent-Logs-Url: https://github.com/nitrocode/token-deathclock/sessions/7149edd6-90cc-4ef9-95e2-f12c11bbadd6 Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> --- src/js/02-counter.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/js/02-counter.js b/src/js/02-counter.js index 8841e66..10853e1 100644 --- a/src/js/02-counter.js +++ b/src/js/02-counter.js @@ -108,13 +108,16 @@ if (now - _lastStatPop >= 1000) { _lastStatPop = now; const impactPerSec = calculateEnvironmentalImpact(currentRate); + // MIN_STAT_POP_THRESHOLD: skip stats whose per-second increase rounds to zero + // (avoids "+0" pops for very slow-growing stats like trees at low rates). + const MIN_STAT_POP_THRESHOLD = 0.5; [ { id: 'statKwh', val: impactPerSec.kWh }, { id: 'statCo2', val: impactPerSec.co2Kg }, { id: 'statWater', val: impactPerSec.waterL }, { id: 'statTrees', val: impactPerSec.treesEquivalent }, ].forEach(({ id, val }) => { - if (val < 0.5) return; // skip if the per-second increase rounds to zero + if (val < MIN_STAT_POP_THRESHOLD) return; const statEl = document.getElementById(id); const statBox = statEl && statEl.closest('.impact-stat'); spawnPop(statBox, '+' + formatTokenCountShort(Math.round(val)), 'token-pop--stat');