10-10-3 getComputedTiming().progressの利用

10-10 アニメーションタイマーとしての利用」で述べた”3 セットした時間を100%として、今がその何%に当たる瞬間かを教えてくれるタイマー”の例です。ここでは、Animation.effect.getComputedTiming()が返すprogressを利用します。下記リンクがそのサンプルです。

2つの円を同時に描く

ページを開くと、赤い太線で2つの円が同時に描かれます。見た目は単純ですが、アニメーションタイマーの進捗値を使って、左の円はSVG + WAAPIで、右の円は<canvas>要素 + JavaScriptで描画しています。

その仕組みは次の通りです。

  1. アニメーションタイマーを、duration:3000で作成します。
  2. 毎フレーム呼び出される関数で、アニメーションタイマーから
    getComputedTiming().progressを呼び出して、その時点での進捗値を取得し、SVGで円を描くWAAPIアニメーションに渡します。
  3. SVGで円を描くWAAPIアニメーションは、duration:1で作成しておきます。すると、進捗値(0から1)をそのまま、アニメーションのcurrentFrameに割り当てることができます。
  4. さらに進捗値を、<canvas>で円を描く関数に渡し、円を描画します。

HTML

<svg width="200" height="200" id="svg-circle">
  <circle class="circle" cx="100" cy="100" r="50" stroke="red" fill="white" stroke-width="15" stroke-linejoin="round"></circle>
</svg>
<div class="progress-circle">
  <canvas id="canvas" width="120" height="120"></canvas>
</div>
<div class="box a">
  <p>SVGで円を描画</p>
</div>
<div class="box b">
  <p>CANVASで円を描画</p>
</div>

JavaScript

// requestAnimationFrame()を止めるためのID値用変数
let lastId = 0;

// 3秒を刻むアニメーションタイマー
// タイミングパラメータには時間だけを設定
const timings = {
    duration: 3000
};
const effect = new KeyframeEffect(null, null, timings);
const animationTimer = new Animation(effect, document.timeline);

// SVG
// SVGの円の半径を調べて、円周の長さを求める
const circle = document.querySelector('.circle');
const r = circle.getAttribute('r');
const circleLength = r * 2 * Math.PI;

// SVGの円のstroke-dashoffsetとstroke-dasharray CSSプロパティに、
// 円周の長さを設定する。
circle.setAttribute('stroke-dashoffset', circleLength);
circle.setAttribute('stroke-dasharray', circleLength);
// 設定できたかをgetComputedStyle()で確認
//const dashoffset = window.getComputedStyle(circle).strokeDashoffset;
//const dashoarray = window.getComputedStyle(circle).strokeDasharray;

// stroke-dashoffsetがcircleLengthから0まで変化するアニメーション
// ただし時間は1
const keyframesCircle = {
    strokeDashoffset: [circleLength, "0"]
};
const timingsCircle = {
    duration: 1,
    fill: 'forwards'
};
const effectCircle = new KeyframeEffect(circle, keyframesCircle, timingsCircle);
const circleAnim = new Animation(effectCircle, document.timeline);

// circleAnimはupdateイベントの発生を待ち受けている
circleAnim.addEventListener('update', (e) => {
    // イベントオブジェクトから進捗値を取り出し、
    // 自分のcurrentTime値に設定する。
    const value = e.detail.value;
    circleAnim.currentTime = value;

    // 進捗値に応じてキャンバスに円を描く
    drawProgressCircle(value);
}, false);

circleAnim.addEventListener('stop', (e) => {
    animationTimer.cancel();
    circleAnim.cancel();
}, false);

// CANVAS
const context = document.getElementById('canvas').getContext('2d');
// 初めに表示する白い円を描く
context.fillStyle = 'white'
context.arc(60, 60, 50, 0, 2 * Math.PI, false);
context.fill();
// アニメーションで使用する、途中で変更しない線やテキストの設定
context.lineWidth = 15;
context.strokeStyle = 'red';
context.textAlign = "center";
// 開始角度は270度(円の真上)
const startAngle = 270;
// 度数をラジアン数に変換
const startRadian = startAngle * (Math.PI / 180);
// 白い円を描き、進捗値に応じて円のストロークを描く
const drawProgressCircle = (progress) => {
    // 現在のキャンバスをクリア
    context.clearRect(0, 0, canvas.width, canvas.height);
    // 塗り用のパスを開始
    context.beginPath();
    // 円を白で塗る
    context.fillStyle = 'white'
    context.arc(60, 60, 50, 0, 2 * Math.PI, false);
    context.fill();
    // 進捗値を描く
    context.fillStyle = 'black';
    const p = progress.toFixed(2);
    context.fillText(p, canvas.width / 2, canvas.height / 2);
    context.save();
    // 進捗度に全体(360度)を掛けて、残りの割合を算出し、それに開始角度を足す
    // const endRadian = (progress * Math.PI*2) + startRadian;
    // 進捗値に360度を掛けて、描画する角度を求める。
    const deffAngle = progress * 360;
    // 度数をラジアン数に変換
    const deffRadian = deffAngle * (Math.PI / 180);
    // それを開始ラジアンに足して、終了ラジアンにする
    const endRadian = startRadian + deffRadian;
    // ストローク用のパスを開始
    context.beginPath();
    // 270度(円の真上)からendRadianまで円のストロークのパスを作成
    context.arc(60, 60, 50, startRadian, endRadian, false);
    // そのストロークを描く
    context.stroke();
    context.restore();
}

const update = () => {
    if (animationTimer.playState === 'running') {
        // 毎フレーム、timerAnimの進捗値を取得する
        const progress = animationTimer.effect.getComputedTiming().progress;
        // console.log(progress);
        if (progress !== null) {
            // circleAnimにupdateイベントとともに、進捗値を入れたオブジェクトを届ける
            circleAnim.dispatchEvent(new CustomEvent('update', {
                detail: {
                    value: progress
                }
            }));
        }
        else {
            circleAnim.dispatchEvent(new CustomEvent('stop'));
        }
    }
}

const render = () => {
    update();
    lastId = window.requestAnimationFrame(render);
}

animationTimer.addEventListener('finish', (e) => {
    console.log('finish');
    // アニメーション終了後、1秒後にrequestAnimationFrame()への呼び出しを止める
    window.setTimeout(() => {
        window.cancelAnimationFrame(lastId);
    }, 1000);

}, false);

animationTimer.ready.then(() => {
    animationTimer.play();
    render();
});

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA