dmvaldman / samsara

Continuous UI
http://samsaraJS.org
Other
1.05k stars 66 forks source link

Manipulating StyleSheets instead of inline styles #62

Open kof opened 7 years ago

kof commented 7 years ago

Based on this prototype https://github.com/davidkpiano/RxCSS and this presentation https://www.youtube.com/watch?v=lTCukb6Zn3g

I thought it might be interesting to see how much benefit it will provide if animating stuff by manipulating sheet rules instead of nodes directly. In this talk he is using css variables which are not supported everywhere, though it doesn't matter one can manipulate rules which apply using classes to the elements.

dmvaldman commented 7 years ago

That's definitely an interesting approach, thanks for bringing it up. Ultimately it's not very different than what's done in Samsara. The difference being that it puts more of the logic into CSS, as if CSS makes the layout scaffolding of your app, and the CSS provide hooks (in the form of custom CSS properties) to JavaScrip to mutate its variables. One immediate benefit is that is provides a nice mechanism for letting the animation thread (as opposed to main thread) do the work when you don't need to interrupt the animation from the main thread when you use CSS animations.

Take the case where a <div> is being dragged by your finger, though. Here the underlying x and y positions are animating at the same frequency as you want the CSS properties to change. Even if there's a CSS animation property on the element, It's unclear how the performance would be, or if that CSS animation property would be useful at all (would it interpolate between frames better? would this have a cost?). I'd also be curious to see how this scales to a larger application, where you'd need to expose dozens if not hundreds of custom CSS variables.

As someone who knows a lot about CSS in JS, maybe you have some opinions on how this approach could be made more modular in JS land, instead of going back and forth between CSS and JS.

kof commented 7 years ago

I am def. not talking about pure CSS animtations, they are for sure more performant because in separate thread, but they completely lack the control part.

From the performance perspective, inline styles manipulation vs css rule manipulation should be similar by design, but

  1. You don't need to have a dom node ref in order to modify styles.
  2. Depending on the internals it might be more work to update inline styles. In react for e.g. inline styles have a lot of overhead if done using react's style object, because it does a bunch of stuff.
  3. If you need to animate multiple elements simultaneously using the same properties, you can apply same class names and manipulate the one rule and it gets applied to multiple elements by the browser (here an example ).
  4. Considered the above idea with observables, it might be not only animations, it might be layouts like samsara can do, but structured together with the styles. Potentially it opens a space for a new approach for layouting, that works with any rendering framework or even without any.
davidkpiano commented 7 years ago

The benefits of manipulating CSS variables instead of direct properties via inline styles has more to do with authoring than with performance. I think the performance benefits (not measured) are negligible - non-trivial animations can still run smoothly at 60fps.

CSS variables are especially useful for animations specific to certain media queries (or even Safari's new reduced-animation media query), cascading/inheriting styles, pseudo-elements, and more.

Basically RxCSS is just a springboard for developers to start experimenting with using Observable streams and reactive programming for animations. You can always .subscribe to RxCSS' output and do whatever you want with the values - even apply them to inline styles, if you'd like. Completely agnostic.

trusktr commented 7 years ago

Depending on the internals it might be more work to update inline styles

But, it depends on the purpose of the library too. Samsara needs to manipulate the CSS transforms of all its elements. This means there would need to be a new stylesheet associated with each element.

So the question is, which is better:

  1. Create a new stylesheet for each element (with JSS), where each sheet has a generated className for it's associated Surface element (surface.currentTarget). To animate the transform of one element (with control, for physics for example, and not limited to CSS animations), animate the stylesheet (I think this means regenerating the textContent of the <style> element every animation frame).
  2. Apply the CSS transform inline directly to the element every animation frame, via the style attribute using a JSS rule.
  3. Apply the CSS transform inline directly to the element every animation frame, via the style attribute as a string without JSS, or perhaps directly to the style.transform property of the element's native style object without JSS.

The use case may matter.

Both of those methods (when written with JSS), are almost the same complexity to write from an author perspective. Determining which is more performant will (correct me if wrong on the JSS usage @kof) depend on which of the following three examples performs better. In each example, make the assumption that the code is associated with (encapsulated in) a single instance of Samsara Surface (correct me if wrong on those parts @dmvaldman):

-- 1 -- Stylesheet method with JSS stylesheet:

// This code is conceptual, and similar code would be distributed within Surface/DOMOutput/Engine classes:
this._elementStyleSheet = jss.createStyleSheet({}, {linked: true}).attach()
this._elementStyleRule = this._elementStyleSheet.createRule({
  transform: 'matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1)' // default initial value
})
this._currentTarget // the DOM element
  .className = this._elementStyleRule.className

requestAnimationFrame(function loop() {
  requestAnimationFrame(loop)
  surface._elementStyleRule.prop('transform', 'matrix3d(<NEW TRANSFORM VALUES HERE>)')
})

-- 2 -- Inline method with JSS rule, no stylesheet:

// This code is conceptual, and similar code would be distributed within Surface/DOMOutput/Engine classes:
this._elementStyleRule = jss.createRule({
  transform: 'matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1)' // default initial value
})
this._elementStyleRule.applyTo(
  this._currentTarget // the DOM element
)

requestAnimationFrame(function loop() {
  requestAnimationFrame(loop)
  surface._elementStyleRule.prop('transform', 'matrix3d(<NEW TRANSFORM VALUES HERE>)')
  surface._elementStyleRule.applyTo(surface._currentTarget)
})

-- 3 -- The third method doesn't need an example, it is the current method implemented in Samsara, and is similar in concept to applying a JSS rule directly to the target element. Using JSS in this case might serve as a way to abstract that functionality away (handled by JSS), whereas currently it is handled by DOMOutput. The performance may be almost the same as method 2, and boils down to

requestAnimationFrame(function loop() {
  requestAnimationFrame(loop)
  surface._currentTarget.style.transform = 'matrix3d(<NEW TRANSFORM VALUES HERE>)'
})

In my case, I am using JSS to apply static styles to each DOM element (no animations to those properties), and I'm applying transform style by hand.