ブログのマスコットのSVGアニメーションを設計した話

娘と作った栗の工作をもとにしたマスコットに、SVGで動きを付けました。「体はのんびり、目だけが素早い」をコンセプトに、視線、まばたき、ジャンプの設計で考えたことと、動きを確かめる方法をまとめます。

このブログで使っている栗のマスコットは、自然素材を使った工作イベントで娘と一緒に作ったものが元になっています。個人的にとても愛着があり、それ以来ブログのマスコットとして使っています。正式な名前はまだ決まっていませんが、娘は「クリッち」と呼んでいました。

今回は、このクリッちのSVGをアニメーション化するにあたり、調べたことや設計・実装の工夫についてまとめました。 もともとは静止画のSVGでしたが、ポインターを目で追ったり、まばたきをしたり、クリックすると跳ねたりするように動かしています。

ただ動かすだけでなく、愛着の湧く魅力的な動きにするために、キャラクターの性格や動きの緩急(イージング)をどう設計したのか。ChatGPTに壁打ち相手になってもらいながら調べた知見や、実装時のこだわりを紹介します。

TL;DR

  • 動きの特徴の言語化(コンセプト)
    • キャラクターの性格から逆算し、あらかじめ「動きの特徴(パーツごとの緩急や反応差)」を言葉にして設計の基準をつくる
  • 「生きている感」を出す動きの設計
    • 視線:目標が変わり続ける動きには「ばねモデル」を使い、位置と速度を滑らかに引き継ぐ
    • まばたき:開閉時間を非対称(速く閉じてゆっくり開く)にし、発生間隔にもランダムな揺らぎを持たせる
    • ジャンプ:ポーズの形だけでなく、動きの接続部分の速度を確認し、滑らかにつなぐ部分と衝撃を出す部分を分ける
  • 違和感を客観的に潰す検証
    • 等速再生で全体の印象を掴みつつ、目視で追えない細かな違和感は「位置・速度のグラフ化」やコマ送りで特定する

コンセプト決め

アニメーションを設計するにあたり、まずは「動きを通じてどんな性格を表現したいか」を考えました。行き着いたコンセプトは、「体はのんびり、目だけが素早い」です。

半目でぼーっとしているように見えるけれど、頭は冴えていて、周りの様子には敏感に反応する――そんなキャラクター像を持たせたかったため、体はあまり動かさず、周囲への反応は主に「目の動き」で表すことにしました。一方でジャンプするときには、沈み込む予備動作や着地の潰れ・伸びをしっかり入れ、体の重みを感じられるようにしています。

なお、体と2本の足は1つのSVGパスで描かれているため、足だけを独立して動かすことはできません。そのため普段の動きは、この「体全体のパス」と「左右の黒目・まぶた」の動きを組み合わせて表現しています。

SVGを動かす仕組み

このブログはAstroで構築しているため、SVGもコンポーネント内に直接インラインで配置しています。アニメーションは、CSSとブラウザ標準のAPIを組み合わせて実装しました。

仕組み担当している動き
CSS @keyframes呼吸や寝息の「z」など、一定周期で繰り返す動き
Web Animations API登場、ジャンプ、まばたき、眠りなど、トリガーによって開始・終了する動き
requestAnimationFrameポインターの位置に合わせて視線をリアルタイムに計算・更新する動き

CSSのキーフレームとWeb Animations APIは、キーポーズとその間の遷移時間を指定し、間をブラウザに補間させます。イージングは、その補間にどのような緩急をつけるかを決めるものです。「素早く閉じてゆっくり開くまばたき」や「跳ぶ前の沈み込み」など、動きのニュアンスに合わせて設定しました。

一方の視線追従は、追従先(ポインター)がユーザーの操作によって常に動きます。あらかじめ決めたキーフレームを再生するわけにはいかないため、こちらはばねの物理計算を用いて毎フレーム位置を求めています。

Framer Motionなどのライブラリ導入も検討しましたが、今回必要だったのは「繰り返す動き」「イベント発火で再生する動き」「ポインターへの追従」の3点だけでした。前者2つはCSSとWeb Animations APIで十分対応でき、視線追従も小さなスプリング計算関数を自作すれば賄えると判断したため、外部ライブラリは採用していません。

目の動きの設計

初期の設計メモでは、「視線はCSSの transition: 420ms で追従させ、まばたきは開眼と閉眼の画像をパッと切り替えるだけ」という単純な案でした。そこから、より生き生きとした緩急を付けるべくChatGPTと相談しながら、人間の眼球運動やアニメーションの原則を取り入れてブラッシュアップしていきました。

サッカードと目の速さ

「サッカード」に着目したきっかけは、かんな@バーチャルJCさんの記事「〖MMD〗何もしない(アニメーション)を極める」でした。「目線を動かす」の節で、視線が瞬時に動いてピタッと止まる様子をアニメーションで表現する重要性が解説されています。

サッカードとは、ある対象から別の対象へと視線を素早く移すときの眼球運動のことです。典型的な持続時間はわずか20〜100msとされています1。

このマスコットでも、目がゆっくり流れるように動くより、見たい方向へ「スッ」と素早く寄るほうがキャラクターのコンセプト(目は素早い)に合致します。このメリハリのある視線移動をどう実現するかが次の課題でした。

ばねで表す視線の追従

サッカードを参考に「見たい方向へ素早く寄る」印象を作ろうと考えたものの、ポインターのように目標が動き続ける対象をどう追わせるかは悩んだところでした。

ChatGPTに相談しながら調べたところ、目標へ引っ張る力とブレーキ(減衰)の組み合わせで位置を計算する「ばねモデル」が扱いやすいと分かりました。

ばねの計算自体は人間の目の医学的な仕組みとは別物ですが、「ポインターが急に向きを変えても直前の速度を引き継げる点」や、「素早く寄せてピタッと落ち着く動きを作りやすい点」が、求めている表現にぴったりだと考え採用することにしました。

設定したパラメータは以下のとおりです。

パラメータ設定値役割
硬さ (stiffness)260目標位置へ引き寄せる強さ
減衰 (damping)30移動の勢いを抑える強さ(ブレーキ)
質量 (mass)1計算上の質量(今回は1で固定)

反応は素早くしたいものの、黒目が行き過ぎて何度もプルプル揺れてしまうと、落ち着いた表情が損なわれます。そのため、素早く寄ったあと揺れずにピタッと止まるバランスを狙いました。

この値の減衰比2を計算すると 30 / (2 × √260) ≈ 0.93 となり、揺れを起こさずに最短で止まる「臨界減衰(減衰比 = 1)」にかなり近い値です。硬さの数値だけでなく、減衰との組み合わせによって「止まり方の質感」をコントロールしています。

ばねの計算と位置の更新

決めたパラメータをもとに、画面のリフレッシュに合わせて黒目を動かす実装が以下です。

requestAnimationFrame を使い、ブラウザの描画タイミングに合わせて毎フレーム計算を行っています3。

ディスプレイのリフレッシュレートや負荷によってフレームごとの経過時間(デルタタイム)は変動するため、前回フレームからの経過時間をもとに速度と位置を算出します。フレーム落ちなどで間隔が空いた際に計算が破綻しないよう、経過時間の上限を32msに抑え、さらに内部で1/120秒刻みに細分化して積分しています。

function createSpring(onChange: (value: number) => void, stiffness = 260, damping = 30) {
  let value = 0;
  let velocity = 0;
  let target = 0;
  let frame = 0;
  let previous = 0;

  const step = (now: number) => {
    let dt = previous ? Math.min((now - previous) / 1000, 0.032) : 1 / 60;
    previous = now;
    // フレーム間隔が空いても発散しないよう、1/120秒刻みで積分する
    while (dt > 0) {
      const h = Math.min(dt, 1 / 120);
      velocity += (-stiffness * (value - target) - damping * velocity) * h;
      value += velocity * h;
      dt -= h;
    }
    if (Math.abs(value - target) > 0.0005 || Math.abs(velocity) > 0.0005) {
      onChange(value);
      frame = requestAnimationFrame(step);
    } else {
      value = target;
      velocity = 0;
      frame = 0;
      previous = 0;
      onChange(value);
    }
  };

  return (next: number) => {
    target = next;
    if (!frame) frame = requestAnimationFrame(step);
  };
}

まばたきの設計

閉じる速さと開く速さ

まばたきは、閉じる時間と開く時間を非対称に設計しています。人間のまばたきでも、開く動きより閉じる動きのほうが速いことが報告されています4。

今回は、閉じる時間を65ms、閉じたまま維持する時間を25ms、開く時間を140msとしました。イージングも、閉じるときは勢いを付け、開くときはゆっくり止まる設定にしています。

また、白いまぶたが縁の線ごと下りて黒目を隠す構造ですが、閉じる途中で黒目も少し上へ逃がす処理を入れました。まぶただけがスライドするよりも、目全体の連動感が出るようにしています。

実際のまばたきは、次のような動きです(GIFでは確認しやすいよう、1回のまばたきのあとに間を置いて繰り返しています)。

速く閉じてゆっくり開くまばたき

以下は、まばたき1回(230ms)を約1/60秒ごとに並べたものです。

まばたきを1/60秒ごとに並べたフィルムストリップ

まばたきの間隔

毎回同じ間隔でまばたきを繰り返すと、規則的な周期が目立ってしまいます。そこで、間隔は2.2秒以上の指数分布で決め、12%の確率で2回続けてまばたきするようにしています。

眠りの設計

うとうとしてから眠る

眠るときは、まぶたを一度に閉じきるのではなく、重くなったまぶたを一度持ち直してから閉じる動きにしています。半目の表情と、のんびりした性格に合うよう、眠りに落ちるまでの「間」を作りました。

まぶたのアニメーション全体は1.7秒です。最初の40%の時間で55%ほど閉じ、60%の時点で閉じ具合が20%に戻り(約8割開いた状態まで持ち直し)、そのあと完全に閉じきります。持ち直す部分には --ease-doze、最後に閉じる部分には --ease-drift-off を使い、単調な閉じ方にならないようにしました。

眠る前には視線を正面へ戻し、眠っている間はポインターを追わないようにしています。目を閉じた直後には体を少し横に広げて縦に縮め、脱力した感じを出しました。

まぶたを一度持ち直してから眠るマスコット

起きるときの速さ

眠る動きはゆっくりですが、起きるときは160msで素早く目を開きます。眠っているところをクリックすると、目を開けてジャンプし、しばらくするとまた眠ります。眠りに入る緩やかさと、操作への反応の速さにメリハリを付けました。

ジャンプの設計

予備動作と着地

ジャンプは、8つのキーポーズをたどる620msのアニメーションです。跳ぶ前に沈み込み、離陸し、頂点を経て落下し、着地で潰れてから元の形へ戻ります。

上下の位置移動だけでなく、離陸で縦に伸び、着地で横に潰れる変化を付けることで重さを表現しています。伸縮時は、極端な面積変化が起きないよう scaleX と scaleY の積による面積比を0.94〜1.06の範囲に収めました。

以下は、8つのポーズを並べたものです。

ジャンプの8つのキーポーズ

左から、静止、予備動作、離陸、頂点、落下、着地、反動、静止

空中の軌道とイージング

空中の軌道は、放物線から位置を計算し、そのキーポーズの間を等速(リニア)で補間しています。放物線の計算自体に「上昇中の減速」と「落下中の加速」が含まれているため、ここにさらに頂点付近で減速するイージングを重ねると、滞空時間が長く感じられることがあります。そのため、空中部分には追加のイージングを付けていません。

イージングは、跳ぶ前の沈み込みや押し出し、着地後の反動など、地面から力を受ける部分に限定して適用しています。

離陸前後の速度

離陸前の押し出しと空中の移動を別々に作ると、その境目で速度が急変することがあります。そこで、押し出しの終端速度が空中の初速へとスムーズにつながるように調整しました。

具体的には、押し出しで体が上がる距離をジャンプの高さの0.16倍から0.24倍へ増やし、--ease-push の終端速度も合わせています。距離とイージングを組み合わせ、同じ時間の中でどの程度加速するかをすり合わせるためです。

コマごとの計測では、離陸前後の速度比が1.62から1.11になりました。

修正前後のジャンプを、20ms刻みで再生するGIFで並べました。短い動きなので、等速では差が分かりづらいかもしれません。GIFは動き全体の印象を確かめるために使い、境目の速度差は、後述するカーブと計測値で確認しています。

押し出しと離陸速度の修正前のジャンプ

修正前

押し出しと離陸速度の修正後のジャンプ

修正後

なお、跳ぶ前の踏み込みはまだ表現が弱いと感じています。地面をしっかり押して跳び上がる感じが出るように、今後もう少し改善したい部分です。

動きの組み合わせ

登場

ページ上部では、罫線の下から顔を出し、少し行き過ぎてから着地する動きにしています。登場のアニメーションは540msです。

罫線の下から顔を出して着地する登場の動き

優先度

ジャンプ中にも視線追従や呼吸が重なると、見せたい動きが散らかってしまいます。動きが整理されて伝わるよう、次の優先度を決めています。

  1. 登場・ジャンプ
  2. まばたき
  3. 視線
  4. 呼吸・見回し

登場やジャンプの最中は、下位の動きを停止します。普段は目線が反応し、跳ぶときには体全体のアクションに注目できるようにしました。

動きを減らす設定

prefers-reduced-motion: reduce が指定されている環境では、呼吸、見回し、視線の追従、登場、ジャンプを停止するように実装しました。眠っているマスコットをクリックした際も、跳ねずに目を開けて、そのまま再び眠る挙動に差し替えています。

アニメーション検証の進め方

動きの設計と並行して、試した動きを客観的に確かめる方法も整えました。気になる箇所を繰り返し比較できるよう、コマ送りの撮影と計測を行うPlaywrightスクリプトを用意しています。

見る順番

設計した動きは、まず等速で見て全体の印象を確認し、そのあとポーズや軌道を分解して見るようにしています。

手順確認すること
等速キャラクターの印象、動きの速さ、間
キーポーズ予備動作や着地など、要所の姿勢が読み取れるか
シルエット体と目、それぞれの動きがどう見えるか
オニオンスキン5前後のコマを重ねて、間隔や軌道を比べる
速度カーブ位置・伸縮・速度が時間とともにどう変わるか

以下は、体だけを残したものと、体を薄くして目を見せたものです。体と目がそれぞれどのタイミングで動くかを確認できます。

体だけと目だけのキーポーズ

上が体だけ(目を非表示)、下が目だけ(体を半透明化)

カーブの読み方

動いているアニメーションを目視していて「一瞬速くなった気がする」と感じても、肉眼だけでその発生コマを特定するのは困難です。位置や速度を時間軸のグラフに並べることで、どの区間で変化が起きているかを確認できます。

以下は、ジャンプ時の位置・速度・伸縮のカーブです。横軸は時間で、左端が開始、右端が620msの終了を表します。青い縦位置と、紫の垂直方向の速度に注目します。

ジャンプの位置・速度・伸縮のカーブ

各点は約1/60秒ごとの値。線ごとに縦軸の尺度が異なるため、線どうしの高さや振れ幅は比較せず、それぞれの値の変化を見る

SVGの座標系は下向きを正としているため、青い線が下がるほど画面上では高く跳んでいる状態を表します。中央付近にある青い線の底が、ジャンプの頂点です。

紫の速度線も、画面上へ向かう上昇速度が負、画面下へ向かう落下速度が正になります。離陸後に紫の線が開始時の高さ(基準線)へ近づいていく区間は上昇の減速を表し、そこを越えて上へ進む区間は重力による落下の加速を表します。着地で線が急変するのは、地面に衝突して落下が止まるためです。

ここで確認したいのは、カーブ全体が滑らかかどうかではありません。着地のように速度が急変すべき箇所もあるためです。重要なのは、離陸前の押し出しから空中へ移る部分のように、滑らかにつながるべき箇所で意図しない速度差(急激な変化)が生じていないかを特定することです。

この図は変化の形を比べるためのもので、正確な速度比や合否は次の数値計測で確認しています。

数値で確認すること

目で見た印象の確認に加えて、接地や速度の変化などを客観的な数値でも確認しています。以下は、このマスコットの大きさと動きに合わせて決めた基準です。

項目基準
接地地面に着いている区間の上下のずれが0.5CSS px以下
面積伸縮による面積比(scaleX × scaleY)が0.94〜1.06
着地直前4コマで下向きの速度が増加し続けている
離陸離陸前後のコマの速度比が1.6以下
まばたき閉じる途中が2コマ以上、閉じきった状態が1〜2コマ

Playwrightからアニメーションを一時停止し、Web Animations APIの currentTime6 で時刻を指定しながらコマごとの姿勢を撮影・計測しています。現在の動きは、これらの基準を満たすように調整しました。

おわりに

同じ1枚のSVGでも、どのパーツを動かすか、どのくらいの速さで動かすか、どこで止めるかによって、キャラクターの印象は大きく変わります。今回は「体はのんびり、目だけが素早い」という軸を決めたことで、それぞれの動きの設定を考える基準ができました。

参考

現在のマスコットの動きは、デザインシステムのカタログで再生できます。

基盤 - KJR020's Blog デザインシステム
KJR020's Blogで共有する色・文字・余白・Gridと、レスポンシブ・アクセシビリティの基準
基盤 - KJR020's Blog デザインシステム favicon kjr020.dev
基盤 - KJR020's Blog デザインシステム

脚注

  1. 大きなサッカード(視線を素早く移す眼球運動)の速度は毎秒500度を超えることがあり、典型的な持続時間は20〜100msとされています。Sushant Puri, Aasef G Shaikh「Basic and translational neuro-ophthalmology of visually guided saccades: disorders of velocity」Expert Review of Ophthalmology, 2017(2026年10月確認) ↩

  2. 減衰の強さを、行き過ぎずに最短で止まる「臨界減衰」の何倍かで表した値。硬さk、減衰c、質量mのとき、c / (2√(km)) で求めます。1未満では行き過ぎて揺れながら止まり、1で行き過ぎずに最短で止まります。Damped Oscillations - OpenStax(2026年10月確認) ↩

  3. 次の画面の再描画前にコールバックを呼び出すようブラウザへ要求するAPIです。続けて動かす場合は、コールバック内で次の呼び出しを予約します。Window: requestAnimationFrame() method - MDN(2026年10月確認) ↩

  4. 健康な成人の自発的なまばたきを光学式に計測した研究では、全員で、閉じる動きが開く動きより40〜47%速かったと報告されています。Sforza C, et al.「Spontaneous blinking in healthy persons: an optoelectronic study of eyelid motion」Ophthalmic & Physiological Optics, 2008(2026年10月確認) ↩

  5. 複数のコマを半透明に重ねて同時に表示する、アニメーション制作の技法。前後の姿勢や間隔を比較するために使います。Onion Skin Docker - Krita Manual(2026年10月確認) ↩

  6. Animation.currentTime は、再生中か一時停止中かにかかわらず、アニメーションの現在の時刻をミリ秒で取得・設定できるプロパティです。Animation: currentTime property(2026年10月確認) ↩

Esc
キーワードを入力して検索