PRODUCT NOTE / PLATFORM PRD

nzw.jp

個人開発者 nzw のポートフォリオ兼 AI プラットフォーム。
この文書は、このサイトそのものをなぜ・誰のために・どんな思想で作っているかから記録した開発ノートです。

ステータスActive(継続運用・開発中)
ドメインnzw.jp(姉妹サイト zerohalf.jp と別ドメインで共存)
運営nzw(Hidekazu Nozawa) — PM × 作家 × バイブコーダー
載せているもの技術書 / モバイルアプリ / Web ゲーム / AI ツール / 開発ノート / グッズ
最終更新2026-10-11
技術構成FastAPI ・ Tailwind ・ Canvas ・ Flutter Web ・ Nginx ・ EC2 / Docker
関連リンク トップ ・ About(運営者) ・ グッズ ・ 開発ノート一覧
TL;DR

ひとことで

nzw.jp は、1 人の開発者がつくるアプリ・ゲーム・AI ツール・技術書・開発ノートを一元公開する場です。

単なる作品展示場ではなく、「AI に任せる」ではなく「AI を使うことで、あなた自身が強くなる」という設計思想(zerohalf ミッション)を体現するプラットフォームを目指しています。

Problem / Mission

背景・なぜ作るか

土台にあるのは zerohalf の考えで、AI に何でも任せられる時代に、自分の手には何が残るのかを確かめたい、というところから始まっている。

そこで、書いた本と、作ったものと、その開発ノートを同じ場所に置くことにした。それが nzw.jp になっている。

Who

ターゲット

見せる相手

  • アプリ・ゲームのエンドユーザー
  • AI エージェント設計に関心のあるエンジニア・PM
  • 設計の相談をくれる相手(仕事の入口)

nzw の立ち位置

PM と作家とバイブコーダーを 1 人でやっている。汎用の AI に無いのは、対象への偏りと、そこまで積み上げてきた文脈のほうだと思っている。

Scope

載せるもの・載せないもの

スコープ(In scope)

  • 技術書『チームに溶け込む AI エージェント開発』
  • モバイルアプリ(Engly / DayGrid / MyDictly)
  • Web ゲーム(Synapse Link / BOM ほか 30+ 作品)
  • AI ツール(AI Workspace / グループチャット)
  • 開発ノート(本ドキュメント群)
  • グッズ(/goods/:アクリルキーホルダーほか)
  • zerohalf ブランドへの導線

やらないこと(Non-goals)

  • Big Tech の土俵(汎用パーソナル OS)での正面勝負
  • 「AI に任せて終わり」を促す体験
  • なぜ作ったのかを書かず、作品を並べるだけにすること
Success Metrics

成功指標(KPI)

「思想とプロダクトが結びついて届く」状態かを見る。指標は仮置き

本 → 相談
書籍からの設計支援問い合わせ
(B2B 導線が回るか)
回遊
アプリ ⇄ 開発ノートの回遊
(裏側まで読まれるか)
継続
各プロダクトの継続利用
("育てる AI" が刺さるか)
Front-end Craft

徘徊するブラックホール

アプリ一覧・開発ノート・デザインシステムのページには、ブラックホールが 1 匹だけ現れて画面上をゆっくり横切る。Synapse Link の敵役をそのまま連れてきたもので、タップするとゲームが開く。

最初はトップページに置いていたが、後述の理由でこちらへ移した。絵の中身(6 層の重ね方・配色・脈動)は Synapse Link の開発ノートに書いたので、ここではサイトに載せるためにやったことを残す。

Flutter の描画を Canvas 2D に置き換える

原典は Flutter の BlackHoleRenderer.draw()。層の順番・係数・色は一切変えず、API だけ差し替えた。数値を触らないと決めたので、見比べても違いが分からない状態を保てている。

FlutterCanvas 2D注意した点
ui.Gradient.radialctx.createRadialGradient()停止位置の配列をそのまま addColorStop に写す
MaskFilter.blurctx.filter = 'blur(7px)'未対応環境があるので起動時に一度だけ判定し、駄目なら素の輪郭で描く
BlendMode.plusglobalCompositeOperation = 'lighter'降着円盤だけに掛け、save/restore で確実に戻す
HSLColor.fromAHSLhsla(...) 文字列色相は 0〜360 に丸めてから渡す
Color.lerp自前の lerpColor()危険度による紫 → 赤 → 金の補間で使う
1 フレーム = 60fps 前提step = dt ÷ (1/60)原典の += 0.02 にこの係数を掛け、高リフレッシュレートでも速くならないようにした

徘徊のさせ方

ゲーム内では重力計算で動くが、サイトには引き合う相手がいない。そこで移動だけを別に書いた。まっすぐ飛ばすと単なるスクリーンセーバーになるので、気まぐれさを足している。

置き場所を読み物のページへ移した

トップページに置いていた頃は、時間帯ごとの抽選(朝・昼・夕は 3 回に 1 回、夜は 2 回に 1 回)で「たまに出会うもの」にしていた。

いまはアプリ一覧・開発ノート・デザインシステムに常駐させ、抽選はやめている。経緯は下の意思決定ログに書いた。

大きさは blackHoleSizes の tiny / small / medium から等確率で引くので、出会う大きさは毎回違う。

速度は原典の表どおり大きいほど遅く、tiny は 36px/秒、medium は 12px/秒。

開発ノートでだけ声量を落としている。このページ群は「色は情報として使わない」で組んであるので、原典の配色のままだと紫がページで唯一の彩度になる。

notes.css の末尾で濃さを 0.34 / 0.22、彩度を saturate(0.3) まで下げ、6 層目の VOID タグは切ってある(本文の上に数字が重なるのが、読むときにいちばん邪魔だった)。

アプリ一覧は黒地の製図台なので素のまま。

コンテンツの前と後ろを行き来する

ずっと手前にいると邪魔になり、ずっと奥だとほぼ見えない。そこで z-index を往復させている。

重なり順の図。手前は z-index 30、奥は z-index -1 で、コンテンツを挟んで往復する。 ヘッダー z-index: 100 ブラックホール(手前)+ 当たり判定 z-index: 30 / 31 カード・セクションなどのコンテンツ z-index: auto ブラックホール(奥)+ 当たり判定 z-index: -1 本文・図版(地は透過) z-index: -1 body の背景(方眼のドット) —

負の z-index は body の背景より上・コンテンツより下に入る。だから奥に回しても消えず、半透明のカード越しにぼんやり透ける。

効いている小技

踏んだ罠 classList.remove() は消す対象が無くても class 属性を書き直す。

後片付けを毎回無条件に呼んだ結果、MutationObserver → 後片付け → 属性の書き換え → MutationObserver …… とマイクロタスクが循環し、ページが完全に固まった。

動作中かどうかのフラグを見て、必要なときだけ片付けるようにして解決。

Scene

About ページの空と生き物

About は、季節と時間帯で見た目が変わる。春夏秋冬の山の色と、朝・昼・夕・夜の空を重ねた上に、鳥・龍・恐竜・うさぎとかめ・花火・流れ星などをときどき通す。2026-10-08 から 10-10 の 3 日間で、1 枚の HTML の中に足していった。

足すたびに「この組み合わせはおかしい」と指摘が入り、その分だけ作り直している。実装の手順と層の順序は、リポジトリの docs/about-scene.md に置いた。ここには、直した理由を残す。

何を出すかは、1 つの表で決める

演出ごとの script が、それぞれ「冬は蝶を出さない」「夜は恐竜だけ」と自前で分岐していた。出す条件の正本を NZW_RULES に集め、各 script はそこを引くだけにした。

出るもの条件
蝶春・夏の朝と昼だけ
コウモリ夜の春・夏・秋。冬は冬眠なので出さない
うさぎとかめの競走春・夏・秋の朝・昼・夕。冬はかめが冬眠、夜は寝ている
花火夏の夜(12〜28 秒おき・1〜3 発)、夏の夕、冬の夜だけ。春・秋・昼は出さない
流れ星・天の川・星座夜。流れ星は夏と冬が多く、星座は季節ごとに入れ替わる
雲海朝に出やすく(0.6)、夜は出にくい(0.15)。秋はさらに 1.5 倍

山の後ろにあるものは、山の形で切り抜く

山は不透明度 0.55〜0.96 の半透明で描いている。後ろに置いた太陽や恐竜が山の色越しに透け、山の手前にも後ろにもあるように見えた。山の色は他の面と揃えているので変えず、後ろの層を稜線より上だけに切った。

稜線の高さは、SVG パスから 3,000 点を拾い、x ごとの最小の y を表にして引く。同じ切り方が、太陽・雲・花火・夜空・恐竜の 5 つにそのまま使えた。山の上を歩く小さな動物は、遠景まで含めると主峰の上で隠れてしまうので、主峰だけで切っている。

動きは、毎フレーム計算する

最初の蝶と龍は、CSS の keyframes と、胴を流れる波で作った。蝶は羽を一定の周期で潰すだけで点滅に見え、龍は波が頭から尾へ流れるので蛇のように捻れた。蝶は羽ばたき・上下動・向きを同じ時計で計算し、龍は地面に固定した 1 本の曲線を頭も胴もなぞる形に作り直した。

放置と負荷

iPhone の Chrome で、放置するとリロードされるという報告を受けた。ページ側にリロードの処理は無い。動きを減らす設定で生き物を止めると、放置中のメインスレッドは 1 秒あたり 0.003 秒で、メモリは 105 秒間 3.8〜4.2MB のまま増えなかった。ブラウザがタブを破棄している可能性が高いが、実機では確かめていない。スマホ幅では背面ぼかしを外し、雲海を 4 層から 2 層に、花火の canvas を等倍にした。

山の動物は 2 列で出し、同じ動物は 1 分あける

恐竜と、山羊・うさぎ・かめを 1 本の時計で順番に出していた。恐竜は 1 分近く歩くので、そのあいだ小動物が出ず、「恐竜ばかり」に見えた。層が別(奥の山の後ろと、二つの山のあいだ)なので、時計を 2 本にして同時に歩かせた。85 秒の計測で、見えていた時間は恐竜 44 秒、うさぎとかめの競走 52 秒、山羊 23 秒だった。

同じ動物は、消えてから 1 分は出さない。鳥と群れは同じ「鳥」、競走は「かめ」と「うさぎ」の 2 匹ぶんを数える。110 秒の観察で、同じ動物の早い再登場は無かった。

大型の怪獣は、縮ませずに歩かせる

恐竜の 3〜5 倍の上半身を、奥の山の後ろに出した。姿は 3 回変わった。最初は牙と赤い目で「怖い」、次にやさしい顔にして「怪獣らしくない」、最後に角ばった顔・青白い目・鋭い背びれの鋼色になった。目は赤にしない。

消え方も 2 回直した。沈めると不自然で、遠ざけて縮めて薄れさせると「なぜ縮むのか」と指摘された。いまは縮まず、歩きながら入って(画面の端から、または途中で坂を上りながら)、歩きながら出る(反対の端まで、または坂を下りながら)。4 通りの組み合わせから毎回選ぶ。

天気は、種類と強さの 1 つの値に畳む

天気は晴・曇・雨・雷・雪の 5 種で、それぞれ弱・中・強の 3 段階にした。値は NZW_WX の表 1 つで、空のおおいの濃さ(cover)、雨雪の粒数・速さ・傾き、稲光の間隔がそこに入っている。花火は cover が 0.35 を超えると上げず、流れ星は 0.2 から止める。雨と雷の日は、鳥・蝶と、猫・うさぎ・かめが出ない。

種類弱 / 中 / 強効くもの
晴快晴 / 晴れ / 雲多め雲の数(快晴は 1 個、雲多めは 2 個増える)
曇薄曇り / 曇り / どんよりcover 0.26 / 0.44 / 0.62。昼の雲は 9 / 13 / 17 個
雨小雨 / 雨 / 大雨粒 110 / 300 / 680(1440×900 換算)。雲海を出しっぱなしにする
雷遠雷 / 雷雨 / 激しい雷雨稲光の間隔 16〜34 / 6〜16 / 3〜8 秒。雨は雨より少し多い
雪ちらつく雪 / 雪 / 吹雪粒 70 / 190 / 480。恐竜と怪獣の頭と背に積もる

おおいは雲の下、太陽・月・星の上に敷く。雲の上へ敷いた最初の版では、雨の日でも太陽の輪郭が透けた。降っているあいだは不透明度を 0.97 にして、太陽・月・星を隠す。薄曇りだけは 0.5 前後にして、太陽がぼんやり透ける。

風は、天気とは別の軸にする

風は無風・そよ風・強風・突風の 4 つで、天気とは独立して選ぶ。自動のときは天気の強さに合わせて抽選し、嵐では強風と突風が出やすい。値は 80 ミリ秒ごとに更新する 1 つの数(0〜1.4)で、雨雪の傾きと横流れ、雲と雲海の流れる速さ、山頂の旗の傾きが、同じ値を見る。

突風は、ふだんの弱い風に、4〜11 秒おきの山を足している。立ち上がりが 0.5 秒、保持が 1.2 秒、収まりが 2 秒。旗の傾きを 12 秒間 0.5 秒ごとに読むと、2〜14 度のあいだで 2 回持ち上がって収まった。

雲海の下だけに降らせる

範囲を「雲海の下」にすると、山より上は晴れのまま、雲海の上端より下だけに雨・雷・雪が降る。実装は、ルール表が「いまの天気」を晴れ・中に読み替えるだけで、生き物・花火・星の script には分岐を足していない。雲海は出しっぱなしにして、雨のときは雨雲の色に暗くする。稲妻は雲海の中から下へ落とす。

降る日の動物は、雨宿りでまとめる

雨・雷・雪の日は、山の小動物が歩かない。2〜4 組が同時に出て、数十秒のあいだじっとしている。雨と雷は稜線に大きな葉を立てて、その下に入る。雪は鎌倉を 1〜2 つ作り、入り口の中に 1〜2 匹ずつ入る。動くのは息づかいと、首・尾のゆっくりした揺れだけ。恐竜と怪獣は残し、降る日は立ち止まる時間を長くした。雲海の下だけに降る回は、山の上が晴れなので、動物はふだんどおり歩く。

降る日の雲は、空のおおいと別の色にした。雨と雷は暗い雨雲、雪は白っぽい雲にして、灰色のおおいに溶けないようにしている。最初の版は雲を雲の色のまま暗くしたので、おおいと同じ灰色になり、数を増やしても「雲が少ない」と言われた。花火は、降っているあいだは範囲に関わらず上げない(雲海の下だけの回でも)。

飛空艇は、龍と城のあいだの頻度で飛ばす

細長い気嚢の下に木の船体を吊り、尾に回るプロペラと 2 枚の尾翼を付けた。飛ぶ頻度は城(0.07)と龍(0.3)のあいだの 0.45 にして、昼夜と季節を問わず出す。降る日(雨・雷・雪)は出さない。夜は船室の窓に明かりが灯る。プロペラは横から見た回転を、縦の刃を伸び縮みさせて見せている。風が強いほど速度を上げる(0.8〜1.6 倍)。

雨雪は雲の底から出し、雲より奥に描く

雨・雪の粒は、画面に出ている雲それぞれの底(絵の高さの 78%)の横幅の内側から出て、下へ落ちる。雲海の下だけに降る回は、雲海の底から出る。描画の canvas は雲の層より奥(空の層の中、雲の手前でなく後ろ)に置いたので、雲の絵が粒の上端を隠す。稲妻も、雲の底から落とす。最初の版は、画面の上端から粒を出し、雲や山、本文の手前に描いていたので、「雨の発生位置が誤っている」と言われた。雲の奥に回したら、昼の灰色のおおいの上では筋が溶けたため、昼は濃い青灰、夜は明るい青に塗り分けた。

雲海の下に降らせるときは、雲海を厚くする

範囲が「雲海の下」のとき、雲海を上端そのままに、下へ 116px 伸ばして厚くする。山頂は雲の上に残る。大きい丸の層は 26% 下げ、上端には小さい丸の層を足して、頂を波打たせた。粒は雲海の上端で切るので、雲が薄いと空から降っているように見える。

大型怪獣が出たら、地上の生き物はみんな端へ寄る

大型怪獣が姿を見せたときだけ、出会った地上の生き物(山羊・うさぎ・かめ・猫・登山夫婦・雨宿り中の動物)が、頭の上に «!» を出して 0.8 秒固まり、近い画面端へ駆け寄って、怪獣のほうを向いて見守る。怪獣が去って 1.6 秒たったら、端から歩いて出ていく。以前は、ふつうの恐竜にも猫が驚いて逃げていた。恐竜では動かさず、怪獣だけにした。出現の重みは 0.015 で、城より少ない。

山頂でテントを張る

晴れの日、登山夫婦は山頂の旗の下で立ち止まったあと、6 割でテントを張る。テントは山頂の平らな台地(x 598〜682)の真ん中に、台地からはみ出さない幅で立てる。最初の版は旗の少し先に立てたので、屋根が斜面にはみ出して見えた。張ったら全員(夫婦と猫 2 匹)でテントの中へ入り、窓に灯りをともして、ご飯の小さい煙を出す。6〜11 秒ごとに外へ出て、人間と猫で台地を走る・猫だけで走る・夫婦で珈琲を飲む(湯気)のどれかを 6〜9 秒して、また中へ戻る。40〜70 秒過ごしたら、たたんで歩き出す。雨・雷・雪の雨宿りでは、4 割の回で 1 組がこのテントの組になり、同じ台地で同じように過ごす。

山頂の台地は、画面幅に合わせて広げる

山の形は viewBox 1440 に対する比で描いてあるので、スマホ幅(390px)だと台地(元は幅 84 単位)が約 23px しかなく、テントも夫婦も乗らなかった。台地が約 34px になるまで、山頂を左右へ広げて描き直す(半幅 42〜70 単位)。左の肩は、谷から台地の端までを 1 本のなだらかな曲線で上らせる。最初の版は肩を近くから急に立ち上げたため、スマホで崖のように見えた。旗の位置は変えない。広げたあとは稜線の高さ表を作り直す。テントの幅も台地の幅から決める。テントの色はサンド(カーキ)の山岳テントにそろえた。

テントの前では、夫婦が鍋でご飯を作る、椅子に座って顔を上げ、星や空を眺める、が外での過ごし方に加わった。大型怪獣が生き物のエリア(体の幅 + 30px)まで入ってきたら、端で見守っていた生き物は画面の外へ逃げ出す。風で揺れるもの(旗・花畑・テントの旗・葉の傘)は、雲と同じ向きで傾く。

夫婦は必ず組で出る

登山夫婦は 1 枚の絵に 2 人を描いてあるので、必ず一緒に出る。猫は茶と黒の 2 匹で組にし、1 匹で歩く回は無くした。1 匹が先に歩き出し、5〜12 秒おくれてもう 1 匹が 1.8 倍の速さで追いかける。先頭は山頂の旗のあたりで待ち(最長 60 秒)、追いついたら一緒に山を越える。雨宿りの場面でも、猫は必ず 2 匹で入る。

猫は、部品を抽選して性格を足す

猫の遊びは、入り方 2 通りと去り方 2 通りのあいだに、つなぎを 2〜3 個はさむ。つなぎは 8 種で、蝶を 2 匹で追う・1 匹だけ追う・毛づくろい・忍び寄り・しっぽ追い・昼寝・散歩・鬼ごっこ。春の昼寝は、花畑(花 9〜12 本)の中で寝る。大型怪獣が出たときだけ、びっくりして画面端へ寄る(ふつうの恐竜には反応しない)。茶キジは足が速くてよく跳び、黒はゆっくり付いていく。

鬼ごっこは、鬼が逃げる側を追い、触れたら交代する。新しい鬼は 0.7 秒その場で数える。茶は足が速いので鬼でもすぐ追いつき、黒が鬼のときは茶がからかうように逃げる。

登山夫婦と、雪の積もり

山羊くらいの大きさで、手をつないだ 2 人が山頂の旗を目指して歩く。旗の下で 4.5 秒立ち止まって跳ね、また歩き出す。冬だけ冬の服(ダウン・ニット帽・マフラー)になる。6.5 割で猫を連れ、猫は先を歩いて、旗の下では一緒に止まる。昼間だけ出て、雨と雷の日は出ない。

雪の日は、積雪量(0〜1)が約 28 秒で上がり、降りやむと約 14 秒で下がる。恐竜と怪獣の頭と背の雪が、その量に合わせて厚くなる。雲海の下だけに降る回は、山の上の恐竜には積もらない。

踏んだ罠

Operations

構築・運用でつまずいた場所

サイトが落ちるときは、たいてい作り込みではなく足回りで落ちる。同じ穴に二度落ちないよう、実際に踏んだものだけを残す。

証明書が 1 枚だけ切れて、5 日気づかなかった

2026-08-30、zerohalf.jp がブラウザで NET::ERR_CERT_DATE_INVALID になった。

証明書は 2026-08-25 に失効していて、同じサーバーの nzw.jp は正常に更新されていた。同居している 3 枚のうち 1 枚だけが更新に失敗し続けていたという壊れ方をしていた。

原因は認証方式の食い違い。zerohalf.jp の renewal/*.conf だけ authenticator = standalone になっていた。

standalone は certbot 自身が 80 番ポートを listen して ACME チャレンジに応える方式なので、nginx が常時 80 番を握っているこのサーバーでは構造的に成功しない。

ログを遡ると、更新開始ライン(失効の 30 日前)にあたる 2026-07-26 の初回試行から失効まで、1 日に数回のペースで同じ失敗を繰り返していた。

約 1 か月ぶんの再試行が、すべて同じ理由で落ち続けていたことになる。

証明書authenticator結果
nzw.jpnginx自動更新され続けていた
zerohalf.jpstandaloneCould not bind TCP port 80 で毎回失敗 → 失効

復旧は certbot certonly --nginx --cert-name zerohalf.jp -d zerohalf.jp -d www.zerohalf.jp の 1 発。

これで renewal 設定が authenticator = nginx / installer = nginx に書き換わり、nzw.jp と同じ形に揃った。

認証方式はサーバー内で 1 つに統一するのが結論で、standalone が使えるのは nginx を止められる新規構築時だけ。

教訓 証明書は「取れた」で終わりにせず、2 回目の自動更新が通るところまでが構築。certbot renew --dry-run は全証明書をまとめて検証するので、1 枚足したら必ず流す。

そして certbot は更新に失敗しても何も通知しない。今回の 5 日間の空白は、失効を知る手段がブラウザで見ることしか無かったせいで生まれた。

証明書まわりに残っている地雷

デプロイスクリプトの癖

公開は ./deploy.sh(html/ を EC2 の /var/www/html/ へ rsync)だけだが、オプションの選び方がそのまま運用上の制約になっている。

オプション意図結果として起きること
--delete なし取り返しのつかない削除を防ぐローカルで消したファイルはサーバーに残る。古いページが URL 直打ちで生き続けるので、消すときは手で消す
--exclude=*.apk数百 MB の転送を毎回やらないAPK は rsync に乗らない。リリースのたびに別途アップロードが要る(現在 10 本・約 1.1 GB)
--exclude=.well-knownACME のチャレンジ置き場を守るrsync で .well-known を消して証明書更新を壊す事故が起きない
--checksum中身で差分を判定するタイムスタンプだけ変わったビルド成果物を取りこぼさない代わりに、転送前の走査が重い

nginx でしか直せない種類の不具合

失効を二度と見逃さないための監視

今回いちばん効いたのは更新の失敗そのものではなく、失敗が誰にも届かなかったことだった。

そこで /usr/local/bin/cert-expiry-check.sh を置き、cron で毎日 JST 09:10(certbot のタイマーが動いたあと)に回している。

設計で決めたのは次の 6 点。

踏んだ罠 .env の SLACK_WEBHOOK_URL は .env.sample のプレースホルダ(YOUR/WEBHOOK/URL)が残っていただけで、送信しても no_team の 404 が返るだけだった。

設定されていることと、届くことは別。プレースホルダのままなら送信をスキップして理由をログに書く作りにし、URL を差し替えた時点から通知が始まるようにしてある。

2026-08-30、Slack の Incoming Webhook を作成して差し替え、実際に投稿が届くところまで確認した。

以後は毎日 JST 09:10 に自動で走り、判定結果は /var/log/cert-expiry-check.log にも残る。

Slack は無料プランのままで問題ない(2022 年 9 月に「連携アプリ 10 個まで」の上限が撤廃されているため)。

ただし無料プランはメッセージ履歴が 90 日で古いアラートを遡れないので、ログ側を一次資料として残している。

秘密情報の置き場が緩い(未解決)

監視の通知先を入れたことで、/home/nzw/api/.env に秘密情報がもう 1 つ増えた。このファイルは現在 644 で、サーバー上の他ユーザーからも読める。

教訓 秘密情報を新しく足すときは、置き場所の権限が足す前から足りているかを先に見る。動かすことを優先して .env に追記していくと、緩い権限のまま中身だけが重くなっていく。

Decision Log

主要な意思決定ログ

サイトを支える判断を、決定・仮説・結果の 3 点で残す。

weather 天気は「種類 × 強さ」の 1 つの値に畳み、空も生き物も同じ値を見る
DECISION
About に天気(晴・曇・雨・雷・雪を弱・中・強)と風(無風・そよ風・強風・突風)を足した。値は NZW_WX の表 1 つで、空のおおい・雨雪の粒・雲の数・生き物と花火の出し分けが、同じ値を引く。雲海の下だけに降る範囲も選べる。そのときは山より上を「晴れ」として扱い、ルール表の側で種類を読み替える(描画の側には分岐を足さない)。
WHY / 仮説
「曇りや雨、雪などの日に太陽や月が見えるのはおかしい。また雲が少ないのも変」と指摘された(2026-10-10)。最初の版は、薄いおおいを雲と太陽の上に敷いていた。雨でも太陽の輪郭が透け、雲も 8〜10 個しか出ていなかった。おおいを雲の下へ移し、降っている日は太陽・月・星を隠し切る濃さにした。「雲海の下だけ」は、山頂から雲の下を見下ろす景色の依頼で、空まで暗くすると山頂にいる感じが消える。
RESULT
夏の昼の「どんより」で太陽が見えなくなり、雨でも同じだった。雲は昼の曇りで 9〜17 個、降る日で 16〜22 個になった。雲海の下だけの回は、夜でも月と星と天の川が出たまま、雨が雲海の上端より下だけに降った。ルール表の変更は読み替え 1 か所で済み、生き物の script は変えていない。
cats 猫の遊びは「部品」を抽選し、性格を足す
DECISION
猫の遊びを、入り方 2 × つなぎ 8 × 去り方 2 の抽選にした。びっくりして画面端へ寄るのは、大型怪獣のときだけにした。茶キジは活発(足が速く、よく跳ぶ)、黒はおっとり(ゆっくり、よく止まる)。ほかに、猫夫婦(茶と黒)の散歩と、登山夫婦に付いていく猫 2 匹を出す。
WHY / 仮説
「猫の動きが毎回ワンパターンです。恐竜を見つけて逃げるが多いです」と言われた(2026-10-10)。それまでは「走って入る → じゃれる → 蝶 → 走り去る」の 1 本で、恐竜が出れば必ず逃げた。恐竜は数十秒おきに出るので、見た人の記憶には「逃げる猫」だけが残る。続けて「茶キジの方が活発」と、蝶を見失ってキョロキョロする動きが不自然だという指摘が来た。後者は、蝶が真下を通るたびに向きを入れ替えていたのが原因だった。
RESULT
つなぎの並びだけで、2 個なら 56 通り、3 個なら 336 通りになった(8 種)。花の匂いを嗅ぐ回は、花が出た瞬間に猫が固まり、花が猫の体に重なって見えるうえ、冬にも花があって不自然だという指摘で外し、春だけの花畑で寝る回に置き換えた。花畑と「1 匹だけ蝶を追う」は、ヘッドレス Chrome で動きを確かめた。蝶を追う回では、向きを変える条件を絞った。実機での長時間の確認はまだ。
ui 操作は 1 枚のパネルに揃え、開閉は保存しない
DECISION
季節・時間・天気・強さ・範囲・風のボタンを、行ごとに「名前 | 切り替え」を揃えたパネルにした。閉じている間は現在値を 1 つのチップにする。ヘッダーと自己紹介カードの開閉は保存せず、開くたびに閉じて始める。カードは、ふだんほぼ透明(不透明度 8%)にして、カードの上にマウスを乗せるかタップしたときだけ 92% に濃くする。マウスを外したら、0.35 秒でまた透明へ戻す。
WHY / 仮説
「ヘッダーがゴチャゴチャしすぎている。コントローラーとして整理されたデザインに」と言われた(2026-10-10)。軸が 2 つから 5 群に増え、1 行に収まらず、右寄せで折り返した群の位置が行ごとに違った。開閉の保存は「状態保存は不要」とのことで外した。透明度は、マウスをカードの外で動かしただけで戻るのが意図と違った。
RESULT
6 行が同じ幅の台に揃い、スマホ幅(390px)でも 1 行 1 群で収まった。パネルはヘッダーに重ねて出すので、ページの高さは変わらない。外側のクリックと Esc で閉じる。
news お知らせは全ページ共通のベルに置き、中身は news.json 1 本にする
DECISION
共通ヘッダーにベルを足した。押すと最新 5 件が開き、未読は数字で出る。ハンバーガー内の「お知らせ」行と、一覧の /news/(種類で絞り込める)も同じ html/static/news.json を読む。リリースや変更のたびに、この 1 ファイルへ 1 件足す。足したあとは scripts/check_news.py が、重複 id・日付の逆順・不正な種類を落とす。
WHY / 仮説
「リリースしたもの、変更した内容などをニュースにしたい」と依頼された(2026-10-09)。About の演出とトップの表示設定は、載せる場所が無かった。ページごとに「新着」の欄を作ると、更新のたびに HTML を何枚も直すことになる。共通ヘッダーは site-nav.js が全ページへ描くので、ベルをそこへ足せば 60 余りのページに一度で出る。未読は localStorage に最後に見た id を 1 つだけ持つ。件数でなく id の位置で数えるのは、件が増えても減っても「それより新しいもの」が正しく出るため。
RESULT
最初の 4 件(About の演出・ベル・トップの表示設定・3 つの画面)で、ベルに「4」が出て、開くと 0 に戻り、別のページでも 0 のままになることを確かめた。site-nav.js を変えたので、60 余りのページの読み込み指定(?v=)を一括で上げた。作業ツリーに別件の変更があるファイルは、版の文字列だけを変えた内容でコミットした。
scene 出す・出さないは 1 つの表で決め、合わないものは確率を下げずに出さない
DECISION
About ページの生き物と空の演出を、季節 4 × 時間帯 4 の表(NZW_RULES)で決める形にした。合わない組み合わせは確率を下げるのではなく、出さない。季節や時間帯が変わった瞬間に、飛行中・歩行中で合わなくなったものは 0.8 秒で消える。
WHY / 仮説
「恐竜ばかり表示され、山羊やうさぎと亀が出なくなりました」「時間帯や季節によって出す内容を今一度整理してください」と指摘された(2026-10-09)。原因は、例外が重なったことにあった。雲海が出ているあいだは恐竜だけ、夜も恐竜だけ、という 2 つの除外が、別々の script に書いてあった。1 つずつは筋が通っていたが、足し合わせると山の動物が恐竜に偏った。確率を下げる案も考えたが、冬の蝶や夜のかめが低い確率で出ると、見た人は偶然ではなく不具合と受け取る。
RESULT
16 通りの組み合わせで、飛ぶもの・山の動物を 300 回ずつ抽選し、表に反するものは0 件だった。夏の夜に山の動物を 400 回抽選した結果は、山羊 181・うさぎ 122・恐竜 97(直前は恐竜だけ)。蝶が飛んでいる最中に冬へ切り替えると、1.5 秒後に 0 匹になった。
scene 半透明の山は変えず、後ろにあるものを山の形で切る
DECISION
山の色と透け具合はそのままにして、太陽・雲・花火・夜空・恐竜の層を山の稜線より上だけに切り抜くことにした。稜線の高さは SVG パスから表にして引く。
WHY / 仮説
「太陽が山や雲に隠れていないです」「山の手前と後ろに同時にあるのがおかしい」と指摘された(2026-10-08〜09)。山の方を不透明にする案もあったが、山は空の色と混ざる前提で配色してあり、不透明にすると全ページの季節の色を取り直すことになる。後ろの層を切る方が、変更が 1 ページで済む。
RESULT
夕方の画面で、右下の太陽の下半分が雲海の向こうに隠れた。同じ切り抜きを 5 つの層に使い回せた。最初は切り抜きが効かなかった。太陽が <svg> で offsetWidth が取れず、clip-path が不正になっていたためで、幅の読み方を直して解決した。
stats 統計は人が手元で回すのをやめ、CI が数え直して止まったら知らせる
DECISION
トップと開発ノートの実数(コミット数・リポジトリ数)を、GitHub Actions が毎回の push と毎日 05:00 JST に数え直す形にした。兄弟リポジトリ 12 本は scripts/fetch_repos.py が treeless の mirror で取る。deploy.sh が握り潰していた警告は build/deploy-warnings.txt に溜め、scripts/notify_slack.py が Slack と Actions の step summary へ出す。「止まっていること自体」を scripts/check_stats_fresh.py が検査する。画面のベタ書きの数字は gen_home_stats.py --fix-html が印へ書き戻し、人は打ち替えない。
WHY / 仮説
「この数値が更新されないのは、なぜですか?」と指摘された(2026-10-03)。トップは 1147 コミット / 11 リポジトリ / 9/20 まで で 2 週間以上止まっていた。原因は 2 つあり、どちらも「人が手元で回して気づく」前提だった。(1) 数える材料が CI に無い。コミット数は nzw.jp の外にある 12 リポジトリを git log --all で数えるが、Actions はこのリポジトリだけをクリーンに checkout するので兄弟が 1 本も無く、生成器が「見つかったのは自分だけ」で更新をやめる。そのうえ gen_home_stats.py がコミット数を null で書き、本番の良い値を毎回潰していた(JSON が null だと home-stats.js も直せない)。(2) 警告を誰も読まない。deploy.sh は生成器の失敗を握り潰して配信を続ける——統計 1 つが作れないだけで本番への配信を止めないためで、判断としては正しい。だがワークフローは毎回 successなので、ログを開かない限り気づけなかった。見張りの判定に生成時刻を使わないのは、数えられないとき前の値を引き継ぐぶん生成時刻だけが新しくなって嘘をつくため。週ごとに進むはずの window_end を見る。
RESULT
treeless の mirror なら 12 本で 2.2MB・30 秒(実測)。CI が数えた窓は 1,304 コミットで、手元の 1,308 との差 4 は push していないローカルコミット——公開ページとしてはこちらが正しい。入れた初回の実行で、それまで握り潰されていた警告が7 件まとめて表に出た。同じ仕組みが app_status_registry.json の食い違い 8 箇所(Certly が v1.5.0・Bearise が v1.1.1 のまま。タグと pubspec はどちらも先に進んでいた)も出したので、裏を取って直した(39 箇所一致)。残るのは Secrets 2 本の登録で、どちらも無くてもデプロイは通る(統計が前の値のまま配信され、警告が step summary に出るだけ)。PAT が期限切れになっても、window_end が進まなくなるので通知が教えてくれる。
motion 開発ノートから徘徊するブラックホールを外す
DECISION

開発ノート 23 ページ(一覧を含む)で読んでいた bh-wanderer.js と bh-ambient.js をやめた。

残すのはアプリ一覧(/apps/)とデザインシステムの 2 ページ。

notes.css にあったトーン調整のうち、アプリ一覧が使い続けている saturate(0.3) は apps.css へ移し、開発ノート用の濃さ(0.34 / 0.22)は消した。

WHY / 仮説

「開発ノートにブラックホールの徘徊は要らない」という本人の判断。2026-09-04 にトップから移したときから、モノクロの紙面に紫の降着円盤が浮く懸念を残し、濃さを下げたうえで「実物を見てから」としていた。

その結論にあたる。

RESULT

開発ノートの各ページで読む JS が 2 本(計 66KB、gzip で約 21KB)減った。アプリ一覧の見た目(濃さ 0.5 / 0.32・saturate(0.3))は変えていない。

デザインシステムと Synapse Link のノートにあった「開発ノートでも徘徊している」という説明も直した。

home コミット数の図を「1 日 1 点・直近 90 日」から「1 週 1 点・直近 52 週」に変える
DECISION

トップの幕 2 と開発ノート一覧で共有しているコミット数の図(activity-chart.js)を、週(月曜〜日曜)の合計で出すことにした。

窓は 90 日から 52 週へ延ばす。1 点が 1 日だったときに引いていた 7 日移動中央値と、中央値の 3 倍に跳ねた日のリングはやめた。

週で足した時点で均されているので、線は点を結ぶだけにする。今日を含む途中の週は図に入れず、最後に終わった日曜までで切る。

幕 1 と quiet 画面の実数も同じ窓(直近 52 週)に揃えた。

WHY / 仮説

日ごとの点を並べると、書いている本人が見張られているように感じる、というのが理由。読む側に「何曜日に休んだか」まで見せる必要は無く、伝えたいのは止めていないことだけで、それは週で足しても残る。

途中の週を入れないのは、必ず低い点が右端に付いて「落ちた」ように見えるのと、「今週ここまで」という見え方がまさに日ごとの図と同じ息苦しさになるため。

窓を延ばしたのは、90 日を週にすると 13 点しか無く線にならないから。

RESULT

2025-09-15 〜 2026-09-13 の 52 週で 1,054 コミット、手を動かした週は 33 週、最多は 1 週 179 コミット。

JSON は 90 点から 52 点に減った。日ごとの数字はどこにも出なくなり、点にかざして出るのも週の範囲と合計だけ。

全期間の月別は 開発の実測 にそのまま残している。

book 技術書の発売日と発売状態は books.json 1 箇所を源にし、HTML のベタ書きは機械で書き戻す
DECISION

発売日と「発売予定/発売」の唯一の源を /book/books.json にした。

HTML 側は日付を含む既存の要素に data-book の印を付けるだけで、値は scripts/sync_books.py が書き戻す(deploy.sh が毎回 --fix-html を回す)。

JS で埋める案と、日付だけを <span> で包む案はやめた。

WHY / 仮説

2026-09-16、Vol.4 の発売日を 2026-09-19 から 2026-09-22 へ 3 日ずらした。

手で 10 ファイル 24 箇所を直したのに、6 箇所が古いまま残った(窓辺の札の「9/19 発売」、カードの hero の「9/19」、ハブのバッジ「2026.09.19」、開発ノート 3 行)。

表記が「2026年9月22日/2026.09.22/9/22/2026-09-22」の 4 種に割れていて、grep でも拾いきれない。

3 画面のトップがアプリの版を独立に持つ問題と同じ形で、直す場所が増えるほど必ずどこかが残る。
JS で埋めると JS 無し・JSON 404・値が後から入る CLS が出る(幕 1 の実数と同じ理由でベタ書きを残す)。

日付だけを <span> で包むと、チップやバッジが inline-flex + gap なので flex item が割れて空白が gap に化ける。

だから既存の要素に属性を足し、要素の中の「日付の形をした字」だけを置き換える。

RESULT

印は 11 ファイル 63 箇所(本文 55・属性 8。zerohalf.jp と開発ノートを含む)。books.json の発売日を 1 日ずらして状態を released にすると 51 箇所が一度に変わり、戻すと元に戻る。

一致していれば書き込まない(mtime 不変)。取りこぼしの 6 箇所は印を付けた時点で機械が直した。

2026-09-22 の発売時は status を released にするだけで「発売予定」が「発売」になる。ハブのバッジのクラス替えと購入ボタン化は手で。

motion ブラックホールの canvas を 1 秒に 1 回は全面で消す — 部分消しは中身が保たれる前提に頼っていた
DECISION

bh-wanderer.js の「前フレームに描いた範囲だけ消す」は残し、1 秒に 1 回は canvas 全面を消して setTransform を当て直すことにした。

あわせて、GPU 側で canvas が作り直されたとき(contextrestored)とタブが前面に戻ったときにも、寸法と変換を当て直して全面を消す。

WHY / 仮説

2026-09-13、「画面を放置すると処理がどんどん重くなる」という申告と一緒に、開発ノート一覧が全体に灰色に曇り、斜めの帯だけ元の明るさというスクリーンショットが届いた。

帯は一辺約 400px(倍率 2)の正方形を、いまブラックホールがいる位置まで斜めにつないだ形で、small の描画範囲(198px)と一致した。

つまりcanvas の中身がどこかの時点で暗く壊れ、部分消しが通り道だけを元に戻していた。開発ノートでは canvas を濃さ 0.34 で本文に重ねているので、黒く壊れるとちょうど灰色の幕に見える。


ヘッドレス Chrome で canvas を黒で塗ると、10 秒後も 91.6% が黒のまま残り、スクリーンショットと同じ見た目になった。

GPU プロセスを落とすと contextrestored は届くのに何もしておらず、変換が初期値に戻って本体が左上寄りの小さな点になったまま戻らなかった。

部分消しをやめて毎フレーム全面を消す案もあったが、全面の clearRect は GPU なら 1 回の塗りつぶしで、1 秒に 1 回なら部分消しの軽さをほぼ失わない。

何が中身を壊したかに関係なく 1 秒で消える形を選んだ。

RESULT

本番で、黒で塗っても 2 秒後には本体だけに戻る(98.8% → 0.2%)ことと、GPU プロセスを落としても元の大きさで描き続けることを確認した。


実機で中身を壊したきっかけは特定できていない(スリープからの復帰などを疑っている)。「放置すると重くなる」も再現できていない。

トップと開発ノートを倍率 2 で 8 分放置し、描画プロセスと GPU プロセスのメモリ、CPU、コマの間隔、描画ループの回数、イベント監視の数を 30 秒ごとに取ったが、どれも増えなかった。

灰色の幕と同じきっかけで Chrome の描画が遅い方式に落ちている、というのは仮説のまま。

home 「一度落ちたら上げ直さない」をやめ、重さの判定を「一瞬」ではなく「続いたとき」だけにする
DECISION

下の「背景を 3 段階に」で決めた「実行時の劣化は下方向へ 1 回だけ、上げ直さない」を撤回した。

濃いが重いと判定されても、その回だけ軽いに替えて、次の番でまた濃いを出す(sessionStorage の nzw:bg-demoted は廃止)。

判定そのものも、1 コマの遅れを 50ms で頭打ちにし、遅い状態が 90 フレーム続いたときだけ落とすように変えた。ウィンドウを狭めたときの「広げ直しても戻さない」も同じ理由でやめ、広げ直せば入れ替えを再開する。

WHY / 仮説

10 秒ごとの入れ替えを出した直後に、「テーマを切り替えたら濃いが出なくなった」と言われた。本番で、濃いの表示中に時間帯テーマを昼へ切り替えると、0.4 秒後に軽いへ落ち、30 秒待っても濃いは戻らなかった。

テーマの切り替えは 8,000px のページを描き直すので、1 コマが 300〜500ms 止まる。

判定は指数移動平均(ema = 0.9·ema + 0.1·dt)が 24ms を超えたら落とす作りで、1 コマが約 90ms を超えるだけで閾値をまたいでいた。

それを「上げ直さない」がタブを閉じるまで固定していた。


「一度カクついた人を滑らかな側へ戻しても得が無い」という仮説で入れた決まりだったが、実際に引っかかったのは重い端末ではなくテーマ切り替えの一瞬で、しかも結果は「片方の絵柄が二度と出ない」だった。

10 秒ごとの入れ替えを入れたときもこの決まりはそのまま残しており、テーマを切り替えて初めて表に出た。

RESULT

濃いの表示中にテーマを切り替えて 1 コマ 517ms 止まっても、濃いのまま 10 秒を終えて入れ替わるようになった(本番で確認)。

CPU を 6 倍遅くした状態では、濃いが 1 回軽いに落ち、約 12 秒後にまた濃いが出た。本当に重い端末の保護は残しつつ、誤判定で絵柄を失うことはなくなった。

home 背景の濃い/軽いを端末で決め打ちせず、10 秒ごとに入れ替える
DECISION

背景が「自動」で、濃いに耐える端末のときは、最初の 1 枚を濃い/軽いのくじで決め、10 秒ごとに 0.7 秒のフェードで入れ替える。

手で段階を選んだとき、狭い画面・タッチ操作が主の端末など初めから軽いになる端末では入れ替えない。窓辺(quiet.html)は対象外にした。

あちらの段階は「描き込みの量」で、絵柄の出し分けではないため。

WHY / 仮説

2026-09-13、同じマシンで撮った 2 枚のスクリーンショットで「この背景の違いは何か、どのタイミングで 1 枚目になるのか」と聞かれた。

答えは時間帯ではなく段階の違い(漂う粒=濃い/点を線で結ぶ=軽い)で、自動判定は端末とウィンドウで決まる。同じ端末なら毎回同じ側が選ばれるので、もう片方は設定を開かない限り見られない。

両方を見られたのは、ウィンドウの高さや重さの判定でたまたま落ちたときだけだった。2 つは作りも見た目もまったく別物なので、どちらも見てもらう側に倒した。

最初は「開くたびにくじ」にし、続けて「10 秒で切り替え」まで進めた。

RESULT

段階を変えるたびに canvas と描画エンジンを作り直す作り(1 枚の canvas から 2 種類のコンテキストは取れない)なので、1 回きりの切り替えでは表に出なかった後始末の漏れが 10 秒ごとに効くようになった。

軽い版の stop() は resize の監視を外さないので destroy() に替え、bg-field.js は読み込み済みなら <script> を足さず、濃いの読み込み待ちを追い越されたときは世代番号で捨てる。

くじはページを開いたときに 1 回だけ引く(resize のたびに引き直すと、幅を変えただけで背景が入れ替わる)。5 分放置して、canvas は 4 枚のまま、resize の監視の数も増えないことを確かめた。

workspace ワークスペースの複製を 1 本に畳み、テストエリアを配信されている側へ戻す
DECISION

/ai-workspace の実体は pages/ai-workspace.html の 1 本と決め、html/static/ai-workspace.html は 24 行のリダイレクトに差し替えた。

教材レビュー(Bearise・Certly・Dotpile・声くらべ)の「テストエリア」タブは、複製側にしか無かったものを配信されている側へ移した。

WHY / 仮説

「テストツールが無くなった」という申告から入ったが、消えていたのは導線だけで、4 本とも本番で 200 を返していた。

2026-09-06 にテストエリアを足したのは html/static/ 側で、docs/ai-workspace-features.md に手順として書いてあった pages/ へのコピーが実行されず、以後 2 ファイルが別々に育っていた(3276 行 / 2456 行)。

main.py には /static/ai-workspace.html → /ai-workspace の 301 があるが、nginx が /static/ を /var/www/html から直接返すのでこの 301 は一度も発火していない。

結果、ログイン不要で開ける旧 UI が生き続け、どちらを見ているかで画面が違った。

RESULT

リダイレクトはハッシュを引き継ぐ形にした(changelog-daygrid.html が #daygrid、changelog-mydictly.html が #langbridge を付けてここを指しているため、meta refresh ではなく JS を先に置く)。

/static/ai-workspace.html#daygrid から DayGrid タブが開くところまで確認。

あわせて、複製側にだけあった旧 id を TAB_ALIASES で受けるようにし、トップページの /ai-workspace#mydictly がホームに落ちていたのを直した。

workspace Tailwind の走査対象に pages/ を入れる — 外していたのは偶然で保たれていただけだった
DECISION

ホームは design-system.css の --nzw-* を読む専用クラス(.ws-*)で組み、ブレークポイントも @media で自前に持たせた。

そのうえで tailwind.config.js の content に ./pages/**/*.html を足した。

当初は足さない方針で始めたが、作業中に足さざるを得ない事実が出た。

WHY / 仮説

content は ./html/**/*.html と ./html/static/js/**/*.js だけで、pages/ を見ていなかった。

ここに書いたクラスは CSS が生成されず、エラーも警告も出ずに当たらない。

最初は「だから pages/ には Tailwind を足さない」で済ませるつもりだった——tailwind.min.css は deploy.sh、pages/ は deploy-backend.sh と別のスクリプトで本番に出るので、content に足しても片方だけ回した瞬間に崩れる形が残るからだ。

ところが上の複製を畳んだ直後のビルドで、bg-green-500・bg-red-400・bg-slate-50/60・focus:border-sky-400・focus:ring-sky-100・gap-2.5・hover:bg-slate-300・mt-1.5 の 8 クラスが CSS から消えた。

どれも pages/ai-workspace.html が使っているもので、これまで生成されていたのは «html/ 側の複製がたまたま同じクラスを使っていた» という偶然に過ぎなかった。

複製を消すと同時に成り立たなくなる。

RESULT

走査対象に pages/ を入れて 8 クラスとも戻し、変更前の CSS と比べて失われたセレクタが 0 であることを機械的に確認してから配った。

デプロイが 2 本に分かれている問題は消えていないので、pages/ にクラスを増やしたら deploy.sh も回す、を残した。

今回のホーム自体は新しいユーティリティを 1 つも使っていない。

枠線の色は --nzw-border に統一し、ブランド色はアイコンと hover の枠線だけに残した——5 枚が別々の色で縁取られていると、並べたときにどれが強いのか読めない。

workspace iframe は開いたときに初めて読む
DECISION

単語帳・DayGrid・Certly・グループチャットの 4 本を src から data-src に変え、タブを開いたときに一度だけ src を入れる形にした。

テストエリアの 4 本は元から同じ作りになっていた。

WHY / 仮説

初期 HTML に src を書くと、ホームを眺めているだけで 4 本とも走る。

LangBridge と Certly の main.dart.js が各 4MB 超、DayGrid は iframe の中でさらに Tailwind の Play CDN を読む。

ここにテストエリア(4 本で 15MB ほど)を足すと最大 8 本になる。postMessage も contentWindow も使っていないことを確認したうえで替えた。

RESULT

ホームを開いた時点の iframe 読み込みが 4 本から 0 本になった。src を入れ直さず DOM に残す作りにしたので、タブを行き来しても読み直さない(入れ直すと読んでいた場所を毎回見失う)。

副作用が 1 つある。グループチャットは localStorage から前の部屋へ自動再入室するので、これまではワークスペースを開くだけで同室の人の画面に「参加しました/退出しました」が出ていた。

これがタブを開いたときだけになった。

splash スプラッシュを 7 画面に広げ、絵はページごとの語彙から作る
DECISION

トップだけに出していた起動スプラッシュを、アプリ・技術書・グッズ・開発ノート・デザインシステム・About・zerohalf の 7 画面に広げた。

仕組みは boot-splash.js 1 本のままで、絵だけをページごとに差し替える。JS が毎フレームやるのは --bs-p(0〜1)の書き込みと % の文字の 2 回だけで、何がどう動くかは知らない。

絵は /static/css/boot-splash.css が calc() で作る。data-splash の値は各ページが既に持っている data-site-nav-current と同じ語彙を使い、識別子を 2 系統に増やさない。

「1 回だけ出す」の単位も nzw:booted から nzw:booted:<key> に分け、回遊しても «そのページの初回» には出るようにした。

WHY / 仮説

7 画面はどれも独自の視覚言語を持っているのに、開いた瞬間だけは無地だった。

サイトの作りが元々「骨格は 1 種類、皮だけ変える」(design-system.css → 各ページ CSS の上書き)なので、演出も同じ形にすれば新しい仕組みを増やさずに済む。

絵は新しい記号を持ち込まず、そのページが既に使っているものから取った — グッズは極太のタブular数字、開発ノートは 14px ピッチの方眼ドット、アプリはステータスを «形» で区別する点、技術書は低ポリのハチドリ、About は等高線とルート、zerohalf はロゴそのものの数直線。

下限は 600ms から 1200ms に伸ばした。静的 HTML は速く、実ロードに連動させるだけでは絵が一瞬で消えて «個性» が見えないため。

時間項の時定数も下限の半分に合わせないと、下限の時点でまだ .63 までしか来ておらず残りが一瞬で飛ぶ。

RESULT

7 画面すべてで、初回に出る・同一タブの再訪では出ない・戻るでは出ない・6 秒のフェイルセーフが効く(JS と CSS をそれぞれ遮断して確認)ところまで通した。

フックの読み方で 1 度つまずいた。

テーマ側の window.NZW_SPLASH_HOOK を起動時に 1 度だけ捕まえる実装にしていたため、登録する inline script が boot-splash.js より後ろにあるページでは永久に null のままになり、ブラックホールが出なかった。

毎フレーム読む形に変えて解決。もう 1 つ、動きを減らす設定でスクロールをロックしないようにした。

About の動き抑制ブロックは animation-delay まで 0 に潰すので CSS の 6 秒フェイルセーフが即座に発火し、覆いが消えたのにロックだけが最大 4 秒残る — 「見えているのにスクロールできない」という最も質の悪い壊れ方になる。

splash ブラックホールを «玄関» ではなくデザインシステム 1 枚に戻し、暗いパネルに閉じ込める
DECISION

下の「スプラッシュの絵を虹ラインへ差し替える」で外した canvas のブラックホールを、デザインシステムのページだけに戻した。

描画は既存の bh-wanderer.js をそのまま使い、bounds で画面中央の 420×220 の 暗いパネルの中だけに閉じ込める。

全面 canvas にはしない。VOID: xx.x% のタクティカルタグに読み込みの進捗を入れるため、ライブラリに autoDanger(既定 true)を足し、false のとき danger を外から与えられるようにした。

WHY / 仮説

当時外した理由は 2 つあって、片方しか今回に当てはまらない。

「ブラックホールは Synapse Link 1 プロダクトの絵であって nzw.jp の看板ではない」は今も正しいが、戻す先はサイトの玄関ではなく、ブラックホールを解説しているページ 1 枚なので衝突しない。

もう片方の性能は、原因が «canvas 一般» ではなく「白背景に加算合成を重ねたこと」だった。

降着円盤が RGB 合計 17 しか変えないのに 1 フレーム 99.3ms のうち 75ms を占めていたのは、加算は白に足しても飽和するだけだから。

暗いパネルの中なら同じ絵が «見える» ぶんの仕事になる。加えて当時の 406 行の移植と違い、bh-wanderer.js はスプライト焼きとダーティ矩形が入っていて毎フレームの仕事が桁で違う。

RESULT

スプラッシュは bh-wanderer.js の到着を待たない作りにした(使えるようになったフレームで初めて作り、読めなければバーだけで成立する)。

覆うはずのものが待ち時間を延ばす側に回らないための線引きで、ここだけは崩さない。

読み込みの置き場でも 1 つ踏んだ: ダウンロードを先行させようと defer で head に移したところ、defer は «body 末尾の非 defer» より後に走るため、ライブ見本の inline script と bh-ambient.js が BlackHoleWanderer 未定義のまま先に動き、どちらも黙って消えた。

実行位置は body 末尾のまま、preload で取得だけ先に始める形に倒した。

about About を /about/ に移し、肩書きを「役割の列挙」から「一行の決め」に変える
DECISION

About の URL を /about.html から /about/ へ移した。

実体は html/about/index.html、旧 URL は FastAPI のミドルウェアで 301(既に /static/ai-workspace.html で同じ形を使っている)。

あわせて肩書きを「AI活用のチームマネージャー/SaaS開発のプロダクトマネージャー」から 「PoC で終わらせない、AI プロダクトマネージャー」の一行に変え、所属と年数は下の小さい行(.profile-role-sub)へ逃がした。

WHY / 仮説

URL の方は、/apps/・/book/・/goods/・/static/notes/ がすべてディレクトリで、About だけが拡張子付きで残っていたのが理由。

.html は配信の実装であって、読む側には関係がない。
肩書きの方は、元の一行が役割を 2 つ並べただけで、何をする人かが読み取れなかった。

職務経歴書側の芯は「PoC(0→1)で止めず、運用とスケール(10→100)まで持っていく」なので、そこを言い切って一行に置いた。

事実(エンジニア15年 → PjM → SaaS開発チーム マネージャー)を同じ行に足すと読点だらけになって何も残らないので、行を分けている。

RESULT

サイト内のリンク 36 箇所と zerohalf.jp からの 2 箇所を /about/ に張り替え、canonical と og:url も差し替えた。

本番で /about.html → 301 → /about/ を確認し、旧ファイルはサーバーから削除(deploy.sh は --delete を付けていないので、消すには ssh が要る)。

/about(末尾スラッシュ無し)は StaticFiles(html=True) が 307 で拾う。

肩書きは About だけでなく、書籍 5 ページの著者紹介と zerohalf.jp の自己紹介の計 6 箇所も同じ一行に揃えた(同じ文言がサイト内に散っているので、片方だけ新しいと «どちらが本当か» が分からなくなる)。

nav 現在地の下線を box-shadow から background に変える
DECISION

ヘッダーの現在地下線を box-shadow: inset 0 -2px 0 で引くのをやめ、background-image の 2px 帯に変えた。

群の先頭([data-group-start])だけ background-size: calc(100% - 13px) で短くする。

WHY / 仮説

群の変わり目の縦罫を .site-header-link::before=リンク自身の子として持たせているので、縦罫 1px + margin-right 12px がリンクの幅に入っている。

inset の影は幅いっぱいに出るため、About(「作り手について」の先頭)では下線が縦罫の下から始まり、アイコンと左端が揃わない。

影は引く幅を指定できないが、background なら指定できる。

RESULT

ズレていたのは data-group-start が付く 2 項目(開発ノート・About)だけで、他は元から正しかった。

data-group-start は i > 0 の条件付きで付くので、先頭の「アプリ」に 13px の引き算が漏れる心配もない。

4 スキン(紙・白・夜・暗)すべてを本番で撮って確認した。

weather 読み物の前面に雨を降らせるのをやめ、雨を持つ画面を 1 つに絞る
DECISION

カード版と 6 幕から雨のオーバーレイをその日のうちに撤去した。雨を絵として持つのは窓辺だけにする。

代わりに、窓辺の「降るもの」(.qt-motes)に大雨の段を足した——雨脚は 1.9 倍の長さで 1.8 倍速く、揺れはさらに小さく、翳りを厚くし、本数は 24 → 60 本(「ふつう」なら 12 → 30 本)。

大雨のボタンは 3 画面とも残す。

WHY / 仮説

「窓辺以外は雨のアニメーションは不要」という指摘。前面のオーバーレイは z-index: 9999 で、粒が本文の上を通る。

カード版と 6 幕は読ませる画面で、窓辺は眺める画面——同じ雨でも置く意味が違う。加えて、窓辺には最初から雨があった(data-qt-weather="rain" で舞うものが細い縦線に変わる)。

雨の実装が 2 系統あることに気づかないまま 3 つ目を前面に足していたのが実際のところで、消すべきは新しく足した方だった。

RESULT

home.css から 5,978 文字、home-theme.js から雨の生成が丸ごと消えた。

この 2 画面で天気が効く先は背景の線と虹ラインの 2 つだけになり、前面に置く要素はゼロになった(#weather-overlay ごと削除)。

大雨のボタンを 3 画面に残したのは、天気の設定が localStorage で共有されているから——片方だけ選べないと、選んだ状態がもう片方で «どのボタンも押されていない» に化ける。

weather 雨を 1 つの状態から 3 段階にし、大雨は「粒を増やす」以外の 3 枚で作る
DECISION

天気の「雨」を 小雨 / 雨 / 大雨の 3 段階に割った。段階は #weather-overlay[data-rain] が持ち、粒の長さ・太さ・傾き・色は CSS 側に集約して、JS が決めるのは何本・どの層に置くかだけにした。

大雨は粒を 200 本に増やすだけでなく、視界の濁り・風のむら・足元のしぶきの 3 枚を別に重ねる。どの判定にも入らず「無天気」だった雷雨(95〜99)も大雨に寄せて拾う。

実測が大雨になるのは年に数日なので、表示設定に手で選べる 6 つ目のボタンを足した(天気の行だけ 6 列になる)。

WHY / 仮説

「大雨のアニメーションも考えて」という依頼で開けたら、.rain-drop の CSS がリポジトリのどこにも無かった。

JS は className を付けて色を #a5f3fc で塗るだけで、幅も高さも animation-name も持たない空の div が 100 枚積まれていた——初回コミットから一度も雨は降っていなかった(デザインシステムのページには降る前提で書いてある)。

仮に描けていても、水色を白い下地に置いていたので昼はほぼ見えない。だから大雨の前に、まず雨を降らせるところからになった。


その上で、大雨を粒の量と速度だけで作らないと決めた。粒を増やすと「たくさん降っている」にしかならず、強い雨として読まれるのは視界が濁ること・風でむらが出ること・足元が白く煙ることの方だから。

奥行きも 3 層に分けて速度差(1.55 / 1.0 / 0.72 倍)を距離として読ませる。

RESULT

粒は小雨 45 / 雨 95 / 大雨 200 本(横 1280px 基準。狭い画面は 0.45 倍まで落ちる。実測 1440px で 200 本、390px で 90 本)。

大雨は落下 0.5 秒・傾き 14 度で、傾きと横流れ(35vh)は必ずセットで動かす——片方だけ変えると粒が自分の向きと違う方向へ滑って見える。

虹ラインは大雨だけ「静か→活発」の 1 軸から外し、周期 60 秒(晴れ寄りに速い)/彩度 0.82(雨より低い)にした。

動きは強いが色は濁る、という別の性格づけ。

背景(Canvas2D / WebGL2)にも大雨のプリセットを足し、判定の境目は 3 か所(home-theme.js の rainTierOf()・bg-synapse.js・bg-field.js)で揃えた。

prefers-reduced-motion では粒を 1 枚も作らない。共通の抑制ルールが animation-iteration-count を 1 にするので、作ってしまうと画面の下端に止まった粒が貼りついたまま残る。

notes ノート 1 本ずつでは書けない「プロダクト同士の血縁」を、図 1 枚に逃がす
DECISION

開発ノートに プロダクトの系譜を新設した。縦を時間、横を扱う領域に取り、改名・後継・作り直し・開発中止だけを線で結ぶ。

ノードは全部それぞれのノートへのリンクにして、各プロダクトの中身はいっさい書かない。一覧ページには本数を数えるフィルタの外側に導線を 1 枚置いた。

WHY / 仮説

Socrates → Grizzlingo → Bearise、LangBridge → MyDictly という血縁は、実際には各ノートのメタ表の「出自」「後継」行と、リポジトリのコミットログに散らばっていた。

1 本を読んでいるあいだは、その 1 本の話しか見えない。一覧に戻っても更新日順のカードが並ぶだけで、何が何から生まれたかは読み取れない。

ノートは「なぜそう決めたか」を書く場所なので、ここに系譜を足すと 1 本ずつが重くなる——だから線だけを持つページを別に立てることにした。

導線をフィルタの中に入れなかったのは、チップが数えているのが「ノートの本数」で、系譜図はノートではないから。

RESULT

ノード 14 個・辺 7 本の 1 枚に収まった。

分かれ道は 4 つ——Socrates からのピボット、Grizzlingo の中止と Bearise への再構築、LangBridge から MyDictly への改名、そして Quick Query Question と DayGrid が名前を残したまま中身を作り直したこと。

最後のひとつは図を描くまで「同じ形の出来事が 2 回あった」と気づいていなかった。色は使わず線種と枠だけで区別している(実線=同じ血、破線=知見の受け渡し、破線枠+✕=開発を終えたもの、枠なし=開発ノートが無い)。

図が読み物の幅(768px)だと 7 割まで縮んで読めないので、.fig-wide で図だけ器の外へ出し、狭い画面では縮めずに横スクロールさせている。

motion 跡そのものをやめ、稀に○を 1 個吐くだけにする
DECISION

通った跡を全部やめた。点の列 → 連続した溝 → 泥の層、と 2 日かけて作り込んだものを丸ごと外し、10〜24 秒に 1 個だけ小さな○を吐くだけにした。

○は本体から離れる向きへ 110px 流れながら 4.2 秒で消える。

WHY / 仮説

「足跡はいらない、稀に小さな○を吐き出すくらいでよい」という指摘。作っている最中は「どう見せるか」ばかり考えて「そもそも要るか」を問い直していなかった。

跡は「どこを通ったか」という情報で、読み物のページに置くと本文より強い意味を持ってしまう。読者が追うべきものは本文であって、装飾の軌跡ではない。

うんこに見えたのも、溝が数珠つなぎに見えたのも、根っこは同じで跡が画面に長く居座りすぎていたことだった。形を直す方向で 2 回作り直したが、直すべきは形ではなく存在の方だった。

RESULT

実測 3.4 個/分、画面に○が居る割合は 24%。同時に 2 個以上出ることは実質ない。

間隔を毎回引き直すのは、等間隔だと「一定周期で出る装置」に見えてたまたま出会うものにならないため。吐く向きを全周から引いているのは、進行方向の後ろに寄せると尾に見えて、やめたはずの跡に戻るから。

飛距離 110px は重力圏(tiny 60px / medium 130px)を抜けるための値で、短くすると本体の暈に飲まれて出たことすら分からない。

コードは 100 行以上減った——泥の二次曲線、溝の区間ごとの太さ計算、点の位相管理が全部消えた。

home トップを 6 幕のスクロール物語に組み直し、以前のデザインを恒久的に残す
DECISION

ヘッダーの真下にあった8 枚の横スクロールカルーセル(6 秒自動送り)を廃止し、トップを 6 つの幕として組み直した。

①サイトを一言で言う見出しと実数、②直近 90 日のコミット数、③いま見てほしい 3 つ、④やめたことで説明する、⑤つくったもの全部(35 枚)、⑥このページ自身のこと。

以前のデザインは /classic.html として凍結して残し、ヘッダーのボタンでいつでも往復できるようにした。出し分けは Cookie を見て main.py がサーバ側で行う。


既定は従来のデザインのまま置いた。作り替えた側を既定にすると、判断が正しかったかを自分で見比べる前に全員へ配ってしまう。

新しい方は「選んだ人が見る側」にして、ヘッダーのボタンを最初の 5 秒だけ薄く光らせて存在だけ知らせる。索引・canonical・OGP は既定側(classic.html)が持ち、新しい方は noindex にしてある。

WHY / 仮説

きっかけは「もっとインパクトのあるサイトにしたい」という一言だったが、実際に測ると足りないのは派手さではなかった。grep -c "og:" が 0 で、<title> は「nzw.jp」の 4 文字。

SNS に貼っても無地のリンクにしかならない。ページ内にもサイト全体を語るコピーが 1 つも無く、いきなり販促スライドが始まる。

docs/VISION.md 自身が「ならないもの」に挙げた「ただの作ったものリスト」に、トップがいちばん近い状態だった。


カルーセルは書籍・グッズ・アプリ 4 種を平等に扱っており、平等は無印象。しかも読ませる前に流れる。

8 枚のうち 5 枚は下のグリッドにも同じものがあり、情報としては重複していた。
作り替えを「一度きりの入れ替え」にしなかったのは、判断が正しかったか自分で見比べられるようにするため。

移行のための並行ファイルがそのまま恒久の classic になるので、捨てる作業がゼロで済む。

RESULT

index.html は 8,238 行 → 3,444 行(インライン CSS 1,900 行と JS 2,700 行を外部化、動かない canvas 5 枚と死にコード 650 行を削除、カルーセル 920 行を削除)。

canvas は 11 個 → 2 個。<title>・meta description・OGP・canonical を新設し、OG 画像は scripts/og/home.html を Playwright で撮って作り直せるようにした。


採用担当にいちばん効く数字として「直近 90 日で 477 コミット / 9 リポジトリ」を幕 1 と幕 2 に出した。

図は開発ノート一覧のものを activity-chart.js として切り出して共有している(あちら側が画素単位で変わっていないことを確認済み)。

数字は HTML に最終値をベタ書きし、JSON は答え合わせに使う——こうすると「JS 無し」「JSON が 404」「数字が後から入って版が飛ぶ」の 3 つが同時に消える。

home スクロール駆動アニメは骨格に使わない。出現は IntersectionObserver 一本に絞る
DECISION

CSS の scroll-driven animations(animation-timeline: view())を「使える環境だけの飾り」として重ねていたが、丸ごと撤去した。

出現は scroll-stage.js の IntersectionObserver 一本にする。content-visibility: auto も同じ理由で撤回した。

WHY / 仮説

view() の進み具合は「スクロールできる量」で決まる。そのためページ下端の要素は animation-range の終わりへ到達できず、永久に半透明のまま残る。

実測で締めの一行が opacity 0.17 で固まった。そもそも Safari は未対応なので、ほとんどの読者は Observer 側を見ている。

読まれない方の経路が、この壊れ方を抱えたまま存在する価値は無い。
content-visibility: auto は中身がレイアウトされない状態になるため、子要素が IntersectionObserver から見えなくなる。

飛び越えたときの掃き出しが矩形を読めず、幕 4 の 5 要素が隠れたまま残った。長いページなので効かせたかったが、出現とは両立しない。

RESULT

実装しながら 3 つ踏んだ。(1) IntersectionObserver は「飛び越えた」要素に通知を出さない(交差の割合が 0 のまま 0 なので閾値をまたがない)。

どれか 1 つでも通知が来たときに控え全体を掃く形にした。(2) 1 要素に 2 つの監視を掛けると後勝ちでハンドラが消える。

幕 3 は出現と進捗の両方を掛けており、出現側が黙って上書きされていた。

(3) グリッドの札は表示の出し入れを home-cards.js が握っているので、出現もあちらから仕込まないと「もっと見る」で出た分が opacity 0 のまま残る。


いずれもエラーを出さずに静かに壊れる種類で、通しで動かして初めて見つかった。

CSS 側は「JS が属性を立てたときだけ隠す」書き方に統一してある(about/index.html の .reveal は素の状態で opacity 0 なので、IntersectionObserver が無い環境では永久に見えない。あれを繰り返さない)。

home 背景を 3 段階にし、軽い方は既存の Canvas2D を無改造で流用する
DECISION

背景を 切 / 軽い(Canvas2D)/ 濃い(WebGL2)の 3 段階にした。端末を見て自動で選び、表示設定から手でも変えられる。

軽い方は bg-synapse.js を改造せず設定だけ絞って使い、濃い方は新規の bg-field.js(357 行・依存ゼロ)。

prefers-reduced-motion は段階に混ぜない。

WHY / 仮説

軽い方を作り直さないのは、SynapseBackground が setConfig で粒数も結線距離も絞れるうえ、WEATHER_PRESETS と setWeather が既にサイトの天気系に繋がっているから。

新規実装するとそこを一から書くことになる。
濃い方にライブラリを入れないのは、描くのが draw call 2 回(全面の三角形 1 枚と gl.POINTS 1 回)だけで、シーングラフもカメラも要らないから。

three.js は 150KB gz あり、HTML 全体が 80KB gz のこのページでは単独で最大のアセットになる。

Play CDN の Tailwind をやめた経緯と真逆になってしまう。

点の動きは (種, 時刻) の閉じた関数なので GPGPU も transform feedback も不要で、静的な VBO 1 本と uniform 数個で足りた。


prefers-reduced-motion を「切」にマップしなかったのは、切が上書きできるユーザーの選好なのに対し、あちらは上書きされてはならない OS の設定だから。

同じ値にすると「濃い」を押しただけで OS 設定を蹴破れてしまう。抑制中は設定行そのものを隠す。

RESULT

自動判定は bh-wanderer.js の _resolveProfile を型にした。写したのは「情報が無いことを理由に降格しない(|| 8 の楽観的な既定)」と「向きが変われば引き直す」の 2 点。

ソフトウェア実装の WebGL(SwiftShader / llvmpipe)も軽い方へ落とす——CPU ラスタライズの WebGL は Canvas2D より遅い。

実行時の劣化は下方向へ 1 回だけで、立ち上がりの 120 フレームは見ない(シェーダのコンパイル・初回ペイント・フォントの差し替え・スプラッシュの退場が最初の 2 秒に固まっているので、素朴な検出器は必ず誤爆する)。

上げ直さない。
踏んだ穴が 2 つ。

1 枚の canvas から 2 種類のコンテキストは取れない——一度 webgl2 を取った canvas に getContext('2d') を投げると null が返り、降格が「背景が消える」に化ける。

段階を変えるたびに canvas を作り直している。もう 1 つは明るいテーマで粒が主張しすぎて本文が読みにくくなったこと。

暗い地と同じ量を乗せると白の上でベージュの斑点になるので、地の明るさで不透明度と大きさを落とす。

motion 通った跡は「落とした粒」ではなく「削れた溝」にする
DECISION

ブラックホールの跡を、点を落とす方式から1 本の連続した溝に置き換えた。0.6 秒ごとに本体の位置を記録し、その折れ線を引く。

太さと濃さは新しいところほど大きく、古い側へ向かって細りながら 30 秒で消える。粒のスプライトと、粒ごとの位置・速度・寿命の管理は丸ごと消えた。

WHY / 仮説

「うんこのように見える」という指摘。実物を見ると否定できなかった。

丸い染みが等間隔で並んでいる絵なので、明るい地の上ではそう読める。原因は形ではなく離散させたことにある。

跡というのは「通った」という連続した出来事の記録なのに、点にした時点でその連続性が落ち、あとに残るのは正体不明の丸だけになる。

線にすれば向きと速さがそのまま形に出る。
寿命を 24 秒から 30 秒へ伸ばしたのは、短いと溝が本体の重力圏(medium で直径 300px)の内側で終わってしまい、降着円盤の光に飲まれて跡として読めないため。

線を 2 本重ねているのは、このライブラリが置かれるページの地の明るさを知らないから。太く淡い紫(アプリ一覧の黒地用)と細く濃い黒(開発ノートの紙地用)を常に両方引いて、どちらの地でも片方が残るようにしてある。

RESULT

区間ごとに太さと濃さが違うので 1 本の Path2D にまとめられず、描画は 1 フレームあたり 51 区間 × 2 本 = 約 100 ストロークになった。

そのかわり消す範囲は溝全体を囲む 1 枚だけにしてある(区間ごとに矩形を積むと、重なった領域を何度も消すことになって割に合わない)。

前の方式が持っていたスプライトの焼き直しと dpr 追従は不要になった。丸い線端が区間の継ぎ目で膨らみ、円が数珠つなぎに並んで見えるのは残った。

あとから、その円と円のあいだを埋める泥の層をいちばん下に 1 枚足して潰している(区間の中点を法線方向へずらした二次曲線を、太さとずれを 2.6 秒周期で揺らしながら引く)。

色は中間調 1 色で足りた——明るい地では暗く、暗い地では明るく転ぶため、芯と縁のように 2 本引く必要がない。位置を止めて時刻だけ 0.65 秒進めたとき、跡の画素の 47% が変わることを確認している。

about 太陽と月を「光の塊」から「測る道具」に描き直し、時間帯もヘッダーから選べるようにする
DECISION

About の空を作り替えた。(1) 太陽は filter: blur(34px) でぼかした円と、ぼかした三角の光条をやめ、同心円・方位環・細い直線の光条を持つ図に置き換えた。

にじみは CSS の blur ではなく SVG のグラデーション 1 枚。(2) 月は白丸をやめ、同じ半径の円をずらしたマスクで満ち欠けを作り、クレーターを塗りではなく細い輪にした。

欠け具合は今日の月齢から出す。(3) 操作卓にもう 1 群足し、季節(自動/春夏秋冬)の隣で時間帯(時計/朝昼夕夜)も選べるようにした。

WHY / 仮説

「太陽や月のデザインがイケてない」という指摘。見に行くと、どちらもこのページの語彙から外れていた。

About は地形図(等高線・稜線・ルート破線)と 8bit で組んであり、線は細く、形は測れるものとして描いてある。そこにぼかした橙色の塊と白丸だけが、輪郭も情報も持たずに乗っていた。

太陽と月も同じ「測る道具」の側に寄せれば、装飾ではなくページの一部になる。
時間帯をヘッダーに出したのは、16 通り(山 4 × 時間帯 4)で作ってあるのに、訪問者は自分の時計が指す 4 通りしか見られなかったため。

季節はボタンで選べるのに時間帯だけ ?time= を手打ちしないと見られないのは、作りの非対称だった。月齢を本物にしたのは、季節を訪問者の暦から、時間帯を訪問者の時計から出しているのと揃えたかったから。

今夜の形と合っている月のほうが、毎回同じ形の飾りより面白い。

RESULT

ぼかしを SVG のグラデーションに替えたので、毎フレーム合成していた blur が 3 枚ぶん消えた(輪郭も締まる)。

時間帯を動かせるようにしたぶん --sun-* / --star-o を @property に登録し、季節と同じ 1.4 秒のクロスフェードに乗せた。

--sun-size だけは min() を含むので登録せず(解決に失敗した環境で initial-value に落ちて絵が壊れる)、大小は数値の --sun-scale で付けている。


手こずったのは「昼」の置き場所。真上=画面上端は、帯(110px)とヒーローのカード(185px)に挟まれた 75px しか空いていない。

輪を持たせたぶん前より場所を食うので、芯をカードの上端に重ねて上半分だけを出す置き方に変えた。狭い画面では操作卓が 2 段に折り返して帯が 165px まで伸びるため、そこだけ別に下げている。

motion 開発ノートでだけ声量を落とし、跡の粒は本体の大きさに比例させる
DECISION

2 つ決めた。

(1) 開発ノートのブラックホールだけトーンを落とす — 濃さを 0.8/0.65 → 0.34/0.22、彩度を saturate(0.3)、6 層目(VOID タグと本体まわりのフレア)は data-label="false" で切る。

適用は notes.css の末尾と各ノートの script タグだけで、レンダラには手を入れない。アプリ一覧とデザインシステムは素のまま。

(2) 通った跡の粒を本体の半径に対する比にする — debrisSize: 2.6px(固定)を debrisSizeRatio: 0.13 に置き換えた。

WHY / 仮説

(1) は前回のログで「紫がノートのモノクロから浮く」と書いて保留にしていた件。実物を見たら、いちばん邪魔だったのは色より本文の上に重なる「VOID: 12.7%」の文字だった。

読んでいる行の上に別の書体で数字が乗ると、意味を持った情報だと思って一瞬読んでしまう。彩度と濃度は下げるだけで済むが、これは消すしかない。

完全な grayscale にしなかったのは、紫がわずかに残るほうが「Synapse Link のあれだ」と分かるため。


(2) は固定 px だったのが単純に間違いだった。tiny(半径 10px)でも medium(35px)でも同じ 2.6px の粒が落ちるので、大きい個体ほど跡が相対的に細って見える。

跡は「何がここを通ったか」の手掛かりなのに、大きさの情報がそこだけ落ちていた。

RESULT

粒は tiny 1.3px / small 2.6px(据え置き)/ medium 4.6px。0.13 という比は、それまでの 2.6px がちょうど small(半径 20px)ぶんだった値を採った。

絵そのものは触っていない——層数も係数も分割数も原典のまま、変えたのは粒の大きさの決め方と、ページ側の CSS 2 行だけ。

検証中に副産物として分かったのが、headless Chrome では rAF がほとんど回らないこと(20 秒待っても elapsed=0.0s)。

フレームを手で送り、toDataURL() で焼いてから撮る形にして、ようやく尾の見た目を確認できた。canvas の絵を CI で撮るときはこれが要る。

motion 徘徊するブラックホールをトップページから降ろし、読み物のページへ移す
DECISION

トップページ(index.html)からブラックホールを外し、アプリ一覧(/apps/)・開発ノート 18 ページ・デザインシステムへ置き換えた。

index.html に直書きしていた 140 行の起動処理は /static/js/bh-ambient.js に切り出し、canvas・当たり判定・CSS までそのファイルが差し込む形にしたので、ページ側は <script> を 2 本読むだけになる。

同時に、時間帯ごとの出現抽選と天気ごとの 4 通りの周期も落とした。

WHY / 仮説

「トップページに動くブラックホールがあるのは不気味」という指摘。トップページは判断の場で、来た人はヒーローと製品カードを見比べて次にどこへ行くかを決めている。

その真上を黒い球が横切ると、意味の読めない動きに視線が引かれ、決めるための時間を削る。一方でアプリ一覧・開発ノート・デザインシステムは腰を据えて読むページで、視線は本文の列に固定されている。

同じ絵でも、視線の外側をゆっくり通り過ぎるだけなら邪魔にならない。しかも「Synapse Link のレンダラを移植した」という話そのものがこの開発ノートに書いてあるので、書いてある場所に現物が居るほうが筋が通る。

抽選をやめたのは、置き場所を狭めた以上「たまに出会う」を二重に掛ける必要が無くなったため(出現確率は data-chance で戻せるようにしてある)。

RESULT

トップページから canvas 2 枚ぶんの CSS(76 行)と起動スクリプト(140 行)が消え、bh-wanderer.js の読み込みも 1 本減った。

移した先では 1 匹だけが手前(z-index: 30)と奥(z-index: -1)を 24 秒周期で往復する。地との相性は移した先で分かれた。

アプリ一覧は黒地の製図台なので、絵としてはトップに置いていた頃より馴染む。逆に開発ノートはモノクロ(Dot Matrix)に組み直したページなので、紫の降着円盤が浮く懸念が残っている。

ページ側から --bh-front-opacity / --bh-behind-opacity で濃さを下げられるようにして、判断は実物を見てからにした。

notes 一覧の図は「ノートの更新日」ではなく「手を動かした量」を出す
DECISION

開発ノート一覧の頭に置く図を、ノート 16 本の最終更新日から直近 90 日のコミット数に差し替えた。

ノートが扱う 9 リポジトリ(nzw.jp / DayGrid / Bearise / Certly / Engly / MyDictly / Synapse Link / Quick Query Question / 書籍)の git log を scripts/gen_notes_activity.py が数えて activity.json に落とし、deploy.sh が毎回作り直す。

図は点=その日のコミット数、線=7 日移動中央値。

WHY / 仮説

最初に置いたのは「ノートの最終更新日」の分布だったが、ノートは 16 本しかなく、点も 16 個しか出ない。しかも中身は各カードに書いてある日付の焼き直しで、図にした分の情報が増えていなかった。

開発ノートの頭に出すべきなのは「ノートをいつ触ったか」ではなく「その裏で何をどれだけ作っていたか」のはず。手元にあって嘘のつけない数字はコミット数だったので、そこから読む形にした。

生成 AI との対話時間も測れた(37 日で 137.3 時間)が、Claude Code の履歴保持が 37 日ぶんしか無く、放っておくと過去から消えて窓が前に滑るので採らなかった。

RESULT

90 日で 466 コミット、手を動かした日は 65 日、最多は 1 日 53 コミット。最初は線形の縦軸で描いて失敗した——中央値が 2 なのに最大が 53 なので、普段の日が全部下端で潰れて死んだ図に見えた。

縦軸を平方根目盛に変え(ラベルは実数のまま)、跳ねた日は中央値の 3 倍以上のときだけリングで囲う形にして読めるようにした。

2 倍にすると 18 日が囲まれて「目立たせる」意味が消える。データはリポジトリ側にあるので、ノートを足しても図の面倒は増えない。

notes 開発ノートから色を抜き、点(ドット)だけで組み直す
DECISION

開発ノート 18 ページが各自持っていたページ内 <style>(合計 4,488 行)を捨て、/static/css/notes.css 1 枚に統合した。

同時に見た目を作り直し、罫線・箇条書き・進捗・時系列・区切りをすべて「点」で描くことにした。プロダクトごとのブランド色(--brand / --topic-brand、実数 14 色)は全廃し、色で意味を運ぶのをやめている。

例外は 3 つだけ:ヘッダー最上部のレインボーライン(サイトの識別子)、アプリアイコンとスクリーンショット(既定はモノクロ、hover で戻す)、カラーパレット見本(色が内容そのもの)。

WHY / 仮説

きっかけは見た目の不満ではなくコピーの劣化。ノートを 1 本足すたびに 250 行前後の CSS を隣のページから貼っていたので、同じ .meta のラベル幅が 1% / 30% / 34% / 38% の 4 通りに分裂していた。

「開発ノートは判断の記録であって宣伝ではない」と書いておきながら、ページごとにブランド色を足していたのも噛み合っていない。

ステータスや警告を色で示していた箇所も、色を抜いたときに情報が消えるなら、そもそも色にしか意味を載せていなかったということになる。

だから色を抜くのを目的ではなく検算として使い、消えた分は点の塗り/中抜き/密度に置き換えた(公開中=二重丸、β版=中抜き、開発終了=薄い中抜き、注意=点の網)。

RESULT

4,488 行 → 1 ファイル 1,471 行。以後ノートを足すときに CSS は書かない。

figure.fig の図版は --nzw-* トークンで描いてあったため、トークンを差し替えただけで図まで自動でモノクロになった。

逆に色を直書きしていた 2 枚(ブラックホール・重なり順)は .fig--ink として切り出し、hover でだけ元の色を出している。

perf Tailwind はブラウザで組み立てるのをやめ、事前ビルドに切り替える
DECISION

cdn.tailwindcss.com(Play CDN)の読み込みと、index.html に直接書いていた tailwind.config を捨てた。

設定はリポジトリ直下の tailwind.config.js に一本化し、npm run build:css が吐く /static/css/tailwind.min.css を <link> で読む。

ビルドは deploy.sh に組み込み、流し忘れて「新しく足したクラスだけ効かない」が起きない形にした。

WHY / 仮説

「トップが重い、とくに画像が出ない」という体感の原因を、最初は画像の重さ(15.5MB)だと思っていた。ところが本番をヘッドレスブラウザで実測すると、DOMContentLoaded まで 39.4 秒かかっていた。

同じ回線で about.html は 3.98 秒、/goods/ は 3.07 秒。違いは 2 つで、トップだけが 499KB あることと、トップだけが Play CDN を読んでいること。

Play CDN は DOM の変化を監視して JIT コンパイルを回すので、8,000 行の文書が流れ込む間じゅう全走査が繰り返される。

画像が出なかったのは帯域ではなく、先読みスキャナが画像を取り終えているのに DOM が組み上がらないためだった。30 秒経過時点で本文はまだ 838 要素しかできていなかった。

RESULT

本番の DOMContentLoaded は 39.4 秒 → 31.6 秒(同時に測った about.html は前後とも 3.7〜4.0 秒で、計測環境が同条件だったことは確認済み)。

期待したほどは縮まなかった。ローカルの切り分けでは 56 秒 → 15 秒という大きな差が出たが、あとで uptime を見たらロードアベレージが 22(10 コア)まで上がっており、その値は計測マシンの負荷で汚染されていた。

信用できるのは本番で前後を同じ手順で測った 2 割の短縮のほうだけ。


ただし切り分け自体は残った: インライン <style> 16 個はほぼ無コストで、残りの支配項はインライン <script> 26 個の実行時間(スクリプトを全部外すと同条件で 4.66 秒 → 1.29 秒、つまり 7 割強)。

12 個の canvas の初期化を可視化まで遅らせるのが次の一手で、宿題として残す。

なお package.json には以前から build:css が書かれていたが、出力先が html/ の外で content グロブもトップを含んでおらず、一度も配信されていなかった。

perf 画像は「元を残して派生を足す」のではなく、置き換える
DECISION

写真・スクリーンショット系 15 枚を表示サイズに合わせて縮小し WebP へ置き換え、元ファイルは消した。ロゴ・アイコン 4 枚は拡張子を変えず PNG のままリサイズ(参照を書き換えずに済む)。

<img> 22 枚に loading="lazy" を入れ、ヒーロー第 1 パネルだけ eager + fetchpriority="high" を残した。

ただし og:image に使っている 2 枚だけは、リンクプレビューの互換性を優先して従来形式を維持した。

WHY / 仮説

初回表示で 15.5MB を一斉に取りにいっていた。

内訳の偏りがひどく、blackhole2.png が 4.74MB(1290x2796 をカードのサムネイルに使用)、zerohalf-logo.png が 2.97MB(1626x1622 をヘッダーで 20px 表示、しかも全ページ共通)。

加えて loading="lazy" が 1 箇所も無く、プロジェクトカード 36 枚が全部マークアップにあった。

「9 枚だけ表示」は JS がパース後に display:none を付けているだけなので、先読みスキャナは既に 36 枚ぶん取り終えている。

派生ファイルを足す案もあったが、同じ絵が 2 つある状態は必ず片方が腐るので置き換えを選んだ。

RESULT

15.5MB → 0.74MB(blackhole2 は 4.74MB → 26KB、ヘッダーロゴは 2.97MB → 23.8KB で全ページに効く)。

作業中に、書籍ページには既に <picture> + WebP の仕組みが入っていたのにトップだけが生の PNG を直接読んでいたことが分かり、そちらは既存の webp を指すだけで済んだ。

書籍ページ自体には手を触れていない。

motion スプラッシュの絵をブラックホールからサイトの虹ラインへ差し替える
DECISION

起動スプラッシュから canvas を落とし、ヘッダー下とフッター上に敷いてあるのと同じ虹のライン 1 本だけにした。中央にサイトのアイコン、その下に LOADING、その下に虹の帯。

進捗の作り(実際の準備度に連動・0.92 で頭打ち・4 秒のハードタイムアウト・CSS 6 秒のフェイルセーフ)は一切触っていない。

差し替えたのは絵だけ。

WHY / 仮説

「ブラックホールにこだわらず、サイトデザインに合ったものにしてほしい」という指摘。ブラックホールは Synapse Link という 1 プロダクトの絵であって、nzw.jp の看板ではない。

サイト全体で共有している視覚要素は虹のラインのほうで、これは既に全ページのヘッダーとフッターに敷かれ、デザインシステムにも .nzw-rainbow-line として載っている。

起動時にそれを見せれば、ページが出た瞬間にヘッダー下の同じ帯へ視線が繋がる。

RESULT

boot-splash.js が 406 行 → 214 行。層 1〜7 の描画・飲み込み・白フラッシュがまるごと不要になり、毎フレームの仕事は transform: scaleX() の書き込み 1 回だけになった。

幅ではなく transform で伸ばすので、進捗が動いてもレイアウトを作り直さない。

下の「白背景では RGB 合計 17 しか変えていないのに 1 フレームの 75ms を占めていた」という問題は、間引きではなく絵そのものを差し替えたことで消えた。

motion 待ち時間は隠さず、Synapse Link のブラックホールで見せる(差し替え済み)
DECISION

トップに起動スプラッシュを置いた。

描画は Synapse Link の _LoadingBlackHolePainter の 7 層をそのまま Canvas 2D へ移植し、フェーズ(load → hold 200ms → expand 600ms easeInQuint → 白フラッシュ 400ms)も原典どおり。

進捗は固定タイマーではなく実際の準備度に連動させ、0.92 で頭打ちにして ready のときだけ 1.0 へ詰める。表示はタブのセッション中 1 回だけ。

WHY / 仮説

ページを速くしても、この規模だと待ちはゼロにならない。それなら白画面のまま黙って待たせるより、このサイトが何を作っている場所かを見せる時間に変えるほうがいい。

徘徊するブラックホールは既に移植済みだったので、同じ絵を起動時にも使えば説明が要らない。固定 2 秒にしなかったのは、速い回線で無意味に待たせたくないから。

逆に遅いときにバーが止まって見えないよう、シグナルが全部滞っても時間だけで進む項を入れた。毎回出さないのは、ページ内リンクで戻るたびに待たされるのが最も嫌われるため。

RESULT

スプラッシュ自身が待ち時間を延ばしていた。ページのパースと 26 個のインライン script と同じメインスレッドを 60fps の rAF で奪い合うので、覆うつもりのものが原因側に回る。

画素で測ると、降着円盤(3 層 x 60 セグメントの加算合成)は白背景では最大でも RGB 合計 17 しか変えていないのに、1 フレーム 99.3ms のうち 75ms を占めていた。

加算合成は白に足しても飽和するだけなので、当然といえば当然だった。読み込み中は円盤を描かず、飲み込みが始まって背景が黒くなってからだけ描くようにし、canvas の更新自体も 30fps に間引いた。

見た目は前後で区別がつかない。
もう一つ、フェイルセーフの検証のほうが設計より重かった。

当初スクロールロックを html:has(#boot-splash:not(.is-done)) という CSS だけで持たせ、「外し忘れる class が無いから安全」と考えていた。

ところが JS を 404 させて試すと、is-done が付かないのでロックだけが永久に残り、ページがスクロール不能になった。

CSS 側のフェイルセーフは opacity と visibility しか戻せない。結局 overflow は JS が付け外しする形に変え、スクリプトが読めない場合はロックも掛からない、という非対称に倒した。

「CSS だけで完結するから安全」は、CSS で戻せない性質のものには通用しない。

motion スマホでは全画面 canvas をやめ、本体ぶんだけ描いて transform で動かす
DECISION

徘徊ブラックホールの low プロファイル(粗いポインタ / 短辺 700px 未満 / コア 4 以下)で、ビューポート大の canvas を捨てた。

canvas 自体を本体ぶんの正方形まで縮め、位置は transform: translate3d() で動かす。絵は一切落としていない。

分割数も層数もぼかしも原典のまま。減らしたのは描き直す回数と、描く場所の大きさだけ。

WHY / 仮説

以前このライブラリを最適化したとき、分割数や層を削る案は「見た目が損なわれるわりに 1 秒あたり 45ms しか稼げない」として却下してある。

その判断を今回もひっくり返したくなかった。

一方で、本体は最大でも 300px 角しか塗っていないのに、毎フレーム画面全体ぶんの clearRect と drawImage を回し、430x932 の端末で 6.4MB のバッキングストアを抱えていた。

位置の更新をコンポジタに渡せば、絵に一切触れずに費用だけ下げられる。

RESULT

バッキングストア 6.4MB → 0.91MB、毎フレームの CPU 描画は「文字列比較 1 回とスタイル書き込み 1 回」になった。

副次的に、モバイルで URL バーが開閉するたび innerHeight が変わって走っていた全画面ぶんの再確保も no-op になった。

移植できなかったのは VOID タグだけで、画面基準のクランプが成り立たないため「画面端では本体の反対側へ折り返す」に変えた。

またこの変更で、デザインシステムのデモが canvas モード前提の _resize を上書きしていることが露見し、スマホで開くと壊れる状態だったので quality: 'high' の明示で塞いだ。

infra TLS の認証方式をサーバー内で nginx に統一する
DECISION

zerohalf.jp だけ standalone だった証明書の認証方式を nginx に寄せ、3 枚すべてを同じ方式に揃えた。

今後ドメインを足すときも --nginx と --cert-name の明示を必須とし、standalone は使わない。

WHY / 仮説

2026-08-30 に zerohalf.jp が証明書失効で開かなくなった。失効は 08-25 で、5 日間気づかなかった。

原因は standalone が 80 番を bind できず更新に失敗し続けていたことだが、本当の問題は「同じサーバーの証明書なのに 1 枚だけ更新経路が違う」状態を放置していたこと。

方式が混在していると、動いている証明書を見て安心してしまい、壊れている 1 枚に気づけない。揃えること自体が監視の代わりになると考えた。

RESULT

再発行後、certbot renew --dry-run が 3 枚すべて success。有効期限は 2026-11-28 まで回復した。

一方で、certbot が pip / apt / snap の 3 系統入りタイマーが 2 本並走していること、nzw.jp-0001 という未使用の重複証明書があること、retrieval.nzw.jp が DNS も SAN も欠けたまま設定だけ残っていることが判明し、宿題として残った。

更新失敗が誰にも通知されない点は、同日に期限監視(後述)を入れて塞いだ。

motion ブラックホールは「必ず居る装飾」ではなく「たまに出会うもの」にする
DECISION

夜は必ず出していたのをやめ、時間帯ごとの抽選にした。朝・昼・夕は 1/3、夜は 1/2。

大きさも原典の blackHoleSizes から tiny / small / medium を等確率で引き、速度は表どおり大きいほど遅くする。

さらに z-index を 30 と -1 で往復させ、コンテンツの前後を行き来させた。

WHY / 仮説

毎晩かならず同じ大きさで同じように横切るものは、2 回目には目に入らなくなる。実際、実装直後に見たときの「何か動いている」という驚きが、翌日にはもう無かった。

出るかどうか・どの大きさか・手前か奥かが毎回変わるなら、見つけたときにだけ嬉しいものになると考えた。ずっと手前にいると読む邪魔になる問題も、同じ仕組みで解ける。

RESULT

確率・サイズ・速度・奥行きの入れ替わりをヘッドレスブラウザで実測して検証した(抽選 2000 回で 0.32〜0.36 と 0.52、速度は表と 1% 以内で一致)。

副産物として、classList.remove() が属性を無条件に書き直すことに起因する MutationObserver の無限ループを検出でき、公開前に潰せた。

goods 2 2点目は「作る前の気分」ではなく「作り終えた人へ渡す言葉」にする
DECISION

No.02「おつかれ!アクリルキーホルダー」を追加し、トップのヒーローでは No.02 を No.01 より前に置いた。

No.01(いくぞ!)を下げるのではなく 2 枚並べ、カルーセルは 7 枚から 8 枚に増やしている。絵柄は「おつかれ!」「山頂達成!」の文字が先に読める配置で、キャラクターは言葉の下に置いた。

WHY / 仮説

No.01 は「準備は雑だけど、とりあえず行く」という自分に向けた絵だった。実際に持ち歩いてみると、人に見せたときに反応が返ってくるのは絵柄ではなく文字のほうで、キーホルダーは「自分の気分の表明」より「相手に言う一言」として機能していた。

2 点目は最初からその用途で設計したほうが素直だと考え、のぼり切ったあとをねぎらう絵にした。トップで新しい方を先に出したのは、グッズ導線が 1 本しか無い以上、最新でないものが先頭にあると「更新が止まっている棚」に見えるため。

RESULT

ラインナップは .goods-card を 1 枚、詳細は .item を 1 ブロック足すだけで済み、goods.css は 1 行も触っていない(.item + .item の区切り線がそのまま効いた)。

ひとつ前の goods の項に書いた「2 点目以降はカードを複製するだけ」という想定は、実際に成立した。

hero ヒーローに敷く写真は、被写体が中央に来ている時点で撮り直さず「画を広げる」
DECISION

No.02 の商品写真はキーホルダーがど真ん中に写っていて、そのまま全面に敷くと左に置く文字と重なった。

トリミングでも右に寄せられないため、写真の左端(ピントの外れた山と空)を反転して横に引き伸ばし、画面そのものを左へ拡張した 1800x1004 を書き出して使っている。

さらに最小幅では横に切ると 2 体のうち片方が必ず欠けるので、上に空を足した縦位置(900x1200)を <picture> で出し分けた。

WHY / 仮説

候補は「文字を右に逃がす」「写真を小さく置く」「画を広げる」の 3 つ。前 2 つは No.01 のパネルと作りが変わり、同じ棚の商品が別レイアウトで並ぶことになる。

元の背景がもともとボケているので、引き伸ばしてぼかしても嘘にならないと判断した。反転してから引き伸ばせば継ぎ目の色が完全に一致するので、合成跡が出ない。

RESULT

被写体が幅の 67% に来て、既存の scrim(左 0.90 → 右 0.00)をそのまま使い回せた。CSS はクラス名を .goods2-* に変えただけで、パネルの構造は No.01 と同一。

写真の都合でレイアウトを変えないという形に落ち着いた。

goods グッズは「商品」ではなく「作った記録」として独立ページに置く
DECISION

アクリルキーホルダーを作ったタイミングで /goods/ を新設し、トップのヒーローと Labs & Prototypes の両方から導線を張った。

EC・カート・価格表示は一切作らず、ステータスは「試作 / 構想」の 2 段階だけを持たせている。配布・販売の可能性は残すが、決まるまでは「未定」と明記する。

WHY / 仮説

キーホルダーは 1 個しか手元になく、売る算段も配る算段も無い状態で出来上がった。この段階で「SHOP」を名乗ると、買えないものを並べた店になって信頼を落とす。

一方で置き場所を作らないと、アプリや書籍の副産物として生まれたキャラクターがどこにも残らない。「作ったものの延長線上にある記録」という枠なら、売れる前でも嘘にならないと判断した。

RESULT
グッズ 1 点でもページとして成立し、2 点目以降はカード(.goods-card)を複製するだけで追加できる形に落ち着いた。販売を始める場合も、ステータスを差し替えて導線を足すだけで移行できる。
arch 静的 HTML + FastAPI + DB カードのハイブリッド構成にする
DECISION

ページは静的 HTML(本番は nginx が直接配信)で高速に見せつつ、トップのカードは /v1/content-cards API から公開中のものを動的注入。

API 停止時は静かにスキップして静的閲覧を維持する。

WHY / 仮説
個人運営では「速く・壊れにくく・更新しやすい」の両立が要る。表示は静的で堅牢に、告知やカードだけ DB で運用できれば、重い CMS なしに更新性を得られると考えた。
RESULT
公開/非公開・予約投稿を管理画面から制御しつつ、バックエンドが落ちてもサイトは閲覧可能な構成に。
design デザインシステム(design-system.css)を単一の基準にする
DECISION
配色・タイポ・余白・コンポーネントを --nzw-* トークンに集約し、フレームワーク非依存の CSS で全ページ共通化した。
WHY / 仮説
プロダクトごとに世界観を作り込むほど、サイト全体のトーンがばらつく。単一の基準があれば、個性を出しつつ一体感を保てると考えた(この開発ノート群も同トークンで構築)。
RESULT
各プロダクト LP・開発ノートが同じ土台で作られ、増やしても崩れない基盤を確保。
vision 「代替する AI」ではなく「育てる AI」を全プロダクトの軸にする
DECISION
各プロダクト(英語学習・習慣・単語・チャットの AI)を、ユーザーの能力を肩代わりするのではなく、伸ばす方向で設計する。
WHY / 仮説
「あなたが私を必要としなくなる日が、私の成功」。AI が人間の力を奪う流れに対し、逆張りの一貫した思想が差別化になると考えた。
RESULT
Engly のオフライン学習、DayGrid の継続可視化などに思想が反映され、サイト全体の一貫した価値観になっている。
Milestones

これまでの歩み

scene2026-10-11

飛空艇を飛ばす(城と龍のあいだの頻度・夜は窓に明かり・降る日は出さない)/雲海の下に降らせる回は雲海を厚くする/雲と雲海の色を 1 組の定義にそろえる/雲海を積雲の形に(放射状のグラデーションの丸を密に重ねる)/大型怪獣が出たら地上の生き物は端へ寄って見守る/登山夫婦と猫夫婦は必ず組、山頂のテント(全員で中へ・煙・ときどき外で走る/珈琲)/自己紹介カードは既定を透明に(乗せると濃く、外すとすぐ戻る)/雨雪は雲の底から出して雲より奥に描く/春は花畑の中で猫が寝る(花の匂いを嗅ぐ回は廃止)/山頂の台地を画面幅に合わせて広げる・テント前でご飯と椅子/怪獣が近づいたら画面外へ逃げる/自動の回しは季節 30 秒・時間 20 秒/競走のうさぎは、目覚めたら «!» を出してから追いかける

weather2026-10-10

天気 5 種 × 強さ 3 段階と風 4 種、雲海の下だけに降る範囲、雪の積もり、降る日の雨宿り(葉の傘・雪の鎌倉)/猫の部品抽選と性格・鬼ごっこ、登山夫婦、恐竜 4 種/操作パネルの整列、開閉は保存しない

scene2026-10-10

山を奥と手前の 2 枚に分け、小動物をその間・恐竜をさらに奥へ/山の動物を 2 列の時計で出し、同じ動物は 1 分あける/キジ猫 2 匹の遊びと、恐竜を見て逃げる動き/大型の怪獣(クールな姿・縮まず歩いて出入り)/自己紹介カードの既定を不透明度 48%・放置 5 秒で 5%/太陽の近くで雲・城・龍も暗くなる/NUKERO! と Dotpile のリリース日を未定に

news2026-10-09

共通ヘッダーにお知らせのベルを足す(未読の数・最新 5 件)/メニューに「お知らせ」の行と一覧 /news//中身は news.json 1 本、足すたびに check_news.py で検査

scene2026-10-09

About に空と生き物(鳥・龍・恐竜・うさぎとかめ・花火・流れ星・天の川・雲海など)を足す/出す条件を NZW_RULES の表 1 つに集め、山の後ろの層を稜線で切り抜く/自己紹介カードとヘッダーを半透明にし、5 秒放置でカードをほぼ透明にする

stats2026-10-03

統計を CI が数え直す(fetch_repos.py が 12 本を treeless mirror で取る・毎日 05:00 JST の cron)/握り潰した警告と「止まっている」の検知を Slack と step summary へ/トップのベタ書きは gen_home_stats.py --fix-html が書き戻す

motion2026-09-19

開発ノート 23 ページから徘徊するブラックホールを外す(bh-wanderer.js / bh-ambient.js の読み込みをやめる)/アプリ一覧とデザインシステムには残す/notes.css の彩度調整を apps.css へ移す

home2026-09-17

コミット数の図を 1 日 1 点・直近 90 日から週の合計・直近 52 週に変更(日ごとだと見張られているように感じるため)/7 日移動中央値と跳ねた日のリングをやめる/幕 1 と quiet 画面の実数も直近 52 週に揃える

book2026-09-16

Vol.4 の発売日を 2026-09-22 に変更(手で直した 24 箇所のうち 6 箇所が古いまま残った)/発売日と発売状態の源を /book/books.json 1 箇所にし、data-book の印 63 箇所へ sync_books.py がデプロイ時に書き戻す/既刊 Vol.1・Vol.2 の日付も同じ仕組みに載せる

motion2026-09-13

放置した開発ノートが灰色に曇ったまま残る不具合を修正(部分消しに 1 秒ごとの全面消しを足す)/GPU 側で canvas が作り直されたとき(contextrestored)に寸法と変換を当て直す/「放置すると重くなる」は倍率 2・8 分の放置で再現できず未解決

home2026-09-13

背景の濃い/軽いを 10 秒ごとに入れ替える(最初の 1 枚はくじ、0.7 秒のフェード)/テーマを切り替えると濃いが二度と出なくなる不具合を修正(1 コマの遅れで «重い端末» と判定し、上げ直さない決まりが固定していた)/技術書の数から発売中止の巻を外す(4 → 3)

workspace2026-09-12

AI ワークスペースのホームを作り直し(中央 512px の 2 列 → --nzw-* の 3 セクション・1→2→3 列、カードは button にしてキーボードで辿れるように)/「テストエリア」タブを配信されている側へ戻す(Bearise・Certly・Dotpile・声くらべ)/複製だった html/static/ai-workspace.html をリダイレクトに畳み、iframe 4 本を開いたときに読む形へ

splash2026-09-12

起動スプラッシュを 7 画面に広げ、絵はページごとの語彙から作る(仕組みは 1 本のまま --bs-p だけを渡す)/ブラックホールをデザインシステム 1 枚に戻し、暗いパネルに閉じ込めて VOID: xx.x% に進捗を入れる/動きを減らす設定ではスクロールをロックしないようにした

about2026-09-12

About を /about/ に移し旧 URL は 301(リンク 36 箇所を張り替え)/肩書きを「PoC で終わらせない、AI プロダクトマネージャー」の一行に/現在地の下線が群の先頭で 13px 左へ食み出していたのを background 方式で直す

weather2026-09-08

読み物の前面に降らせるのをやめ、雨を持つ画面を窓辺だけに絞る(カード版・6 幕のオーバーレイを撤去)/窓辺の「降るもの」に大雨の段を足す(雨脚 1.9 倍・本数 24 → 60)/雨の実装が 2 系統あることに気づかないまま 3 つ目を前面に足していた

weather2026-09-08

雨を小雨 45 / 雨 95 / 大雨 200 本の 3 段階に(段階は #weather-overlay[data-rain]、見た目は CSS 側に集約)/大雨は粒に加えて 濁り・風のむら・足元のしぶき の 3 枚を重ね、雷雨(95〜99)も大雨として拾う/.rain-drop の CSS が初回コミットから存在せず、雨は一度も降っていなかったのを直した

notes2026-09-05

開発ノートに「プロダクトの系譜」を新設(ノード 14・辺 7 の 1 枚)/改名・後継・作り直し・開発中止だけを線で結び、中身は各ノートへ逃がす/一覧にはフィルタの外側から導線を 1 枚

motion2026-09-05

スクロール中にブラックホールの回転が止まる不具合を修正(スプライトの描き直しを見送る scrollPauseMs を廃止)/守っていた負荷は実測 1 回 0.07〜0.10ms・CPU 占有 0.2% で、見た目の損に見合っていなかった/短い窓(短辺 700px 未満)だとデスクトップでも low プロファイルに落ちるのが発端

motion2026-09-05

ブラックホールの跡を全廃し、10〜24 秒に 1 個だけ○を吐く形へ(実測 3.4 個/分・画面に居る割合 24%)/溝と泥のコード 100 行以上を削除/直すべきは跡の形ではなく存在の方だった

about2026-09-05

夜へ切り替えたときの太陽と月を「入れ替わり」から「運行」に(太陽は今の位置から右下へ沈み、月は左下から昇る)/沈みきってから面を暗くし、朝は日が出てから明るくする(transition-delay を行き先の状態に持たせて順番を作る)/向かい合う時間帯(夜⇄昼・朝⇄夕)は必ずあいだの位置を通す(transition は 2 点を直線で結ぶだけなので、間に 1 つ挟んで 2 本に分ける)/夜から出るときは横位置を地平線の下で飛ばして真上に昇らせる/運行を弧にする(横は等速・縦は上り下りで緩急。x と y に同じ緩急を当てると、どんな cubic-bezier でも 2 点を結ぶ直線にしかならない)/朝は遅らせずに緩急で「じわっと明けて、日が出た瞬間にぱっと」/時間帯にも 15 秒の自動回転を足す(既定は切。開いたときの時間帯は訪問者の時計のまま)/横と縦で緩急を変えて弧を描かせる(同じ緩急だと真っ直ぐな斜めの線になる)/空の明るみの中心を太陽の位置から切り離す(--glow-x/y。夜の光源は月なので、沈む太陽に持っていかれない)

home2026-09-05

トップを 6 幕のスクロール物語に組み直し(8 枚の自動送りカルーセルを廃止)/以前のデザインを /classic.html として恒久化しヘッダーのボタンで往復(出し分けは Cookie でサーバ側)/<title>・description・OGP・canonical を新設(それまで og: はゼロ)/背景を 切 / 軽い(Canvas2D)/ 濃い(WebGL2)の 3 段階に/index.html 8,238 → 3,444 行、canvas 11 → 2 個

motion2026-09-05

ブラックホールの跡を点の列から連続した溝へ(丸い染みが並んで見えるのをやめる)/寿命 24 秒 → 30 秒で重力圏の外まで伸ばす/地の明るさが分からないので淡い紫と濃い黒の 2 本重ねに/継ぎ目の膨らみが数珠つなぎに見えるので、円と円のあいだを埋める泥の層をうっすら敷く(2.6 秒周期で揺れる)

about2026-09-05

太陽を同心円+方位環+細い光条の図に描き直し(CSS の blur 3 枚を SVG グラデーションへ)/月を満ち欠けするものにし、欠け具合を今日の月齢から出す/ヘッダーの操作卓に時間帯(時計/朝昼夕夜)を追加し --sun-* を @property 化

motion2026-09-05

開発ノートのブラックホールを彩度 0.3・濃さ 0.34 まで落とし VOID タグを非表示に(notes.css と data-label)/通った跡の粒を本体の半径に比例させる(固定 2.6px → 比 0.13、tiny 1.3px〜medium 4.6px)

motion2026-09-04

徘徊するブラックホールをトップページからアプリ一覧/開発ノート/デザインシステムへ移設/起動処理を bh-ambient.js に切り出し(index.html から CSS 76 行 + JS 140 行を削除)/時間帯の抽選と天気ごとの周期を廃止して 1 通りに

スマホ2026-09-03

メニューのパネルを画面の高さいっぱいに使い、続きがあることを下端のぼかしで示す(利用規約まで辿れるように)/デザインシステムの横あふれを解消し(文書幅 515px → 390px)、目次が横に流れることを両端のぼかしで示す/起動スプラッシュの絵をブラックホールからサイトの虹ラインへ差し替え(406 行 → 214 行)

開発ノート2026-09-03

ノート 18 ページのページ内 CSS 4,488 行を notes.css 1 枚(1,471 行)に統合/見た目をドットのみで組み直しブランド色 14 色を全廃/一覧の頭に 9 リポジトリの直近 90 日コミット数グラフ(466 コミット)を追加

高速化2026-09-03

トップの DOMContentLoaded を 39.4 秒 → 31.6 秒に(Tailwind を Play CDN から事前ビルドへ。残りの支配項はインライン script と判明、宿題)/初回表示の画像を 15.5MB → 0.74MB にし lazy 読み込みを導入/Synapse Link のブラックホールで起動スプラッシュを作成/スマホの徘徊ブラックホールを全画面 canvas から本体サイズ + transform へ(6.4MB → 0.91MB)

グッズ2026-09-01

第2弾「おつかれ!アクリルキーホルダー」を /goods/ に追加/トップのヒーローを 8 枚にして新しい方を先頭側へ/商品写真の左を伸ばしてヒーロー用の 16:9 を書き出し、最小幅は縦位置に出し分け

インフラ2026-08-30

zerohalf.jp の証明書失効から復旧(standalone → nginx 認証に統一)/運用でつまずいた場所を本ノートに集約/期限監視スクリプトと日次 cron を追加し Slack 通知まで開通させて失効の見逃しを塞ぐ/certbot 3 系統・重複証明書・retrieval.nzw.jp の未整理を宿題として記録

ブラックホール2026-08-15

Synapse Link の描画をトップページへ移植し徘徊させる/出現を時間帯ごとの抽選に変更/コンテンツの前後を天気に応じて往復

グッズ2026-08-11

/goods/ を新設/第1弾「いくぞ!アクリルキーホルダー」を掲載/配布・販売は未定のまま「作った記録」として公開

開発ノート2026-07

各プロダクトの Product Note(PRD / 設計ノート)を整備・公開

Design System2026

design-system.css で配色・トーンを標準化

書籍発売2026-06-23

『チームに溶け込む AI エージェント開発』を公開

アプリ群2026

Engly / DayGrid / LangBridge / Synapse Link を順次リリース、Flutter Web を同一オリジンに統合

プラットフォーム-

FastAPI + 静的 HTML + DB コンテンツカードのハイブリッドとして運用開始

Roadmap / Open Questions

これから

未決事項:MISSION(zerohalf 名義)と VISION(nzw.jp 名義)の主語の整理、B2B 導線の設計。