-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path02-counter.js
More file actions
211 lines (188 loc) · 9.36 KB
/
Copy path02-counter.js
File metadata and controls
211 lines (188 loc) · 9.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
// ---- Counter updater -------------------------------------
// Pre-computed reversed schedule for rate-event label lookup (avoids
// cloning and reversing on every animation frame in updateCounters).
const REVERSED_RATE_SCHEDULE = [...RATE_SCHEDULE].reverse();
// 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)
// 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' + (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 = 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).
const POP_ANIM_MS = 1500; // matches animation duration in CSS
const POP_CLEANUP_BUFFER_MS = 200;
let removed = false;
const removeEl = () => {
if (!removed) { removed = true; clearTimeout(fallback); el.remove(); }
};
el.addEventListener('animationend', removeEl, { once: true });
el.addEventListener('animationcancel', removeEl, { once: true });
const fallback = setTimeout(removeEl, POP_ANIM_MS + POP_CLEANUP_BUFFER_MS);
}
function updateCounters() {
const now = Date.now();
const tokens = getCurrentTokens();
const currentRate = getDynamicRate(new Date(now));
// Use firstArrivalTime so the counter accumulates across return visits
const sessionTokens = Math.round((now - firstArrivalTime) / 1000 * currentRate);
const elapsed = Math.floor((now - firstArrivalTime) / 1000);
const totalEl = document.getElementById('totalCounter');
const sessionEl = document.getElementById('sessionCounter');
const sessionTimeEl = document.getElementById('sessionTime');
const rateEl = document.getElementById('rateCounter');
const rateEventEl = document.getElementById('rateEvent');
if (totalEl) totalEl.textContent = numFmt(tokens);
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 = 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();
if (now > baseMs) {
rateEventEl.textContent = 'and growing · tokens/sec';
} else {
const rateEntry = REVERSED_RATE_SCHEDULE.find(
(r) => now >= new Date(r.date).getTime()
);
if (rateEntry) rateEventEl.textContent = rateEntry.event + ' · tokens/sec';
}
}
// Floating "+N" pops — spawned once per second (rate counter: once per minute)
if (now - _lastTokenPop >= 1000) {
_lastTokenPop = now;
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
const impact = calculateEnvironmentalImpact(tokens);
setStatText('statKwh', formatTokenCountShort(impact.kWh));
setStatText('statCo2', formatTokenCountShort(impact.co2Kg));
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 milestone progress bars
const triggered = getTriggeredMilestones(tokens, MILESTONES);
MILESTONES.forEach((m, idx) => {
const card = document.getElementById('milestone-' + m.id);
if (!card) return;
const wasTriggered = card.classList.contains('triggered');
const isTriggered = tokens >= m.tokens;
if (isTriggered && !wasTriggered) {
card.classList.add('triggered');
if (!shownEmergencyBroadcasts.has(m.id)) {
shownEmergencyBroadcasts.add(m.id);
showEmergencyBroadcast(m);
}
}
const fill = card.querySelector('.progress-fill');
if (fill) {
const prev = idx === 0 ? 0 : MILESTONES[idx - 1].tokens;
const pct = milestoneProgress(tokens, prev, m.tokens);
fill.style.width = pct + '%';
const pctLabel = card.querySelector('.progress-pct');
if (pctLabel) pctLabel.textContent = pct.toFixed(1) + '%';
}
});
requestAnimationFrame(updateCounters);
}
function setStatText(id, value) {
const el = document.getElementById(id);
if (el) el.textContent = value;
}
// ---- Extinction countdown (header) -----------------------
// Module-level constants and helpers so they are created once, not on every
// call to updateExtinctionCountdown() (which runs every second).
const _EXT_SECS_PER_YEAR = 365.25 * 24 * 3600;
const _EXT_SECS_PER_DAY = 24 * 3600;
const _EXT_SECS_PER_HOUR = 3600;
const _EXT_SECS_PER_MIN = 60;
const _EXT_UNIT_IDS = ['extYears', 'extDays', 'extHours', 'extMins', 'extSecs'];
const _extPad2 = (n) => String(Math.floor(n)).padStart(2, '0');
const _extPad3 = (n) => String(Math.floor(n)).padStart(3, '0');
// Finds the milestone flagged `extinctionMarker: true`, computes the
// remaining seconds at the current dynamic rate, and updates the header
// countdown display once per second.
function updateExtinctionCountdown() {
const extinctionMilestone = MILESTONES.find(m => m.extinctionMarker);
if (!extinctionMilestone) return;
const secsRemaining = computeExtinctionSecsRemaining(extinctionMilestone.tokens, Date.now());
if (secsRemaining <= 0) {
// Extinction threshold reached — show zeroed-out timer
_EXT_UNIT_IDS.forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = '00';
});
return;
}
const years = Math.floor(secsRemaining / _EXT_SECS_PER_YEAR);
const remAfterY = secsRemaining % _EXT_SECS_PER_YEAR;
const days = Math.floor(remAfterY / _EXT_SECS_PER_DAY);
const remAfterD = remAfterY % _EXT_SECS_PER_DAY;
const hours = Math.floor(remAfterD / _EXT_SECS_PER_HOUR);
const remAfterH = remAfterD % _EXT_SECS_PER_HOUR;
const mins = Math.floor(remAfterH / _EXT_SECS_PER_MIN);
const secs = Math.floor(remAfterH % _EXT_SECS_PER_MIN);
const yrsEl = document.getElementById('extYears');
const daysEl = document.getElementById('extDays');
const hrsEl = document.getElementById('extHours');
const minsEl = document.getElementById('extMins');
const secsEl = document.getElementById('extSecs');
if (yrsEl) yrsEl.textContent = String(years);
if (daysEl) daysEl.textContent = _extPad3(days);
if (hrsEl) hrsEl.textContent = _extPad2(hours);
if (minsEl) minsEl.textContent = _extPad2(mins);
if (secsEl) secsEl.textContent = _extPad2(secs);
}