🦁

円形のプログレスバーを良い感じのアニメーションで実装したい

2021/11/29に公開

記事中に載せているコードは抜粋なので、全文はCodePenの方を参照してください。

SVGの円を2つ重ねる

SVGの円は <circle> を使うと直感的に描けますが、パスをアニメーションさせる場合は起点を明示できたほうが良いので、<path> でArc(円弧)を描きました。円をいっぱいに描画してしまうと線がはみ出してしまうため、stroke-width / 2の余白をとっています。内側に線を描けるプロパティが欲しい。

<div class="loading">
  <svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" class="loading-icon loading-icon--back">
    <path d="M64 1 A63 63 0 1 1 64 127 A63 63 0 1 1 64 1" stroke="#ccc" stroke-width="2" fill="none"/>
  </svg>
  <svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" class="loading-icon loading-icon--front">
    <path d="M64 1 A63 63 0 1 1 64 127 A63 63 0 1 1 64 1" stroke="#333" stroke-width="2" fill="none"/>
  </svg>
</div>

pathを翻訳するとこんな感じです。
アルファベットが大文字の場合は絶対座標、小文字の場合は相対座標(移動量)になります。

# M(Move To) x y
M64 1: (64, 1)に移動して

# A(Ark) rx ry x-axis-rotation large-arc-flag sweep-flag x y
A63 63 0 1 1 64 127: (63, 63)を中心に、時計回りに(64, 127)までの円弧を描いて
A63 63 0 1 1 64 1:   (63, 63)を中心に、時計回りに(64, 1)までの円弧を描く

https://developer.mozilla.org/ja/docs/Web/SVG/Tutorial/Paths

線を隠す

SVGでパス芸をするときの定番ですが、破線を設定するためのプロパティを使ってパスを見えない状態にします。

stroke-dasharray が破線の間隔で、 stroke-dashoffsetがオフセット値です。パスの長さは getTotalLength()で取得できるので、その値をstroke-dasharraystroke-dashoffsetに設定すると、破線の間隔が設定されて、同じ値だけオフセットされるので、見えない状態になります。

const path = document.querySelector('path')
const pathLength = path.getTotalLength()

path.style.strokeDasharray = pathLength
path.style.strokeDashoffset = pathLength	

ここから、 stroke-dashoffset の値を 0 に近づけていくと、プログレスな動きになります。

変化にアニメーションをつける

あとは、stroke-dashoffsetの変化に緩急をつけます。サンプルではgsapを使っていますが、SVGのtweenに対応しているライブラリであれば問題ありません(anime.jsなど)。気合いがあれば自力でも書けますが、イージングをつけたい場合はライブラリに頼った方が楽です。

import gsap from 'https://cdn.skypack.dev/gsap'

const path = document.querySelector('path')
const pathLength = path.getTotalLength()

/**
 * 0 to 1
 */
function progress(progress) {
  gsap.to(path, {
    strokeDashoffset: pathLength * (1 - progress),
    duration: 0.8,
    ease: 'power3.out'
  })
}	

時間差ではけさせる

最後にパスをはけさせるアニメーションを実装しようとしたら、stroke-dashoffsetの負の値に対するブラウザ毎の振る舞いが違ったので、正の値で実装しました。pathLength * 3からアニメーションを始める、みたいな苦しい実装になってしまって悲しい。

await Promise.all([
  // プログレスバーの要素自体を時計回りに回す
  gsap.fromTo(progressBarElement, {
    rotation: 0,        
  }, {
    rotation: 360,
    duration: 1.8,
    ease: 'expo.inOut'
  }).then(),
  
  // 表のパスをはけさせる
  gsap.to(frontPathElement, {
    strokeDashoffset: pathLength,
    duration: 1.8,
    ease: 'expo.inOut'
  }).then(),
  
  // 裏のパスを時間差ではけさせる
  gsap.fromTo(backPathElement, {
    strokeDashoffset: pathLength * 2,        
  }, {
    strokeDashoffset: pathLength,
    duration: 1.8,
    delay: 0.2,
    ease: 'expo.inOut'
  }).then()
])
  1. Let offset be the value of the stroke-dashoffset property on the element.
  2. If offset is negative, then set offset to sum − abs(offset).

https://www.w3.org/TR/SVG2/painting.html#StrokeDashing

Safariの動きが思い通りじゃなかったので、またSafariか・・・と思ったら仕様的にはSafariの実装が正しいのか?どちらにせよstroke-dashoffsetに負の値は使わないほうが安全。

数字のカウントアップの記事も以前に書いているので、もしよかったら。
https://qiita.com/nishinoshake/items/b91236c77b1987036656

Discussion