-
Notifications
You must be signed in to change notification settings - Fork 757
Description
One aspect of Web Animations that is missing, and that requires people to abandon the idea of using Web Animations in favor of JavaScript libraries like Tween.js (I'm a maintainer), is that the current design allows animating only DOM element styles (unless cumbersome and non-ideal workarounds are applied).
Current problem:
Suppose we are creating WebGL-powered graphics using JavaScript. The JavaScript objects are not DOM elements, and we want to animate them.
This does not work:
const someObject = {
position: {x: 0, y: 0, z: 0} // Suppose this represents the position of an object in 3D space.
}
const keyframes = new KeyframeEffect(someObject.position, [{x: 0}, {x: 200}], 3000)
const animation = new Animation(keyframes, document.timeline)
animation.play()
// render the WebGL content while the animation is playing
requestAnimationFrame(function loop() {
renderScene(someObject, camera) // hypothetically render the scene
if (animation.playState !== 'finished') requestAnimationFrame(loop)
})Besides plain JS objects, Custom Elements are proliferating. Custom Elements can have plain JS properties too, and users may desire to animate these properties. Under the hood, a custom element's properties could map to CSS, WebGL, canvas 2D, plot or chart value, etc. Being able to animate custom element JS properties (as opposed to only styles) would be highly useful.
As a simple real life example, I work on Lume that provides custom elements for 3D, and here's a very basic animation of a number property on a Lume element:
<lume-scene webgl>
<lume-camera-rig></lume-camera-rig>
<lume-point-light position="300 300 300"></lume-point-light>
<lume-box id="myBox" size="10 10 10" opacity="0.9"></lume-box>
</lume-scene>
<script type="module">
import "lume"
const box = document.querySelector('#box')
requestAnimationFrame(function loop() {
console.log((box.opacity -= 0.005))
if (box.opacity > 0) requestAnimationFrame(loop)
else box.opacity = 0
})
</script>It would be nice to be able to use the Web Animations API for such use cases not involving style, but still highly relevant to HTML elements (custom elements). Animating the opacity value of the custom <lume-box> element could look like the following:
const box = document.querySelector('#box')
const animator = new Animator(box)
const animation = animator.animate([{opacity: 1}, {opacity: 0}], 3000) // similar to `element.animate()`, more on that below
// done! (no need for manual requestAnimationFrame with Lume elements)Current workarounds
Here are the current workarounds:
-
Don't use Web Animations API, instead use Tween.js for example. This workaround is not ideal because we want to embrace what the web gives us, instead of dropping it in favor of additional libraries.
npm install @tweenjs/tween.js
-
Animate a hidden DOM element, and read properties from its computed style. The downsides of this are that it is cumbersome and hacky, plus incurs unnecessary performance cost due to cycling the browser's CSS engine and obtaining values from the element's computed styles.
const someObject = { position: {x: 0, y: 0, z: 0} // Suppose this represents the position of an object in 3D space. } const dummy = document.createElement('div') dummy.style.display = 'none' // hide it so the browser avoids spending resources rendering it. document.body.append(dummy) const keyframes = new KeyframeEffect(dummy, [{translate: '0px'}, {translate: '200px'}], 3000) const animation = new Animation(keyframes, document.timeline) animation.play() const dummyStyle = getComputedStyle(dummy) // render the WebGL content while the animation is playing requestAnimationFrame(function loop() { someObject.position.x = parseFloat(dummyStyle.translate) || 200 console.log(someObject.position.x) renderScene(someObject, camera) // hypothetically render the scene if (animation.playState !== 'finished') requestAnimationFrame(loop) })
Proposal 1, keyframe effects for plain JS objects
Idea 1:
- Make a new
StyleKeyframeEffectclass that works howKeyframeEffectcurrently works and animates the style of the given DOM element. This class extends fromKeyframeEffectfor semantics. Perhaps this class should rather (or also) directly acceptCSSStyleDeclarationinstances,CSSStyleRuleinstances directly, etc, for better semantics and more possibilities (for example, animate the properties of aCSSStyleRulein a stylesheet and it can animate multiple elements at once!). - Add a new
ObjectKeyframeEffectclass that extends fromKeyframeEffect, and that accepts any JS objects, similar to what was attempted in the first example above.- This class behaves similarly to
StyleKeyframeEffect, but thetargetis simply any JS object, and the properties in thekeyframesarray are plain JS number properties that correspond to the same-name properties oftarget.
- This class behaves similarly to
- soft-deprecate
KeyframeEffectas a class that people should instantiate directly, and make it an "abstract" class that bothStyleKeyframeEffectandObjectKeyframeEffectextend from. For backwards compatibility,KeyframeEffectshould continue working as-is (but sites like MDN will show a warning telling people to useStyleKeyframeEffectorObjectKeyframeEffect, etc).
The usage of ObjectKeyframeEffect would look like this:
const someObject = {
position: {x: 0, y: 0, z: 0} // Suppose this represents the position of an object in 3D space.
}
const keyframes = new ObjectKeyframeEffect(someObject.position, [{x: 0}, {x: 200}], 3000)
const animation = new Animation(keyframes, document.timeline)
animation.play()
// render the WebGL content while the animation is playing
requestAnimationFrame(function loop() {
renderScene(someObject, camera) // hypothetically render the scene
if (animation.playState !== 'finished') requestAnimationFrame(loop)
})Idea 2:
- Instead of multiple classes as in Idea 1, perhaps allow multiple types of objects to be passed into
KeyframeEffect. - If an
Elementis detected, it works as currently. - If a
CSSStyleRuleis detected, it operates on the properties of the rule in a similar way as howKeyframeEffectcurrently operates on the properties of an element's style. - etc
- Finally, if none of the above instance types match (f.e. with
instanceof), then treat it as a plain JS object, and assume that the keyframes contain objects with same-name properties and number values.
This would work exactly like in the very first example above.
Being more explicit with specific classes also means that the disctinction can be more clear in non-browser runtimes. For example, a Node.js library, or Node-like JS runtime, could provide ObjectKeyframeEffect but not StyleKeyframeEffect. Alternatively, a non-browser environment can also accept less types of objects if using a single class (for example, accept only JS objects, but not elements because there are no elements).
Proposal 2, Animator
Idea 1
This is a beginning exploration of an idea for animating anything, not just DOM elements.
A new Animator class would allow wrapping any types of objects, and the wrapper instance would have methods like .animate() that are similar to the current Element.animate().
For animating plain JS objects, it could look like the following:
const someObject = {
position: {x: 0, y: 0, z: 0} // Suppose this represents the position of an object in 3D space.
}
const animator = new ObjectAnimator(someObject)
const animation = animator.animate([{x: 0}, {x: 200}], 3000)
const animation2 = animator.animate([{y: 0}, {y: 400}], 4000)
// render the WebGL content while the animation is playing
requestAnimationFrame(function loop() {
renderScene(someObject, camera) // hypothetically render the scene
if (animation.playState !== 'finished' || animation2.playState !== 'finished') requestAnimationFrame(loop)
})There'd also be similar ElementAnimator or StyleAnimator classes, where for example StyleAnimator could accept a CSSStyleRule or similar.
Idea 2
Similar to with Proposal 1, maybe instead of having multiple distinct classes for styles, objects, etc, a single Animator class can accept different types of objects and act accordingly.
Being more explicit with specific classes also means that the disctinction can be more clear in non-browser runtimes. For example, a Node.js library, or Node-like JS runtime, could provide ObjectAnimator but not StyleAnimator. Alternatively, a non-browser environment could also accept less types of objects if using a single class (for example, accept only JS objects, but not elements because there are no elements).
Summary
I think I like Idea 2 of each of the two proposals (accepting multiple types of objects). But I'm not sure, maybe separate classes results in semantically cleaner code (include type definitions in TypeScript for example).
In any case, being able to animate plain JS objects would be not only be very useful for DOM elements in a browser, but for other cases like
- JS properties on Custom Elements,
- JS properties on web libraries (f.e. imagine a lib that has JS API for drawing charts, or something)
- WebGL,
- canvas 2D,
- and even non-browser JS runtimes.