GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

キャンバス 炎 アニメーション9選 — シード固定パーティクルで描く火のシーン

炎 アニメーションとは、燃焼画像を順番に再生する画面ではなく、燃料の値が毎フレーム減り、浮力で上昇し、色温度の階段を下っていく規則を計算して描く画面です。ライブラリ·WebGLなし、キャンバスのピクセル計算だけで、ろうそくから火種の拡散までの9つの場面を実測しました。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

燃料セルひとつの規則が九つの炎に枝分かれする理由

並び順の基準は派手さではなく、炎がいくつの力を背負っているかです。01のろうそくは燃料パーティクルが生まれて浮力で上り、色温度で冷えていく最小単位です。02はその単位を450個集め、ポテンシャル場の回転(カール)から作った風でひと塊にうねらせます。03と07は同じ浮力の上に外のベクトルを乗せますが、03が向き変わらない風に緩やかな揺れを足すのに対し、07はpointermoveで測った実座標から風を作ります。04はパーティクルの代わりに半径ごとに減衰する光の層で後光を描き、06と08は全パーティクルの予算を一瞬の爆発と一つの進行頭に注ぎ込みます。05と09はパーティクルを捨てて、熱と燃料を格子セルに保存し隣とやり取りする拡散の計算に移ります — 9つ全部を貫く軸は絵ではなく、減っていく燃料というひとつの数です。

燃料の数がすべての名前になっている

パーティクルのkも格子セルのheatも、中身は同じ「1に近い値から0へ向かって減る数」です。明るさ・半径・色温度の階段は全部この数から読み取ります。css 炎 エフェクトをキーフレームで作ると形そのものを手で描くことになりますが、計算で作る炎はこの引き算ひとつから形が勝手に生えます。

炎はなぜ計算で描くのか — 燃料セル·浮力·カール乱流の計算

燃料は0へ向かって数えるただの数

パーティクルのkもセルのheatも、名前が違うだけで同じ「1に近い値から0へ向かう数」です。炎 アニメーション 描き方をスプライトシートで探すと毎フレーム絵を用意することになりますが、燃料が数になった瞬間に色の階段·縮む半径·消える時刻の全部がフレームごとの一回の引き算から落ちてきます。フレームを増やしてもファイルは重くならず、ポインターの入力にその場で反応できます。

浮力は速度ではなく加速である

本物の炎と煙は一定の速さで漂うのではなく、加速します — 熱い気体が周囲の空気より軽く、押し上げられ続けるからです。01と04は生まれた瞬間のvyをパーティクルごとに違う乱数で一度だけ決め、02はさらに毎フレームMath.min(1, dt * 3.2)でカール場の目標速度へ引き寄せた上にBUOY * 190の加速を足します。 rocket のような硬い上昇にならない鍵は、どちらもパーティクルごとに違う時刻·違う強さで揺れることにあります。

カール乱流は集まらずに回り続ける風

02の風の場はスカラー場ψの回転v = (∂ψ/∂y, −∂ψ/∂x)から作り、この作り方には発散がどこでもゼロになる性質があります。単純な乱数の風でパーティクルを運ぶとたまに全部が一点に寄ってしまいますが、発散ゼロの場は質量を回すことしかできないので、3本の火柱は互いに巻き込みながらもひとつの塊に潰れません。

格子の拡散はパーティクルなしで燃焼を運ぶ

05と09はParticleをひとつも作りません。熱と燃料を格子と同じ大きさの2つの平らな配列に保存し、各セルが毎ステップ隣の平均を取るだけです。パーティクル方式が個々の火の粒の動きを見せるのに対し、格子方式は燃焼が地図を食う面積の変化を見せます。同じ炎のアニメーションでも、見せるものが違うのです。

項目コード

各項目とも、動作を決める定数の名前を明示し、動いているデモのソースからそのまま写した関数を示します。打ち直した要約ではなく実コードなので、このページを炎 アニメーション 作り方の引きとして、そのまま自分の画面に持ち込めます。

01ろうそく一本 — りんの上に立つ最小の火

りんの先で生まれたパーティクルが浮力で上り、寿命とともに黄→橙→赤へ冷えていく — その規則だけでろうそくになります。瞑想・夜のムードページの背景のろうそくに、これ以上のものはいりません。実測では画面の38.292%がフレーム間で変化し、23/23フレームが動いていました。

ろうそく一本 — りんの上に立つ最小の火
新しいタブで開く

01 · ろうそく一本 — りんの上に立つ最小の火 — 計測 累積面積 38.292% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

fillRectglobalCompositeOperationrequestAnimationFrame

燃料パーティクルひとつがろうそくになる規則

var K_BURN = 2.3;      // 燃料消費率 (1/s)
var STILL_T = 1.6;     // reduced-motion で描く時刻 — 後光が最も読める一場面

for (var j = 0; j < N_PART; j++) {
  var q = parts[j];
  q.k -= K_BURN * dt;
  if (q.k <= 0) { spawn(q); continue; }
  var sway = Math.sin(tSec * 7.3 + q.ph) * 8 * q.k;
  var fx = cx + q.x + q.vx * q.k + sway + wind * (1 - q.k) * 26;
  var fy = wickTop + q.y * (0.6 + 0.4 * power);
  q.x += q.vx * dt;
  q.y += q.vy * LIFT * dt;
}

パーティクルが持つ値は位置·速度·燃料kだけで、N_PART240個が毎秒K_BURN2.3ずつkを失い、0になったらりんから生まれ直します。りんに立つ火の細さは生まれた瞬間のvy(-20〜-48)がパーティクルごとに違うことで生まれ、光の層はLAYERS13枚をFALLOFF1.05で減衰させて重ねます。

02焚き火 — カールノイズが揺らす火の柱

ポテンシャル場の回転(カール)をサンプリングして各パーティクルに接線の風を与えると、薪がひと塊になってうねる焚き火が計算で立ち上がります。キャンプ·アウトドアLPのヒーロー背景です。風の場は発散ゼロなのでN_COL3本の火柱は互いの乱流に巻き込まれながら、一点に潰れることもありません。

焚き火 — カールノイズが揺らす火の柱
新しいタブで開く

02 · 焚き火 — カールノイズが揺らす火の柱 — 計測 累積面積 37.507% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

fillRectglobalCompositeOperationrequestAnimationFrame

接線への漸近が慣性になる補間

for (var j = 0; j < N_PART; j++) {
  var q = parts[j];
  q.k -= K_BURN * dt;
  if (q.k <= 0) { spawn(q); continue; }
  var wx = baseX + q.x;       // 世界座標 — カール場はここをサンプリング
  var wy = ground + q.y;
  var cw = curl(wx, wy, tSec);
  q.vx += (cw[0] - q.vx) * Math.min(1, dt * 3.2);
  q.vy += (cw[1] - q.vy) * Math.min(1, dt * 3.2) - BUOY * 190 * dt;
  q.x += q.vx * dt; q.y += q.vy * dt;
}

カール場の目標速度に毎フレーム少しずつ寄せるMath.min(1, dt * 3.2)の補間が慣性で、そこにBUOY0.35の定数加速が上向きに加わります。ポインターは火の位置ではなく火力だけを動かします — yがpower(0.6〜1.4)を決め、xの値は保持されるだけで描画には使われません。この火力の介入のみがパーティクルの運動に外から触る値です。

03松明 — 風ベクトルを背負った炎

一定の風ベクトルにゆっくりした揺れを足して炎全体を一方向へ流すと、持ち歩く松明の傾いた火になります。ゲーム·イベントページの聖火の松明です。WIND_VX46は自分では変わらず、変わるのはその向きの角度だけです。

松明 — 風ベクトルを背負った炎
新しいタブで開く

03 · 松明 — 風ベクトルを背負った炎 — 計測 累積面積 34.055% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

fillRectglobalCompositeOperationrequestAnimationFrame

一定ベクトルに足す回転ひとつ

var baseAng = Math.atan2(WIND_VY, WIND_VX) + dirTilt;
var wobAng = baseAng + Math.sin(tSec * 0.7) * 0.35;
var windMag = Math.hypot(WIND_VX, WIND_VY) * strength;
var windVX = Math.cos(wobAng) * windMag;
var windVY = Math.sin(wobAng) * windMag;

var fx = cx + q.x + q.vx * q.k + sway
       + windVX * lean * 0.55 * dt * 60;
q.x += (q.vx + windVX * lean * 0.4) * dt;
q.y += (q.vy + windVY * lean * 0.3) * dt;

基本の角度にMath.sin(tSec * 0.7) * 0.35という低い周波数の回転をひとつ足すだけで、火柱が硬く死なず聖火が「息をしながら寝る」ように見えます。LAYERS15枚の光の層がFALLOFF0.9で減衰しながら夜の原を照らします。

04ランプの炎 — ガラスの後ろに滲む後光

小さな炎に半径ごとに減衰する光の層を重ねると、ガラスランプの中で揺れる火が周囲を温める場面になります。静かな店舗·ホテルの紹介画面です。LAYERS11枚のグラデーション環は炎の揺れを追うひとつの中心を共有し、外側の環も完全には消えません。

ランプの炎 — ガラスの後ろに滲む後光
新しいタブで開く

04 · ランプの炎 — ガラスの後ろに滲む後光 — 計測 累積面積 43.983% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

fillRectcreateRadialGradientglobalCompositeOperation

半径の減衰が主役、パーティクルは従

var R0 = Math.min(W, H) * 0.11 * breathe;
var A0 = 0.52 * (0.8 + 0.2 * flick) * (0.86 + 0.14 * Math.sin(tSec * 2.6 + 1.1));
var RMAX = Math.hypot(W, H) * 0.98;   // 最外環は遠い隅まで届く

for (var L = 0; L < LAYERS; L++) {
  var Lt = L / Math.max(1, LAYERS - 1);
  var R = R0 + (RMAX - R0) * Math.pow(Lt, 0.72);
  var a = Math.max(0.06, A0 * Math.pow(R0 / R, FALLOFF));
  // 環 L は半径 R・不透明度 a の放射グラデーションで描く
}

環ごとにcreateRadialGradientを作り直してlighter合成で重ねると、中心の熱々な金色と外側の室内温度のローズトーンが自然に混ざります。K_BURN1.35と消費率を低く抑えた分だけ各パーティクルが長く生き、後光が細かく途切れません。

05溶岩の流れ — 格子セルに積もる熱

画面をセルに分けて隣接セル同士が熱をやり取りさせ、閾値を超えたセルだけ溶岩色で描くと、固まっては溶ける流れが低解像度格子に現れます。ゲーム背景·火山コンセプトの紹介画面が得るのは、パーティクルの飛沫ではなく地質的な遅い流れです。セルは隣接4マスの平均を取って熱を配り、T_MELT0.62を超えたマスだけが橙に溶けます。

溶岩の流れ — 格子セルに積もる熱
新しいタブで開く

05 · 溶岩の流れ — 格子セルに積もる熱 — 計測 累積面積 39.884% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ImageDataputImageDatarequestAnimationFrame

隣接平均と放射冷却の拡散規則

for (var yy = 1; yy < GH - 1; yy++) {
  for (var xx = 1; xx < GW - 1; xx++) {
    var i2 = yy * GW + xx;
    var c = heat[i2];
    var nU = heat[i2 - GW], nD = heat[i2 + GW], nL = heat[i2 - 1], nR = heat[i2 + 1];
    var avg = (nU * 0.9 + nD * 1.1 + nL + nR) / 4;
    var nh = c + K_DIFF * (avg - c);
    nh = nh - nh * COOL * (0.6 + nh);   // 熱いセルほど強く冷える
    heat2[i2] = nh < 0 ? 0 : (nh > 1.4 ? 1.4 : nh);
  }
}

隣接4マスの平均を取るとき下側を0.9·1.1と非対称に重み付けすると、熱は拡散するだけでなく上へ育つ流れに見えます。K_DIFF0.34は格子全体が循環に入るように調整された値で、VENT_N5つの火口が下端の幅全体に均等に並びます。

06花火の爆発 — 重力と残り火の算数

一点から飛び出した破片が重力で放物線を描き、色温度の階段を下って消える — この規則だけで夜空の爆発が完成します。お祝い·年末LPのハイライトは、この落下から来ています。PER_BURST170個の破片が環状の角度で飛び出し、GRAV34がすべてを引き下げます。

花火の爆発 — 重力と残り火の算数
新しいタブで開く

06 · 花火の爆発 — 重力と残り火の算数 — 計測 累積面積 39.459% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

fillRectglobalCompositeOperationrequestAnimationFrame

ひとつの爆発、環状の角度と二段の速度

function burst(x, y, tone) {
  var n = PER_BURST;
  var placed = 0;
  for (var i = 0; i < MAXP && placed < n; i++) {
    var p = pool[i];
    if (p.live) continue;
    var ang = (placed / n) * TAU + (rand() - 0.5) * 0.22;
    var shell = placed % 5 === 0 ? 0.62 : 1.0;   // 内側の環はゆっくり落ちる
    var sp = (46 + rand() * 58) * shell;
    p.live = true;
    p.x = x; p.y = y;
    p.vx = Math.cos(ang) * sp;
    p.vy = Math.sin(ang) * sp - 12;
    p.k = 0.85 + rand() * 0.3;
    placed++;
  }
}

GRAV34が破片を引き下げ、DRAG0.985が毎フレーム速度を削って軌道を丸くたわませます。5個に1個はshell0.62の内環に回り、二段の速度差が爆発の立体感を作ります。MAXP1,700は同時多発発射に耐えるプールです。

07ろうそくと風 — ポインタへ寝る炎

ポインター位置から炎までの方向を風ベクトルに入れ、距離で強さを減衰させると、手を近づけるたびに寝るろうそくになります。インタラクティブカード·特別な瞬間の飾りが、実際のカーソルに反応します。pointermoveの実座標を読むのはこの項目だけで、力はMath.pow(60 / dist, FALLOFF)で遠ざかるほど急に弱まります。

ろうそくと風 — ポインタへ寝る炎
新しいタブで開く

07 · ろうそくと風 — ポインタへ寝る炎 — 計測 累積面積 51.817% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

pointermovefillRectglobalCompositeOperation

測ったポインタからの二乗反比例の風

function pointerWind() {
  if (px < 0 || py < 0) return [0, 0];
  var wickX = W * 0.5, wickY = H * 0.62;
  var dx = wickX - px, dy = wickY - py;
  var dist = Math.sqrt(dx * dx + dy * dy);
  if (dist < 1) return [0, -WIND_MAX];
  var s = Math.min(WIND_MAX, WIND_MAX * Math.pow(60 / dist, FALLOFF));
  return [(dx / dist) * s, (dy / dist) * s];
}

WIND_MAX3.2がポインタがりんに寄った瞬間の最大加速、FALLOFF2.0が距離の二乗反比例の指数です。IDLE_MS2,400ミリ秒のあいだ入力が静まると、りんの真上の風へbreezeの縦の浮力で戻ります。

08流星 — 残像を残す火の軌跡

先頭の後ろにパーティクルの尾を付け、通った場所をゆっくり冷やすと、大気で燃え尽きる流星の軌跡が画面を横切ります。夜景·天気コンセプトページの背景です。頭の軌道はリサージュ曲線で、TAIL_N42個の尾が毎フレームCOOL0.017ずつ冷えながら残像を伸ばします。

流星 — 残像を残す火の軌跡
新しいタブで開く

08 · 流星 — 残像を残す火の軌跡 — 計測 累積面積 47.778% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

fillRectglobalCompositeOperationrequestAnimationFrame

直線ではなくリサージュの頭

function headAt(tSec) {
  var u = tSec * 0.5;
  return [
    W * (0.5 + 0.72 * Math.sin(u * 0.9)),
    H * (0.5 + 0.54 * Math.sin(u * 1.7 + 0.6))
  ];
}

頭の進行方向の単位ベクトルを取り、その逆向きにTAIL_N42個を毎フレーム撒きます。振幅を画面より大きく取ってあるので、頭は画面の外で入り込み外で抜け、23フレーム中21フレームだけが動いているという実測値になりました。尾の冷却が軌跡を埋める長さを決めます。

09火種の拡散 — 燃料セルが燃えて広がる格子

セルごとに燃料を保存し、燃えているセルが隣へ熱を渡して燃料が減る規則を繰り返すと、地図を食い進む火災の拡散が近似できます。ゲームの状態画面·シミュレーションの紹介では、テクスチャをループさせるのではなく、火が実際に地図を消費する様子を見せられます。熱がIGNITE0.34を超えたマスだけが燃え、8方向の隣人に熱を渡します。

火種の拡散 — 燃料セルが燃えて広がる格子
新しいタブで開く

09 · 火種の拡散 — 燃料セルが燃えて広がる格子 — 計測 累積面積 52.501% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ImageDataputImageDatarequestAnimationFrame

8隣接への熱の分配、対角は割り引く

if (heat[i] > IGNITE) {
  if (f > 0) {
    var burn = Math.min(f, BURN_RATE * (0.4 + f));
    fuel[i] = f - burn;
    var give = SPREAD * (0.3 + f * 0.7);
    for (dy = -1; dy <= 1; dy++) {
      for (dx = -1; dx <= 1; dx++) {
        if (!dx && !dy) continue;
        nx = (i % COLS) + dx; ny = ((i / COLS) | 0) + dy;
        if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) continue;
        ni = ny * COLS + nx;
        nheat[ni] += give / (dx && dy ? 1.41 : 1);   // 対角は遠い
      }
    }
  }
}

COLS120列の格子で、燃えているセルがBURN_RATE0.0032ずつ燃料を減らしながらSPREAD0.115の割合で熱を配ります。燃料が尽きたマスは二度と燃えない点が、火が地図を食い潰していく理由です。実測では9項目で最も広い52.501%の画面がフレーム間で変化しました。

炎エフェクトを実際に使う画面

成功メッセージやイベントページのお祝い演出には、重力と残り火で散っていく06の花火の爆発が合います。決済完了や目標達成の瞬間に一度だけ弾けさせれば十分です。ダークモードの切り替えや照明アイコンの場所には、後光だけがガラスの奥に滲む04のランプの炎が馴染みます。

ヒーロー背景に静かな動きを足したいときは、残像を残して流れる08の流星や、最小の単位から始まる01のろうそく一本を低い強度で敷いておきます。

浮力の値を上げすぎると炎がここで壊れる落とし穴

燃料消費率を上げると軌跡が点線になる

06の花火は初めBURNを0.62に置いたところ、破片が2〜3フレームでk0.34を下回り、軌跡が点線のように途切れて見えました。描画そのものは正常で、色温度の階段を速く通り過ぎる消費率が悪かった — 0.34まで下げて初めて残り火がフレームのあいだを埋めるだけ生き残りました。08の冷却率も同じ落とし穴で、短い残像側の値ではなくCOOL0.017と低く取って残像がフレームを埋め、その分プールPOOL1,800も一緒に大きくしました。zipのソースとReact版を開いてこの値を直接揺らしてみるには、解凍のパスワードcb4svrhmを入れてください。

拡散係数が低いと格子全体が循環に入らない

05の溶岩はK_DIFFを0.24に置いたとき、火口の周りの数マスだけが明るく、残りの格子は永遠に閾値を超えませんでした。隣接4マスの平均で運ぶ熱の量が放射冷却に勝てないと、拡散が冷却に食われて画面の半分が死んだ絵として残ります。0.34まで上げて初めて格子全体が温まり冷え、循環に入りました。

格子の幅を狭めると対角割引が目立つ

09は対角の隣人に渡す熱を1.41(√2)で割らないと、火が正方形の形に広がって格子の筋が強く出ました。COLSを増やしてマスを狭くしてもこの割り算がなければ同じ問題が再現します。低解像度の格子で等方的に見せるには、マスの数より先にこの重み付けです。

粒子数ひとつで変わる3つのバリエーションとアクセシビリティ対応

定数ひとつで変わる3つのバリエーション

名前 変える値 手触り
パーティクル数 01ろうそく 240 → 480 炎が濃くなる代わりにフレームが重くなる
浮力 02焚き火 BUOY 0.35 → 0.7 火柱が倍の高さに伸びるが薄くなる
色 $fire-amber → $fire-rose 橙の炎がローズトーンのろうそくに変わる

どれもソース先頭の定数ブロックの一行を変えるだけの軸です。同じ計算のまま値ひとつで違うムードの火が手に入るのが、気分ごとに手で作り直すキーフレームに対する正直な利点です。

動きを減らす設定で残る静止フレーム

prefers-reduced-motion: reduceが検知されたとき、9つ全部がrequestAnimationFrameのループを始めません。代わりに各項目が選んだ一時点を1回だけ描きます — 01はSTILL_T1.6秒(後光が最も読める時刻)、04は1.85秒、06は爆発3発が開いた瞬間、08は時間を進めて残像を作ってから1枚を納めます。格子の05と09は時間ではなく手順で進めます。05はSTILL_TICK210ステップを回して流れが落ち着いた一場面を、09はstill()関数が火種を2つ植えて40ステップ回し、燃え広がりが定位置に収まった一枚を描きます。計算はアニメーションと同じで画面だけ一度なので、品質は変わりません。

キーボードとポインタで触れる場面

02·05·07·09はrole="application"とtabindex="0"を持つ実験盤なので、タブで焦点を移せます。02·05·07はポインタ(タッチを含む)で直接触り、02は火力、05は新しい火口を置き、07は風の向きを変えます。09はポインタ入力がなく、Enter·Spaceキーを押すたびに格子の上の乱数の位置に新しい火種を植えます。07だけはreduced-motionではpointermoveの監視そのものを外して静止した一枚を保ちますが、矢印キーなら静止画を1枚描き直せます。

炎エフェクトを組み込む前によくある質問

cssの炎アニメーションとキャンバスの炎アニメーションは何が違いますか?

CSSアニメーションは決めたキーフレームの間をブラウザが補間する方式なので、揺れの規則がいくつかの値に制限されます。ここにある9つは毎フレーム燃料·浮力·拡散を計算し直すキャンバスの炎 アニメーションなので、シードが同じでも毎瞬違う組み合わせが生まれ、ポインターや格子との相互作用のようにCSSのキーフレームでは作りにくい反応も入ります。

ドット 絵 炎 アニメーションみたいなレトロな見た目にもこの手法は使えますか?

使えます。09の燃料格子はそのままドット絵の炎の骨組みです — マスを大きく取れば取るほどピクセルが粗くなり、SPREADとBURN_RATEで燃え広がる速さと持続力を調整できます。05の閾値2段(溶ける·白く熱する)も、色パレットを段差の強いレトロな色に置き換えるだけでドット絵の炎に変わります。

08の流星だけなぜ23フレーム中21フレームしか動いていないのですか?

頭の軌道がリサージュ曲線で、画面の縁に近い折り返し点では頭が数フレームほとんど止まるためです。その間も尾は冷えながら縮んでいますが、隣接する2つのサンプルの間の移動距離が一定速度の直線ほど伸びず、動いたフレームとして計測の閾値に届きませんでした。軌道の計算自体は23フレーム全部で動いています。

もっと読む

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。