BLOG

はじめての SVG Animation〜足跡デモで理解する〜

Written by XU

INDEX

はじめに

SVG には、図形そのものにアニメーションを付けられる仕組みがあります。
CSS や JavaScript を使わなくても、SVGの中だけで動きを完結できるのが特徴です。
実務では、アニメーションは CSS や JS で作ることが多いと思います。

ただ、SVG Animation は次のような場面で意外と役に立ちます。

  • 小さな UI 演出を軽く入れたいとき
  • デモやサンプルをシンプルに見せたいとき
  • 外部ライブラリに依存したくないとき
  • ユーザーの視線を自然に誘導したいとき
  • 「ここを押してほしい」「ここを見てほしい」ポイントをやさしく目立たせたいとき

強いアニメーションは逆に邪魔になりますが、小さく、意味のある動きは UI の中でとても良い「案内役」になります。
この記事では、難しい仕様の話は置いておいて、「何を動かしたいか」でタグを選ぶという考え方を、足跡のデモを使って説明します。

使うのは次の 3 つです。

  • <animate>:数値や状態が変わるとき
  • <animateTransform>:形・向き・大きさが変わるとき
  • <animateMotion>:決まったルートに沿って動くとき

1. <animate>:数値や状態が変わるとき

<animate> は、SVG 要素の「属性の値」を時間で変えるためのタグです。

よくある使い方は:

  • opacity を 0 → 1 → 0 にしてフェードさせる
  • サイズを少し変えて「注目」を集める
  • 線の進捗や数値を少しずつ進める

つまり、位置は動かさず、見た目の「状態」だけを変えるときに使うのが <animate> です。

Demo 1:足跡が順番に出て、まとめて消える

このデモでは、足跡がひとつずつ順番に現れて、少し残ったあと、最後にまとめて消えます。
やっていることはシンプルで、opacity を <animate> で時間制御しているだけです。
位置や形は一切動かしていません。変わっているのは「見えているかどうか」だけです。
でもそれだけで、「次はここを見てね」という流れを作ることができます。
チュートリアルのステップ表示や、入力フォームの注目ポイントをやさしく目立たせたいときなど、強すぎない視線誘導にちょうどいいタイプのアニメーションです。

<svg viewBox="0 0 460 220" width="460" xmlns="http://www.w3.org/2000/svg" style="color:#4b5563">
  <defs>
    <symbol id="paw" viewBox="0 0 68 60">
      <path d="M34 60C45.0457 60 54 51.0457 54 40C54 28.9543 45.0457 20 34 20C22.9543 20 14 28.9543 14 40C14 51.0457 22.9543 60 34 60Z" fill="currentColor"/>
      <path d="M9 28C13.9706 28 18 23.9706 18 19C18 14.0294 13.9706 10 9 10C4.02944 10 0 14.0294 0 19C0 23.9706 4.02944 28 9 28Z" fill="currentColor"/>
      <path d="M59 28C63.9706 28 68 23.9706 68 19C68 14.0294 63.9706 10 59 10C54.0294 10 50 14.0294 50 19C50 23.9706 54.0294 28 59 28Z" fill="currentColor"/>
      <path d="M34 18C38.9706 18 43 13.9706 43 9C43 4.02944 38.9706 0 34 0C29.0294 0 25 4.02944 25 9C25 13.9706 29.0294 18 34 18Z" fill="currentColor"/>
    </symbol>
  </defs>

  <g transform="translate(76 86) scale(0.32) rotate(90)">
    <use href="#paw" x="-34" y="-30" opacity="0">
      <animate attributeName="opacity" dur="6s" repeatCount="indefinite" values="0;0;1;1;0;0" keyTimes="0;0.00;0.08;0.80;0.90;1"/>
    </use>
  </g>

  <g transform="translate(170 54) scale(0.32) rotate(90)">
    <use href="#paw" x="-34" y="-30" opacity="0">
      <animate attributeName="opacity" dur="6s" repeatCount="indefinite" values="0;0;1;1;0;0" keyTimes="0;0.12;0.20;0.80;0.90;1"/>
    </use>
  </g>

  <g transform="translate(280 86) scale(0.32) rotate(90)">
    <use href="#paw" x="-34" y="-30" opacity="0">
      <animate attributeName="opacity" dur="6s" repeatCount="indefinite" values="0;0;1;1;0;0" keyTimes="0;0.24;0.32;0.80;0.90;1"/>
    </use>
  </g>

  <g transform="translate(390 54) scale(0.32) rotate(90)">
    <use href="#paw" x="-34" y="-30" opacity="0">
      <animate attributeName="opacity" dur="6s" repeatCount="indefinite" values="0;0;1;1;0;0" keyTimes="0;0.36;0.44;0.80;0.90;1"/>
    </use>
  </g>
</svg>

2. <animate> の応用:startOffset で「視線の流れ」を作る

<animate> は opacity だけでなく、数値の属性なら何でもアニメーションできます。
その分かりやすい例が、<textPath> の startOffset です。
startOffset は、テキストを「パスのどこから配置するか」を決める値です。
この値を <animate> で動かすと、パス自体は動いていないのに、文字だけがパスに沿って流れているように見えるという効果が作れます。

Demo:文字が形に沿ってぐるっと回る

このデモでは、形も画像もそのままで、startOffset の値だけを動かしています。
それだけで、文字がパスに沿って流れているように見え、視線も自然に形のまわりを回るようになります。
transform で要素を動かしているわけでも、テキスト全体を移動しているわけでもなく、「文字をどこから配置するか」というレイアウト用のパラメータを少しずつずらしているだけです。
つまりこれは、<animate> で数値の状態を動かしているだけの、とても素直な使い方です。
ロゴやキービジュアルのまわりに、さりげなく「流れ」を作りたいときにちょうどいい表現だと思います。

<svg viewBox="0 0 200 200" width="300" xmlns="http://www.w3.org/2000/svg">
  <style>
    .ring {
      opacity: 0;
      transition: opacity .25s ease;
      pointer-events: none;
    }
    .track-text {
      fill: #000;
      transition: fill .25s ease;
    }
    svg:hover .ring {
      opacity: 1;
    }
    svg:hover .track-text {
      fill: #fff;
    }
  </style>

  <defs>
    <clipPath id="blobClip">
      <path
       d="M43.6,-73.1C56.9,-67.8,68.5,-57,74.1,-43.9C79.6,-30.8,79.2,-15.4,77.6,-1C75.9,13.5,73,27,67.2,39.7C61.4,52.5,52.8,64.5,41.1,72.7C29.4,80.9,14.7,85.3,-0.4,86.1C-15.6,86.9,-31.2,84,-44.7,76.8C-58.3,69.7,-69.8,58.3,-74.6,44.8C-79.5,31.3,-77.8,15.6,-77.6,0.1C-77.5,-15.4,-78.9,-30.9,-74.3,-45C-69.8,-59.2,-59.4,-72,-46,-77.2C-32.6,-82.5,-16.3,-80.2,-0.6,-79.2C15.1,-78.2,30.3,-78.4,43.6,-73.1Z" transform="translate(100 100)" />
    </clipPath>
  </defs>

  <image
    href="https://www.publicdomainpictures.net/pictures/40000/velka/cute-dog-1364063120lnA.jpg"
    width="200"
    height="200"
    clip-path="url(#blobClip)"
    preserveAspectRatio="xMidYMid slice"
  />

  <path
    id="track"
d="M43.6,-73.1C56.9,-67.8,68.5,-57,74.1,-43.9C79.6,-30.8,79.2,-15.4,77.6,-1C75.9,13.5,73,27,67.2,39.7C61.4,52.5,52.8,64.5,41.1,72.7C29.4,80.9,14.7,85.3,-0.4,86.1C-15.6,86.9,-31.2,84,-44.7,76.8C-58.3,69.7,-69.8,58.3,-74.6,44.8C-79.5,31.3,-77.8,15.6,-77.6,0.1C-77.5,-15.4,-78.9,-30.9,-74.3,-45C-69.8,-59.2,-59.4,-72,-46,-77.2C-32.6,-82.5,-16.3,-80.2,-0.6,-79.2C15.1,-78.2,30.3,-78.4,43.6,-73.1Z" transform="translate(100 100)"
    fill="none"
    stroke="none"
    pathLength="1000"
  />

  <use
    href="#track"
    class="ring"
    transform="translate(100 100)"
    fill="none"
    stroke="#000"
    stroke-width="2.2"
    stroke-linecap="round"
    stroke-linejoin="round"
  />

  <text class="track-text" font-size="8">
    <textPath href="#track" startOffset="0%">
      I ❤ WANKO I ❤ WANKO I ❤ WANKO I ❤ WANKO I ❤ WANKO I ❤ WANKO I ❤ WANKO I ❤ WANKO I ❤ WANKO I ❤ WANKO I ❤ WANKO
      <animate attributeName="startOffset" from="0%" to="100%" dur="20s" repeatCount="indefinite" />
    </textPath>

    <textPath href="#track" startOffset="0%">
      I ❤ WANKO I ❤ WANKO I ❤ WANKO I ❤ WANKO I ❤ WANKO I ❤ WANKO I ❤ WANKO I ❤ WANKO I ❤ WANKO I ❤ WANKO I ❤ WANKO
      <animate attributeName="startOffset" from="-100%" to="0%" dur="20s" repeatCount="indefinite" />
    </textPath>
  </text>
</svg>

3. <animateTransform>:形・向き・大きさが変わるとき

次は <animateTransform> です。
これは、移動・回転・拡大縮小といった transform をそのまま動かすためのタグです。
少し上下に揺れたり、押されたように潰れたり、わずかに傾いて戻ったり。
こういう「物としての動き」を作りたいときに向いています。
ボタンやアイコンに「ここ触れるよ」「今アクティブだよ」と伝えるための、さりげない合図としてもよく使われます。

Demo 2:足跡が「カクッ」と揺れて、クリックを誘う

このデモでは、足跡を 拡大縮小(scale)回転(rotate) だけで揺らしています。
ポイントは calcMode="discrete" を使っているところで、動きがヌルっと滑らず、拍で「カクッ」と切り替わります。結果として、足跡がちょっと反抗的に暴れてるみたいな(teen rock っぽい)ノリになって、UI 的には 「ここ押せるかも」 という軽いサインにもなります。
このように、要素の形や表示を変えずに、オブジェクト全体の transform だけでテンポを作りたいときは、<animateTransform> が一番素直です。

<svg viewBox="0 0 340 140" width="340" height="140" xmlns="http://www.w3.org/2000/svg" style="color:#374151">
  <defs>
    <g id="paw">
      <g transform="translate(-34 -30)">
        <path d="M34 60C45.0457 60 54 51.0457 54 40C54 28.9543 45.0457 20 34 20C22.9543 20 14 28.9543 14 40C14 51.0457 22.9543 60 34 60Z" fill="currentColor"/>
        <path d="M9 28C13.9706 28 18 23.9706 18 19C18 14.0294 13.9706 10 9 10C4.02944 10 0 14.0294 0 19C0 23.9706 4.02944 28 9 28Z" fill="currentColor"/>
        <path d="M59 28C63.9706 28 68 23.9706 68 19C68 14.0294 63.9706 10 59 10C54.0294 10 50 14.0294 50 19C50 23.9706 54.0294 28 59 28Z" fill="currentColor"/>
        <path d="M34 18C38.9706 18 43 13.9706 43 9C43 4.02944 38.9706 0 34 0C29.0294 0 25 4.02944 25 9C25 13.9706 29.0294 18 34 18Z" fill="currentColor"/>
      </g>
    </g>
  </defs>

  <g transform="translate(170 72)">
    <g>
      <g transform="scale(0.46)">
        <use href="#paw" fill="currentColor" stroke="#fff" stroke-width="1.5" paint-order="stroke"/>
      </g>

      <animateTransform attributeName="transform" type="scale" additive="sum"
        dur="0.8s" repeatCount="indefinite" calcMode="discrete"
        values="1;0.76;1;0.76"/>

      <animateTransform attributeName="transform" type="rotate" additive="sum"
        dur="0.8s" repeatCount="indefinite" calcMode="discrete"
        values="-8 0 0;8 0 0;-8 0 0;8 0 0"/>
    </g>
  </g>
</svg>

4. <animateMotion>:決まったルートに沿って動くとき

<animateMotion> は、「このルートをたどって見てほしい」というときに使えるアニメーションです。単に動くだけでなく、視線を「この順番で見てね」と案内するという役割も持たせることができます。

Demo 3:足跡が S 字カーブに沿って進む

このデモでは、足跡が S 字カーブのパスに沿って移動します。
rotate="auto" を指定しているので、進む方向に合わせて足跡の向きも自然に回転します。
ここで大事なのは:

  • 「どこを通るか」が主役
  • X/Y を計算して動かしているわけではない

という点です。
ルートそのものに意味がある動きなら、<animateMotion> を使うととても分かりやすく書けます。

<svg viewBox="0 0 340 160" width="340" xmlns="http://www.w3.org/2000/svg" style="color:#1f2937">
  <defs>
    <symbol id="paw" viewBox="0 0 68 60">
      <path d="M34 60C45.0457 60 54 51.0457 54 40C54 28.9543 45.0457 20 34 20C22.9543 20 14 28.9543 14 40C14 51.0457 22.9543 60 34 60Z" fill="currentColor"/>
      <path d="M9 28C13.9706 28 18 23.9706 18 19C18 14.0294 13.9706 10 9 10C4.02944 10 0 14.0294 0 19C0 23.9706 4.02944 28 9 28Z" fill="currentColor"/>
      <path d="M59 28C63.9706 28 68 23.9706 68 19C68 14.0294 63.9706 10 59 10C54.0294 10 50 14.0294 50 19C50 23.9706 54.0294 28 59 28Z" fill="currentColor"/>
      <path d="M34 18C38.9706 18 43 13.9706 43 9C43 4.02944 38.9706 0 34 0C29.0294 0 25 4.02944 25 9C25 13.9706 29.0294 18 34 18Z" fill="currentColor"/>
    </symbol>

    <path id="sCurve" d="M20,108 C90,23 250,143 320,73"/>
  </defs>

  <use href="#sCurve" fill="none" stroke="currentColor" stroke-opacity=".2" stroke-dasharray="4 6"/>

  <g>
    <g transform="scale(0.45)">
      <use href="#paw" x="-34" y="-30"/>
    </g>
    <animateMotion dur="4s" repeatCount="indefinite" rotate="auto">
      <mpath href="#sCurve"/>
    </animateMotion>
  </g>
</svg>

5. 組み合わせ例:1 つの足跡で「道」と「視線」を案内する

最後は、これまでの考え方をまとめたデモです。
地図の上にルートが描かれ、その上を 1 つの足跡が進み、ゴール地点で少し止まってから、また最初に戻ります。
見た目としては、「この道を進んでね」と足跡が案内してくれるような UI になっています。

このデモでは、3 種類のアニメーションを役割ごとに使い分けています。
まず、ルートの線と足跡の表示・非表示は <animate> で制御しています。
線は stroke-dashoffset を動かして「道が示されている感じ」を出し、足跡は opacity でフェードイン・フェードアウトさせています。
ここは、見え方や数値の状態をコントロールする役です。
次に、足跡の「踏み込み感」は <animateTransform> で付けています。
少し大きくなったり、少し縮んだりする scale の変化を重ねることで、ただ移動するだけでなく、「ちゃんと歩いている感じ」を出しています。
ここは、オブジェクトとしての姿勢や形の変化を担当しています。
そして、実際にルートに沿って移動するのが <animateMotion> です。
rotate="auto" を指定しているため、進行方向に合わせて足跡の向きも自然に切り替わります。
最初の約 7 秒でルートを最後まで進み、残りの 1 秒はゴール地点で止まってから、また最初に戻る、という流れになっています。
ここでは、「どの道を通るか」そのものをこのタグに任せています。
技術的にはそれぞれ役割が分かれているだけですが、見た目としては「道が示され、足跡が進み、ゴールが強調される」という流れになります。
このデモは「地図が静止画ではなく、動きで情報を伝える」という発想から作りました。
ハリーポッターの“動く地図”のように、ルートと現在地を軽く示すだけでも、UI の理解はぐっとスムーズになります。

<svg viewBox="0 0 360 200" width="360" xmlns="http://www.w3.org/2000/svg" style="color:#2b2015">
  <defs>
    <g id="paw-shape">
      <path d="M34 60C45.0457 60 54 51.0457 54 40C54 28.9543 45.0457 20 34 20C22.9543 20 14 28.9543 14 40C14 51.0457 22.9543 60 34 60Z" fill="currentColor"/>
      <path d="M9 28C13.9706 28 18 23.9706 18 19C18 14.0294 13.9706 10 9 10C4.02944 10 0 14.0294 0 19C0 23.9706 4.02944 28 9 28Z" fill="currentColor"/>
      <path d="M59 28C63.9706 28 68 23.9706 68 19C68 14.0294 63.9706 10 59 10C54.0294 10 50 14.0294 50 19C50 23.9706 54.0294 28 59 28Z" fill="currentColor"/>
      <path d="M34 18C38.9706 18 43 13.9706 43 9C43 4.02944 38.9706 0 34 0C29.0294 0 25 4.02944 25 9C25 13.9706 29.0294 18 34 18Z" fill="currentColor"/>
    </g>

    <g id="paw-centered" transform="translate(-34 -30)">
      <use href="#paw-shape"/>
    </g>

    <path id="route" d="M30 160 H90 V120 H150 V75 H220 V125 H300 V85"/>

    <linearGradient id="paper" x1="0" y1="0" x2="1" y2="1">
      <stop offset="0%" stop-color="#eadbbd"/>
      <stop offset="100%" stop-color="#c9ad83"/>
    </linearGradient>

    <filter id="grain" x="-20%" y="-20%" width="140%" height="140%">
      <feTurbulence type="fractalNoise" baseFrequency="0.95" numOctaves="2" seed="11" result="n"/>
      <feColorMatrix in="n" type="saturate" values="0"/>
      <feComponentTransfer>
        <feFuncA type="table" tableValues="0 0.07"/>
      </feComponentTransfer>
    </filter>
  </defs>

  <rect width="360" height="200" fill="url(#paper)"/>
  <rect width="360" height="200" filter="url(#grain)" fill="#000"/>
  <rect x="8" y="8" width="344" height="184" fill="none" stroke="#7b5a35" stroke-width="2.1" opacity=".62"/>
  <rect x="14" y="14" width="332" height="172" fill="none" stroke="#8f6f47" stroke-width="1" stroke-dasharray="2 6" opacity=".34"/>

  <g transform="translate(324 34)" fill="none" stroke="#65492c" opacity=".56">
    <circle r="12" stroke-width="1.2"/>
    <circle r="5.5" stroke-width=".8" opacity=".7"/>
    <path d="M0-16V16M-16 0H16M-8-8L8 8M8-8L-8 8" stroke-width=".8"/>
    <path d="M0-12L3.2 0L0 12L-3.2 0Z" fill="#65492c" fill-opacity=".22" stroke-width=".7"/>
  </g>

  <path d="M-8 114C32 96 72 98 108 108C146 118 186 122 230 114C270 106 306 106 370 122"
        fill="none" stroke="#4d6475" stroke-width="2.6" stroke-opacity=".22"/>

  <g fill="none" stroke-linecap="round">
    <g stroke="#5f4529" stroke-width="1.7" opacity=".42">
      <path d="M20 46C84 40 132 44 198 46C262 48 302 46 338 42"/>
      <path d="M24 78C90 74 140 78 204 80C270 82 306 80 336 76"/>
      <path d="M24 152C90 148 138 151 206 153C270 155 306 154 336 150"/>
      <path d="M64 20C62 56 64 92 66 178"/>
      <path d="M132 20C130 56 132 92 134 178"/>
      <path d="M206 18C204 56 206 94 208 178"/>
      <path d="M286 20C284 56 286 92 288 178"/>
    </g>
    <g stroke="#b49873" stroke-width=".5" opacity=".24">
      <path d="M20 46C84 40 132 44 198 46C262 48 302 46 338 42"/>
      <path d="M24 78C90 74 140 78 204 80C270 82 306 80 336 76"/>
      <path d="M24 152C90 148 138 151 206 153C270 155 306 154 336 150"/>
      <path d="M64 20C62 56 64 92 66 178"/>
      <path d="M132 20C130 56 132 92 134 178"/>
      <path d="M206 18C204 56 206 94 208 178"/>
      <path d="M286 20C284 56 286 92 288 178"/>
    </g>
  </g>

  <g fill="none" stroke="#684d2d" stroke-width="1.1" opacity=".42">
    <path d="M98 102V123M196 110V126M292 106V123"/>
  </g>

  <g fill="#5e4528" font-family="Georgia, 'Times New Roman', serif" letter-spacing=".5" opacity=".5">
    <text x="24" y="18" font-size="7.2">OLD QUARTER</text>
    <text x="130" y="18" font-size="7.2">MERCHANT WARD</text>
    <text x="245" y="18" font-size="7.2">RIVER DOCK</text>
  </g>

  <use href="#route" fill="none" stroke="#3f2d1d" stroke-width="2.2" stroke-linecap="round" stroke-dasharray="5 7" opacity=".58">
    <animate attributeName="stroke-dashoffset" values="0;-120" dur="8s" repeatCount="indefinite"/>
  </use>

  <g style="color:#251a11">
    <g opacity="0">
      <g transform="rotate(90)">
        <g>
          <g transform="scale(.5)">
            <use href="#paw-centered"/>
          </g>

          <animateTransform attributeName="transform" type="scale"
            dur="8s" repeatCount="indefinite"
            values="1;1.12;0.92;1.04;1"
            keyTimes="0;0.08;0.16;0.26;1"
            calcMode="spline"
            keySplines=".25 0 .25 1;.2 0 .2 1;.25 0 .25 1;.25 0 .25 1"/>
        </g>
      </g>

      <animateMotion dur="8s" repeatCount="indefinite" rotate="auto"
        keyTimes="0;0.875;1" keyPoints="0;1;1" calcMode="linear">
        <mpath href="#route"/>
      </animateMotion>

      <animate attributeName="opacity" dur="8s" repeatCount="indefinite"
        values="0;1;1;0"
        keyTimes="0;0.02;0.96;1"/>
    </g>
  </g>
</svg>

まとめ:動きは「飾り」ではなく「案内役」

今回のデモを作っていて思ったのは、SVG Animation は「かっこよくするための演出」というより、ユーザーを迷わせないための小さなナビゲーションとして使うのが一番きれい、ということです。
ざっくり言えば、

  • 状態や数値を変えたいなら <animate>
  • 形や姿勢を動かしたいなら <animateTransform>
  • ルートに沿って動かしたいなら <animateMotion>

と考えておけば、大きく外れることはありません。
SVG Animation は軽くて、構造も分かりやすく、デモや小さな UI 演出にはちょうどいい選択肢です。
「今、何を動かしたいんだろう?」を先に考えてからタグを選ぶ。これだけで、無理のない実装にしやすくなります。

参考

https://developer.mozilla.org/ja/docs/Web/SVG/Reference/Attribute/startOffset

https://developer.mozilla.org/zh-CN/docs/Web/SVG/Reference/Element/animate

https://developer.mozilla.org/zh-CN/docs/Web/SVG/Reference/Element/animateMotion

https://developer.mozilla.org/zh-CN/docs/Web/SVG/Reference/Element/animateTransform

https://www.w3.org/TR/SVG11/animate.html

https://codepen.io/tag/svg-animation