Skip to content

[web-animations] proposal idea: animating non-element JS objects, ObjectKeyframeEffect and Animator API, or similar #9974

@trusktr

Description

@trusktr

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>

Live example on CodePen

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:

  1. 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
  2. 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)
    })

    Live example on CodePen

Proposal 1, keyframe effects for plain JS objects

Idea 1:

  • Make a new StyleKeyframeEffect class that works how KeyframeEffect currently works and animates the style of the given DOM element. This class extends from KeyframeEffect for semantics. Perhaps this class should rather (or also) directly accept CSSStyleDeclaration instances, CSSStyleRule instances directly, etc, for better semantics and more possibilities (for example, animate the properties of a CSSStyleRule in a stylesheet and it can animate multiple elements at once!).
  • Add a new ObjectKeyframeEffect class that extends from KeyframeEffect, and that accepts any JS objects, similar to what was attempted in the first example above.
    • This class behaves similarly to StyleKeyframeEffect, but the target is simply any JS object, and the properties in the keyframes array are plain JS number properties that correspond to the same-name properties of target.
  • soft-deprecate KeyframeEffect as a class that people should instantiate directly, and make it an "abstract" class that both StyleKeyframeEffect and ObjectKeyframeEffect extend from. For backwards compatibility, KeyframeEffect should continue working as-is (but sites like MDN will show a warning telling people to use StyleKeyframeEffect or ObjectKeyframeEffect, 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 Element is detected, it works as currently.
  • If a CSSStyleRule is detected, it operates on the properties of the rule in a similar way as how KeyframeEffect currently 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions