paper-cinder-transition について
シーン間の炎によるトランジションを作るグラフィックススキルです。固定した旧シーンと動的な新シーンを、ピクセル単位で測る炎の前縁に沿って合成します。描画、性能、アクセシビリティの指針も含みます。
主な機能
- 01旧シーンを固定し新シーンを動的に描画
- 02ピクセル単位の距離で炎の前縁を合成
- 03残り火、焦げ、孔の表現
- 04ノイズを事前生成し描画範囲を制限
- 05視差効果の軽減とページ状態への対応
活用シーン
- 01章の切り替えに炎のリビールを追加
- 02炎の縁に裂け目や揺れる残り火を表現
- 03全画面トランジションの描画負荷を調整
- 04視差効果を抑える設定向けに静止画面を表示
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mengto/ skills --skill paper-cinder-transitionインストール後、Claude Code で/paper-cinder-transitionを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
demo.htmlPROMPT.mdagents/openai.yaml詳細
紙が燃え尽きるトランジション
ページは次のページへフェードするのではなく、炎の筋に焼き尽くされます。ローディング画面上のリング状の炎を、メインスレッドがブロックされても動かし続ける必要がある場合は fire-paper-loader を使ってください。自分で用意した 2 つのシーン間の 切り替わり を炎で表現する場合は、こちらを使ってください。燃やすのではなくマスクでテキストを書き出す場合は masked-reveal を、炎を使わないスクロール連動シーケンスには scroll-scrubbed-visual-sequence を使ってください。
仕組み
全画面を一度に描画するパスで、ノイズのあるフロントラインまでの符号付き距離を基に、各ピクセルが凍結した旧フレーム(ラインの前方)、燃焼ゾーン(ライン上)、ライブの新しいシーン(ラインの後方)のどれに属するかを判定します。 それ以外はすべて演出用です。この 3 層のいずれかを取り除くと、炎として見えなくなります。凍結フレームがなければ炎の下で旧ページが動き続け、ライブのシーンがなければ表示が静止画になり、ピクセル単位で正確な距離がなければエッジがぼやけて不規則になります。
xc = signed distance to the front in CSS px (+ ahead, - burnt)
reveal = clamp(0.5 - xc/aa, 0, 1) // new frame from the front back, cut to the pixel
col = mix(fromFrozen, toLive, reveal)
col = mix(col, charcoal, bed * pores) // 26 px band of torn, porous char (alpha-masked)
col += flame(xc) + charGlow(xc) // additive: thread, ember zone, tight glow
演出と実装の分担
| スキルに含まれるもの | 演出(自由に変更可能) |
|---|---|
| 合成順序、距離場、火の粉の色のランプ、炭化ルール、ノイズのベイク | 2 つのシーンの内容、シーンの配色、タイポグラフィ、ページ上の文言 |
| 2 つのシーンバッファー(凍結した遷移元、ライブの遷移先)とシザー | ラインがどちら側から入るか(下記参照) |
| タイミング、イージング、DPR、視差を抑えた場合の静止画 | 操作 UI |
防止する失敗に結び付けたルール
凍結した旧フレームとライブの新しいシーンを描画し、一方を他方へフェードさせないでください。 トランジションの開始時に、出ていくシーンをフル解像度のターゲットへ一度だけスナップショットし(時間を停止)、入ってくるシーンはライブで描画し続けます。クロスフェードしたり、アニメーション中の旧シーンの上で燃やしたりすると、画面の一部に 2 つの章が同時に表示され、炎がページを切り替える役割を果たさなくなります。
フロントラインはフィールド単位ではなく、ピクセル単位で計測してください。 xc = (field - T)/grad * pxHeight / pxScale。
レベルセットだけでは、ノイズがスイープと同じ向きに傾く部分で縫い目が非常に細く、逆向きに傾く部分では太くなり、炎の幅が理由もなく不均一になります。
ベイク済みテクスチャの dFdx ではなく、数 px 離れた位置との有限差分で傾きを求めてください。
ベイク済みノイズの傾きはテクセルごとに一定なので、ピクセル単位ではラインを横切る縞模様が生じます。広域フィールドを x と y それぞれの方向に +4 px の位置でサンプリングし(hStep = 4*uPxS/uPxH)、勾配を 0.35/aspect .. 3/aspect の範囲にクランプしてください。
火の粉のゾーンは、段階的な色ではなく、滑らかに発光するグラデーションにしてください。 症状:現実味のないポスターのように見えます。熱量の変数で連続的に色を調整します。フロントラインは黄色、その後オレンジ、最後に輪郭がくっきりと裂けた濃い赤になります。高温の部分では、黄色がさらに後方まで広がります。色のランプ定数(線形、トーンマップ前):
| 段階 | rgb | 位置 |
|---|---|---|
| 濃色 | 0.34, 0.022, 0.003 | リップ、tau 0.05 |
| オレンジ | 0.86, 0.20, 0.024 | tau 0.42 |
| ゴールド | 1.02, 0.38, 0.055 | tau 0.76 |
| ホットゴールド | 1.10, 0.64, 0.17 x (0.30 + 0.70*hot) | tau 0.86 超 |
| 火の筋 | 1.40, 0.56, 0.09 から 1.85, 1.02, 0.30 まで熱量で変化 | フロントライン |
| グロー | 0.95, 0.20, 0.022 x 0.60 | 火の筋の周囲 2~5 px |
高温色は金色に保ち、クリーム色にしないでください。 淡い黄色は、高温時にはクリーム色に、暗くするとカーキ色に見えます。色のランプにはグレーも白も含めません。暗い部分は単に暗くするのではなく、ランプに沿って低い色へ移行させます。
フロントラインは明るい筋にし、ビーズ状のふくらみやくびれを持たせてください。幅は 0.7~5 px とし、均一な線にしないでください。
均一な線はネオンのように見えます。幅は (0.9 + 2.6*heat) * bead とし、bead はエッジに沿ったノイズで 0.5~1.5 の範囲にします。タイトなグローのみを付けてください(exp(-|xc+0.5wc| / (2 + 2.8*heat)))。幅広のハローは付けません。
静止時やスイープ中も、目に見えて動く必要があります。 症状:「動きが少なく、静的すぎる」。エッジに沿って両方向に走るパルス(sA*0.018 - t*1.30、sA*0.027 + t*1.70)で熱の動きを表現し、t*7 でちらつかせ、ゾーンを通って -t*2.6 で巻き戻し、ビーズを漂わせ(-t*0.8)、前面に舌状の炎をなめるように走らせます(t*1.1、t*2.8、画面の最大 0.022 と 0.010)。静止画ではこれらがすべて見えないため、実時間で再生したクリップを一時停止して、燃え方を判断してください。
黒い炭化部分はアルファマスクにします。平坦な板にしてはいけません。 炭化部分は幅 26 px(さらに前面に 12 px の余裕)を持つ帯で、ライブシーン上で、ちぎれた奥側のエッジから崩れ落ちます。時間とともに穴が開き、下にあるものが見えるようになります。黒一色の帯はワイプのように見えます。覆う部分 = bed * pores。エッジは 8*noise(0.08) + 3*noise(0.25) px だけちぎれさせます。これは前面自体の傾斜よりも穏やかなため、島状の部分が切り離されることなく、ほつれたように見えます。
穴の周囲に光る縁取りを決して付けないでください。 縁取りが炭化部分のエッジに沿って、ひとつながりの輪郭として描かれていました。穴はただの穴です。
炭化部分のエッジには、細かい炎ではなく、幅の広い前面を使ってください。 2 次元ノイズでは炭化部分の島が切り離され、新しいフレーム上に浮かんでしまいます。エッジに沿ったノイズのみを使い(kbEdgeNLine)、edge = zoneW + fs + bedW*(0.35 + 1.10*n) とします。fs は最大 12 です。
炎より前方にある古いフレームを、20~40 px の範囲でひと続きに暗く焦がします。 ノイズで範囲に変化を付け、tk^2 によって、黄褐色の 0.80, 0.62, 0.42 から濃い茶色の 0.20, 0.10, 0.045 へと暗くします。3 つの個別の段階では、炎の形をなぞる輪郭が描かれてしまっていました。
ノイズは事前に生成し、ピクセルごとに fbm を重ねないでください。 2880x1800 でのコストは、ピクセルごとの fbm が 1 パスあたり 1.5~3 ms、同じ見た目を実現するタイル可能なテクスチャとハッシュテーブルなら 0.3~0.6 ms です。一度だけ生成します。16 セル周期の 1024^2 RGBA16F テクスチャ(r は 3 オクターブの fbm、g は 3 オクターブの ridged、b は gradient、a は 2 オクターブの ridged で、すべて x*0.0625 でサンプリング)と、2048^2 R16F テーブルの整数ハッシュ格子を使います。fraction の smoothstep でフィルタリングすれば、value noise と厳密に一致します。
ライブシーンは表示された側に限定して切り抜き、小さく描画します。 ラインの奥側だけを対象とし、ベイと霞のために画面の 0.16 を追加します。燃焼中は 0.75 スケールで描画します(コンポジットはフルサイズのままです。目に映るのはエッジだからです)。遷移が 150 ms 静止したら、0.3 s かけてフルサイズのシーンにクロスフェードします。その間は両方を描画するため、シャープネスの変化は見えず、動いているフレームのコストにもなりません。ライブ側のテキストはコンポジット内でフルサイズにします。0.75 バッファーに描画するとぼやけます。
何も届かない場所では、ノイズ処理を省略します。 hx の位置によって、前面より 0.215 + 0.15 以上先の部分では凍結フレームを返し、後方の 0.25 - 0.215 の部分ではライブフレームを返します。その帯域内でも、前方 62 px より先(さらに余裕分を追加)または後方 125 px なら、同じように処理します。これによって、デバイスのフル解像度で描画できます。
流れ込む文字がある側からスイープします。 hx = dir>0 ? uv.x : 1-uv.x。テキストが右側にある章は、右側から燃え込ませます。逆方向に進む場合も、遷移先の側を使います。常に左から右へ進むラインでは、新しいタイトルが最後に横切られてしまいます。
燃焼ごとにシードを設定します。 seed = (chapter+1)*17.3 によって、フレーム上の幅の広いベイを 2.0~3.6 個選び、その深さ(0.055~0.115)と傾き((sv.x-0.5)*0.07)を決めます。これにより、燃焼のシルエットが毎回異なります。
定数
| 項目 | 値 |
|---|---|
| 帯の半幅 | 0.215 画面の高さ |
| 前線、線形 | -0.26 + 1.62 * rc、rc = clamp((f-0.01)/0.97)、フレーム外から入り、フレーム外へ抜ける |
| 時間 | デフォルト 3.0 秒。すべて読めるのは 1.5〜5 秒 |
| 線ノイズのスケール | 広域 q*1.10(0.055-0.115)、q*2.60(0.050-0.030)。微細 0.014、0.006、0.0025 |
| 残り火ゾーンの幅 | 12〜66 px、(18 + 20*min(w,2.6)) * (0.55 + 0.45*life) |
| 炭 | ベース 26 px、表面 3〜6 px、墨 0.0085, 0.0062, 0.0048 |
| 熱による揺らぎ | uPxH の約 1.8 px、前線から 18 px 以内のみ |
| ブルーム | 明るい部分のしきい値 1.15、半解像度と 1/4 解像度のぼかし、重み 0.26*4 と 0.34*4 |
| トーン | ビネット 0.42 + 0.58v、ACES、sRGB、粒子 0.003 |
| ライブシーン | 移行中は 0.75 スケール、150 ms 停止、0.3 秒のクロスフェード |
| 上限値 | dt は 1/30 秒に制限、DPR の上限は 2 |
注意点と症状
- 炎が黒い炭の上の細い赤い線になり、残り火ゾーンがありません。 焼き込み済みノイズとハッシュサンプラーがバインドされていないため、
lifeがフレームテクスチャの値を読み取っています。uKbNとuKbHをバインドしてください。 - 線の後ろに何もない状態、または急な切り替わりが表示されます。 ライブシーンはシザー領域(
front + 0.215 + 0.16)の内側にしか描画されません。余白が狭いと、コンポジットが未描画のテクセルを読み取ります。 - 炭の縁に階段状のギザギザや 2x2 のブロックが現れます。 スキップされた分岐の先で微分を計算しています。フィールド独自のスケール(
kbAA(v, w))からピクセルの変化量を計算してください。 - 新しいフレーム上に炭の島が浮いています。 遠い側の縁を微細ノイズから切り出しています。広域の前線から取得してください。
- 残り火の縁の内側に白っぽい帯が現れます。 カーブで黒を持ち上げると、紙の上で白っぽく色あせます。代わりにゲインで暗い領域を照らしてください。
- 燃焼が終わった後、線の後ろのシーンがぼやけています。 0.75 スケールのバッファが残っています。停止時間の後にフルサイズのクロスフェードを実行してください。
- タイミングが遅く見えます。 非表示の Browser ペインがレンダリングを続け、GPU を占有しています。GPU フラグ(
--use-angle=metal --enable-gpu)を指定したヘッドレスブラウザーで、ペインを最小化して測定してください。Kibori のトランジションは、ノイズを焼き込み、シーンをシザーで切り抜いた後、p50 8.3 ms(上限 120 Hz)を維持しました。同じ方法で、このデモの燃焼を 1440x900 で測定した結果は p50 8.3 ms、p90 8.8 ms でした。 - 前線が通過する際、粒子と残り火が揺れて見えます。 それらは常に紙面上に配置してください(紙面座標の
gに、燃焼ごとのオフセットを加える)。画面空間には配置せず、動かすのは前線だけです。
アクセシビリティとライフサイクル
prefers-reduced-motion: reduce:燃焼は行いません。各シーンを固定時刻でデザインされた静止画として描画し、停止フレームとライブフレームを 0.35 秒かけてクロスフェードします(smoothstep)。コントロールは引き続き操作できます。document.hiddenのとき、およびキャンバスがビューポートから外れたときは一時停止します。再開時には時間の基準をリセットし、一時停止時間が最初のフレームの積分に含まれないようにします。dtを 1/30 秒に制限し、DPR の上限を 2 とします。ResizeObserverでサイズを決定し、ビューポートがゼロの場合は何も構築しません。- 燃焼中に押されたボタンの対象をキューに追加します。線が抜けた後に、最後の対象だけを実行します。
- 実際のボタン、矢印キー、1〜3 のキーで章を切り替えられます。ライブリージョンで到着した章を読み上げます。
コスト
ソースページでの測定では、コストの原因は炎ではなく、ピクセル単位のノイズでした。ノイズの焼き込みと、線から離れた領域で通常のシーンを返すことが、効果の大きい対策です。0.75 スケールとシザーは次に有効です。コンポジットに対する MSAA の調整は避けてください。縁は kbAA によってピクセル単位で描画されます。
出典
KIBORI から抜粋しました。章同士が燃え移る WebGL の木工工房ページで、フッターの紙の燃焼表現が見た目の元になっています。demo.html の EDGE GLSL(炎、炭、細孔、表面、焦げ)と燃焼パスは、そのページ独自のものを移植し、シーンをシェーダーで描画する 3 つの部屋に置き換えています。