dmvaldman / samsara

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

POC: react-samsara #9

Open mcbain opened 8 years ago

mcbain commented 8 years ago

Since react 0.14+ it is possible to create custom renders more easily. I created a POC impl for samsara.js https://github.com/mj-networks/react-samsara-poc. There is also a demo https://github.com/mj-networks/react-samsara-demo1.

Demo

mcbain commented 8 years ago

btw. i used react-samsara-poc as npm module name to not claim react-samsara

kof commented 8 years ago

react components + samsara events, animations and physics = awesome ui lib

dmvaldman commented 8 years ago

woooaah!!! this looks great from the demo source code. I wasn't able to build it successfully yet. I got weird errors with node v4, and they disappeared when I switched to v0.10. I'll take a closer look tomorrow. Let me know how I can help from the API side!

dmvaldman commented 8 years ago

Got the demo running. Looks very clean! I tried to update it to use img tags instead of backgroundImage css properties, and it was straightforward. Btw, documentation around that isn't public but it's just

new Surface({
    size : [whatever, whatever], 
    tagName: 'img', 
    attributes : {src : yourURL}
});

I'll be taking a closer look at your integration code. Looks like you've gone pretty deep into the Samsara codebase - where there's little to no documentation to help! - impressive.

mcbain commented 8 years ago

The whole process of reconciliation is described here. IMHO at the core level tree operations add, delete, and move must be implemented. Currently i'm not sure whether insert must be supported, but very often the order of elements is taken into account (SequentialLayout).

Also setting an re-setting the properties of a react-elements after initialization must be supported like Surface.setOptions(options) does for RenderTreeNode and Context.

The next steps could be: delete a Node on a Context/Node.

Also when adding a node after first render they do not show up directly. When forced by resizing the browser window, it is placed on wrong position (not sure if this happens in plain samsara code as well?) https://github.com/mj-networks/react-samsara-demo1/blob/master/src/Insert.jsx

Maybe its possible to get the demo-source-code running a CodePen somehow?!

dmvaldman commented 8 years ago

Removing a node and surface will be supported in a future version for sure. There will be support for removal in some kind of animated way (like animating opacity to 0 and then removing), and a binary way.

Move and Insert are certainly trickier. I haven't put much time into thinking how to achieve this effect. You could, in theory, swap which surfaces the CSS is being committed to, but this is quite a hack. It is in the ethos of Samsara that these be done in a way that supports animated transitions as well.

In general, I would rather React's reconciliation process be applied within a Samsara surface rather that to Samsara surfaces. A surface is a DOM node that needs to animate, if you're not animating then IMO there's no point to using Samsara.

The approach I originally had in mind for integrating with React and other libraries was to export size transform and opacity styles computed by Samsara into React components. I'd like to see how both approaches go however.

faceyspacey commented 8 years ago

@dmvaldman what's the latest on integration with React? Could really be huge for the React community, especially if we could get it to work with React Native in place of its Animated API.

dmvaldman commented 8 years ago

I'd like to investigate this more. Because of your comment, I decided to play around. For simple examples, there's not much you need to do at all.

I started from React's HelloWorld app from their installation page and modified it to render into a Samsara Surface. Here's the code.

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
import Samsara from 'samsarajs';

// SAMSARA STUFF
var Surface = Samsara.DOM.Surface;
var Context = Samsara.DOM.Context;
var Transform = Samsara.Core.Transform;

// create surface
var surface = new Surface({
  content : 'hi',
  size : [200, 200],
  origin : [.5, .5],
  properties : {background : 'blue'},
  classes : ['hi-from-samsara']
});

// create context
var context = new Context();

context
  .add({
    transform : Transform.rotateZ(Math.PI/4),
    align : [.5,.5]
  })
  .add(surface);

context.mount(document.getElementById('root'))

// REACT STUFF

// render React component into Samsara surface
ReactDOM.render(
  <App />,
  document.querySelector('.hi-from-samsara')
);

And here's what it looks like:

screen shot 2016-10-26 at 10 19 11 pm

Pretty simple! And it can be a lot simpler! Ultimately things will be a little more complicated when you have nested React components that need to be injected into multiple Surfaces. But I think the basic idea of "render React components into Samsara Surfaces" is a good starting point.

Ultimately I'd like to see this kind of integration come from the Samsara community. Though sadly, there's not much of a community yet :-( So either I'll have to start this off, or some brave reader of this GitHub issue will get inspired!

@mcbain @kof

faceyspacey commented 8 years ago

@dmvaldman my first thoughts--and this may be a bit naive--is: what if we could mimic the css styles samsara needs on react-generated elements, and then drive the styles from samara-style code?

If you check out the Animated API: https://www.npmjs.com/package/animated you will see that the same API for React Native is available on the web. In fact it's what React Native Web uses. RNW uses the same RN API but on the web. I've been using it daily for 2 months after building a React Native project, and it's excellent. So 2 things about it:

<View style={{ transform: [{rotateZ: Math.PI/4 * 180/Math.PI+'deg'}] }} />

vs.

this.state.rotateZ = new Animated.Value(Math.PI/4 * 180/Math.PI+'deg');
<Animated.View style={{ transform: [{rotateZ: this.state.rotateZ}] }} />

And of course the second example isn't animated, but here's how that would look (and perhaps it serves as better inspiration):

import React from 'react';
import {
  Animated,
  View,
} from 'react-native';

class MyComponents extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
       rotateZ = new Animated.Value(0)
    };
  }

  componentDidMount() {
    Animated.timing(this.state.rotateZ, {
        toValue: 1,
        duration: 500,
    }).start();
  }

  interpolateRotateZ() {
    return this.state.rotateZ.interpolate({
      inputRange: [0, 1],
      outputRange: ['0deg', '360deg'],
      extrapolate: 'clamp'
    });
  }
  render() {
    <Animated.View style={{transform: [{rotateZ: this.interpolateRotateZ()}] }} />
  }
}

So that's the the cutting edge of React [Native] animations. Also it's important to note, that most animations besides rotate don't need an interpolation, which converts to a degrees string. But that said, obviously you can do all sorts of interesting things if you want to use one value to interpolate on and drive different values for different elements, e.g. if you interpolate on scrolling/swiping, you can hook different animations into different "windows" of the changing value, and of course multiply/add to the value for each individual element etc. This obviously is the common grounds with Samsara and its ability to combine streams. Where Animated leaves off and Samsara takes over is the coordination of large numbers of elements in relation to one another, composition via Views and advanced combination of streams. Here's the full Animated API:

https://facebook.github.io/react-native/docs/animated.html

So from a marketing perspective if usage of the API felt similar, it would be a great thing. My guess is achieving full usage of the Samsara API might be difficult, but we may be able to achieve parity across a large number of use cases. So I guess the main thing is first creating the samsara style elements, and then building the bridge. Animated bridges (aka gets references to the elements) by having you pass the driving animated values to the components, which I assume behind the scenes passes a reference to any elements that were passed Animated.Value back to the Animated animation engine. This is as opposed to a more imperative way were the developer grabs a reference to the element he/she wants to animate and then manually passes that to the samsara/animated engine. I'm not sure that's the biggest deal, but it does keep as close to the declarative interface React is going for as possible.

...DISCLOSURE: I've never used Samsara, but I just right now read the entire docs and played around in the various codepens...(LOVE, IT! I'M SUPER EXCITED)

Here's how I imagine using react-samsara:

import React from 'react';

import {
  Text,
} from 'react-native';

import {
  Context as SamsaraContext, 
  Surface as SamsaraSurface, 
  Node as SamsaraNode,
  Transitionable,
  Transform,
} from 'samsarajs';

class MyComponent extends React.Component {
  constructor(props) {
    super(props);

    this.t = new Transitionable(0);

    this.rotation = this.t.map(function(angle){
        return Transform.rotateZ(2 * Math.PI * angle);
    });

    this.opacity = this.t.map(function(x){
        return 1 - x / 2;
    });
  }
  handleClick() {
    this.t.set(1, {curve : 'easeInOut', duration : 700});
  }
  render() {
     return (
       <SamsaraContext>

        <SamsaraSurface 
          ref='samsara'
          className='hi-from-samsara'
          size={[200,200]} 
          origin={[.5, .5]}
          style={{backgroundColor: 'red'}}
          onClick={() => this.handleClick()}
        >
          <SamsaraNode transform={rotation} align={[.5, .5]}>
            <SamsaraNode opacity={opacity}>
              <Text>{this.props.someDynamicText}</Text>
            </SamsaraNode>
          </SamsaraNode>
        </SamsaraSurface>

        <SamsaraSurface 
          ref='samsara'
          className='hi-from-samsara'
          size={[200,200]} 
          origin={[.5, .5]}
          style={{backgroundColor: 'blue'}}
        >
          <Text>{this.props.moreText}</Text>
        </SamsaraSurface>

      </SamsaraContext>
    );
  }
}

Nodes are nested in the reverse direction of the rendering pipeline of samsara proper. Or at least you add the surface before the nodes. Otherwise, the way the driving animations--what samsara calls 'Transitionables'--are passed into nodes as properties just as is currently done with Animated and basically identical to Samsara proper where it's done via standard javascript objects rather than JSX props. And I assume streams of user input can be handled in a similar fashion, or perhaps they are full on components like the nodes:

<MouseInput>
   <SamsaraSurface />
</MouseInput>

And obviously this is just a tree structure that needs to be decoded into Samsara-speak. So that's where the work must be done, and what must happen is different than standard react component composition. Generally you nest children components and the way you communicate between the two is passing down props. And it's important to note you're passing props to some component imported from another file or library, you're NOT passing props between elements directly nested on the same page, which is exactly what we'd need to coordinate to make this API work for Samsara. For exampe the MouseInput deltas need to be passed to SamsaraSurface and so on. And then the context parent component needs to scoop it all up.

Here's likely how we would do that in React:

import {
  contextFromChildren, 
  parseChild, 
  samsarifyReactElements,
} from 'samsarajs-core';

class SamsaraContext extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      context: null,
    };
  }
  componentDidMount() {
    let children = React.Children.map(this.props.children, (child) => {
      return parseChild(child, child.props); //recursively do this same work (props will have samsara-specific values)
    });

    //parent components can get a reference to this context
    this.context = contextFromChildren(children); 
    this.setState({context: this.context});
  }
  render() {
    {samsarifyReactElements(this.props.children, this.state.context)}
  }
}

So samsarifyReactElements outputs what React expects with elements with appropriate classNames and IDs so we can hook into them and with the appropriate base styles, etc. That's probably the more straightforward part. The less straightforward part--and atypical of the standard react workflow--is analyzing what is in all those children elements. That's something basically only library developers do. You hardly ever need to do that. The way React is supposed to work is parents dont get told what to do by children. But the fact of the matter is: these aren't even our children. We're gonna render completely different children. We're just looking at them to get what Samsara needs to know to generate it's render tree.

So in conclusion we have:

Now the real question: rendering performance?? I think in the beginning we just assume what's being animated is "presentation" with content that doesn't change during animations. If Samsara can respond to updates to content/props with standard speeds (a few ms), all would be well and it's up to the developer to time updates to props/state in between animations. In a V2, Samsara exposes an API that developers can use to change the content. So developers would use componentWillReceiveProps (or rathercomponentShouldUpdate and return false so no re-renderings are triggered) on their components and make calls on their samsara reference to change it's content. It would be up to Samsara to change the content (text and pictures), so that it could do it in the most efficient way possible. I think we can get a lot of mileage though just by assuming all the content is fully rendered and available to Samsara.

FINAL THOUGHT: Though I have no idea of the Samsara implementation details, from where I'm looking at the API surface level--and based on what RN has already done with the Animated library--Samsara is a great match for React. It essentially takes the Animated library to a professional level of animation/presentation capabilities while keeping a familiar interface. I can imagine the React and RN teams being very excited about a Samsara integration. The RN team is churning out new releases every other week--there's really a lot of energy and attention being put into RN. They're constantly updating things and making things better. Things that were javascript-driven quickly become native-driven for better performance, animation, etc. Like, a lot of things/features/apis/components start out being built in javascript without efficient access to the underlying native APIs. Then, they upgrade the component/api and it now makes full use of hardware capabilities. For example, the Navigator component originally animated things in javascript, going back and forth over the "bridge" inefficiently. But now they have NavigatorExperimental which interpolates all its animations natively. The Animated library itself didn't exist in the beginning and was a watershed moment for RN apps. Samsara could be the next such step for their animation/presentation capabilities. Ultimately that combination is likely what everyone who was let down by Famo.us was looking for. ...ps. I'm not sure where you differ from what Famo.us was trying to achieve, but from the looks of it, you nailed the MVP they shoulda developed long ago. And by "MVP" I mean the complete succinct solution that solidly does what it sets out to do, and sets out to do what 80% of application developers actually need (rather than 1000% of what all kinds of developers need [and never deliver anything as a result]), while letting other libraries fill in any gaps by doing what they do best. A React integration for Samsara would be the final cherry on top, given React's success handling the "content" aspect.

dmvaldman commented 8 years ago

I'm not so clear on all the details but if you are interested in getting a proof of concept up, I'm happy to answer any questions you have on the Samsara side of things!

faceyspacey commented 8 years ago

My first question (perhaps request) is an option to disable appending elements to the dom, as React will handle that instead. A relate question is: are all the elements within the context always appended to the dom throughout the duration of the scene?

Because if they are only appended once, that makes things a lot easier. We just trick samsara into thinking that it's working with elements it appended to the DOM, when really it's using elements React appended to the DOM. Perhaps that's not the way Samsara works, but maybe it's a way samsara can work and be used for a subset of possibilities. The main idea is this: to the viewer, it doesn't matter whether the elements are removed from the DOM or just have opacity set to 0 (or the like). If you are removing elements, I assume it's for performance reasons, and perhaps that could be an initial limitation of react-samsara--you just can't rev up as many elements on the dom as you can as in samsara proper. I think that would be a fine place to start.

If we can make this happen, it's just about parsing the jsx tree structure of the samsara config, passing the parsed result to samsara, and generating react/html elements with the correct classNames and style properties for samsara to be able to think it created them. Then all I would need from you is the flag/option to turn off Samsara appending/removing to/from the DOM and a parameter available for me to pass the parent node of the context that i generated in React (plus some documentation on the rules for generating that flat set of child DOM nodes).

dmvaldman commented 8 years ago

This can certainly be done. Everything a Surface does that touches the DOM happens in samsara/dom/_DOMoutput.js. For example, the part that affects appending the HTML happens here.

I should make Context also use this code (currently it does not, but it can and should). You could then add a flag to suppress whichever DOM-touching parts.

dmvaldman commented 8 years ago

Refactored Context and _DOMOutput in https://github.com/dmvaldman/samsara/commit/9978f63a8fc91d70da81cf2de45374284edfec72 to make this integration a little easier.

hornos commented 7 years ago

JSPM + SystemJS React starter kit: https://github.com/rnprojekt/jspm-beta-sketch-test