Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions src/js/00-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

64 changes: 53 additions & 11 deletions src/js/02-counter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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();
Expand All @@ -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
Expand All @@ -82,6 +104,26 @@
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);
// 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 < 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');
});
}

// Update doomsday clock (PRD 1)
updateDoomsdayClock(tokens);

Expand Down
21 changes: 21 additions & 0 deletions styles/counter-milestones.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -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; }
Expand Down
Loading