← DEVLOG
인터랙티브2025.12.195 min read

Daily — 매일 만나는 우주 한 조각

매일 달라지는 천체 카드 — LCG 시드 기반 결정적 선택, Canvas 카드 렌더링, 방문 스트릭, 이미지 공유까지

canvasdailylcgconstellationstellarshare-image

시작 — 매일 열어보는 이유

사이트에 들어올 때마다 같은 화면이 보이면 재방문의 동기가 약해집니다. 무작위도 아니고 순서대로도 아닌, "오늘"에만 해당하는 콘텐츠가 있으면 어떨까 — 그 아이디어에서 Daily가 시작되었습니다.


카드 풀과 결정적 선택

Daily의 카드 풀은 세 가지 소스에서 구성됩니다.

타입소스예시
scaleScale of the Universe 오브젝트양성자, 지구, 은하
constellation88개 별자리 데이터오리온, 큰곰, 전갈
stellar항성 진화 단계성운, 주계열성, 블랙홀

카드 선택에는 LCG(Linear Congruential Generator)를 사용합니다. 날짜를 시드로 변환하면 매일 다르지만 같은 날에는 누가 접속해도 동일한 카드가 나옵니다.

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 — 고전적인 LCG 상수로, 주기가 충분히 길면서도 구현이 단순합니다.


Canvas 카드 렌더링

카드 중앙의 천체 이미지는 Canvas로 직접 그립니다. 카드 타입별로 전용 렌더러를 사용합니다.

  • Scale 오브젝트: drawObject() — Scale of the Universe의 렌더러를 재활용합니다
  • 별자리: drawConstellationCard() — Gnomonic 투영으로 별 위치를 계산하고, 연결선과 함께 그립니다
  • 항성 진화: drawStellarCard() — Stellar Life 게임의 스테이지 렌더러를 활용합니다

카드 외곽에는 6초 주기로 천천히 점멸하는 글로우 애니메이션을 적용했습니다. CSS @keyframesborder-colorbox-shadow를 동시에 제어합니다.

@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);
  }
}

방문 스트릭

캘린더 UI에는 방문 기록이 보라색 점으로 표시됩니다. localStorage에 방문한 날짜 목록을 저장하고, 연속 방문 일수(스트릭)를 계산합니다.

storageSet('daily:visited', { ...prev, [dateKey]: true });
storageSet('daily:streak', calculateStreak(visitedDates));

스트릭이 쌓이면 캘린더 헤더에 연속 일수가 표시됩니다. 작은 장치이지만, 매일 한 번씩 접속하게 만드는 동기 부여가 됩니다.


이미지 공유

공유 버튼을 누르면 단순 URL이 아니라 카드 이미지 자체가 공유됩니다. 별도의 오프스크린 캔버스에 공유용 카드를 새로 렌더링합니다.

function renderShareCard(srcCanvas, resolved, locale): Promise<Blob | null> {
  // 400×dynamic 크기, 2x DPR
  // 배경 그라디언트 + 테두리 + 뱃지 + 천체 이미지 + 이름 + 설명 + 워터마크
}

공유 카드에는 천체 이름, 영문명, 크기 정보, 설명 텍스트, 워터마크가 포함됩니다. navigator.share({ files }) API로 이미지 파일을 직접 전달하고, 미지원 환경에서는 텍스트 복사로 폴백합니다.


마치며

Daily는 기존 콘텐츠(Scale, Constellation, Stellar)를 재조합해 매일 새로운 경험을 만들어내는 구조입니다. 새 카드 타입을 추가하려면 CARD_POOL에 항목을 넣고 렌더러를 연결하면 됩니다.

하루에 하나, 우주에서 꺼낸 카드 한 장. 그것만으로 오늘 이 사이트를 방문한 이유가 됩니다.

이 포스트와 연결된 콘텐츠

직접 체험하기