← DEVLOG
Interactive2025.12.195 min read

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

canvasdailylcgconstellationstellarshare-image

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:

TypeSourceExamples
scaleScale of the Universe objectsProton, Earth, Galaxy
constellation88 constellation datasetOrion, Ursa Major, Scorpius
stellarStellar evolution stagesNebula, 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.

Content related to this post

Try it yourself