Daily — A Piece of the Universe, Every Day
A daily celestial card system — deterministic LCG seed selection, Canvas card rendering, visit streaks, and image sharing
The Goal — A Reason to Come Back
When a site looks the same every visit, the motivation to return fades. Not random, not sequential, but content tied to "today" specifically — that idea sparked Daily.
Card Pool and Deterministic Selection
The card pool draws from three sources:
| Type | Source | Examples |
|---|---|---|
scale | Scale of the Universe objects | Proton, Earth, Galaxy |
constellation | 88 constellation dataset | Orion, Ursa Major, Scorpius |
stellar | Stellar evolution stages | Nebula, Main Sequence, Black Hole |
Card selection uses an LCG (Linear Congruential Generator). The date becomes the seed, so the card changes daily but remains the same for everyone on the same day.
function getCardForDate(d: Date): DailyCard {
let seed = d.getFullYear() * 10000 + (d.getMonth() + 1) * 100 + d.getDate();
seed = (seed * 9301 + 49297) % 233280;
return CARD_POOL[seed % CARD_POOL.length];
}
9301, 49297, 233280 — classic LCG constants with a sufficiently long period and trivial implementation cost.
Canvas Card Rendering
The celestial image at the center is drawn directly on Canvas. Each card type uses a dedicated renderer:
- Scale objects:
drawObject()— reuses the Scale of the Universe renderer - Constellations:
drawConstellationCard()— computes star positions via Gnomonic projection and draws connection lines - Stellar evolution:
drawStellarCard()— leverages the Stellar Life game's stage renderer
The card border features a slow 6-second pulsing glow animation, driven by CSS @keyframes on border-color and box-shadow simultaneously.
@keyframes cardGlow {
0%,
100% {
border-color: rgba(var(--js2-primary), 0.35);
box-shadow: 0 0 10px rgba(var(--js2-primary), 0.15);
}
50% {
border-color: rgba(var(--js2-primary), 0.7);
box-shadow: 0 0 14px rgba(var(--js2-primary), 0.4);
}
}
Visit Streaks
The calendar UI marks visited days with purple dots. Visit dates are stored in localStorage, and consecutive visit counts (streaks) are calculated from the history.
storageSet('daily:visited', { ...prev, [dateKey]: true });
storageSet('daily:streak', calculateStreak(visitedDates));
As the streak grows, the consecutive day count appears in the calendar header. A small mechanism, but enough to motivate a daily visit.
Image Sharing
Tapping the share button shares the card image itself, not just a URL. A separate offscreen canvas renders a purpose-built share card.
function renderShareCard(srcCanvas, resolved, locale): Promise<Blob | null> {
// 400×dynamic size, 2x DPR
// Background gradient + border + badge + object image + name + description + watermark
}
The share card includes the object name, English name, size info, description text, and a watermark. navigator.share({ files }) passes the image file directly, with a text-copy fallback for unsupported environments.
Closing Thoughts
Daily recombines existing content (Scale, Constellation, Stellar) into a fresh experience every day. Adding a new card type is as simple as extending CARD_POOL and wiring up a renderer.
One card per day, pulled from the universe. That alone is a reason to visit today.