mcpField
F
fire-paper-loader

fire-paper-loader

github.com/mengtoデザイン・UI7.1k358 回インストール3 日前に更新
リポジトリ

fire-paper-loader について

Vanilla JavaScript、WebGL、Worker で燃える紙のリングを描くローダーのスキルです。ページ構築でメインスレッドが塞がる間も OffscreenCanvas でアニメーションを続け、ウォームアップ、進行状況の引き継ぎ、フェード、アクセシビリティを扱います。

主な機能

  1. 01Worker と OffscreenCanvas による描画
  2. 02メインスレッド停止中のアニメーション
  3. 03シェーダーの並列コンパイルと準備
  4. 04ページアニメーションへの進行状況引き継ぎ
  5. 05動きを減らす設定と進行状況表示

活用シーン

  1. 01構築に時間がかかるページのローダー制作
  2. 02メインスレッド停止中もアニメーションを維持
  3. 03ローダーからページ導入アニメーションへ接続

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mengto/skills --skill fire-paper-loader

インストール後、Claude Code で/fire-paper-loaderを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

demo.html
PROMPT.md
agents/
openai.yaml

詳細

Fire-Paper Loader

ページの構築中、黒い背景に燃える紙の輪が開きます。**仕組み:炎は<head>から起動するOffscreenCanvas上でWorkerが描画するため、メインスレッドが数百ミリ秒ブロックされても輪は燃え続けます。**炎の見た目は演出です。どんな見た目でも、450 ms停止するローダーはフリーズしたように感じられます。

関連するスキル:web-design-performanceは一般的なページ速度を扱います。このスキルは、スレッドがブロックされても動き続けるローダーに特化しています。スクロール遷移で紙が燃える演出(ローダーではありません)には、同じkbFlameの炎のGLSLを使いますが、ページが描画します。スピナーやAIステータスのオーブには、thinking-orbsを使ってください。

ソースで確認したスタック:Vanilla JavaScript、生のWebGL 1(GLSL ES 1.00、ローダーではThree.jsを使用しません)、WorkerとOffscreenCanvas、ラベルとフェード用のCSS。

bodyではなくheadから開始する

症状:ローダーの表示が1~2秒遅れる、WorkerがGLコンテキストを取得できない、またはthree.jsのダウンロード中に炎が止まる。Workerの起動とWebGLコンテキストの作成が可能なのは、メインスレッドが空いているときだけです。それは<head>内でスタイルシートを読み込んでいる間であり、ページの解析が終わるまでは再び空きません。

  • <head>内の<script>でcanvasを作成し、transferControlToOffscreen()を呼び出します。小さなブートスクリプトをBlob URLからWorkerとして起動し、{kbPre, canvas}をpostして、Worker内でgetContext('webgl', {antialias:false, alpha:false, depth:false, stencil:false})をすぐに呼び出します。canvasとWorkerはwindow.__kbPreに保持します。
  • ブートスクリプトはシェルにすぎません。ローダー自身の<script>(後でbody内に記述)から、{kbInit, src, canvas:null, t0, origin: performance.timeOrigin, reduced, view, target}を付けて、そのtextContentをWorkerに送信し、Workerがそれをevalします。1つのソーステキストを両方のスレッドで実行します:const IN_W = typeof document === 'undefined'。コードを2つに分けると、互いにずれていきます。
  • 各ステップをすべてガードします。Worker、OffscreenCanvas、transferControlToOffscreenが使えない場合、Workerのonerrorが発生した場合、またはfailメッセージを受信した場合は、同じウォームアップの後、同じコードでメインスレッドに描画します。
  • クロックを変換します。Workerのperformance.now()はページのものと異なります。performance.timeOriginを送信し、Workerでperformance.timeOrigin - originを加算して、ページが読み取るすべてのタイムスタンプをページ側のものにそろえます。

並行してコンパイルし、パイプラインをウォームアップしてから表示する

症状:最初に表示されるフレームが、読み込み中で最も遅くなります(30~130 ms、Metalでは一度408 ms)。

  1. 2つのプログラム(ring、cinders)を作成し、シェーダーをアタッチして、両方に対してlinkProgramを呼び出してから、ステータスを確認します。KHR_parallel_shader_compileを使うと、ドライバーが独自のスレッドでコンパイルします。シェーダーごとにCOMPILE_STATUSを読み取ると、その効果が損なわれます。この拡張機能がある場合は、シェーダーごとのチェックを省略します。
  2. warmUp():両方のプログラムを使った実際のフレーム描画を1x1のビューポートに対して実行し、1ピクセルをreadPixelsで読み取って待機します。最初の描画でGPUパイプラインが構築されるため、何も表示される前にこれを済ませます。
  3. その後で初めてrequestAnimationFrameを開始します。Workerが描画すると(通常0.15~0.3秒、タイムアウトは1.2秒)、ページのLOAD.ready()が解決します。ビルド処理はこれを待機するため、炎が止まったままスレッドを1秒間占有することはありません。
  4. canvasは2つではなく1つだけ表示します。両方のスレッドで炎を描画すると、最初に表示されるフレームでコンパイル時間が発生しました。

リングの進み具合は、見た目の変化に合わせて計画します

症状:炭の大きさが1秒間変わらず、その後2.5倍に跳ね上がります。または、止まったリングを速いリングが追い越し、2つの炎のように見えます。

  • 生の進行度 u ではなく、S = gain + ln(radius / first radius) で成長を計画します。Sの等間隔なステップは、どのサイズでも同じような成長に見えます。フェードインはSの0.3を占めます(1では、0.9秒のあいだサイズが抑えられていました)。
  • S を臨界減衰スプリング(角周波数5/s、速度を1.6/sに制限、1/120秒刻みで積分)で追従させます。目標は実際の進行度ですが、クリープを下回ることはありません。creep = 0.38*(1 - exp(-t/800 ms)) とし、待機中は目標が WAIT 0.290を超えないようにし、時定数2.5秒で CEIL 0.335へ近づけます。強制的に停止させたあとに進行度がまとまって届くと、リングがいったん止まってから押し出されたように見えます。
  • 動きを止めないでください。速度の最小値は VMIN 0.016/s(8 fpsで1フレームあたり約5 px)、最大値は VMAX 0.05/sです。見た目の半径は、進行度0で4 pxの小さな点、0.15までに5.8 pxとなり、その後は炎本来の半径になります。
  • ページに何も表示される前から、進行度は独自のクロックでゆっくり増えていくため、three.jsがまだ読み込み中でもリングは表示されます。

滞りなく引き継ぐ

症状:リングがページのイントロに飛び込むように移る、またはローダーが消えるときにページが55~240 ms停止します。

  • done() は、クリープの速度からイントロの速度 V_HAND 0.24(u/s)へ、S上でエルミート補間を行います。所要時間は ds = clamp(2.8*du/vH, 0.30, 1.2) 秒で、リムが引き継ぎ位置の進行度 HAND 0.36に達したときに通知します。ページはその進行度とクロックから、イントロを正確に再開します。rAFが停止した場合も解決されるよう、安全タイマー(doneDur + 400 ms、ページ側では2.4秒)を設けます。
  • ラベルは100に達し、コンポジター上でフェードします(transition: opacity .40s linear .12s)。最初のフレームが混み合っていても、フェードが途中で打ち切られることはありません。
  • close():alive = false に設定し、描画を停止するようワーカーに伝えます。フェード中に2つのコンテキストが描画するとGPUを共有し、フェードのフレーム時間は35~67 msになりました。最後のフレームを表示したままにして、CSSのopacityでフェードさせます(120 ms、視差効果を減らす設定時は250 ms)。イントロ本来のカーブに沿ったWeb Animationsのキーフレームで拡大縮小するため、フェード中もページ上のリムに沿った状態を保てます。
  • ページが表示されているあいだは、worker.terminate() を呼び出さないでください。 呼び出すタイミングを問わず、メインスレッドが55~240 ms停止しました。free を送信し(ワーカーのスレッド上でワーカー内のGLコンテキストを破棄し)、終了するのは visibilitychange で非表示になったとき、または pagehide のときだけにします。メインスレッド上で loseContext() を呼び出すのも避けてください。イントロの最初のフレーム(100 ms)を含む、キューに入ったすべてのGPU処理の完了を待つことになります。
  • ビルドの各工程の間で処理を譲ります。await LOAD.step(p) は進行度を設定し、最大45 msごと(ワーカーが描画する場合は、カウントに必要なだけなので90 msごと)に、requestAnimationFrameを1回とsetTimeout 0を待ちます。非表示のタブではrAFが実行されないため、待機しません。

燃えるような見た目:フッターが基準です

オーナーが挙げた症状の順番:「動きが少なすぎる」、「黒い部分はアルファマスクにするべき」、その後、4段階にポスタライズしたグラデーションを試した結果、「非現実的すぎる。前のほうがよかった。フッターの燃え方のほうがよかった」。

  • ポスタライズした段階ではなく、発光するグラデーション。 残り火のゾーンは黄色からオレンジ、濃い赤へとなめらかに変化し、熱い部分は黄色がさらに後方まで伸び、最後はくっきりとした裂けた縁で終わります。はっきり分かれた段差はポスターのように見えます。
  • 先端に明るく粒立つ糸状の光を配置し、幅は0.7~5 px、長さに沿って細くなったり膨らんだりさせます(均一な線はネオンのように見えます)。2~5 pxのタイトな加算グローを加えます。それ以上広くしてはいけません。太いブルームを加えると、縁がネオンの輪になってしまいます。
  • 動かす必要があります。 熱の脈動が縁に沿って両方向に走り(run)、パッチが7 Hzでちらつき(flk)、火の粉が漂い、ロールがゾーンを後方へ移動します。静止画では問題なく見えても、ローダーは動いている状態で評価されます。
  • クリーム色ではなく、ゴールド。 最も高温のコアカラーは(1.85, 1.02, 0.30)、残り火の色は最大でも(1.10, 0.64, 0.17)で、熱い部分だけに使います。クリーム色やカーキ色になったのは、ホットエンドを白くしたり、暗くするために全体を暗くしたりしたためです。暗い部分はその代わりに、グラデーションに沿って濃い赤へと変化させます。
  • 縁取りのない、多孔質の炭化部分。 残り火の後ろにある炭化帯は、繊維の粒子感を持つ、ほぼ黒の(0.0085, 0.0062, 0.0048)です。時間の経過とともにその中の穴が開き、下にあるフレームが見えるようになります。穴の周りを発光する縁で囲んではいけません。穴同士がつながり、縁に沿った単一の輪郭線になってしまいました。
  • 輪は、円盤や「o」の形から始めてはいけません。 3.5~7 pxの厚さで、半周を少し超える、裂けた残り火の弧から始めます。その両端が這うように進んで6.5~12 pxの間で合流します。輪の幅を3.5~7 px未満にすると、描いた輪郭線のように見えてしまいます。
  • 火の粉:4つのインスタンスセット(1800、720、360、160個のクワッド、mulberry32 0x2f6b9d1でシード固定)。上部の弧(20~160度)からだけ発生し、寿命は短く、色は熱を帯びたオレンジから濃い赤までで、白にはなりません。ACESでトーンマッピングした後、シェーダーでsRGBに変換する(aces、次にsrgb)ため、gl_FragColorのアルファ値は1、キャンバスはalpha:falseにしてください。

実測コスト

  • メインスレッド上で描画すると、ビルドによって一度に250~450 msブロックされ、炎のフレーム間の最長時間は450 msでした。同じページでワーカーを使うと9 msでした。
  • DPRは最大1.5で描画します(ソースでの上限値。一般的なウェブの上限は2)。輪は全画面三角形1つと火の粉のクワッド3040個で構成されます。コストがかかるのはフラグメントシェーダーであり、火の粉ではありません。
  • タイムステップ:ワーカーは独自のフレームで描画します。ディスプレイのクロックはページ側にあるため、アニメーションのdtには設計上クランプが適用されます(dtpの最大値は1.5秒、ばねのサブステップは1/120秒)。

ライフサイクルとアクセシビリティ

  • ラベルにrole="progressbar"を設定し、aria-valuenowを進捗に合わせて更新します。キャンバスにはaria-hiddenを設定します。カウントはテキストなので、炎がなくても読み取れます。
  • prefers-reduced-motion: reduceの場合:時間を進めず(tSec = 0)、火の粉を表示せず、コンポジターによる拡大縮小も行いません。それでも輪は進捗に合わせて開き、静止した粒子感のある縁として表示され、フェードは250 msです。フレームは非表示にするのではなく、静止画として設計されています。
  • タブが非表示の場合:両方のスレッドでrAFが一時停止し、step()は待機を止めます。done()はすぐに解決され、ワーカーはその後にのみ終了されます。
  • 12秒経過しても進捗が40パーセント未満の場合、ラベルに「まだ着火中」と表示します。接続が遅い場合、いつまでも静かに燃やし続けるのではなく、そのことを知らせる必要があります。
  • リサイズ時:新しいview(w、h、DPR)をワーカーに送信します。キャンバスは次のフレームでリサイズされます。

このスキルでできることの範囲

仕組み(維持):先行起動するワーカー、2つのスレッドで共有する1つのソース、並列コンパイルと完了を待つ1 pxのウォームアップ、S空間でのペース配分、Hermiteによる引き継ぎ、停止後のCSSフェード、早期終了なし。演出(自由に置き換え可能):炎の配色と形状、ラベルの文言、引き継ぎ先のページ。デモではページのイントロを模しています。done()の後、ローダーの輪はイントロのカーブに沿って1.8秒間動き続け、フレームの外へ出ます。その時点で実際のページが、引き継いだ進捗から独自の縁を描画します。

ソース:KIBORI 木彫のKINDLINGローダーから抽出。KIBORI 木彫はスクロール連動型のThree.js木工ページで、1.1 MBのビルドがメインスレッドを最大1.2秒間ブロックしていました。

コメント