これは私のブログ、宙と木だ。ページを移るたびに、琥珀色の点がひとつ、このページから次のページへ飛んでいき、次のページが持っている一本の線に降り立つ。10月25日の未明、このサイトは誕生から572日目にこの姿に変わった。

このサイトは最初、自分でゼロから組み上げたポートフォリオにすぎず、その後はページが一つ増えるたびに、その時の感覚で一ページずつ足していった。一年ほど続けたころ、どこか AI っぽさが出てきたと感じるようになった。多くの部分が、どのテンプレートからでも出てきそうな見た目だったのだ。そこで今年の5月から6月にかけて innei さんのブログのやり方をいくつか参考にし、のちに WebGPU で描く星空の背景に替えた(その記事はこちら)。記事も、つぶやきも、観たアニメも、撮った写真も全部ここに置いているから、どこかのテーマを少しいじったものではなく、「私のもの」に見えてほしいとずっと思っていた。

正直に言うと、半年前の私はこのブログにそこそこ満足していた。ただ、ずっと一ページずつ別々に手を入れてきただけで、サイト全体を並べたらどう見えるかを考えたことはなかった。4月に「歩み」のページを直したとき、カードの背板を外して、背景に巨大な年号を浮かべた。見た目は良くなったのに、何かが足りない気がしていた。あとになってわかったのは、足りないのは没入感だということだった。カードの一枚一枚が独立したスライドのようで、それらをつなぐものが間に何もなかった。

私はふだんからよくぼんやりして、空を眺めている人間だ。自分の内側はこのサイトに収まるよりずっと広いとずっと感じていたけれど、それをどう表現すればいいのかわからなかった。十月のある日、音楽、本棚、機材の三つのページを並べて見たら、どれもまったく同じ形をしていた。大きな見出し、サブタイトル、その下にカードの列。その瞬間、これらのページはただ見せるためにそこに置いてあるだけだと思った。どのページもこの形なら、読者は何を探検すればいいのだろう。私は一度に全部ひっくり返すことにした。

「そして今から、地球を動かす。」

『チ。―地球の運動について―』

これは私の大好きな作品のセリフだ。物語の中の人々は何世代にもわたり、命の危険を冒して地動説ちどうせつを受け継いでいく。私のしていることはもちろんずっと小さい。それでも、サイト全体をひっくり返すと決めたその瞬間、頭に浮かんだのはこの言葉だった。

結局、四日かかった。10月22日に方向性を考え始め、25日の未明に公開した。マージした PR は449個のファイルに手を入れ、41,744行を追加し、53,184行を削除した。十四のページをすべて描き直し、どのページも一つの天体か、宇宙で実際に使われる何かに置き換えた。この記事に書くのはこの四日間と、その前の572日間のことだ。トランジションとフォントの技術的な詳細は末尾の付録にまとめたので、自分で作ってみたい人はそちらへ直接飛んでもらってかまわない。

最初の版は、失敗だった#

最初はデザインカンプを描かずに、ローカルで直接作ったほうが効果がわかりやすいと思っていた。プロトタイプと、実際に背景と組み合わせたときの印象はかなり違うからだ。最初の版では写実的な質感の土星を残し、ページ移動のときに惑星が爆発して星屑に砕けるようにした。ノートのカードは下へ伸びてページの底で地平線につながり、つぶやきのページはピクセルアート風にした。

できあがったものを見て、自分ではまったく満足できなかった。土星は動くと白飛びするし、爆発して散った粒子は背景にもともとある星とつながらず、サノスの指パッチンのようにも、消しゴムで消したようにも見えた。ノートの星の軌跡は最初、見ていると目眩めまいがするほど密で、内容がほとんど隠れてしまった。レイアウトはどう調整しても一昔前のブログの匂いがして、昔の Yahoo 奇摩のようだった。

午後いっぱい直し続けたが、一か所直すたびに、別の場所がますますちぐはぐに見えた。問題は、先に基調を決めていなかったことだ。ページごとにそれぞれエフェクトを足していたので、質感も配色も動きもばらばらなことを言っていた。正直その版も醜くはなかった。ただ、私の欲しいものではなかった。だから削除はせず、GitHub の archive/sky-morph ブランチに封印して、方向性から考え直すことにした。

「不正解は無意味を意味しない。」

『チ。―地球の運動について―』

この言葉は『チ。』の中で何度も出てくる。最初の版は間違っていた。でも先に間違えていなければ、自分が本当に欲しいものが何なのかもわからなかっただろう。

点と線と、少しの琥珀だけ#

アプリを作るときから私はセリフ体の質感が好きだったし、サイト名「宙と木」の「宙」はもともと宇宙のことだ(「木」は私のハンドルネーム木村盆栽きむらぼんさいから来ている)。だから方向性は思いのほか早くまとまった。星空はもともと点と線だけで描ける。星は点で、軌道も星の軌跡も彗星の尾も線だ。線でページをつなげば、足りないと感じていた没入感をちょうど補える。

いちばん手放しがたかったのはホームの土星だ。質感のあるモデルで作ったあの土星は今年の4月まで調整を続けていて、パフォーマンスも画質も全体の印象も何度も磨き込んできた。まるごと捨てるのは本当に惜しかった。結局、土星は残したまま、点と線の描き方に変えた。不透明な球の上に白い筆跡を一本一本描き、環も線で描く。変えてみると見違えるほど良くなって、そこまで写実的な質感にこだわらなくてもいいのだと気づいた。それでも惜しいものは惜しいので、古いほうは記念に旧サイトに残してある。

決めたルールはごくわずかだ。

項目決めたこと
色黒 #0A0A0C を紙に、暖かい白 #F1ECE3 を墨に、琥珀 #E9C17A は「一つの点」の合図にだけ使う
質感カード、影、すりガラスは使わない。区切り線もできるだけ使わない
中国語の文字源ノ明朝(Noto Serif TC)
英語の文字英語だけの段落の大きな文字にだけ使う。Instrument Serif で、太字にはしない

配色にはあまり詳しくないけれど、一つだけ確信していることがある。色が増えると画面から主役がいなくなる。だからサイト全体で琥珀色なのはあの点だけだ。今回は私も学んで、方向性をしっかり考えてからコードを書いた。どのページもまずブラウザで直接クリックできるモックとして作り、一版ずつ直して、全部で15版になった。すべてのページが決まってから、ようやくサイトに手をつけた。

どのページも一つの天体#

方向性が決まってから、どのページにも同じ問いを立てた。これは宇宙では何になるだろう?各ページのサブタイトルはすべて自分で書いた。サイト全体に少し文学的な香りを持たせたかったけれど、余計な言葉は入れたくなかった。

ページサブタイトル
ホームコードを書く人。ときどき何かを作る。
ノート星々がめぐるなか、ひとときの好奇心に一筋の弧を残す。
つぶやき静寂を裂いたひとすじの雑音を捕まえる。
作品音のない深宇宙に、一寸ずつ痕を刻む。
コレクション引力の届くところすべてに、かつて私を照らした光のかけら。
写真虚空を越えてきた微かな信号を、ここで一行ずつ現像する。
機材精密を解きほぐし、こだわりを据える。
友達深宇宙は長く孤独だけれど、振り返ればいつも星の光がある。
メッセージ音のない長い夜に、遠くからのパルスを静かに待つ。
航海日誌引力を振りほどけば、この身がそのまま大空になる。
私についてしっかりつかまって。
このサイトここには何もない。夜と、白い墨と、かすかな光だけ。

ホーム:何も置かない#

ホームはあの土星で、ノート、つぶやき、作品、コレクション、私についての五つのエリアは、そのまわりを回る五つの衛星だ。私はあえて玄関に情報を何も置かなかった。ふだんは線と点しかなく、マウスを衛星に近づけると、その軌道だけが光り、そのエリアの名前と最新の一件が浮かび上がる。何もしていないときは、ときどき衛星がひとつ自分から光って、それがクリックできることを知らせる。訪れた人には自分で探検してほしい。私がこの宇宙の中で自分自身を探検しているのと同じように。

土星を3Dのまま残すことにこだわったのは、いずれホームを、中を自由に巡れる立体空間にしたいからだ。

つぶやき:一瞬でよぎる彗星#

つぶやきは私にとって、頭の中をよぎる一瞬一瞬のようなもので、Threads のように思いついたまま投稿する。だから彗星にした。彗星は大きなS字の弧に沿って飛び、最新の一件が彗核すいかくで、古いものは彗尾すいびに沿って左右に並ぶ。ずっと古いものは尾の上の一つの点だけになっていて、カーソルを重ねると浮かび上がる。

一件のつぶやきを開くと、カメラは彗尾のその部分へズームインしていく。望遠鏡で引き寄せるように、その部分が明るくなり、ほかは暗く退く。下のコメントは、その彗尾から散った塵だ。私が目指したのは「彗星のある一部分を見ている」感覚で、別のページに飛ぶことではなかった。

ノート:長時間露光の星の軌跡#

ノートは私がいちばん好きなページだ。一本の記事が一本の星の軌跡になっている。新しい記事ほど外側の輪を回り、長く書いた記事ほど弧が長く伸びる。背景の淡い弧も星の軌跡なので、夜空全体が長時間露光の写真のように見える。ページを開くとシャッターが開いたばかりのように、すべての星がいっせいに軌跡を引き、そのあと全体が天極てんきょくのまわりをゆっくり回る。星の軌跡は実は地球の自転が残した跡だ。空の星が回っているのではなく、私たちが立っているこの星が回っている。

いちばん気に入っているのは記事を開くその瞬間だ。クリックした記事の星の軌跡がまっすぐに引き伸ばされて手前に寄り、記事ページの右側の軌道になる。

この軌道は目次も兼ねている。章は軌道上の目盛りで、目盛りの間隔は各章の実際の長さに合わせてある。読み終えた部分は細い実線、まだ読んでいない部分は点線で、琥珀色の点は今読んでいるあなただ。ノートの一覧は幾重もの星の軌跡で、一本を開くことは、カメラがそのうちの一本に寄っていくことと同じだ。

機材:まだ納得できない図面#

機材のページは、最後まで直してもあまり納得できなかったページだ。最初の版は無重力での組み立てに、宇宙船のサブシステムのラベルとリアルタイムのテレメトリーを付けたものだったが、できあがりは平凡で、しかも宇宙とはあまり関係がなかった。本当はエンジニアリングの概念設計図にしたいとずっと思っていた。でも自分で一本ずつ線を引くのは時間がかかりすぎる。結局、Gemini に白地に黒線の分解アクソメ図を描かせて下絵にし、その上に番号の丸、引出線ひきだしせん、寸法線、部品表をプログラムで重ねた。番号にカーソルを重ねると、下絵のその部品が琥珀色に染まる。

最初の図面 SHEET 0 は、実家にある、たぶん2012年ごろに買ったパソコンだ。写真が残っていないので、筐体きょうたいは記憶を頼りに点で描いたスケッチで、点はかすかに揺れ、スペックは起動時の自己診断のように一行ずつ打ち出される。あれは私の子ども時代なので、いちばん前に置いた。

この版にもまだ満足していないので、たぶんまた直すと思う。

ほかのページ#

ほかのページにも、それぞれ一つずつ天体がある。

  • 作品は中心から外へ伸びていく螺旋の航路だ。起点に置いたのは卒業制作の VoltiCar で、カーボンクレジットをゲーム化するプロジェクトだった。テーマ選びはいまひとつで、チームもあまりうまく回らず、今振り返れば技術的には子どもだましだ。それでも、当時知っていたことを全部注ぎ込んだ初めてのプロジェクトで、一年以上かけて、よく寝食しんしょくを忘れて取り組んだ。だからこれが航路の起点になっている。作品の一つひとつは線で描いた小さな惑星で、大きさ、地形、環、衛星はすべて GitHub のデータから生まれる。
  • 写真は、宇宙で撮った写真が地球に送られてくる流れに沿って描いた。探査機が撮影して0と1に変換し、深宇宙ネットワークの大きなパラボラアンテナが受信して、一行ずつモノクロで現像され、最後に色がつく。
  • 友達の中心はこのサイトで、友達はそのまわりを回る星だ。一つクリックするとその人とドッキングし、二隻の船がつながって、横にその人の最近の記事が広がる。
  • メッセージは一件一件が受信した信号だ。入力中は打った文字がリアルタイムで波形になり、送信するとアンテナへ飛び込んでいく。
  • 航海日誌はページ全体が打ち上げの場面で、地上からの高度は GitHub への貢献の累計だ。このページは記事の最後にもう一度登場する。
  • このサイトは星空に向けた一台の六分儀ろくぶんぎで、中のおもちゃはすべてサイトに実際にあるものを使い、一つずつ分解して、当時どう考えたのかを書いている。遊び終えた人に「こういうやり方もあるんだ」と思ってもらえたらうれしい。

私について:ここに降り立つ#

「私について」のページは自由落下だ。点が真上からゆっくり落ちてきて、「しっかりつかまって。」の句点になって止まる。読者が下へスクロールすることは、その点と一緒に落ちていくことになる。

着想は、以前ニュースで見た動画だ。ポルトガルの深夜、青い火球かきゅうが空を横切り、夜空全体が数秒間照らされた。天文現象にはいつも心を揺さぶられる。星が一つ地球に落ちるのは人生と同じで、いちばん鮮やかな光を放ったあと、刹那せつなのうちに消えていく。だからこのページは、幼くて迷ってばかりいた昨日を通り過ぎて、「ここに降り立つ」で止まる。でも降り立ってからが新しい始まりで、その先は地下だ。一層ずつ掘っていくと私がやってきたことが出てきて、いちばん深いところに私のいちばん好きな言葉を置いた。

「たぶん感動は寿命の長さより大切なものだと思う。」

『チ。―地球の運動について―』

コレクション:四回作り直した信号#

コレクションのページ(観ている、本棚、聴いている)は、いちばん多く作り直したページだ。ここで見せたいのはセンスだからだ。たくさんのものを観てきたことと、私が何を好きなのかが伝わるようにしたかった。最初の版は三つのエリアを並べただけで、繰り返しが多すぎた。二つ目の版は切り替えに星屑の再構成を組み合わせたが、わざとらしすぎた。三つ目の版は三つの星雲で、下へスクロールするとカメラが三つの間を飛ぶのだが、それでも宇宙の壮大さは感じられず、円形の表紙も大半が切り取られてしまった。

三つの版がどれも違うとわかってから、「表紙をどう並べるか」を考えるのをやめて、こう考えるようにした。宇宙で、これらのものは実際にどう使われるのだろう?答えは信号だった。作品の一つひとつは、私が観終えた瞬間に地球から発した一つの輪の信号で、地球からの距離が、今からどれだけ前かを表している。下へスクロールすると地球からどんどん遠ざかっていき、実際の位置にある月、ウェッブ望遠鏡、惑星、ボイジャー1号、太陽圏の境界、オールトの雲を通り過ぎて、最後は約9光年先の近傍きんぼうの恒星まで退いていく。

遠ざかる途中でボイジャー1号を通り過ぎる。1990年、ボイジャー1号は約60億キロ離れたところから振り返って地球を撮影した。写真の中の地球は1ピクセルにも満たなかった。カール・セーガンはその写真を見てこう書いた。

「もう一度あの点を見てほしい。それがここだ。それが我が家だ。それが私たちだ。」

カール・セーガン『ペイル・ブルー・ドット』

コレクションのページにはこういう感覚を持たせたかった。遠ざかるほど地球は小さくなり、私を照らしてくれた作品はどれも、あの小さな点から発せられた信号なのだ。

一つの点が、すべてのページをつなぐ#

リニューアルで一つだけ残せるとしたら、私はトランジションを選ぶ。トランジションのなめらかさとつながりの良さが、サイトの質感を決めるとずっと思ってきた。読者はどこが良いのかを言葉にできるとは限らないけれど、一瞬引っかかったり、ちらついたり、位置が跳んだりすれば必ず気づく。

私が「なめらかさ」にこだわるのは今回に始まったことではない。7月に記事ページの読み込みを直したときは、スケルトン画面と本文のつなぎ目で行き詰まった。10月に星空のパフォーマンスを調整したときの目標は、訪れた人に「3Dなのにどうしてこんなになめらかなの」と思わせることだった。だからリニューアル初日のデモはとりわけ受け入れがたかった。ページ移動の飛行はそっけなく単調で、入るときと出るときが同じ方向だった。翌日直した版も、フェードイン・フェードアウトで二つのページを無理やりつなげて見せているだけだった。

そこで自分にルールを一つ課した。ページを移るたびに、必ず一つの点から広がっていくこと。ホームなら軌道上の衛星、つぶやきなら彗核、ノートなら一本一本の星の軌跡にもそれぞれ星がある。どのページにも自分の点がありさえすれば、ページ移動は一つの点がこのページから次のページへ飛んでいくことになる。

最初の版は無理やり描いたようだった#

最初の版はとても素朴だった。点が決まった経路に沿って飛び、着くと新しいページがフェードインする。実際に見ると点とページ上の線が合っておらず、琥珀色の点は彗核にそろっていないし、トランジションが描く線も彗尾から少しずれていた。

何度か直すうちに、「作品のページに入る」部分がいちばんなめらかに仕上がった。本当に一筆書きのようだった。

私はこれを基準にして、ほかのページもすべてそれに合わせて直した。なめらかな理由を分解してみると、三つのことがあった。

古いページが自分からその点に収まる

ホームの土星はクリックされた衛星に収まり、つぶやきの彗尾は彗核へ縮んで戻る。ページ上のものはすべてその一点へ集まり、しかも集まりきってからページが切り替わる。

カメラは映画のように被写体を追う

ここが私のいちばんこだわった点だ。カメラはまず琥珀色の点まで移動して、完全に中央にとらえる。点が出発するとカメラはそれを追い、点はずっと画面のど真ん中にある。到着が近づくとカメラは止まるが、このとき点はまだ動いているので、その後ろの軌跡が映る。映画がまず一つの対象にズームして追いかけ、カメラが止まったときに初めてそれがたどってきた道が見えるのと同じだ。

最後の区間はページ自身の線を通る

点は最後に、次のページにもともとある線(星の軌跡、彗尾、航路)に沿って降り立つ。点の後ろの尾は時間とともに消えていくので、着地したとき画面に残るのはそのページ自身の線だけになる。

各ページの受け止め方#

この三つが決まってからは、各ページでやることは二つの問いに答えることだけになった。点は最後にどの線に沿って降りてくるのか、離れるときこのページはどうやって一つの点に収まるのか。ここがいちばん楽しいところだと思う。同じ一つの点が、ページごとに違う意味を持つ。

ページ点の降り方離れるとき
ホーム土星の中心まで飛び、土星がその一点から広がる全体がクリックされた衛星に収まる
ノート一本の星の軌跡に入り、天極のまわりを大きく回って、その記事の星で止まる記事を開くと、その記事の星の軌跡が記事の目次へ引き伸ばされる
つぶやき彗尾に沿って降り、彗核になる彗尾が彗核へ縮んで戻る
作品起点に降り、ペン先になって航路全体を描く航路が起点から消されていき、最新の作品の衛星に収まる
コレクション深宇宙から地球へ急降下するカメラが地球に戻り、点が台湾から波面の輪をまとって発射される
写真点そのものが信号で、探査機からアンテナへ降りるページ全体が0と1に戻り、点が探査機から発射される
メッセージ起点が一度震え、琥珀色の光がチャンネルに沿ってアンテナへ走る光がアンテナから起点へ逆向きに走って戻る
友達中心に降り、軌道が内側から外へ一本ずつ伸びていく軌道が中心へ戻る
航海日誌発射台に降り、ロケットが打ち上がるロケットが機体に沿って飛び去り、点は機首についていく
私について真上からゆっくり句点へ落ちる落ちていく速度を保ったまま離れる
このサイト望遠鏡の鏡筒の先端に降り、視線が最初の星へ伸びる測定中の星から飛び去る

訪問者がホームから入ってくるとは限らないので、オープニングも同じ仕組みにした。初めてサイトに来たときは一面の夜で、中央に輪が一つ描かれている。ページの読み込みが終わるとその輪が描き終わり、中央の点が画面のど真ん中からこのページへ飛んでいく。

心を決めてからの四日間、止まりたいと思ったことは一度もなかった。今いちばん好きなのは写真、コレクション、ノートのページで、とくにノートの星の軌跡から記事ページへつながる部分だ。まだ少しカクつくところがあるし、素早く続けてページを移ったときの手触りも納得のいくところまで詰められていない。それでも今のところ、私の思う「なめらかさ」にいちばん近い形だ。

まだできていないこと#

ここまで読むと何もかも順調だったように見えるかもしれないが、まだできていない部分がいくつかあることは自分でよくわかっている。

スマートフォンと iPhone Duo#

デザイン全体はワイドな画面で考えたもので、スマートフォンでは「一応見られる」程度でしかない。ホームの3Dの土星はスマートフォンではとても小さくなり、衛星の名前はカーソルを重ねないと浮かばないのに、スマートフォンにはカーソルを重ねるという操作がない。作品のページの螺旋はスマートフォンでは縦向きになり、作品名がひしめき合う。今年9月に出た iPhone Duo はさらに厄介で、開くと内側の画面が7.6インチ、比率はほぼ正方形で、ちょうどスマートフォンとデスクトップの中間に収まる。今のレイアウトはそのどちらにも見えない。

この部分にはまだ答えがない。多くのページは画面そのものが内容なので、カードを一列に並べれば済む話ではない。たぶんページごとに「小さな画面で、この天体はどう見られるべきか」を考え直す必要がある。

読みやすさ#

公開後、X で、読みやすさが少し悪い、文字が読みづらいし、どこをクリックすればいいのかもよくわからない、というコメントをもらった。正直、デザインしたばかりのころは私もそう思っていた。ナビゲーションバーはふだん畳まれていて、カーソルを重ねると開く。ホームに入るとあるのは土星と何本かの線だけで、「ここをクリック」と教えてくれるボタンは一つもない。

これは意図した選択だ。訪れた人にまず感じてほしいのは画面いっぱいの迫力と、宇宙の中を自分で手探りする感覚だった。でもこの二つは「読みやすい、見つけやすい」と両立させるのが難しい。文字を太くしたり、記事の内容に色を増やしたりして、以前の MDX ブロックのように色とりどりにすれば、確かに読みやすくはなる。でもそうするとサイト全体のスタイルが調和しなくなっていく。少しずつ調整は続けているけれど、全体をどこまで寄せるべきか、その線はまだ見つかっていない。

見つけてもらえた日#

公開の翌日、管理画面で X からのアクセスがまとまって来ているのに気づいた。たどってみると、innei さんがこのサイトを紹介するポストをわざわざ投稿してくれていて、私のつぶやきにもコメントを残してくれていた。私はずっと彼のブログが好きだった。和風の紙の質感がとても美しく、記事の中のさまざまな要素のインタラクションや組版くみはんエンジンもとても緻密で、その開発への姿勢にも敬服していた。こんな小さなサイトはたぶん見ないだろうと思っていたので、その瞬間は本当に現実感がなくて、うれしいやら恐縮するやらだった。でも喜んだのもつかの間、すぐに考え始めてしまった。まだ直すべきところがあるんじゃないか、足りないところがあるんじゃないか。そう思うと、また少し人に見せたくなくなった。

私は彼のやり方を参考にした。そして今、彼が私の作ったものを見てくれた。この感覚は、『チ。』で描かれている継承に少し似ている。一人が残したものを次の人が受け取り、さらに遠くへ運んでいく。

「私がより遠くを見渡せたのだとすれば、それは巨人の肩の上に立っていたからです。」

ニュートン、1675年、フックへの手紙

innei さんのグループで私の相互リンク申請を見て訪ねてきてくれたサイト運営者もいた。このサイトはとてもきれいだと言ってくれて、ついでにメッセージのページで送信できないことも報告してくれた。そのバグはその日のうちに直した。このサイトは少し深いところに隠れた宝物のようなもので、細部はたくさんあるけれど、誰かに見てもらえるとは限らないとずっと思っていた。今回のリニューアルのあと、よそから訪ねてきて、中をあちこちクリックして回り、どこが読みにくいかを伝えに戻ってきてくれる人が現れはじめた。

572日目#

リニューアルを公開した日は、このサイトの572日目だった。航海日誌のページが描いているのはまさにこの期間だ。遠くへ向かう方向が時間で、地上からの高度は GitHub への貢献の累計、今のところ5,356回になる。

この線を振り返ると、最初はヒーローセクションとポートフォリオだけの一ページだった。2025年8月にようやくブログができ、10月に自分のドメインを持ち、今年の1月にサイト全体を自宅の HomeLab に移した。それからはほとんど毎月のように壊しては作り直してきた。4月に多言語対応と初めての大規模なデザインのやり直し、6月にはサイト全体を JavaScript から TypeScript に移し、フロントエンドを TanStack Start に移行した。7月には Express のバックエンドを丸ごと Rust に置き換え、WebGPU の宇宙背景を正式に採用した。そして今回だ。WebGPU、Rust のバックエンド、自前のサーバー、五つの言語への翻訳。多くのものはこのサイトのおかげで初めて触れたもので、そのほとんどは落とし穴にはまってから身につけた。その過程の多くはノートに書いてある。

旧サイトは削除せず、archive.koimsurai.com に凍結してある。もう私の望む姿ではないけれど、新しいサイトがこれほどなめらかになり、この四日間でこれほど速く作れたのは、旧サイトが一年あまりかけて築いてくれた土台によるところが大きい。

この572日間でいちばん大変だったのは、実はコードを書くことではなく、記事のテーマを考えることだった。このサイトをやめようと思ったことは一度もない。でも、なぜこんなことをしているんだろうとよく考えた。ずっと自分ひとりでやっているような気がしていた。だから今回、よそから誰かが訪ねてきて、中をあちこちクリックして回ってくれたことは、私にとって何よりも確かな手応えだった。

今回のリニューアルは、この572日間で学んできたことを、初めて自分の美意識で一つの完成した形にまとめたものに近い。まだ完璧ではなく、スマートフォン版も、読みやすさも、パフォーマンスも直すべきところが山ほどある。それでもこの線は、これからも上へ登り続ける。

付録:自分で作ってみたい人へ#

以下はトランジション、フォント、旧サイトの実装の詳細で、上の話とは関係ない。興味があれば読んでほしい。

収める、飛ぶ、伸びる#

トランジションは三つの段階に分かれている。収める、飛ぶ、伸びる。ページ移動はまずブロックされ、古いページが収まりきってから通される。飛んでいる間、新しいページはすでにマウントされていて、カメラと一緒に、点が向かう側から滑り込んでくる。点が着地してから、新しいページは自分の要素を伸ばしはじめる。

リンクをクリック / 戻るhistory blocker がこのページ移動をブロックleave()収まる先の点、初速、収まりきったときの Promise収まりきってから通す新しいページをマウント(まずカメラの外に隠す)anchor()、arrive() でアンカーと最後の区間を渡すフォーカス、追従、急停止、点が approach に沿って着地点が着地し、ページが伸びはじめる読者ルータートランジション層古いページ新しいページ読者ルータートランジション層古いページ新しいページ

トランジション層(TransitionLayer)は、ページ移動のブロック、点と尾の描画、カメラの移動を受け持つ。各ページは一つの hook で四つのことを伝えるだけでいい。アンカーはどこか、点が最後にどの道を通るか、どう収まるか、着地したら何をするか。以下は「私について」のページの版で、点が真上からまっすぐ「しっかりつかまって。」の句点に落ちる。

tsx
usePageTransition({
  // 点が飛び込んできて、タイトルの句点になって止まる
  anchor: () => stopPoint(titleStopRef.current),
  // 最後の区間:句点の真上からまっすぐ下へ落ちる。カメラも下へ追い、ページは下からせり上がる
  arrive: () => {
    const p = stopPoint(titleStopRef.current);
    if (!p) return null;
    const drop: Pt[] = [];
    for (let d = FALL_IN_PX; d >= 0; d -= 6) drop.push({ x: p.x, y: p.y - d });
    return { approach: drop, join: Math.round((FALL_IN_PX - FALL_SHOWN_PX) / 6), pace: FALL_PACE };
  },
  // 収める:文字が退き、琥珀色の点は落ちる速度を保ったまま離れる
  leave: () => {
    const point = dotRef.current;
    if (!point) return null;
    pageRef.current?.classList.add('is-leaving');
    return { point, velocity: { x: 0, y: 0.8 }, done: after(300) };
  },
});

approach は点が最後に通る道のりで、join はカメラがどの点で急停止するかを示す。その手前の部分は画面外にあってもかまわない。pace が1より小さいとゆっくりになる。「私について」のページは自由落下なので、ほかのページよりゆっくり落ちるようにした。ページが leave を渡さない場合、トランジション層は円形のマスクでページ全体をアンカーに収めるので、新しいページは最初からトランジションを書ききらなくてもいい。ノートから記事に入るとき、その星の軌跡はトランジション層に引き継がれて描かれ続け、記事ページが伸びてから消えるので、二つのページの間の線は途切れない。ページ移動のブロックには TanStack Router の router.history.block を使っている。戻る・進む(popstate)も同じ blocker を通るので、マウスのサイドボタンでページを移ってもトランジションがかかる。

エッジケース#

飛行そのものがなめらかになってから、いちばん時間を取られたのはさまざまなエッジケースだった。いちばん目立ったのは素早く続けてページを移る場合で、点が描きかけの線とホバー中の点が、次のページに残ってしまう。

  • 素早く続けてページを移る:トランジションの途中でまたページを移ったら、古いほうはその場で終わらせ、今回は演出しない。ページの「収める」は最大0.8秒まで待ち、一回のトランジションが8秒を超えたら異常とみなして即座に打ち切る。
  • 背景の星空がページを移るたびに元の位置へ跳ね戻っていた。空全体がずれたように見える。カメラの移動量を累積させ、ゼロに戻さないようにした。
  • マウスのサイドボタンでページを移ると、デフォルトの飛び方に戻ってしまっていた。ページが「どこへ収まるか」を判断するときにクリックされた要素を見ていたのが原因で、サイドボタンでの移動にはクリックされたものがない。行き先の ctx.to で判断するように変えた。
  • ある記事から別の記事へ移るときはトランジションを演出しない。記事ページはその場で入れ替わる。左のカラムは残り、タイトル、本文、目次だけが入れ替わる。
  • 光の滲みや明るい線はすべてゆっくり退かせる。一瞬で暗くなると停電したように見えるので、サイト全体で統一して、ページ本来の明るさへゆっくりフェードして戻すようにした。
  • オープニング:回線が遅くても最大3.5秒待てば飛び立つので、読み込み画面で足止めされることはない。言語を切り替えただけのとき、管理画面に出入りするとき、そしてシステムで「動きを減らす」を設定している読者には、トランジションは表示されない。

ページごとに必要な文字だけを読み込む#

繁体字の源ノ明朝の可変ウェイト版は1本で16.8 MBあり、簡体字、日本語、韓国語もそれぞれ1本ずつある。記事のフォント切り替えにはほかにもゴシック体、霞鶩文楷、MiSans などがあり、丸ごと読者に送るわけにはいかない。私のやり方は、ページごとに文字を切り出すことだ。ブラウザが MutationObserver で画面に使われている文字を集め、サーバーに /fonts/<style>/<weight>/<key>.woff2?c=<chars>&f=<locale> をリクエストする。サーバーは subset-font(中身は HarfBuzz の wasm)で、それらの文字だけを含む woff2 を切り出す。URL は文字集合で決まるので、永久にキャッシュできる。前回の記事 FurSpark の本文で実測したところ、全文で836種類の文字が使われていて、16.8 MB の元ファイルから切り出したウェイト400の woff2 は185 KB だった。

サーバー側で文字を切り出す中心のコード
ts
import subsetFont from 'subset-font';
// subset-font は実行時に require.resolve で HarfBuzz の wasm を探すので、バンドラーが追跡できない。
// 一度明示的に import して、harfbuzzjs をまるごと(wasm も含めて)出力に含める
import { Blob as HbBlob, Face as HbFace } from 'harfbuzzjs';

// 一回切り出すたびに元ファイル全体を wasm にコピーするので、長い記事で約0.7秒かかる:一度に一つだけ切り出し、順番待ちにする
let queue: Promise<unknown> = Promise.resolve();
const serial = <T>(job: () => Promise<T>): Promise<T> => {
  const run = queue.then(job, job);
  queue = run.catch(() => undefined);
  return run;
};

async function build(file: string, weight: 400 | 600, cps: number[]) {
  const font = await sourceOf(file); // 元ファイルは最近使った数本だけをメモリに置く
  return subsetFont(font, String.fromCodePoint(...cps), {
    targetFormat: 'woff2',
    variationAxes: { wght: weight }, // 可変ウェイトの元ファイルから、必要なウェイトを取り出す
  });
}

書き留めておきたい落とし穴が二つある。一つ目はバンドルだ。subset-font は実行時に require.resolve で HarfBuzz の .wasm を探すが、Nitro の追跡ツールはこれが依存関係だと見抜けず、デプロイしてからファイルが見つからないことに気づいた。解決策は、これを外部依存(traceDeps)に設定し、さらに harfbuzzjs を一度明示的に import することだった。二つ目は文字の欠けだ。繁体字のページにもときどき簡体字が出てくる(たいていはコメント)。今はリクエストに x=sc を付けられるようにしていて、サーバーはまず繁体字の元ファイルにない文字を調べ、足りない文字だけを簡体字のファイルから追加で切り出す。

旧サイトの残し方#

公開前に、旧版を git tag v1-final と Docker イメージとして凍結し、別のコンテナを立てて archive.koimsurai.com に置いた。新サイトと同じバックエンドを共有している。

バックエンドを共有するリスクは、旧サイトのフォームからまだデータを書き込めてしまうことだ。そこで nginx の層で読み取り専用に固定した。ログインは空の応答を返し、API は読み取りだけを通し、管理画面はメインのサイトへリダイレクトし、検索エンジンにもインデックスさせない。

旧サイトの nginx 設定
nginx
server {
    server_name archive.koimsurai.com;
    add_header X-Robots-Tag "noindex, nofollow" always;

    # 検索エンジンに旧サイトを登録させない。新サイトと同じ記事を奪い合わないように
    location = /robots.txt {
        default_type text/plain;
        return 200 "User-agent: *\nDisallow: /\n";
    }
    # 管理画面はメインのサイトへリダイレクト
    location /admin { return 302 https://koimsurai.com/admin; }
    # ログインは空の応答を返す
    location ^~ /api/auth/ { return 204; }
    # API は読み取りだけを通す
    location /api/ {
        if ($request_method !~ ^(GET|HEAD)$) { return 204; }
        proxy_pass http://127.0.0.1:<backend-port>;
    }
    location / {
        proxy_pass http://127.0.0.1:<archive-port>;
    }
}

パフォーマンスと公開後#

パフォーマンス修正の原則は、画面を変えないことだ。ノートから記事を開くと、以前は一瞬止まっていた。本文全体が同じフレームでマウントされていたからで、サイト内のページ移動で入ってきたときは分けてマウントするようにした。コレクションのページを再読み込みしたときの引っかかりは、背景の星空の worker が WebGPU の device を作っている最中だったせいで、入場アニメーションが終わってメインスレッドが空いてから作るように変えた。

50–67ms
ノートから記事へのフレーム
以前は 183–217ms
0ms
コレクション再読み込み時の引っかかり
以前は 67–85ms

まだ解決していないのはコレクションのページのいちばん外側だ。信号の輪の破線は、いちばん外までスクロールしたとき毎フレーム4万〜6万本の線分に切り直す必要があり、65 fps から45 fps 前後まで落ちる。機材のページのネットワーク図とテレメトリーにも一つ原則がある。構造は残し、機密につながる型番は外す。テレメトリーは温度、負荷、メモリ、消費電力だけを出し、数値は丸めて30秒キャッシュする。公開後の二日間でさらに二十数本の PR を出し、移行時に漏れていた右クリックメニュー、検索パネル、ライトボックス、ニュースレター、管理画面を補った。各ページのシェア画像もバックエンドで Rust の tiny-skia を使って描き直し、APNG で出力している。


ここまで書いても、このサイトはまだ伸び続けている。リニューアルは終着点ではなく、あの線の上にある、もう一つの節目にすぎない。最後に、やはり私のいちばん好きな言葉を。

Stay hungry, stay foolish.

參考連結