SVGのパスの軌跡に沿って文字を流してみる(備忘録)

今回は、見えない線の軌跡に沿って文字を流しているアニメーションをよく見かけるので、やってみました。

SVGのパスとoffset関連のCSSを使って対応しています。

※今回のdemoはレスポンシブ対応させていません。PC幅でご覧いただくことを推奨します。

HTMLの記述について

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>軌跡に沿って文字を流してみる</title>
  <link rel="stylesheet" href="../reset.css">
  <link rel="stylesheet" href="./style.css">
</head>

<body>
  <main>
    <div class="flow-wrap">
      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 320" width="640" height="320" class="svg-wrap">
        <path id="text-path" d="M 0,160 Q 160,-160 320,160 T 640,160" fill="none"></path>
      </svg>
      <div class="text-wrapper">
        <!-- JavaScriptで生成 -->
      </div>
    </div>
  </main>
  <script src="./style.js"></script>
</body>

</html>

今回は下記画像のような、SVGのサインカーブの波上の軌跡のパスを作成しています。

<path id="text-path" d="M 0,160 Q 160,-160 320,160 T 640,160" fill="none"></path>

このpathタグのdの中で描画していくのですが、なかなか複雑でした。

自分なりに解説しますが、飛ばしていただいても大丈夫です。

M(0,160)でパスの開始点を設定します。

Q(160,-160 320,16)で二次ベジェ曲線を定義します。160,-160 は制御点で、320,160 は曲線の終点です。

M(0,160)とQ(160,-160)、Q(160,-160)とQ(320,16)を線で結びます。この2つの直線に沿った接線のようなかたちで二次ベジェ曲線が描画されます。これで左側の赤い山の部分が描画されます。

最後にT(640,160)です。これはスムーズな二次ベジェ曲線を続けるコマンドです。前のQコマンドの終点320,160を開始点として、新しい終点640,160へとパスが引かれます。

今回は横軸を320の2倍の640で設定したため、点対称な図形が描画され、右側に赤い谷の山が描画されます。

少し難しいかもなので、慣れていない方は、簡単な図形で描画することをおすすめします。参考ページも下記にご紹介します。

FigmaやIllustratorなどを使えばもっと複雑な軌跡を作成することもできます。興味のある方はやってみてください。こちらの手順は今回は割愛させていただきます。

また、実際の線に沿った軌跡をご覧になりたい場合は、下記のようにstrokeのwhiteを追加してください。

<path id="text-path" d="M 0,160 Q 160,-160 320,160 T 640,160" stroke="white" fill="none"></path>

すると下記のように、軌跡を見ながら実装を進めることができます。

CSSやJavaScriptでも使えるようにするため、svgにはsvg-wrapのclassを、pathにはtext-pathのidをつけています。

自分のオリジナルのSVGを作成した場合は、CSSやJavaScriptの一部を変更しないといけない可能性もありますので、ご注意ください。

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 320" width="640" height="320" class="svg-wrap">
  <path id="text-path" d="M 0,160 Q 160,-160 320,160 T 640,160" fill="none"></path>
</svg>

下記部分はJavaScript側で対応するため、中身を空にしています。

<div class="text-wrapper">
  <!-- JavaScriptで生成 -->
</div>

CSSの記述について

@charset "utf-8";

/* ==========================
  初期設定
========================== */
*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  position: relative;
  word-wrap: break-word;
}

img {
  width: 100%;
}

/* レイアウト設定 */
.flow-wrap {
  max-width: 640px;
  margin: 80px auto;
  position: relative;
  background-color: #ffb400;
  padding: 80px 0;
  overflow: hidden;
}

.text-wrapper {
  position: absolute;
  width: 100%;
  height: 320px;
  font-size: 16px;
  white-space: nowrap;
  top: 50%;
  translate: 0 -50%;
}

.char {
  position: absolute;
  font-size: 20px;
  animation: moveAlongPath 10s linear infinite;
}

@keyframes moveAlongPath {
  from {
    offset-distance: 0%;
  }

  to {
    offset-distance: 100%;
  }
}

svg {
  position: relative;
  top: 0;
  left: 0;
  width: 100%;
  height: 320px;
}

flow-wrapでは、はみ出した部分を表示させないようoverflowをhiddenにしています。

charでは、animationを設定しています。「文字が軌跡に沿って動いています」の文字のアニメーションの設定をしています。
offset-distanceの位置が最初(0%)から最後(100%)まで一定の速度で繰り返し流れるよう設定しました。

offset-distanceの詳細はこちら

JavaScriptの記述について

"use strict";

// 表示するテキストを定義
const text = "文字が軌跡に沿って動いています";
// テキストを表示するためのラッパー要素を取得
const wrapper = document.querySelector(".text-wrapper");
// SVG内のパスのd属性を取得。このパスに沿ってテキストが動きます。
const textPath = document.getElementById("text-path").getAttribute("d");

// テキストの各文字に対してループ処理
for (let i = 0; i < text.length; i++) {
  // span要素を作成し、各文字を囲む
  const charSpan = document.createElement("span");
  // 作成したspanに'class'属性を追加
  charSpan.classList.add("char");
  // span要素のテキストとして、ループ中の文字を設定
  charSpan.textContent = text[i];
  // CSSのoffsetPathプロパティを使用して、文字が動くパスを設定
  charSpan.style.offsetPath = `path('${textPath}')`;
  // 文字の向きがパスに沿って自動で調整されるように設定
  charSpan.style.offsetRotate = "auto";
  // アニメーションが開始するまでの遅延を設定。各文字ごとに遅延時間を増やすことで、文字が順番に動き出す
  // マイナスの値を使用して、アニメーションが始まる前にすべての文字が一斉に表示されるように調整
  charSpan.style.animationDelay = `-${i * 0.3}s`;
  // 最終的に、各文字をラッパー要素に追加
  wrapper.appendChild(charSpan);
}

コメントアウトで対応していることを入れています。

テキストをそのまま流し込むと一直線のまま移動してしまうため、各文字をspanで囲いバラバラに動かす必要がありました。

SVG内のパスのd属性を取得し、パスに沿ってテキストを動かすため、offset-pathのCSSに設定します。必要に応じてoffset-rotateのCSSで角度を調整しましょう。今回はautoにしています。

文字をバラバラにしてpositonをabsoluteに設定している関係で、スタートの時間が一緒になると、文字が重なった状態でアニメーションが始まってしまいます。これを回避するために、animation-delayを設定して重ならない状態でスタートできるよう対応しています。

animation-delayの詳細はこちら

実際にやってみると、SVGを描画がうまくいかなかったり、軌跡と文字がずれてしまったりなどありましたが、実装することができてよかったです。

offset0pathについてはモダンブラウザには対応しているようです。ただ、Safariについては17.0以降のため、使う場合は検討する必要はありそうです。

以上になります。