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
28 changes: 28 additions & 0 deletions death-clock-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,32 @@ function getRateAtDate(date) {
return RATE_SCHEDULE[0].ratePerSec;
}

// Fractional annual growth applied to the token rate beyond BASE_DATE_ISO.
// Sourced from the RATE_SCHEDULE: the rate roughly doubled every 12–18 months
// between 2023 and 2026 (~100 % → 30 % CAGR as growth moderates post-AGI ramp).
// This conservative 30 % figure is used for forward projections and the live counter.
const RATE_GROWTH_PER_YEAR = 0.30;

/**
* Return the dynamic (ever-growing) global AI inference rate for a given date.
* For historical dates at or before BASE_DATE_ISO the result is identical to
* getRateAtDate(). For future dates beyond BASE_DATE_ISO the rate is projected
* forward using continuous exponential growth at RATE_GROWTH_PER_YEAR.
*
* @param {Date} [date] - defaults to now
* @returns {number} tokens per second (always a positive integer)
*/
function getDynamicRate(date) {
const d = (date instanceof Date && !isNaN(date.getTime())) ? date : new Date();
const baseMs = new Date(BASE_DATE_ISO).getTime();
if (d.getTime() <= baseMs) {
return getRateAtDate(d);
}
const SECS_PER_YEAR = 365.25 * 24 * 3600;
const elapsedYears = (d.getTime() - baseMs) / 1000 / SECS_PER_YEAR;
return Math.round(TOKENS_PER_SECOND * Math.pow(1 + RATE_GROWTH_PER_YEAR, elapsedYears));
}

/**
* Calculate the collective daily environmental impact if a fraction of global users
* consistently applies a token-saving tip.
Expand Down Expand Up @@ -733,6 +759,8 @@ const DeathClockCore = {
getTimeDelta,
milestoneProgress,
getRateAtDate,
RATE_GROWTH_PER_YEAR,
getDynamicRate,
calculateTipImpact,
generateEquivalences,
calculatePersonalFootprint,
Expand Down
10 changes: 8 additions & 2 deletions src/js/00-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
getTimeDelta,
milestoneProgress,
getRateAtDate,
RATE_GROWTH_PER_YEAR,
getDynamicRate,
calculateTipImpact,
generateEquivalences,
calculatePersonalFootprint,
Expand Down Expand Up @@ -70,8 +72,12 @@

// ---- Helpers ---------------------------------------------
function getCurrentTokens() {
const elapsed = (Date.now() - BASE_DATE_MS) / 1000;
return BASE_TOKENS + TOKENS_PER_SECOND * elapsed;
const elapsed = (Date.now() - BASE_DATE_MS) / 1000; // seconds since BASE_DATE
// Integrate the exponentially-growing rate: tokens = BASE_TOKENS + R0/k * (e^(k*t) - 1)
// where k = ln(1 + RATE_GROWTH_PER_YEAR) / SECS_PER_YEAR is the continuous growth rate constant.
const SECS_PER_YEAR = 365.25 * 24 * 3600;
const continuousGrowthRate = Math.log(1 + RATE_GROWTH_PER_YEAR) / SECS_PER_YEAR;
return BASE_TOKENS + (TOKENS_PER_SECOND / continuousGrowthRate) * (Math.exp(continuousGrowthRate * elapsed) - 1);
}

function numFmt(n) {
Expand Down
57 changes: 51 additions & 6 deletions src/js/02-counter.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,42 @@
// ---- 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();

let _lastTokenPop = 0;

function spawnTokenPop(ratePerSec) {
const totalEl = document.getElementById('totalCounter');
if (!totalEl) return;
const container = totalEl.closest('.counter-box');
if (!container) return;
const el = document.createElement('div');
el.className = 'token-pop';
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);
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 = getRateAtDate(new Date(now));
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);
Expand All @@ -23,11 +57,22 @@
}
if (rateEl) rateEl.textContent = formatTokenCount(currentRate);
if (rateEventEl) {
// Show the event that triggered this rate step
const rateEntry = [...RATE_SCHEDULE].reverse().find(
(r) => now >= new Date(r.date).getTime()
);
if (rateEntry) rateEventEl.textContent = rateEntry.event + ' · tokens/sec';
// 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';
}
}

// Spawn a floating "+N" pop on the total counter once per second
if (now - _lastTokenPop >= 1000) {
_lastTokenPop = now;
spawnTokenPop(currentRate);
}

// Impact stats
Expand Down
28 changes: 27 additions & 1 deletion styles/counter-milestones.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
padding: 1.5rem;
text-align: center;
position: relative;
overflow: hidden;
/* overflow: visible — required so .token-pop elements can float above the box */
overflow: visible;
transition: box-shadow 0.3s;
}

Expand Down Expand Up @@ -56,6 +57,31 @@
margin-top: 0.5rem;
}

/* ---- Token pop (+N float-up animation on total counter) ----
.counter-box uses overflow: visible (see below) so that these pops
can float above the box boundary, giving the impression that tokens
are streaming out of the counter into the page. */
.token-pop {
position: absolute;
bottom: 55%;
font-family: "Orbitron", monospace;
font-size: 0.8rem;
font-weight: 700;
color: var(--accent);
text-shadow: 0 0 8px var(--accent-glow);
pointer-events: none;
white-space: nowrap;
transform: translateX(-50%);
animation: token-pop-float 1.5s ease-out forwards;
z-index: 20;
}

@keyframes token-pop-float {
0% { opacity: 0; transform: translateX(-50%) translateY(0); }
15% { opacity: 1; }
100% { opacity: 0; transform: translateX(-50%) translateY(-56px); }
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/* ---- Impact Stats Strip ---- */
.impact-strip {
display: grid;
Expand Down
62 changes: 62 additions & 0 deletions tests/death-clock.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const {
BASE_TOKENS,
TOKENS_PER_SECOND,
getSimulatedViewerCount,
getDynamicRate,
RATE_GROWTH_PER_YEAR,
} = core;

// ============================================================
Expand Down Expand Up @@ -573,6 +575,66 @@ describe('getRateAtDate', () => {
});
});

// ============================================================
// getDynamicRate
// ============================================================
describe('getDynamicRate', () => {
const BASE_DATE = new Date(core.BASE_DATE_ISO);

test('returns TOKENS_PER_SECOND exactly at BASE_DATE_ISO', () => {
expect(getDynamicRate(BASE_DATE)).toBe(TOKENS_PER_SECOND);
});

test('returns more than TOKENS_PER_SECOND for a date one year after BASE_DATE', () => {
const oneYearLater = new Date(BASE_DATE.getTime() + 365.25 * 24 * 3600 * 1000);
expect(getDynamicRate(oneYearLater)).toBeGreaterThan(TOKENS_PER_SECOND);
});

test('grows by roughly RATE_GROWTH_PER_YEAR after one year', () => {
const oneYearLater = new Date(BASE_DATE.getTime() + 365.25 * 24 * 3600 * 1000);
const rate = getDynamicRate(oneYearLater);
const expected = TOKENS_PER_SECOND * (1 + RATE_GROWTH_PER_YEAR);
// Allow ±1 % tolerance for rounding
expect(rate).toBeGreaterThanOrEqual(expected * 0.99);
expect(rate).toBeLessThanOrEqual(expected * 1.01);
});

test('matches getRateAtDate for a historical date well before BASE_DATE', () => {
const historicalDate = new Date('2024-01-01');
expect(getDynamicRate(historicalDate)).toBe(getRateAtDate(historicalDate));
});

test('returns a positive number for any date', () => {
expect(getDynamicRate(new Date('2020-01-01'))).toBeGreaterThan(0);
expect(getDynamicRate(new Date('2028-01-01'))).toBeGreaterThan(0);
});

test('falls back to current time when no date is provided', () => {
const rate = getDynamicRate();
expect(typeof rate).toBe('number');
expect(rate).toBeGreaterThan(0);
});

test('falls back gracefully for an invalid date', () => {
const rate = getDynamicRate(new Date('not-a-date'));
expect(typeof rate).toBe('number');
expect(rate).toBeGreaterThan(0);
});

test('rate at BASE_DATE equals rate two years later times inverse growth factor', () => {
const twoYearsLater = new Date(BASE_DATE.getTime() + 2 * 365.25 * 24 * 3600 * 1000);
const rateLater = getDynamicRate(twoYearsLater);
const expectedFactor = Math.pow(1 + RATE_GROWTH_PER_YEAR, 2);
expect(rateLater / TOKENS_PER_SECOND).toBeCloseTo(expectedFactor, 1);
});

test('RATE_GROWTH_PER_YEAR is a positive number less than 1', () => {
expect(typeof RATE_GROWTH_PER_YEAR).toBe('number');
expect(RATE_GROWTH_PER_YEAR).toBeGreaterThan(0);
expect(RATE_GROWTH_PER_YEAR).toBeLessThan(1);
});
});

// ============================================================
// RATE_SCHEDULE sanity checks
// ============================================================
Expand Down
Loading