138億年=1年の宇宙時計を作った
宇宙誕生138億年を1年に圧縮したコスモスカレンダー時計を実装した記録。BigIntで435兆秒を正確に数え、ISS追跡は世界地図をビルド時バンドルしてAPI依存を最小化した設計を解説。
登場人物
- リナ社長 … 高校生なのに会社経営するやり手ギャル。テックにも強くてAI活用が得意。
- タクヤ … 入社3年目の男性社員。真面目で少しだけコードが書ける。
今回は、138億年を1年に圧縮した「宇宙時計」を実装するまでの試行錯誤を振り返る回。
タクヤ「リナ社長、ツールページに『宇宙時計』が増えてますけど……なんですかこれ」
リナ社長「コスモスカレンダー。カール・セーガンが考えた、138億年の宇宙の歴史を1年に圧縮して人類の立ち位置を見せる時間スケールをそのまま実装した。えぐくない?」
タクヤ「138億年が1年、ということは……いま何月何日になるんですか?」
リナ社長「常に12月31日の23時59分59.9999……秒。宇宙の歴史を1年に圧縮すると、人類が文明を持ち始めてからの時間でもカレンダーの最後の秒の端っこにしか収まらないの。宇宙時計を開いたら、そのままリアルタイムで動いてる」
宇宙カレンダーは常に大晦日
コスモスカレンダーの圧縮比はシンプルな1行で決まる。
const UNIVERSE_AGE_YEARS = 13.8e9; // 宇宙の年齢(年)
const YEAR_SEC = 365.25 * 86400; // 1年の秒数
const REAL_YEARS_PER_CAL_SEC = UNIVERSE_AGE_YEARS / YEAR_SEC; // カレンダー1秒 = 実437.3年
カレンダー上の1秒が現実の437.3年に相当する。つまり秒針が1つ進む間に、現実世界では400年以上が経過している計算だ。
タクヤ「人類の全歴史って、このカレンダーだと何秒くらいになるんですか?」
リナ社長「約30万年の人類史で約686秒、つまり11分26秒。現生人類が登場してからでもカレンダーの大晦日の最後の12分に全部収まっちゃう」
タクヤ「686秒……138億年の歴史でたった12分。だとすると今この瞬間はカレンダーで何秒目なんですか?」
リナ社長「宇宙誕生から435兆秒以上経ってる。宇宙カレンダーで言えば大晦日の終わり0.0000004秒前。ここでJavaScriptのNumberが詰む」
JavaScriptの通常の数値型(64ビット浮動小数点)は整数として正確に扱える上限が約9000兆だが、精度の問題が出始めるのはもっと手前だ。435兆秒という数値に対して「現在時刻の経過秒数」を足し引きすると、浮動小数点の精度が飛んで秒単位のカウントが意味をなさなくなる。
そこでBigIntを使った。
const BASE_UNIVERSE_SEC = 435494880000000000n; // 13.8e9 * 31,557,600
universeSecondsEl.textContent = formatBigInt(BASE_UNIVERSE_SEC + BigInt(elapsedSec)) + ' 秒';
タクヤ「nサフィックスでBigIntリテラルにして、経過秒を足してるんですね」
リナ社長「そう。elapsedSecは2026年1月1日UTC(宇宙の年齢をちょうど13.8e9年とみなした基準点)からの経過秒数で、これ自体はふつうのNumberで問題ない。でかい基準値だけBigIntにして足し合わせることで、表示が精度落ちせず1秒ずつ正確に増えていく」
誕生日を入れると人生が秒数になる
ツールには誕生日入力欄がある。生年月日を入れると「あなたの人生は宇宙カレンダーで何秒か」が計算されて表示される。
// 年齢(年)→ 宇宙カレンダー上の秒数
const cosmicSeconds = ageYears * YEAR_SEC / UNIVERSE_AGE_YEARS;
タクヤ「20歳だと何秒になるんですか?」
リナ社長「約0.046秒。カレンダーが0.05秒も経過しないうちに人生が終わる」
タクヤ「……なかなかキツい数字ですね」
リナ社長「でもこれがちゃんとした宇宙スケール感なんだよ。地球が誕生したのはカレンダーの9月1日ごろ、恐竜が絶滅したのは12月30日、人類が農業を始めたのが12月31日の23時59分37秒ごろ。人一人の一生は最後の0.05秒にも届かない」
タクヤ「逆に考えると、宇宙の歴史を1秒1秒積み上げてきたスケールがわかりますね。686秒(11分26秒)って言葉で聞くと短いですけど、人類30万年分の積み重ねだって思うと」
人類の全歴史をコスモスカレンダーで並べると、こうなる。
| 出来事 | 現実の時間 | 宇宙カレンダー |
|---|---|---|
| 現生人類の出現 | 約30万年前 | 12月31日 23:48:33 ごろ |
| 農業の始まり | 約1万年前 | 12月31日 23:59:37 ごろ |
| ピラミッド建設 | 約4600年前 | 12月31日 23:59:49 ごろ |
| あなたの誕生(20歳の場合) | 20年前 | 12月31日 23:59:59.9543 ごろ |
リナ社長「この表を見てほしかったんだよね。農業が始まってから現在まで約23秒しかないのに、文明って全部その23秒で起きてる」
ISSはどこ?外部APIとバンドルの線引き
宇宙時計にはISSの現在位置をリアルタイム追跡するマップ機能もある。世界地図の上を5秒ごとに動くISS、という実装で、ここの設計判断が一番面白かった。
タクヤ「世界地図ってどこかから取ってきてるんですか?Leafletとか、タイル地図とか?」
リナ社長「使ってない。world-atlasパッケージのTopoJSONデータ(Natural Earth 110m, パブリックドメイン)をビルド時にバンドルして、Canvas上に自前で描いてる」
import landTopo from 'world-atlas/land-110m.json';
import { feature } from 'topojson-client';
タクヤ「外部のタイル地図APIを使わずに済むんですね。でも毎フレーム地図を描き直すのは重くないですか?」
リナ社長「描き直してない。地図はオフスクリーンCanvasに一度だけ描いて、ISS位置の更新のたびにdrawImage()で転写するだけ」
const mapLayer = document.createElement('canvas'); // オフスクリーン
// ページ読み込み時に1回だけ描画
(function buildMapLayer() {
const land = feature(landTopo, landTopo.objects.land);
// ...陸地ポリゴンをPath2Dで描画...
})();
function drawIssMap() {
issCtx.drawImage(mapLayer, 0, 0); // 毎フレームは転写だけ
// ISS位置・軌跡を追加描画
}
タクヤ「なるほど、重い計算は初回1回で済ませて、あとは画像コピーだけにするわけですね」
リナ社長「そう。地図の陸地ポリゴンをPath2Dで描くのはそこそこ処理が重い。それを5秒ごとに繰り返したらガクガクになる。オフスクリーンに焼いておけばコスト0」
ISS追跡部分だけは外部APIが必要だ。ISS現在位置はhttps://api.wheretheiss.at/v1/satellites/25544から5秒ごとに取得している。これだけはリアルタイムデータなのでビルド時バンドルができない。
| データ | 取得方法 | 理由 |
|---|---|---|
| 世界地図(陸地形状) | ビルド時バンドル | 静的データ。外部リクエスト不要 |
| ISS現在位置 | 5秒ごとにAPIフェッチ | リアルタイムデータなので必須 |
| 月の満ち欠け | 数学的計算のみ | 基準日からの経過日数で算出できる |
| 惑星の現地時刻 | 数学的計算のみ | 自転周期は既知の定数 |
タクヤ「月の満ち欠けや惑星時計はAPIを使ってないんですか?」
リナ社長「使ってない。月の満ち欠けは基準日からの経過日数で計算できる。惑星ごとの太陽日(自転周期)はすでに測定されてる定数だから、UTC 1970-01-01を基準点にして計算すれば現地時刻が出る。リアルタイムでないと得られないデータだけAPIを使って、計算で出せるものは計算するっていう切り分け」
タクヤ「そのほうが信頼性も上がりますね。APIが落ちても大部分は動き続ける」
リナ社長「そうそう。ISSのAPIが落ちたら地図のドットが止まるだけで、宇宙カレンダーも月も惑星時計も動き続ける」
座標変換は正距円筒図法のシンプルな式でやっている。
const x = ((d.longitude + 180) / 360) * issCanvas.width;
const y = ((90 - d.latitude) / 180) * issCanvas.height;
経度を0〜360の範囲に正規化してCanvas横幅に掛け、緯度を上から下へ反転させてCanvas高さに掛けるだけだ。複雑な地図投影ライブラリを入れずに3行で済む。
持ち帰り
数値精度が怪しいと思ったらBigIntを検討する。 JavaScriptのNumberは53ビット整数精度まで正確だが、435兆秒のような巨大な整数に小さな値を足し引きすると精度が失われる。BigIntにすれば任意精度で整数を扱えるため、宇宙カレンダーの秒カウンターのように「巨大な基準値 + 小さな変化量」を正確に表示したいケースに使える。通常のNumberとの演算は型が合わないためキャスト(BigInt(value))が必要になるが、表示用の最終値を計算する場所に限定して使えばコードへの影響は最小限に収まる。
コメント