いろいろなパターンで画像をループさせてみた
初めまして。
BALANCeのエンジニアの岡村です。普段は、主にWebフロントエンド全般ですが、ビルド環境を整えたり、サーバ触ったり、unityやったりと幅広くやってます。
今回は、Webで画像のループをさせる際にいくつかの実装方法を試したので、その検証内容と結果についてご紹介します。
具体的には、最近実装を担当した下記サイトのフッターにある、「join」や「scroll」などが書かれた部分です。
この記事でわかること
検証した3つの実装方法
上記サイトのループ部分を、次の3つの実装方法に分けて、どれが一番スムーズかを検証しました。
- マイフレーム毎にdomの要素のx値を直接更新
- cssのkeyframe animationを使ってループ
- canvas2d を使ってループ
ちなみに当初の予想では【3】のcanvas2dが圧倒的にスムーズかなと思っていました。
が、実際にやってみると予想とは違う結果だったので、検証してみて良かったです。
コード(github)
demo
それぞれ共通の演出としてhover時にループ速度が速くなるようにしてます。
【1】 マイフレーム毎にdomの要素のx値を直接更新
こちらは、初めに要素を横に並べて、それぞれの要素に対して、マイフレーム左に動かしています。
//マイフレームごとに呼ばれるupdate関数
update(diff) {
const frameRate = Math.min(diff / ((1 / 60) * 1000), 1);
this.currentPosX += this.speed.value * frameRate;
this.v = this.currentPosX;
this.inner.style.transform = `translate3d(${-this.v}px, 0px, 1px)`;
if (this.v > this.resetPoint) {
this.v = 0;
this.currentPosX = this.currentPosX - this.resetPoint;
}
}
この方法を試してわかったのは以下の問題点です。
- 並べた要素が画面幅を超えるように作るため、必要以上の要素を並べる必要がある
- 並べた要素数が多ければ多いほど、マイフームで余計なループと余計なdom操作が必要になる
- 大きいdomを動かす必要がある
結果的にあまり良いパフォーマンスが出ませんでした。特にhover時にカタカタとカクツク瞬間が見えます。
【2】cssのkeyframe animationを使ってループ
コード
こちらは、dom構造は【1】と変わりませんが、ループはcss keyframe animationを使ってループさせています。
また、速度を上げる際には、あらかじめ取得しておいたAnimationオブジェクトのplaybackRateを上げたい速度に合わせて変更しています。
wrap.addEventListener("mouseenter", (e) => {
gsap.to(animation, 2, {
playbackRate: 6,
ease: "expo.out",
});
});
wrap.addEventListener("mouseleave", (e) => {
gsap.to(animation, 2, {
playbackRate: 1,
ease: "expo.out",
});
});
初めはcss animationのdurationをjsから変更する方法で実装していたのですが、この方法だと他のモックと違った動きをしたので、上記の方法にしました。
パフォーマンス的には上々で、滑らかに動いてると思います。
その原因として考えられるのは以下の点です。
- そもそもhover時にdomの変更を加えていない
- cssでのanimationだとブラウザへの負荷が少ない?(未検証)
補足
durationを動的に変えた場合、例えば『duration 2s を設定していて、1sに変える場合』に、こんな動作をします。
css animationは内部で経過時間を計測していて、2000ms経過したときにkeyframesの100%の値になるような動きをします。
そのため、animationの途中(1000ms)でdurationを1sに修正した場合は、1000ms = keyframes 100%になるため、いきなりアニメーションが終わったり、リピートが始まったりします。
【3】canvas2d を使ってループ
コード
こちらは、dom構造は至ってシンプルで、canvasを同じ高さで置いてます。
画像を読み込み、画面内の画像だけを描画しています。
drawImg(index = 0, x = 0) {
const img = this.imgs[index % this.imgs.length];
if (!img) return;
const imgWidth = (img.naturalWidth / img.naturalHeight) * HEIGHT;
const _x = x + imgWidth * pixelRatio + MARGIN * pixelRatio;
if (_x >= 0 || x >= window.innerWidth * pixelRatio) {
// this.drawImg(index + 1, _x);
const y = (this.canvas.height - HEIGHT * pixelRatio) / 2;
this.ctx.drawImage(
img,
0,
0,
img.naturalWidth,
img.naturalHeight,
x,
y,
imgWidth * pixelRatio,
HEIGHT * pixelRatio
);
}
if (x < window.innerWidth * pixelRatio)
this.drawImg((index % this.imgs.length) + 1, _x);
}
この方法では下記の問題があります。
- 画面幅が広くなればなるほど、ループ回数など描画コストが上がる
- retinaや、4kなどの解像度も重さに影響される
こちらのデモでは、解像度の分canvasを大きくしてるので描画自体は綺麗に見えますが、その一方でコストが上がってます。
この方法よりも【2】のほうがスムーズだと感じる原因は、やはり【2】のほうが「cssでのanimationだとブラウザへの負荷が少ない可能性がある」からなのかなーと思っています。
終わりに
3つの方法を検証した結果、今回の例では【2】がベスト、【3】がベターという結果になりました。
ただ、【2】は今回実験的なAPIを利用しているので、環境によっては違う動作になってしまうかもしれません(現状はIE以外は対応してるみたいです)。
また、今回の例では【1】は最もパフォーマンスが悪いという結果となりましたが、後から別の演出を追加したい時など、コードに柔軟性はあります。
そのため一旦【1】の方法で作って、最終的にこうした簡単な動きだけで良い、となったタイミングで【2】にするのがいいのかもしれません。