Web Animations APIの出番が少なすぎて使い方を忘れそうなので覚えてるうちに。シマウマを回したくなったときの参考にしてください。
See the Pen Web Animations API Zebra Rotation by nishinoshake (@nishinoshake) on CodePen.
Web Animations API
- CSSアニメーションのJS版
- CSSで良くない?
- GSAPで良くない?
仕様を見ていて便利だなぁと思ったのが、再生速度をコントロールできるところ。現時点での印象は、JSでキーフレームを使いたい、かつアニメーションライブラリが使えない状況で役に立つAPIという感じ。ニッチだな!
Web Animations APIのcompositeが凄過ぎてすごいからみんな見てくれを見てみたら、composite プロパティはたしかに使えそう。あの罪深い <div>
を減らせるならそれだけでも価値がある。たまさんかわいい。
些細なところですが、デフォルトではアニメーション時にインラインスタイルを書き換えないので、なんでこの要素動いてるの!っていう驚きを同業者に与えられる。結局ニッチだな!
commitStyles() でインラインスタイルにぶち込めます
シマウマを回す
/* CSSアニメーション */
@keyframes rotation {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.zebra {
/* animation-timing-function の初期値は ease*/
animation: rotation 2s linear infinite;
}
/* Web Animations API */
document.querySelector('.zebra').animate(
[
{ transform: 'rotate(0deg)' },
{ transform: 'rotate(360deg)' }
], {
duration: 2000,
iterations: Infinity,
easing: 'linear' // こっちは linear が初期値
}
)
CSSアニメーションとの比較。パッと見は似ていますが、プロパティがちょこちょこ違います。 無限ループが "infinite"
ではなく Infinity だったり、イージングの初期値も地味に違います。他にも違いがありそうなので、使う前に仕様のご確認を。
シマウマと遊ぶ
See the Pen Web Animations API Zebra Rotation by nishinoshake (@nishinoshake) on CodePen.
回すだけだとCSSで良くない?ってなるので、再生速度を変更できるようにしました。CSSで animation-duration を変えるとアニメーションがバキっとなりますが、こっちだとそれがないのが優秀。
あと、finished プロパティが Promise を返してくれるので、終わったら要素を消したい、みたいなときに便利。animationend イベントよりも扱いやすいので、単発で消すアニメーションには良いかも。(このサンプルだとボタンを押したときの Faster! の文字とか)
再生速度の playbackRate は 0 で動きが止まり、負の値は逆再生になります。
playbackRate が負の状態で currentTime が 0 まで巻き戻ると アニメーションが止まってしまうので、currentTime を duration * 10000
からスタートさせる心温まる実装をしています。狂気の1万イテレーション!
何かを回したい人のために関数を置いておくので、よかったら使ってください。
TypeScript
type RotationOption = {
el: HTMLElement
duration?: number
step?: number
}
const createRotation = (option: RotationOption) => {
const OFFSET_ITERATIONS = 10000
const duration = option.duration === undefined ? 1000 : option.duration
const step = option.step === undefined ? 0.5 : option.step
const animation = option.el.animate(
[
{ transform: 'rotate(0deg)' },
{ transform: 'rotate(360deg)' }
], {
duration,
iterations: Infinity
}
)
animation.currentTime = duration * OFFSET_ITERATIONS
return {
faster: () => {
animation.playbackRate += step
},
slower: () => {
animation.playbackRate -= step
}
}
}
const target = document.querySelector<HTMLElement>('.target')
if (target) {
const rotation = createRotation({
el: target,
duration: 1000,
step: 0.5
})
// Faster!
document.querySelector<HTMLElement>('.faster')?.addEventListener('click', () => rotation.faster())
// Slower!
document.querySelector<HTMLElement>('.slower')?.addEventListener('click', () => rotation.slower())
}
JavaScript
const createRotation = (option) => {
const OFFSET_ITERATIONS = 10000
const duration = option.duration === undefined ? 1000 : option.duration
const step = option.step === undefined ? 0.5 : option.step
const animation = option.el.animate(
[
{ transform: 'rotate(0deg)' },
{ transform: 'rotate(360deg)' }
], {
duration,
iterations: Infinity
}
)
animation.currentTime = duration * OFFSET_ITERATIONS
return {
faster: () => {
animation.playbackRate += step
},
slower: () => {
animation.playbackRate -= step
}
}
}
const target = document.querySelector('.target')
if (target) {
const rotation = createRotation({
el: target,
duration: 1000,
step: 0.5
})
// Faster!
document.querySelector('.faster')?.addEventListener('click', () => rotation.faster())
// Slower!
document.querySelector('.slower')?.addEventListener('click', () => rotation.slower())
}