Chapter 6
Seasons
The year clock. A season sets the site's colours and shapes through a data-season attribute on <html>. The browser works the season out from the date in Japan and checks again at every midnight, so nothing about it is stored or fetched.
The four seasons
SeasonId is a closed union in types/index.ts, and shared/seasons.ts lists the ids and presets. A season may be added only once its CSS preset exists in app/assets/css/tailwind.css and its id is in both.
| Season | Id | Months |
|---|---|---|
| 🌸 Spring (sakura) | sakura (default) | Mar–May |
| 🎐 Summer (natsu) | summer | Jun–Aug |
| 🍁 Autumn (koyo) | autumn | Sep–Nov |
| ❄️ Winter (fuyu) | winter | Dec–Feb |
How a season reaches the page
- The calendar.
seasonForDate()inshared/seasons.tsmaps the month in Japan (UTC+9, no daylight saving) to a season. It is a pure function of the date, which is why no server, cron or datastore is involved: the same answer is worked out anywhere, and it cannot go stale. - Before first paint. An inline script in
nuxt.config.ts(built byseasonBootScript()) setsdata-seasonon<html>: the reader'sseason-choiceif there is one, else the season for this month in Japan, read from a table of the months embedded in the script. A first visit therefore never flashes the default palette, and blocked storage cannot stop it. The season is applied in the browser, never in the HTML, which is why a page cached for everyone is safe. - The daily check. Once the page is up,
useSiteTheme()(started fromapp.vue) applies the season again, then checks at the next midnight in Japan and at every midnight after, and whenever the tab becomes visible again. A page left open past the turn of a month therefore changes season without a reload, and a hidden tab, whose timers sleep, catches up the moment it returns. The check does nothing while the reader has picked a season. - Season button.
SeasonButton.vuelets a reader pick any season or “Follow the calendar”. The pick lives only inlocalStorage(season-choice). - Music.
BgmControl.vueanduseBgm.tsplay a looping track for each season, all encoded to the same integrated loudness (−16 LUFS, measured on the MP3s) so a season change never changes the level. It is off on every load; only the volume (bgm-volume) is remembered. Looping is gapless through a decoded audio buffer and aGainNode(iOS ignoreselement.volume); a browser without Web Audio falls back to a plain looping audio element, with no crossfade. When the season changes mid-song the old track keeps playing until the new one has loaded, then the two crossfade over 2 seconds. The music pauses while the tab is hidden.
The palettes and shapes themselves are in Colour and shape.