使い方
各ページの <head> で CSS を読み込み、プロパティ値に変数を参照します。
<link rel="stylesheet" href="https://nzw.jp/static/css/design-system.css">
.my-title { color: var(--nzw-text-strong); font-size: var(--nzw-text-2xl); }
.my-button { background: var(--nzw-accent); border-radius: var(--nzw-radius-full); }
.my-card { box-shadow: var(--nzw-shadow-sm); padding: var(--nzw-space-6); }
配色 — Color
基準は index.html。アクセント=Sky、テキスト=Slate 系。
- 白背景の本文には
--nzw-text(14.6:1) /--nzw-text-strong/--nzw-sub(4.8:1) /--nzw-blue/--nzw-accent-deep(5.9:1) を使用。 --nzw-accent(2.8:1)・--nzw-muted(2.6:1)・--nzw-warning(2.2:1) はテキスト不可。塗り・枠線・アイコン・大きな装飾のみ。リンク/本文のアクセント文字は--nzw-accent-deepを使う。- アクセント面に白文字を載せるボタンは
--nzw-accent-strong以上(4.1:1)を背景に使う(.nzw-btn--primaryは対応済み)。
Surface / Border
Text
Accent (Sky)
Secondary / Semantic
Brand accents(プロダクト別)
タイポグラフィ — Typography
和文 Noto Sans JP + 欧文 Inter。等幅 JetBrains Mono。表示用 Orbitron / ゲーム Rajdhani。
サイズスケール
余白 — Spacing
4px グリッド。--nzw-space-1 = 4px 〜 --nzw-space-20 = 80px。
スマホの版組 — Mobile layout
狭い画面で崩れるのは、たいてい 余白・表の列幅・改行の位置 の 3 つです。ただし「スマホだから小さくする」は解決にならない(一度やって戻しました)。直すのは構造のほうで、余白は広い画面と同じまま保ちます。基準にしている数字は iPhone の論理幅 390px と 430px の実測です。
折返し点は 3 つだけ
@media の条件にカスタムプロパティは使えない(メディアクエリは変数を評価しない)ので、数字はどうしても各ページに直書きになります。だからこそ書いてよい数字を決めておく。増やす前に、この 3 つで足りないかを先に疑ってください。
| 変数 | 値 | そこで何を変えるか |
|---|---|---|
| --nzw-bp-sm | 640px | 表を畳み、ラベルと操作を 2 列グリッドで対にする。このページの直しはほぼ全部ここ。余白と級数はここで変えない |
| --nzw-bp-md | 720px | 追従する目次を 1 行の横流しにする。折り返すと項目数ぶん背が伸びて、追従ヘッダーが画面を食う |
| --nzw-bp-lg | 820px | 2 段組を 1 段に落とす(カードの解剖図など) |
余白は画面幅で落とさない
最初は 640px 以下でページの左右とカードの内側を 24px → 16px、節の間を 64px → 40px、 見出しの級数まで 1 段ずつ落としました。本文に回る幅は 294px → 326px に増えます。 それでも読みやすくはなりませんでした。窮屈さだけが増えて、直したかった 3 つとは関係が無かったからです。
狭い画面で幅が足りなかった本当の原因は、畳めない表と割れないインラインコードでした。 そちらを構造から直したら、294px のままで足ります。余白は 24px を崩さない。 この 2 つのトークンは「段を落とすため」ではなく、ページの左右とカードの内側を 1 か所で決めるために置いてあります。
| 変数 | 値 | 使うところ |
|---|---|---|
| --nzw-pad-page | 24px | ページの左右。ヘッダー・目次・本文・群の帯を同じ 1 本に揃える |
| --nzw-pad-card | 24px | カード・パネルの内側 |
| --nzw-gap-section | 64px | 節と節のあいだ |
.ds-steps の 1 行は左端に番号の板(26px + 間隔 16px)を持っていて、それ自体が余白の役をしています。
ここに 24px を足すと、390px の画面で本文の列が 299px → 227px まで痩せ、
等幅のチップが 1 行に入らなくなって改行が増えました。余白を足したのに読みにくくなる、という形です。
この行だけ 16px にしています。余白は「同じ数字を配る」ではなく「残る列の幅」で確かめること。
ol.ds-steps { padding: 0 } はリストの字下げ(既定の padding-inline-start: 40px)を
消すつもりの 1 行でした。ところが要素+クラスの指定は .ds-panel より強いので、
<ol class="ds-panel ds-steps"> ではカードの内側余白まで一緒に消えます。
本文が枠線に触れていた原因はこれで、広い画面でも同じように触れていました。
区切り罫はカードの端まで通したいので、余白は li 側に持たせ、カード側は上下だけにしています。
一括指定の打ち消しは、消したい 1 辺だけを狙うこと。
表は「積む」が既定、「流す」が例外
狭い画面で表を width: 100% のまま押し込むと、いちばん長い列が 1 文字ずつ縦に流れます。幅が足りないときに削られるのは、いつも「説明」の列です。
- 1既定は積む —
.ds-tableそのまま640px 以下で 1 行を 1 かたまりに畳み、見出し行(
thead)は隠します。1 行目に「見出しセルと値」、2 行目に説明。列の見分けは太字・等幅・灰色という書体側に移します。既定パラメータの表はこれで 1 画面に 4 行 → 10 行になりました。 - 2例外は流す —
.ds-table--wide列を左右に並べて見比べること自体に意味がある表だけ。畳むと比較できなくなるからです。器には
.nzw-scroll-xを付けて端にぼかしを出します。このページでは時間帯テーマの値・天気プリセット・ブラックホールの大きさの 3 つだけです。 - 3畳むと意味が消える列には
data-label<td data-label="彩度">と書くと、積んだときだけ小さな見出しが値の前に出ます。「—」や数字だけの列は、見出し行が消えると何の値か分からなくなるので、そこにだけ付けます。書体で見分けがつく表には付けません。
器に padding を残したまま横に流すと、右端まで送ったときだけ余白が消えて文字が枠線に触れます
(overflow は padding ボックスの外側で切られるため)。
器の左右を 0 にして、th:first-child / td:last-child に --nzw-pad-card を移します。
表が収まる広い画面では見た目は変わりません。
改行の位置
--nzw-accent-deep のようなトークン名は、既定だとハイフンのところで切れて、
行末に -- だけのチップが残ります。word-break: keep-all はハイフンには効きません(実測)。
止められるのは white-space: nowrap だけです。
ただし nowrap だけでは足りません。<code>--nzw-muted</code>(2.6:1)・<code>--nzw-warning</code> のように
チップ・括弧・中黒が続くと、ハイフンの前と開き括弧の前は改行禁止(UAX#14 の LB21 / LB30)なので、
全体がひと続きの「1 語」になって 467px まで伸びます。display: inline-block にすると
チップが 1 個の置換要素として扱われ、その前後に改行の機会ができます。実測で横あふれ 99px → 0px。
1 行に入らないほど長いもの(おおよそ 45 字。390px の端末で本文 326px、等幅 11.9px の送りが 7.1px)は
.ds-code--wrap で折り返しに戻します。付け忘れても版面が押し広がらないよう、
読み込み後に実測して付け直すスクリプトを置いています。
| 対象 | 指定 | ねらい |
|---|---|---|
| インラインコード | display: inline-block white-space: nowrap | チップを 1 個の置換要素にして、中では割らせず、前後に改行の機会を作る。どのブラウザでも効く |
| 見出し | text-wrap: balance | 「(WCAG コントラス/ト)」のように1 文字だけ次の行に落ちるのを防ぐ。行数の少ない塊にだけ使う。Chrome 114+ / Safari 17.5+ / Firefox 121+ なので、iPhone にも届く |
| 和文の本文 | word-break: auto-phrase line-break: strict | 狭い列でいちばん読みにくいのは文節の途中で切れること。ただし auto-phrase は Chrome 119+ だけで、iOS Safari は 27 時点でも未対応(Technology Preview のみ)。iPhone での改善は文節ではなく「幅が 32px 広がったこと」のほうから来ていると思っておくのが正しい。未対応でも既定のまま落ちるので、入れておいて損はない |
| 本文 | text-wrap: pretty | 最終行のぶら下がりだけを直す。これも効かないブラウザでは何も起きない |
| 等幅 | word-break: normal | 「語」の形が意味を持つので、文節の判定を掛けない |
ラベルと操作は必ず対にする
現在地の下線 — Current underline
「いまここ」を線で示すときの寸法。ヘッダーのナビがこれを使っています。
線を文字の直下(0px)に置くと、「グ」「ノ」のような下に伸びる字が線に触れて窮屈に見えます。
--nzw-underline-offset(4px)空けるのが既定です。
この値は「行ボックスの下端」からの距離で、字の下端からではありません。
13px の文字だと行ボックスの底は字の下端より 2〜3px 下にあるので、見た目の空きは 6〜7px になります。
8px にしたら離れすぎました。
太さは --nzw-underline-weight(2px)。1px にしないこと。
紙スキン(#F5F3EE の地)では 1px の線が罫線と区別できず、現在地に見えません。
余白の入れ方
下線は箱の下端に引かれるので、余白は padding で作ります。このとき 2 つ守ること。
上下そろえて入れる。align-items: center の並びで下だけ広げると、
文字がロゴやボタンに対して浮き上がります。
現在地の項目「だけ」に付けない。項目ごとに箱の高さが変わり、並びの重心がずれます。
下線が出るのは 1 つでも、余白は全項目が持ちます。
線の長さ
文字の幅と線の幅をぴったり同じにしないこと。「測っている」ように見えて落ち着きません。
左右へ --nzw-underline-bleed(4px)ずつはみ出させて、「支えている」線にします。
下げ幅と同じ値なので、「中身より 4px 外側から 4px 外側まで、4px 下」の 1 本のルールで覚えられます。
このため線は ::after で描きます。box-shadow も background も
箱の「中」にしか描けないので、外へはみ出せません。
箱の中に区切り罫やその余白が入っているときは、そのぶんを線の始まりから引きます。
ヘッダーでは群の先頭の項目が箱の左端に縦罫を持っているので、
縦罫 1px + その margin 12px + 箱の gap 6px = 19px を引いています。
gap を引き忘れると線だけが 6px はみ出し、アイコンの左から線が生えて見えます(実際にそうなりました)。
--nzw-underline-offset = var(--nzw-space-1)(4px)/
--nzw-underline-bleed = var(--nzw-space-1)(4px)/
--nzw-underline-weight = 2px。実装は /static/css/site-nav.css の
.site-header-link と [aria-current="page"]::after。
角丸 — Radius
影 — Shadow
グラデーション — Gradient
モーション — Motion
標準トランジション --nzw-transition(300ms)。共通キーフレーム nzw-float / nzw-spin-slow / nzw-rainbow-flow。
虹ライン — Rainbow line
ヘッダー下とフッター上に敷く .nzw-rainbow-line の候補比較。実サイトと同じ 2px・同じ周期・同じ天気倍率で動かしています。
明度を上げて白へ寄せるほど、隣り合う色相どうしの RGB 差が縮まって単なる薄いグレー線になります。
高さ 2px の細線ではこの効果が特に強く、明度 93% を超えたあたりで虹として判別できなくなります。
やわらかさを保ったまま虹を残すには、彩度をむやみに下げるのではなく明度をパステル域(86〜90%)に置くのが有効です。
候補
隣り合う色相の差が数段階しかなく、線が動いていることも判別できなくなったため不採用。
虹とわかる下限。白背景の 2px ではこれ以上落とすと色が消える。
採用値。--nzw-rainbow はこの 7 色。やわらかさと視認性の折り合いが最も良い。
一段はっきりする。まだ上品な範囲。
最初にトーンダウンしたときの値。C より明度が低いぶん重い。
純パステル 7 色。指摘のあったギラギラの一因。
天気連動のスケール
雪 → 曇 → 雨 → 晴 の順に、彩度と速度が段階的に上がります。判定範囲は bg-synapse.js の WEATHER_PRESETS と共通。彩度は 3 秒かけて滑らかに変化し、周期の切り替えは位相を保持するため見た目のジャンプは起きません。大雨だけはこの 1 軸から外れ、周期は晴れ寄りに速く・彩度は雨より低くします(動きは強いが色は濁る、という別の性格づけ)。
.weather-snow×0.5110s71–77 / 85–86.weather-cloudy×0.7590s2 / 3 / 45 / 48.weather-rain×1.072s51–67 / 80–82.weather-rain-heavy×0.8260s65 / 67 / 82 / 95–99(.weather-rain に足す).weather-clear×1.356s0 / 1時間帯テーマ — Time of day
トップページは body に時間帯クラスを 1 枚だけ付けて配色を切り替えます。4 種類ありますが、地と文字が入れ替わるのは theme-night だけです。
朝・夕は白地に淡い色を重ねただけの「白の変奏」に留めてあります。こうしておくと本文・罫線・カードの
色をそのまま使い回せて、テーマごとの上書きが要りません。反転が要るのは夜の 1 テーマだけなので、
body.theme-night ... の上書きもそこに閉じ込められます。
4 テーマそれぞれに専用の配色を持たせる案は取りませんでした。カードが 30 枚以上あってブランド色も バラバラなので、4 通りに増やすと上書きが 4 倍に増えるうえ、時間帯ごとに違うサイトに見えてしまいます。 「時間帯は空の色、プロダクトの色は変えない」という切り分けにしています。
4 つのテーマ
空の色だけが淡く水色に寄る。
デザイントークンの既定値そのもの。
文字だけ焦茶に寄せてなじませる。
フッター・文字色を上書きする。
値
| クラス | AUTO の時間帯 | body 背景 | 本文色 | アイコン |
|---|---|---|---|---|
| .theme-morning | 05 – 10 時 | linear-gradient(to bottom, #e0f7fa, #ffffff) | #006064 | fa-cloud-sun |
| .theme-day | 10 – 16 時 | #ffffff | #1e293b | fa-sun |
| .theme-sunset | 16 – 19 時 | linear-gradient(to bottom, #fff3e0, #ffe0b2) | #4e342e | fa-mountain-sun |
| .theme-night | 19 – 05 時 | #0f172a | #f1f5f9 | fa-moon |
切り替えと保存
| 選び方 | ヘッダーの表示設定メニュー。Theme (Time) は auto / morning / day / sunset / night の 5 択、Effect (Weather) は auto / sunny / cloudy / rain / snow の 5 択。 |
|---|---|
| 保存 | localStorage の timeMode / weatherMode。既定はどちらも auto。 |
| AUTO の中身 | 時間帯は端末時刻(上の表の区切り)。天気は Open-Meteo の current_weather を東京の座標で 1 回だけ取得してキャッシュする。取得に失敗したら晴れ扱いにフォールバックする。 |
| RANDOM | 時間帯と天気を同時に引き直す。auto ではなく具体値として保存するので、押したあとは端末時刻に戻らない。 |
| 遷移 | body { transition: background-color 1.5s ease, color 1.5s ease; }。切り替わりを 1.5 秒かけるのは、時計をまたいだ瞬間に画面がパチンと変わらないようにするため。 |
夜モードで上書きするもの
反転で読めなくなる面と文字だけを、狙って上書きします。原則は「面は slate 系に置き換え、ブランドの色は枠線に残す」。
| 対象 | 夜の値 | ねらい |
|---|---|---|
| .site-header--glass | rgba(15, 23, 42, .85) / 罫線 rgba(255,255,255,.1) | すりガラスの地を暗くする。共通ヘッダーのスキン変数(--sh-*)を差し替える |
| .card-hover | rgba(30, 41, 59, .6) | カードの地 |
| footer | rgba(15, 23, 42, .9) | 同上 |
| .text-slate-800 | #f1f5f9 | 見出し |
| .text-slate-600 | #e2e8f0 | 本文 |
| .text-slate-500 | #94a3b8 | 補助テキスト |
| .premium-bg-* | linear-gradient(145deg, rgba(30,41,59,.7), rgba(15,23,42,.8)) | カードの面は全ブランド共通の 1 枚に寄せ、border-color だけブランド色(α 0.3)を残す |
| .premium-btn-* | ブランド色 α 0.2 の地 + 明るい文字 | 昼は淡色ベタ、夜は透過。ボタンだけ浮かないように |
時間帯と天気が効く先
配色以外にも波及します。ここが「時間帯 / 天気」という 2 軸で画面が変わる全体像です。
背景の線 — Synapse background
トップページの全面に敷いた #bg-canvas。神経の伝播を模した、ノードと線のアニメーションです。実装は /static/js/bg-synapse.js(SynapseBackground クラス)。
ドメイン直下(トップページ)で動いているのは、ここで見せる軽い(Canvas 2D・点を線でつなぐ)と、
もう1つの濃い(背景の粒、WebGL2・漂う粒+稲妻)のどちらか一方です。
選ぶのは /static/js/bg-manager.js。端末が「濃い」に耐えるかを見て、耐える端末では最初の1枚をくじで決め、
以後10秒ごとに軽い/濃いを入れ替えます(濃いが落ちたときはその回だけ軽いに差し替え、次の番でまた濃いを試す)。
動きを減らす設定(prefers-reduced-motion)のときはどちらも動かしません(off)。
ヘッダーの歯車メニューから「軽い・濃い・切」を手で選ぶと、以後は自動判定より優先されます。
白地では意図的にほとんど見えません(本文より目立たせないため)。線の形と伝播の順番を確かめたいときは 夜に切り替えてください。1 サイクルは 発火 → フェード → 休憩 なので、 何も光らない時間が数秒あります。
何を描いているか
- 1ノードを撒く
画面端
margin: 50pxの内側にparticleCount: 45個。半径 1–3px。ごく遅いブラウン運動で漂い、速度は目標値の 0.5–1.5 倍に収束させ続ける(速すぎても遅すぎても補正が入る)。壁では反射する。 - 2近いもの同士を結ぶ
距離が
connectionDistance: 250px未満のノードを双方向に結線。ただしMath.random() > 0.1の判定で約 1 割は間引く。全結合にすると総当たりの網になって「たまたま繋がっている」感じが消えるため。毎フレーム 2% の確率で結線を作り直し、動きに追従させる。 - 31 点から伝播させる
ランダムな 1 ノードを起点に幅優先探索(BFS)で到達順を作り、
nodeActivationDelay: 150msずつずらして順に発火させる。発火したノードはactivationTime = 1.0から毎フレーム 0.008 ずつ減っていく。両端が発火中の線だけが色とshadowBlurを持ち、それ以外はrgba(180,180,180, …)の細い線として残る。
1 サイクルの流れ
baseHue + rand × hueVariation で決め直す既定のパラメータ
| キー | 既定値 | 効果 |
|---|---|---|
| particleCount | 45 | ノード数 |
| connectionDistance | 250 | この距離(px)以内を結線 |
| baseHue / hueVariation | 0 / 60 | 発火色の基準色相と、1 サイクルごとのばらつき |
| saturation / lightness | 38% / 74% | HSL の彩度・明度。虹ラインと同じく、白へ寄せすぎると色が消える |
| speedMultiplier | 0.6 | 移動速度の係数 |
| nodeActivationDelay | 150 | ノード間の発火遅延(ms)。実質の伝播速度 |
| lineWidth / shadowBlur | 1.5 / 4 | 発火中の線の太さとグロー量 |
| fadeOutTime / restTime | 3000 / 5000 | フェード待ちと休憩(ms) |
| maxConnectionsRebuildChance | 0.02 | 毎フレームで結線を組み直す確率 |
| idleColor | rgba(180, 180, 180, 0.2) | 発火していないノードの色 |
天気プリセット
天気コード(Open-Meteo)で setWeather(code) を呼ぶと、この表の値が現在設定にマージされます。判定範囲は虹ラインと共通です。
| コード | 色相 | 形状 | 彩度 / 明度 | 速度 | 結線距離 | 発火間隔 | |
|---|---|---|---|---|---|---|---|
| 晴 | 0 / 1 | 30 ±40 | circle | 40% / 72% | 0.8 | 280 | 120ms |
| 曇 | 2 / 3 / 45 / 48 | 210 ±20 | circle | 20% / 70% | 0.4 | 250 | 180ms |
| 雨 | 51–67 / 80–82 | 200 ±40 | ripple | 42% / 70% | 0.8 | 300 | 100ms |
| 雪 | 71–77 / 85–86 | 190 ±20 | snowflake | 24% / 85% | 0.25 | 180 | 200ms |
雪だけ opacityFactor: 0.9 / lineOpacityFactor: 0.7 を足して、結晶より線が目立たないようにしています。
未知のコードのときは nodeActivationDelay だけ既定に戻し、他は直前のプリセットを引き継ぎます。
3 つの形状
| shape | 描き方 |
|---|---|
| circle | 半径 1–3px の塗り。発火中は shadowBlur × 1.5 のグローが付く。既定。 |
| snowflake | 6 本の主枝+先端の小枝+中心の六角形を線で描く。毎フレーム 0.02rad 回転する。発火中のノードだけ結晶になり、消えている間は点のまま。 |
| ripple | 中心の点+同心円 3 重。外側ほど細く薄い(lineWidth = 2 - i × 0.3、alpha = 0.4 - i × 0.1)。 |
ページへの敷き方
<canvas id="bg-canvas"></canvas>
<script src="/static/js/bg-synapse.js"></script>
<script>
const bg = new SynapseBackground(document.getElementById('bg-canvas'));
if (!window.PREFERS_REDUCED_MOTION) bg.start();
bg.setWeather(0); // 天気コードでプリセット切替
</script>
/* トップページでの見え方。線そのものは主張させない */
#bg-canvas {
position: fixed; inset: 0;
z-index: -1; /* body の背景より上・コンテンツより下 */
pointer-events: none;
filter: blur(0.3px); /* セクション背景の上を流れるときの硬さを取る */
opacity: 0.55;
}
opacity: 0.55 と blur(0.3px) は後から足したものです。素のままだと線がくっきりしすぎて、
カードの上を横切ったときに本文より目立ちました。動きを減らす設定(prefers-reduced-motion)の
ときは start() 自体を呼びません。
背景の粒 — Field background
上の背景の線と交互に出る、もう1つの段階(濃い)。漂う粒と、ときどき走る稲妻です。実装は /static/js/bg-field.js(BackgroundField クラス)。素の WebGL2 で、外部ライブラリは使っていません。
稲妻は4〜8秒に1回、ランダムな間隔で走ります(大雨・雷雨では間隔が詰まります)。しばらく見ていると、 粒から粒へビリヤードのように跳ねる折れ線が見えるはずです — スクリーンショットで「星座」に見えるのはこれです。
何を描いているか
- 1地の色
全面を覆う三角形1枚に、2オクターブのノイズで
tintA↔tintBをゆっくり混ぜる(アニメGIFのような繰り返しではなく、time uniform で連続的に流す)。不透明度は明るい地で0.16、暗い地で0.30しかない。「見える」より「気配」を狙う層で、ブラックホールの重力圏の輪と同じ考え方。 - 2粒を漂わせる
pointCount: 1800個の点を、CPU側のシミュレーションなしで動かす。各点の位置は (種, 位相, 時刻) だけで決まる閉じた式(サイン・コサインで小さな楕円をなぞる)なので、頂点シェーダだけで完結する。位相の大小で奥行き(サイズと不透明度)を表現し、遠近感を出す。 - 3稲妻を走らせる
4〜8秒に1回、ランダムな1粒を起点に、進行方向 ±55° の円錐内から「狙いの跳躍距離にいちばん近い粒」を選んで5〜8回跳ねる(ビリヤードのように、当たるたびに向きを振る)。各辺をギザギザに4分割して折れ線にし、当たった粒には閃光を置く。跳ねが3回未満で終わったら「ただの線」にしか見えないため捨てて、次の周期に引き直す。
既定のパラメータ
| キー | 既定値 | 効果 |
|---|---|---|
| pointCount | 1800 | 粒の数 |
| dpr | 1.6 | 解像度の上限。実機の devicePixelRatio がこれより大きくても頭打ちにする |
| speed | 1.0 | 漂う速さの係数。稲妻の間隔にも効く(速いほど間隔が詰まる) |
| size | 22 | 粒の基準サイズ(px)。位相で0.35〜1.0倍される |
| color | [0.42, 0.58, 0.78] | 粒の色(RGB 0–1)。天気プリセットで差し替わる |
| pointAlpha / sizeScale | 0.45 / 0.7 | 粒の不透明度と大きさの係数。setTheme() が地の明暗に合わせて上書きする |
| tintA / tintB | 明るい地の2色 | 地の色のグラデーション両端。setTheme() が時間帯テーマに合わせて差し替える |
| boltAlpha | 0.28 | 稲妻の濃さ。0にすると draw call ごと出さない |
天気プリセット
setWeather(code) で速度・大きさ・色相を差し替えます。判定範囲は虹ラインや背景の線とほぼ共通ですが、こちらだけ「大雨・雷雨」が独立した1段濃い区分として増えています(65 / 67 / 82 / 95–99)。判定は「大雨・雷雨」を「雨」より先に見る — コード65・67・82は雨の範囲(51–67 / 80–82)にも入るため、あとに置くと大雨側の判定に届かない。
| コード | 速度 | 粒の大きさ | 色相(RGB) | |
|---|---|---|---|---|
| 晴 | 0 / 1 | 1.25 | 26 | 1.00, 0.86, 0.72 |
| 曇 | 2 / 3 / 45 / 48 | 0.80 | 22 | 0.80, 0.84, 0.90 |
| 大雨・雷雨 | 65 / 67 / 82 / 95–99 | 1.60 | 14 | 0.55, 0.70, 0.88 |
| 雨 | 51–67 / 80–82 | 1.05 | 18 | 0.68, 0.82, 0.95 |
| 雪 | 71–77 / 85 / 86 | 0.55 | 30 | 0.92, 0.96, 1.00 |
地の明暗に合わせる
setTheme(dark, tintA, tintB) を時間帯テーマの切り替えに合わせて呼ぶ(背景の線と同じ橋渡しを bg-manager.js が持つ)。明るい地では粒を弱く小さくする — 暗い地と同じ量を白地に乗せるとベージュの斑点になって本文が読みにくくなるため。稲妻も明るい地では通常合成、暗い地では加算合成(光として足す。通常合成だと線が粒を消してしまう)に切り替える。
| pointAlpha | sizeScale | boltAlpha | 稲妻の合成 | |
|---|---|---|---|---|
| 明るい地 | 0.45 | 0.7 | 0.28 | 通常(SRC_ALPHA, ONE_MINUS_SRC_ALPHA) |
| 暗い地 | 1.0 | 1.0 | 0.62 | 加算(SRC_ALPHA, ONE) |
壊れたら「軽い」へ
WebGL2 はソフトウェア実装や低スペック端末で Canvas 2D より遅いことがあるため、動かす価値が無いと判断したら呼び出し側(bg-manager.js)に軽いへ落とすよう伝えます。
| 条件 | 挙動 |
|---|---|
| WebGL2が取れない/ソフトウェアレンダラ | start() が false を返す。bg-manager.js はその場で軽いを起動し、以後は濃いを試すこと自体をやめる(毎回同じところで落ちるだけのため) |
webglcontextlost が2回連続 | onFallback('context-lost') を呼ぶ。モバイルSafariは背面から戻ると普通にコンテキストを失うため、1回だけでは落とさない |
| 90フレーム連続でフレーム時間24ms超 | onFallback('slow') を呼ぶ。起動直後120フレーム(シェーダのコンパイル・初回ペイントが集中する)は見ない。この回だけ軽いに差し替わり、次の10秒周期でまた濃いを試す |
ページへの敷き方
<script>
// bg-manager.js は「濃い」に耐える端末と判定したときだけ、これを遅延読み込みする
const s = document.createElement('script');
s.src = '/static/js/bg-field.js';
s.onload = () => {
const bg = new BackgroundField(document.getElementById('bg-canvas'));
if (!bg.start()) return; // WebGL2 なし・ソフトウェア実装 → light へ
bg.setWeather(0);
bg.onFallback = (why) => { bg.stop(); /* ここでも light へ差し替える */ };
};
document.head.appendChild(s);
</script>
bg-synapse.js と同じ表面(start / stop / setConfig / setWeather)を実装しているので、
bg-manager.js は2つを無改造で入れ替えられます。setTheme は背景の線には無い、こちら専用の口です。
ブラックホール — Black hole
サイト内に 2 つあります。アプリ一覧・このページをゆっくり横切る徘徊するブラックホール(/static/js/bh-wanderer.js + 起動係の /static/js/bh-ambient.js)と、1 画面で完結するゲーム Singularity(/static/blackhole.html)。描画の原典はどちらもアプリ側の synapse-link です。
徘徊するブラックホール
6 つの層
- 1Distortion Rings — 重力圏の同心円
4 本の輪(
pullDistanceの 40%–100%)と、中心から外へ抜ける放射グラデーション。いちばん濃いところでrgba(139, 92, 246, 0.06)しかない。ここは「見える」より「気配がある」を狙う層。 - 2Ambient Particles — 吸い込まれていく粒子
12 + radius × 0.5個。半径の 1.5–4.0 倍の距離に置き、少しずつ内側へ寄せる。事象の地平面(0.5 倍)に届いたら外周へ戻して alpha を引き直す。色は紫(270–300)かオレンジ(20–40)のどちらか。 - 3Accretion Disk — 降着円盤
加算合成(
globalCompositeOperation: 'lighter')。3 層 × 60 分割の円弧で、層ごとに半径・線幅・回転速度を変える。色相はt × 120 + 260 + layer × 30なので、紫から回して外側に暖色が乗る。 - 4Event Horizon Glow — 事象の地平面のにじみ
半径
r × 1.4 × pulseの放射グラデーション。紫#a855f7と暖色#d97757を 0.3 / 0.7 の位置に置いて、内側は紫、外周は赤みへ倒す。 - 5Core — 本体
外側は
blur(7px)付きのグラデーションで縁をにじませ、内側 0.6r はほぼ真っ黒でベタ塗り。中心を完全な黒にすることで、周りの加算合成の明るさが際立つ。 - 6Tactical Info — VOID タグ
VOID: xx.x%と照準のかぎ括弧。値はdanger = radius / (画面幅 / 4)。危険度が上がるほど色が紫 → 赤 → 金へ動き、0.8 を超えると点滅する。書体は--nzw-font-game(Rajdhani)。
大きさ(原典の表そのまま)
| 段階 | 半径 | 重力圏 | 速度 | 自然発生 |
|---|---|---|---|---|
| tiny | 10px | 60px | 36 px/秒 | する |
| small | 20px | 95px | 24 px/秒 | する |
| medium | 35px | 130px | 12 px/秒 | する |
| large | 55px | 190px | 6 px/秒 | しない(原典では合体のみ) |
| huge | 80px | 300px | 3 px/秒 | しない(原典では合体のみ) |
大きいほど遅い。質量が大きいほど鈍いという原典のルールをそのまま持ってきています。 Web 側で湧くのは tiny / small / medium の 3 段階から等確率。
徘徊のさせ方
動きだけは原典に無い追加分です(ゲームでは重力計算で動く)。「直進して壁で跳ね返る」を避けるための作りになっています。
| やること | 値 | ねらい |
|---|---|---|
| 角速度をランダムウォーク | turnJitter 1.7 rad/s² turnDamping 0.8 /s maxTurnRate 0.9 rad/s | 向きを直接ランダムにすると小刻みに震える。曲がり方(角速度)の側を揺らして減衰させると、しばらく同じ向きに曲がり続けて弧を描く |
| 端では中央へ向き直る | edgeMarginRatio 3.0 edgeTurnGain 4.0 | 跳ね返りではなく緩やかな旋回。ゲインが小さいと端を舐めるように滑って戻ってこない |
| 上端に立入禁止 | topInset 64px | 固定ヘッダーの下に潜るとタップできなくなるため |
| 歩調を揺らす | paceCycle 47s pacePower 0.65 | 長周期のサインで速度を上下させ、たまに歩みが遅くなるようにする |
| 稀に○を吐く | spitEveryMs 10000〜24000 spitLifeMs 4200 spitMax 2 spitSizeRatio 0.22 spitDrift 110px | 跡は残さない。10〜24 秒に 1 個だけ小さな○を吐き、本体から離れる向きへ流れながら 4.2 秒で消える。間隔は毎回引き直す(等間隔だと「一定周期で出る装置」に見える)。向きは全周から引く — 進行方向の後ろに寄せると尾に見えて、やめたはずの跡に戻るため。飛距離 110px は重力圏(tiny 60px / medium 130px)を抜けるための値で、これより短いと本体の暈に飲まれて出たことが分からない。○は 2 色重ね(淡い紫の輪=暗い地用/濃い黒の芯=明るい地用)。実測 3.4 個/分、画面に居る割合は 24%。要素モード(スマホ)では canvas ごと動くので出さない |
| タップ判定 | 半径 × 1.7(最小 22px) | canvas 全面を pointer-events: auto にするとページが操作不能になるので、本体に追従する丸い <button> を別に置いて毎フレーム transform で動かす |
置き場所
もとはトップページに置いていましたが、やめました。ヒーローと製品カードの真上を
黒い球が横切るのは、読み手にとって「意図の読めない動くもの」でしかなく、落ち着いて見られません。
いまはアプリ一覧(/apps/)・このページに放しています。
開発ノート(/static/notes/*)にも放していましたが、徘徊は要らないと判断して 2026-09-19 に外しました。
残した 2 ページは腰を据えて読むページで、視線は本文の列に固定されるので、その外側をゆっくり通り過ぎるぶんには
邪魔になりません。アプリ一覧は地が製図台(黒地に方眼)なので、絵としてはここがいちばん馴染みます。
起動係は /static/js/bh-ambient.js。canvas も当たり判定も CSS もこのファイルが差し込むので、
ページ側は <script> を 2 本読むだけで済みます。
読み込みから 1.2–2.7 秒置いてから body に .bh-visible が付き、
opacity が 0 → 0.8 へ 1.5 秒かけて上がります(いきなり居ると
「ページの一部」に見えてしまうため)。本体をタップすると Singularity が別タブで開きます。
| script の data-* | 既定 | 役目 |
|---|---|---|
| data-chance | 1 | 出現確率。0.5 にすれば「たまに出会う」側へ振れる |
| data-href | /static/blackhole.html | 本体をタップしたときの行き先 |
| data-top-inset | 64 | 上端の立入禁止(px)。スティッキーヘッダーの下に潜るとタップできないため |
| data-delay | 1200 | 徘徊を始めるまでの最短待ち(ms)。これに 0–1500ms のばらつきが乗る |
| data-label | (既定は出す) | "false" で 6 層目(VOID タグ + 本体まわりのフレア)を消す。本文の上に文字が重なるのを避けたいページ用 |
| ページ | front / behind | 彩度 | VOID タグ |
|---|---|---|---|
| アプリ一覧 | 0.5 / 0.32 | saturate(0.3) | 消す |
| デザインシステム | 0.8 / 0.65 | そのまま | 出す |
濃さと彩度はページ側で決めます。アプリ一覧は開発ノートの Dot Matrix を黒地に反転したページで、
「色は情報として使わない」テーマなので、原典の配色のままだと紫がページで唯一の彩度になってしまいます。
/apps/apps.css でそこだけ声量を落としています。
奥行きの入れ替わり
手前(z-index: 30/コンテンツより上・ヘッダーより下)と奥(z-index: -1/カードの裏)を
行き来します。奥にいる間はカードやセクションの背景に隠れ、半透明のところだけ透けて見えます。
当たり判定も一緒に沈むので、カードの上からは押せず、隙間なら押せます。
| 値 | 既定 | 意味 |
|---|---|---|
| cycle | 24s | 奥 → 手前 → 奥 で一周する秒数 |
| frontRatio | 0.60 | そのうち手前にいる割合 |
| SWAP_MS | 450ms | 薄くしてから層を入れ替えるまで。CSS の .bh-swap の transition-duration と揃える |
待ち時間には毎回 0.6–1.4 倍のばらつきを掛けます。きっちり等間隔で入れ替わると仕掛けが読めてしまうため。 トップページに置いていた頃は天気ごとに 4 通りの周期を持たせていましたが、開発ノート側に天気クラスが無いので、 この 1 通りに畳みました。周期を天気に結びつける必然性も、もともと薄かったところです。
重くしないための決め
本体は重力圏ぶんの正方形(medium で 300px 角)にしか描かれないのに、以前は毎フレーム画面全体を消して 塗り直していました。1440×900 / dpr 2 で 1 フレーム 42ms(23fps)。原因はほぼこれです。
いまは本体をオフスクリーンに 1 枚描いて転送するだけにし、消すのも前フレームで汚した矩形だけにしています。 分割数も層数もぼかしも原典のまま。絵を粗くして稼げるのは 1 秒あたり 45ms 程度で、 外周の暖色リングが消えて青く寄る見た目の損に見合いませんでした。
| 品質 | spriteMs | minFrameMs | 選ばれる条件 |
|---|---|---|---|
| high | 33 | 0 | 下の条件に当たらないとき |
| low | 66 | 32 | 粗いポインタ(指)/短辺 700px 未満/論理コア 4 以下 のいずれか |
spriteMs はオフスクリーンを描き直す間隔で、位置の更新とは別です。粗くしても移動はなめらかなまま
(速くても 36px/秒なので、遅くなるのは内部の揺らぎだけ)。maxDpr: 2 で 3 倍端末でも 2 倍までしか描きません。
Singularity — 1 画面のゲーム
/static/blackhole.html。徘徊するほうと違い、こちらは全画面・黒地・30 秒で終わる作りです。トップページのトーンとは意図的に切り離しています。
| 決め | 値 | ねらい |
|---|---|---|
| 配色 | 地 #000 重力圏 rgba(255,30,0,.2) 自機・ゴール枠 #00f2ff / #fff | 色数を 3 つに絞る。危険は赤、操作できるものは水色、目的地は白 |
| 書体 | font-weight 100 letter-spacing 0.8em | 極細+広い字間。「静寂の宇宙」というトーンをタイポだけで出す |
| ブラックホールの成長 | 半径 15px → 45px(線形) 重力圏 80px → 約 157px | 重力圏は progress / 3.5 で伸ばす。等速で 350px まで伸ばすと途中で詰みになるため、意図的に 1/3 以下へ落としている |
| 星に当たったとき | 角度 = 入射 × 0.4 + 真上 × 0.6 速度 = max(現速度, 22) さらに vy -6.5 | 物理的に正しい反射より「上へ行ける」を優先した補正。正しく跳ねさせると重力に負けて誰もゴールできない |
| タイマー | 240 × 2px / #ff1100 + glow | 画面下端の細い線 1 本だけ。数字を出すと残り時間ばかり見てしまう |
| 明滅させない | 回転する 12 枚の放射グラデ | 点滅で危険を伝えるのをやめ、回転と赤の濃さ(0.1 + progress × 0.4)だけで進行を示す |
コンポーネント — Components
トークンのみで構築した任意採用のベースクラス。新規ページはこれらを使うと統一が取れます。
Button .nzw-btn
Card .nzw-card
カードタイトル
hover でアクセントのグロー影が付きます。.nzw-card--hover
標準カード
罫線+ --nzw-shadow-sm + --nzw-radius-xl。
Badge .nzw-badge
プロジェクトカード — Labs & Prototypes
トップページの #projects-grid に並ぶカード。1 枚に「何か」「いつ」「どこで動くか」「どうやって手に入れるか」を全部載せる、という前提で組み立てています。
カードは 30 枚以上あり、それぞれ別のプロダクトです。並びも構造もバラバラにすると一覧として読めなくなるので、
寸法・角丸・影・行の並びは全枚数で共通にし、変えていいのは
「サムネの中身」と「面のごく淡い色みと影の色」(premium-bg-*)だけにしています。
面の色は rgba(255,255,255,0.8) から始まるグラデーションで、ブランド色が乗るのは終端の
50% 透過側だけです。強いブランド色をカード全体に敷くと、隣り合ったときに一覧がタイル状の色見本になってしまいます。
1 枚の解剖
SYNAPSE LINK
脳のシナプスをリンクさせる。直感的な操作と物理演算が融合した新感覚パズルゲーム。Web 版と iOS 版でリリース。
- 1外枠
rounded-2xl(--nzw-radius-xl/ 16px)+overflow-hidden+backdrop-blur-xl。枠線はプロダクト色の 100 番台を 80% 透過で(border-sky-100/80など)。flex flex-colなので、説明文が短くてもフッターは必ず底に付く。 - 2サムネ 16:9
aspect-video固定。写真はobject-coverで枠いっぱいに敷き、上からガラスのオーバーレイ(bg-white/20+ 下から上へのグラデーション +backdrop-blur-[1px])を重ねて、その上に大きなロゴタイトルを置く。左上にカテゴリのバッジ。ここだけがプロダクトごとに自由。 - 3本文
px-5 pt-5。タイトルはtext-xl font-bold、group-hoverでアクセント色に変わる。説明はtext-sm+line-clamp-3で 3 行に固定。行数を固定しないと、カードの高さが揃わずグリッドがガタつく。 - 4メタ行
mt-autoで底に寄せ、border-tで本文と切る。左が日付(YYYY.MM.DD)、右が 32px の円形アクション。ラベルを書かずアイコンだけにして、枚数が増えても行が伸びないようにしている。 - 5プラットフォームのバッジ帯
手書きせず、カード内のリンクを読んで JS が後から差し込む。App Store リンクがあれば iOS、
openApkSheet(か Play ストアがあれば Android、といった判定。ネイティブアプリのカードは「詳細ページへのリンク」を Web と誤認しないよう、明示的な Web ボタンかst/games/の実行リンクがあるときだけ Web を付ける。
組み立て
<div class="project-card group backdrop-blur-xl border border-sky-100/80 rounded-2xl
overflow-hidden card-hover flex flex-col relative
premium-bg-synapse premium-card-content"
data-category="game">
<a href="…" class="flex flex-col flex-grow">
<div class="aspect-video relative premium-image-synapse">…</div>
<div class="px-5 pt-5 flex-grow flex flex-col">…</div>
</a>
<div class="px-5 pb-5 mt-auto">…</div>
<!-- .plat-badge-strip は JS が追加する -->
</div>
| クラス | 役目 |
|---|---|
| .project-card | 絞り込みの対象。transition: all .4s var(--nzw-ease) |
| .card-hover | 浮き上がり。translateY(-6px) + 0 14px 26px -14px rgba(15,23,42,.16)。動かすのは transform と box-shadow だけ(レイアウトを触らない) |
| .premium-bg-* | 面のグラデーションと影の色。下のギャラリー参照 |
| .premium-card-content | 夜モードで中の文字色・ボタン色をまとめて差し替えるための目印 |
| group | カード hover でタイトル色を変えるための Tailwind グループ |
| data-category | game / tool / learning / book / other |
| data-platform | 手書きしない。JS がリンクから判定して入れる |
面の色 — premium-bg-*
共通の型は linear-gradient(145deg, rgba(255,255,255,.8) 0%, <プロダクト色 50% 透過> 100%) + box-shadow: 0 10px 30px -5px rgba(<プロダクト色>, .15), inset 0 1px 0 rgba(255,255,255,.8)。上辺の inset 1px がガラスの縁になります。トークンでは --nzw-shadow-accent が同じ形です。
premium-bg-bom だけ昼でも暗色で、hover 中は bom-danger-flash(1 秒周期で枠線の緑が明滅)を回します。
例外は 1 枚だけと決めています。2 枚目を作った時点で「淡い面で揃える」という取り決めが取り決めでなくなるため。
プラットフォームのバッジ
| バッジ | 背景 / 文字 | 判定 |
|---|---|---|
| .plat-badge.ios | #eef2f7 / #0f172a | apps.apple.com へのリンクがある |
| .plat-badge.android | #e7f7ed / #1f8a4c | openApkSheet( か play.google.com がある |
| .plat-badge.web | #e0f2fe / #0369a1 | その場で動くリンクがある(changelog やストアは除外) |
| .plat-badge.other | #f3efe7 / #6b5e4a | どれにも当たらなかったとき |
帯は margin-top: auto でカードの底に貼り付き、rgba(255,255,255,.55) + backdrop-filter: blur(4px) でサムネや面の色から一段浮かせています。
絞り込み
| UI | 中身 | |
|---|---|---|
| カテゴリ | アイコン+小文字ラベルのタブ(下線で選択を示す) | all / book / apps / game / other。apps は learning と tool の 2 カテゴリをまとめて拾う |
| プラットフォーム | 丸いピル型のセグメント(狭い画面ではアイコンのみ) | all / ios / android / web / other。カードの data-platform と突き合わせる |
| 件数 | 「もっと見る」 | 初期 9 件、押すたびに +9。グリッドは 1 / md:2 / lg:3 列なので、9 は 3 行ちょうど |
| 出し入れ | — | 対象外は .hidden-card(display: none)。出すときは opacity 0 → 1、カード自体は fadeInCard(0.5s、下から 10px)で入る |