├── .gitignore
├── App0.jsx
├── App1.jsx
├── App2
├── App2.jsx
└── tweenMixin.js
├── App3.jsx
├── App4
├── App4.jsx
├── algo.js
└── diff.js
├── README.md
├── easingTypes.js
├── index.html
├── index.jsx
├── out.js
├── package.json
└── stateStream.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/App0.jsx:
--------------------------------------------------------------------------------
1 | // surprice placeholder!
2 |
--------------------------------------------------------------------------------
/App1.jsx:
--------------------------------------------------------------------------------
1 | /*global -React */
2 | var React = require('react');
3 | var M = require('mori');
4 | var stateStream = require('./stateStream');
5 |
6 | var Child = React.createClass({
7 | mixins: [stateStream.Mixin],
8 | getInitialStateStream: function() {
9 | var self = this;
10 | return M.iterate(function(state) {
11 | // note that we use this.props here, and because of laziless, we know it's
12 | // gonna be the very current value of props (when the item is evaluated).
13 | // This is abusing the behavior of laziness and likely not a good idea
14 | // (e.g. in clojure, lazy seqs are chunked 32 items at time rather than 1,
15 | // so this shortcut wouldn't work)
16 | return M.hash_map(
17 | 'deg',
18 | M.get(state, 'deg') + 2 * (self.props.turnLeft ? -1 : 3)
19 | );
20 | }, M.hash_map('deg', 0));
21 | },
22 |
23 | render: function() {
24 | // turn right 3 times faster to offset parent turning left. Just visual nits
25 | var s = {
26 | border: '1px solid gray',
27 | borderRadius: '20px',
28 | display: 'inline-block',
29 | padding: 18,
30 | WebkitTransform: 'rotate(' + this.state.deg + 'deg)',
31 | transform: 'rotate(' + this.state.deg + 'deg)',
32 | };
33 | return (
34 |
35 | asd
36 |
37 | );
38 | }
39 | });
40 |
41 | var App1 = React.createClass({
42 | mixins: [stateStream.Mixin],
43 | getInitialStateStream: function() {
44 | return M.map(function(i) {
45 | return M.hash_map(
46 | 'deg', i * -2,
47 | 'childTurnLeft', false
48 | );
49 | }, M.range());
50 | },
51 |
52 | handleClick: function() {
53 | // key part! Alter the stream
54 |
55 | // for an infinite stream this is just asking for memory leak, since each
56 | // modification lazily accumulates functions to apply when a stream item is
57 | // taken. This is just a trivial demo however. Realistically we'd stop the
58 | // stream to signal that for that point onward it's the same state value
59 | // every frame
60 |
61 | // note that we can't just initiate a new stream completely here; some state
62 | // transformation might be happening and we'd lose them
63 | var newTurn = !M.get(M.first(this.stream), 'childTurnLeft');
64 | var s = M.map(function(stateI) {
65 | return M.assoc(stateI, 'childTurnLeft', newTurn);
66 | }, this.stream);
67 |
68 | this.setStateStream(s);
69 | },
70 |
71 | render: function() {
72 | var s = {
73 | border: '1px solid gray',
74 | borderRadius: '30px',
75 | display: 'inline-block',
76 | padding: 30,
77 | WebkitTransform: 'rotate(' + this.state.deg + 'deg)',
78 | transform: 'rotate(' + this.state.deg + 'deg)',
79 | marginLeft: 100,
80 | };
81 | return (
82 |
83 |
84 |
85 |
86 |
87 |
88 | );
89 | }
90 | });
91 |
92 | module.exports = App1;
93 |
--------------------------------------------------------------------------------
/App2/App2.jsx:
--------------------------------------------------------------------------------
1 | /*global -React */
2 | var React = require('react');
3 | var M = require('mori');
4 | var stateStream = require('../stateStream');
5 | var easingTypes = require('../easingTypes');
6 | var tweenMixin = require('./tweenMixin');
7 |
8 | var ease = easingTypes.easeInOutQuad;
9 |
10 | var App2 = React.createClass({
11 | mixins: [stateStream.Mixin, tweenMixin],
12 | getInitialStateStream: function() {
13 | return M.repeat(1, M.js_to_clj({
14 | blockX: [0, 0, 0],
15 | goingLeft: false,
16 | }));
17 | },
18 |
19 | handleClick: function() {
20 | var duration = 1000;
21 | var frameCount = stateStream.toFrameCount(duration);
22 | var initState = this.state;
23 | var start = initState.goingLeft ? 400 : 0;
24 | var dest = initState.goingLeft ? 0 : 400;
25 |
26 | var newStream = M.map(function(stateI) {
27 | return M.assoc(stateI, 'goingLeft', !initState.goingLeft);
28 | }, this.stream);
29 |
30 | newStream = this.mapState(newStream, duration, function(stateI, ms) {
31 | return M.assoc_in(
32 | stateI,
33 | ['blockX', 0],
34 | ease(ms, start, dest, duration)
35 | );
36 | });
37 |
38 | newStream = this.mapState(newStream, duration, function(stateI, ms) {
39 | return M.assoc_in(
40 | stateI,
41 | ['blockX', 1],
42 | ease(ms, initState.blockX[1], dest, duration)
43 | );
44 | });
45 |
46 | // TODO: get a better APi. For everything actually
47 | newStream = this.tweenState(newStream, duration, ['blockX', 2], {
48 | endValue: dest,
49 | easingFunction: ease,
50 | });
51 |
52 | this.setStateStream(newStream);
53 | },
54 |
55 | render: function() {
56 | var s1 = {
57 | border: '1px solid gray',
58 | borderRadius: '10px',
59 | display: 'inline-block',
60 | padding: 20,
61 | position: 'absolute',
62 | top: 10,
63 | WebkitTransform: 'translate3d(' + this.state.blockX[0] + 'px,0,0)',
64 | transform: 'translate3d(' + this.state.blockX[0] + 'px,0,0)',
65 | };
66 | var s2 = {
67 | border: '1px solid gray',
68 | borderRadius: '10px',
69 | display: 'inline-block',
70 | padding: 20,
71 | position: 'absolute',
72 | top: 60,
73 | WebkitTransform: 'translate3d(' + this.state.blockX[1] + 'px,0,0)',
74 | transform: 'translate3d(' + this.state.blockX[1] + 'px,0,0)',
75 | };
76 |
77 | var val = this.getAdditiveValue(['blockX', 2]);
78 | var s3 = {
79 | border: '1px solid gray',
80 | borderRadius: '10px',
81 | display: 'inline-block',
82 | padding: 20,
83 | position: 'absolute',
84 | top: 110,
85 | WebkitTransform: 'translate3d(' + val + 'px,0,0)',
86 | transform: 'translate3d(' + val + 'px,0,0)',
87 | };
88 |
89 | return (
90 |
91 |
92 |
93 |
94 |
95 |
96 | );
97 | }
98 | });
99 |
100 | module.exports = App2;
101 |
--------------------------------------------------------------------------------
/App2/tweenMixin.js:
--------------------------------------------------------------------------------
1 | var stateStream = require('../stateStream');
2 | var M = require('mori');
3 |
4 | var tweenMixin = {
5 | getInitialState: function() {
6 | return {
7 | _contrib: {},
8 | };
9 | },
10 | // TODO: prolly doesn't have to be a mixin
11 | // TODO: better name
12 | mapState: function(stream, duration, cb) {
13 | var frameCount = stateStream.toFrameCount(duration);
14 | var newStream = stateStream.extendTo(frameCount, stream);
15 |
16 | var chunk = M.map(
17 | cb,
18 | M.take(frameCount, newStream),
19 | M.map(stateStream.toMs, M.range())
20 | );
21 |
22 | // TODO: don't force evaluate this
23 | var restChunk = M.map(
24 | M.identity,
25 | M.repeat(M.last(chunk)),
26 | M.drop(frameCount, newStream)
27 | );
28 |
29 | return M.concat(chunk, restChunk);
30 | },
31 |
32 | mapChunk: function(stream, duration, cb, asd) {
33 | var frameCount = stateStream.toFrameCount(duration);
34 | var newStream = stateStream.extendTo(frameCount, stream);
35 |
36 | var chunk = M.map(
37 | cb,
38 | M.take(frameCount, newStream),
39 | M.map(stateStream.toMs, M.range())
40 | );
41 |
42 | var restChunk = M.drop(frameCount, stream);
43 | return M.concat(chunk, restChunk);
44 | },
45 |
46 | // TODO: inappropriate name
47 | tweenState: function(stream, duration, path, config) {
48 | var frameCount = stateStream.toFrameCount(duration);
49 | var newStream = stateStream.extendTo(frameCount + 1, stream);
50 |
51 | var pathKey = path.join('|');
52 | var contribPath = ['_contrib', pathKey];
53 |
54 | newStream = M.map(function(stateI) {
55 | return M.assoc_in(
56 | stateI,
57 | path,
58 | config.endValue
59 | );
60 | }, newStream);
61 |
62 | var beginValue = config.beginValue == null ?
63 | M.get_in(M.first(stream), path) :
64 | config.beginValue;
65 |
66 | newStream = this.mapChunk(newStream, duration, function(stateI, ms) {
67 | var contrib = -config.endValue + config.easingFunction(
68 | ms,
69 | beginValue,
70 | config.endValue,
71 | duration
72 | );
73 |
74 | return M.assoc_in(
75 | stateI,
76 | contribPath,
77 | M.conj(M.get_in(stateI, contribPath, M.vector()), contrib)
78 | );
79 | }, true);
80 |
81 | return newStream;
82 |
83 | // easing: easingFunction,
84 | // // duration: timeInMilliseconds,
85 | // // delay: timeInMilliseconds,
86 | // beginValue: aNumber,
87 | // endValue: aNumber,
88 | // // onEnd: endCallback,
89 | // // stackBehavior: behaviorOption
90 |
91 | },
92 |
93 | getAdditiveValue: function(path) {
94 | var p = path.join('|');
95 | var stateVal = this.state;
96 | for (var i = 0; i < path.length; i++) {
97 | stateVal = stateVal[path[i]];
98 | }
99 | var contribs = this.state._contrib;
100 | if (!(contribs && contribs[p])) {
101 | // not been set, or already finished and removed
102 | // TODO: actually remove from _contrib
103 | return stateVal;
104 | }
105 |
106 | return contribs[p].reduce(function(a, x) {
107 | return a + x;
108 | }, stateVal);
109 | },
110 | };
111 |
112 | module.exports = tweenMixin;
113 |
--------------------------------------------------------------------------------
/App3.jsx:
--------------------------------------------------------------------------------
1 | /*global -React */
2 | var React = require('react');
3 | var M = require('mori');
4 | var stateStream = require('./stateStream');
5 | var easingTypes = require('./easingTypes');
6 |
7 | function toObj(children) {
8 | return React.Children.map(children, function(child) {
9 | return child;
10 | });
11 | }
12 |
13 | function diff(o1, o2) {
14 | var res = [];
15 | for (var key in o1) {
16 | if (!o1.hasOwnProperty(key)) {
17 | continue;
18 | }
19 | if (!o2.hasOwnProperty(key)) {
20 | res.push(key);
21 | }
22 | }
23 |
24 | return res;
25 | }
26 |
27 | var Container = React.createClass({
28 | mixins: [stateStream.Mixin],
29 | getInitialStateStream: function() {
30 | var children = toObj(this.props.children);
31 | var configs = {};
32 | for (var key in children) {
33 | if (!children.hasOwnProperty(key)) {
34 | continue;
35 | }
36 | configs[key] = {
37 | left: 0,
38 | height: 60,
39 | opacity: 1,
40 | };
41 | }
42 |
43 | return M.repeat(1, M.js_to_clj({
44 | children: children,
45 | configs: configs,
46 | }));
47 | },
48 |
49 | componentWillUpdate: function(nextProps) {
50 | var o1 = toObj(nextProps.children);
51 | var o2 = toObj(this.props.children);
52 | var enters = diff(o1, o2);
53 | var exits = diff(o2, o1);
54 |
55 | if (exits.length === 0 && enters.length === 0) {
56 | return;
57 | }
58 |
59 | var children = M.js_to_clj(toObj(nextProps.children));
60 | var duration = 700;
61 | var frameCount = stateStream.toFrameCount(duration);
62 | var initState = this.state;
63 | var newStream = stateStream.extendTo(frameCount + 1, this.stream);
64 |
65 | if (exits.length > 0) {
66 |
67 | var chunk = M.map(function(stateI, i) {
68 | exits.forEach(function(exitKey) {
69 | var ms = stateStream.toMs(i);
70 | var config = initState.configs[exitKey];
71 |
72 | stateI = M.assoc_in(stateI, ['configs', exitKey], M.hash_map(
73 | 'left', easingTypes.easeInOutQuad(ms, config.left, -200, duration),
74 | 'opacity', easingTypes.easeInOutQuad(ms, config.opacity, 0, duration),
75 | 'height', easingTypes.easeInOutQuad(ms, config.height, 0, duration)
76 | ));
77 | });
78 |
79 | return stateI;
80 | }, M.take(frameCount, newStream), M.range());
81 |
82 | var restChunk = M.map(function(stateI) {
83 | exits.forEach(function(exitKey) {
84 | stateI = M.assoc(
85 | stateI,
86 | 'children', children,
87 | 'configs', M.dissoc(M.get(stateI, 'configs'), exitKey)
88 | );
89 | });
90 |
91 | return stateI;
92 | }, M.drop(frameCount, newStream));
93 |
94 | newStream = M.concat(chunk, restChunk);
95 | }
96 |
97 | if (enters.length > 0) {
98 | var chunk2 = M.map(function(stateI, i) {
99 | enters.forEach(function(enterKey) {
100 | var ms = stateStream.toMs(i);
101 | var config = initState.configs[enterKey];
102 |
103 | stateI = M.assoc_in(stateI, ['configs', enterKey], M.hash_map(
104 | 'left', easingTypes.easeInOutQuad(ms, config.left, 0, duration),
105 | 'opacity', easingTypes.easeInOutQuad(ms, config.opacity, 1, duration),
106 | 'height', easingTypes.easeInOutQuad(ms, config.height, 60, duration)
107 | ));
108 | stateI = M.assoc(stateI, 'children', children);
109 | });
110 |
111 | return stateI;
112 | }, M.take(frameCount, newStream), M.range());
113 |
114 | var restChunk2 = M.map(function(stateI) {
115 | enters.forEach(function(enterKey) {
116 | stateI = M.assoc_in(stateI, ['configs', enterKey], M.hash_map(
117 | 'left', 0,
118 | 'height', 60,
119 | 'opacity', 1
120 | ));
121 | stateI = M.assoc(stateI, 'children', children);
122 | });
123 |
124 | return stateI;
125 | }, M.drop(frameCount, newStream));
126 |
127 | newStream = M.concat(chunk2, restChunk2);
128 | }
129 |
130 | this.setStateStream(newStream);
131 | },
132 |
133 | render: function() {
134 | var state = this.state;
135 | var children = [];
136 | for (var key in state.children) {
137 | if (!state.children.hasOwnProperty(key)) {
138 | continue;
139 | }
140 | var s = {
141 | left: state.configs[key].left,
142 | height: state.configs[key].height,
143 | opacity: state.configs[key].opacity,
144 | position: 'relative',
145 | overflow: 'hidden',
146 | WebkitUserSelect: 'none',
147 | };
148 | children.push(
149 | {state.children[key]}
150 | );
151 | }
152 |
153 |
154 | return (
155 |
156 | {children}
157 |
158 | );
159 | }
160 | });
161 |
162 | // notice that this component is ignorant of both immutable-js and the animation
163 | var App3 = React.createClass({
164 | getInitialState: function() {
165 | return {
166 | items: ['a', 'b', 'c', 'd'],
167 | };
168 | },
169 |
170 | handleClick: function(item) {
171 | var items = this.state.items;
172 | var idx = items.indexOf(item);
173 | if (idx === -1) {
174 | // might not find the clicked item because it's transitioning out and
175 | // doesn't technically exist here in the parent anymore. Make it
176 | // transition back (BEAT THAT)
177 | items.push(item);
178 | items.sort();
179 | } else {
180 | items.splice(idx, 1);
181 | }
182 | this.setState({
183 | items: items,
184 | });
185 | },
186 |
187 | render: function() {
188 | var s = {
189 | width: 100,
190 | padding: 20,
191 | border: '1px solid gray',
192 | borderRadius: 3,
193 | };
194 |
195 | return (
196 |
197 | Click to remove. Double click to un-remove (!)
198 |
199 | {this.state.items.map(function(item) {
200 | return (
201 |
205 | {item}
206 |
207 | );
208 | }, this)}
209 |
210 |
211 | );
212 | }
213 | });
214 |
215 | module.exports = App3;
216 |
--------------------------------------------------------------------------------
/App4/App4.jsx:
--------------------------------------------------------------------------------
1 | /*global -React */
2 | var React = require('react');
3 | var M = require('mori');
4 | var stateStream = require('../stateStream');
5 | var easingTypes = require('../easingTypes');
6 | var algo = require('./algo');
7 | var diff = require('./diff');
8 |
9 | function toObj(children) {
10 | return React.Children.map(children, function(child) {
11 | return child;
12 | });
13 | }
14 |
15 | var ease = easingTypes.linear;
16 |
17 | var Container = React.createClass({
18 | mixins: [stateStream.Mixin],
19 | getInitialStateStream: function() {
20 | var children = toObj(this.props.children);
21 | var configs = {};
22 | var i = 0;
23 | for (var key in children) {
24 | if (!children.hasOwnProperty(key)) {
25 | continue;
26 | }
27 | configs[key] = {
28 | left: 0,
29 | height: 60,
30 | opacity: 1,
31 | top: 60 * i,
32 | };
33 | i++;
34 | }
35 |
36 | return M.repeat(1, M.js_to_clj({
37 | children: children,
38 | configs: configs,
39 | }));
40 | },
41 |
42 | // TODO: show this: sometimes it's desirable not to have moves, as items
43 | // overlapping and moving to new positions might be disturbing. better just
44 | // remove and add somewhere else at the same time. This can be done without
45 | // special handling (not here at least) by incorporating the position into the
46 | // key. But what if we have 12 -> 21? We want 1 add/remove, and 2 move. But if
47 | // 2 has the key '2-1' then '2-0' it'll also get killed.
48 |
49 | // there might be a new insight here. maybe the whole diffing and state
50 | // children is already determinable through key assignment
51 |
52 | // TODO: abstract away this logic!
53 |
54 | // TODO: abc -> c will see d restart its transitioning out
55 | componentWillReceiveProps: function(nextProps) {
56 | var nextChildrenMap = toObj(nextProps.children);
57 | var currChildrenMap = toObj(this.props.children);
58 | var enters = diff(nextChildrenMap, currChildrenMap);
59 | var exits = diff(this.state.children, nextChildrenMap);
60 |
61 | var childrenKeys = algo(Object.keys(this.state.children), Object.keys(nextChildrenMap));
62 | var children = {};
63 | childrenKeys.forEach(function(key) {
64 | children[key] = nextChildrenMap[key] || this.state.children[key];
65 | }, this);
66 |
67 | var duration = 700;
68 | var frameCount = stateStream.toFrameCount(duration);
69 | var initState = this.state;
70 | var newStream = stateStream.extendTo(frameCount + 1, this.stream);
71 | var finalTops = {};
72 | childrenKeys.forEach(function(key, i) {
73 | var config = initState.configs[key];
74 | if (exits.indexOf(key) > -1) {
75 | finalTops[key] = config.top;
76 | } else {
77 | finalTops[key] = Object.keys(nextChildrenMap).indexOf(key) * 60;
78 | }
79 | });
80 |
81 | childrenKeys.forEach(function(key, i) {
82 | var chunk;
83 | var restChunk;
84 | var finalTop = finalTops[key];
85 | // config might already exist if the component is still unmounting
86 | var config = initState.configs[key] || {
87 | left: 200,
88 | height: 0,
89 | opacity: 0,
90 | top: finalTop,
91 | };
92 |
93 | if (exits.indexOf(key) > -1) {
94 | chunk = M.map(function(stateI, i) {
95 | var ms = stateStream.toMs(i);
96 |
97 | return M.assoc_in(stateI, ['configs', key], M.hash_map(
98 | 'left', ease(ms, config.left, 200, duration),
99 | 'opacity', ease(ms, config.opacity, 0, duration),
100 | 'height', ease(ms, config.height, 0, duration),
101 | 'top', ease(ms, config.top, finalTop, duration)
102 | ));
103 | }, M.take(frameCount, newStream), M.range());
104 |
105 | restChunk = M.map(function(stateI) {
106 | return M.assoc(
107 | stateI,
108 | 'children', M.js_to_clj(nextChildrenMap),
109 | 'configs', M.dissoc(M.get(stateI, 'configs'), key)
110 | );
111 | }, M.drop(frameCount, newStream));
112 |
113 | } else if (enters.indexOf(key) > -1) {
114 | chunk = M.map(function(stateI, i) {
115 | var ms = stateStream.toMs(i);
116 |
117 | stateI = M.assoc_in(stateI, ['configs', key], M.hash_map(
118 | 'left', ease(ms, config.left, 0, duration),
119 | 'opacity', ease(ms, config.opacity, 1, duration),
120 | 'height', ease(ms, config.height, 60, duration),
121 | 'top', ease(ms, config.top, finalTop, duration)
122 | ));
123 | stateI = M.assoc(stateI, 'children', M.js_to_clj(children));
124 |
125 | return stateI;
126 | }, M.take(frameCount, newStream), M.range());
127 |
128 | restChunk = M.map(function(stateI) {
129 | stateI = M.assoc_in(stateI, ['configs', key], M.hash_map(
130 | 'left', 0,
131 | 'height', 60,
132 | 'opacity', 1,
133 | 'top', finalTop
134 | ));
135 | stateI = M.assoc(stateI, 'children', M.js_to_clj(nextChildrenMap));
136 |
137 | return stateI;
138 | }, M.drop(frameCount, newStream));
139 | } else {
140 | chunk = M.map(function(stateI, i) {
141 | var ms = stateStream.toMs(i);
142 |
143 | return M.assoc_in(
144 | stateI,
145 | ['configs', key, 'top'],
146 | ease(ms, config.top, finalTop, duration)
147 | );
148 | }, M.take(frameCount, newStream), M.range());
149 |
150 | restChunk = M.map(function(stateI) {
151 | return M.assoc_in(stateI, ['configs', key], M.hash_map(
152 | 'left', 0,
153 | 'height', 60,
154 | 'opacity', 1,
155 | 'top', finalTop
156 | ));
157 | }, M.drop(frameCount, newStream));
158 | }
159 |
160 | newStream = M.concat(chunk, restChunk);
161 | });
162 |
163 | this.setStateStream(newStream);
164 | },
165 |
166 | render: function() {
167 | var state = this.state;
168 | var children = [];
169 | for (var key in state.children) {
170 | if (!state.children.hasOwnProperty(key)) {
171 | continue;
172 | }
173 | var s = {
174 | left: state.configs[key].left,
175 | height: state.configs[key].height,
176 | opacity: state.configs[key].opacity,
177 | top: state.configs[key].top,
178 | position: 'absolute',
179 | overflow: 'hidden',
180 | WebkitUserSelect: 'none',
181 | };
182 | children.push(
183 | {state.children[key]}
184 | );
185 | }
186 |
187 | return {children}
;
188 | }
189 | });
190 |
191 | // animation-unaware component
192 | var App3 = React.createClass({
193 | getInitialState: function() {
194 | return {
195 | items: ['a', 'b', 'c', 'd'],
196 | // items: ['c'],
197 | };
198 | },
199 |
200 | handleClick: function(sequence) {
201 | this.setState({
202 | items: sequence.split(''),
203 | });
204 | },
205 |
206 | render: function() {
207 | var s = {
208 | width: 100,
209 | padding: 20,
210 | border: '1px solid gray',
211 | borderRadius: 3,
212 | };
213 |
214 | return (
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 | {this.state.items.map(function(item) {
226 | return {item}
;
227 | }, this)}
228 |
229 |
230 | );
231 | }
232 | });
233 |
234 | module.exports = App3;
235 |
--------------------------------------------------------------------------------
/App4/algo.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var assert = require('assert');
4 |
5 | function notIn(as, a) {
6 | return as.indexOf(a) === -1;
7 | }
8 | function head(a) {
9 | return a[0];
10 | }
11 | function tail(a) {
12 | return a.slice(1);
13 | }
14 |
15 | function orderImpl(a, b, res) {
16 | if (a.length === 0 || b.length === 0) {
17 | return res.concat(a, b);
18 | }
19 |
20 | if (head(a) === head(b)) {
21 | return orderImpl(tail(a), tail(b), res.concat(head(a)));
22 | }
23 | if (notIn(b, head(a))) {
24 | return orderImpl(tail(a), b, res.concat(head(a)));
25 | }
26 | return orderImpl(tail(a), b, res);
27 | }
28 |
29 | function order(a, b) {
30 | return orderImpl(a, b, []);
31 | }
32 |
33 | function testA(a, b, res) {
34 | assert.deepEqual(order(a.split(''), b.split('')), res.split(''));
35 | }
36 |
37 | testA('ø', '', 'ø');
38 | testA('', 'ø', 'ø');
39 | testA('∫ø', '∫', '∫ø');
40 | testA('ø∫', '∫', 'ø∫');
41 | testA('ø∫', '∫ø', '∫ø');
42 | testA('åø∫', '∫ø', 'å∫ø');
43 | testA('åø∫', 'åø®∫', 'åø®∫');
44 | testA('åø∫', 'å∫', 'åø∫');
45 | testA('åø∫', 'ø', 'åø∫'); // or å ∫ ø? no
46 | testA('ø', 'åø∫', 'åø∫');
47 | // at the end, or beginning
48 | testA('åø∫', '¬', 'åø∫¬');
49 | // can't fully change in place.
50 | testA('åø∫', '∫¬ø', 'å∫¬ø');
51 | testA('åø∫', '∫¬åø', '∫¬åø');
52 | testA('åø∫', '¬å∫ø', '¬å∫ø'); // no choice
53 | testA('åø∫', '嬸', 'å∫¬ø'); // or å ¬ ∫ ø
54 | testA('øå∫', '¬å∫', 'ø¬å∫');
55 | testA('ø∫', '¬å∫', 'ø¬å∫'); // or å ∫ ø ¬
56 | testA('ø∫', '¬å', 'ø∫¬å');
57 | testA('∫ø', '¬å', '∫ø¬å');
58 |
59 | // testB('ø', '', '');
60 | // testB('', 'ø', '');
61 | // testB('∫ø', '∫', '');
62 | // testB('ø∫', '∫', '');
63 | // testB('ø∫', '∫ø', 'm∫'); // or mø
64 | // testB('åø∫', '∫ø', 'm∫');
65 | // testB('åø∫', 'åø®∫', '');
66 | // testB('åø∫', 'å∫', '');
67 | // testB('åø∫', 'ø', '');
68 | // testB('ø', 'åø∫', '');
69 | // testB('åø∫', '¬', '');
70 | // testB('åø∫', '∫¬ø', 'mø');
71 | // testB('åø∫', '∫¬åø', 'm∫'); // not må,mø
72 | // testB('åø∫', '¬å∫ø', 'mø');
73 | // testB('åø∫', '嬸', '');
74 | // testB('øå∫', '¬å∫', '');
75 | // testB('ø∫', '¬å∫', '');
76 |
77 | module.exports = order;
78 |
--------------------------------------------------------------------------------
/App4/diff.js:
--------------------------------------------------------------------------------
1 | // returns what's missing in o2
2 | function diff(o1, o2) {
3 | var res = [];
4 | for (var key in o1) {
5 | if (!o1.hasOwnProperty(key)) {
6 | continue;
7 | }
8 | if (!o2.hasOwnProperty(key)) {
9 | res.push(key);
10 | }
11 | }
12 |
13 | return res;
14 | }
15 |
16 | module.exports = diff;
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React State Stream
2 |
3 | React animation on steroid. [Live demo](https://rawgit.com/chenglou/react-state-stream/master/index.html).
4 |
5 | **This is highly experimental**. For a more stable and performant library, try [React-motion](https://github.com/chenglou/react-motion).
6 |
7 | Not providing a npm package for the moment.
8 |
9 | The current master branch uses [mori](https://github.com/swannodette/mori) because there are some distinctive features I need that are not yet in [immutable-js](https://github.com/facebook/immutable-js/). The latter version is in the other branch and is (even more) underperformant.
10 |
11 | ## What This Library Solves
12 | General animation API, including unmounting transition (and de-unmounting!). `npm install && npm run build` and open `index.html`. There are 3 demos:
13 |
14 | 1. Infinitely spinning child inside infinitely spinning parent.
15 | 2. Normal tweening animation using third-party easing functions.
16 | 3. Non-blocking unmounting and de-unmounting list.
17 |
18 | ## General Concept
19 | Instead of one state, set all the states that will ever be, aka a lazy state stream. This is akin to FRP but uses an ordinary `LazySeq`. Also reminiscent of Flash's timeline.
20 |
21 | A normal tween can simply be expressed as a map over a chunk of the lazy seq. Same for physics engine. Works even if the tween never stops (e.g. a physics engine where a menu item floats in mid air).
22 |
23 | Unmounting is taken care of by storing a copy of `props.children` in the transition wrapper component's state stream, along with a `styleConfig` for animation, most probably. Each frame (`componentWillUpdate`) it diffs the entering/exiting children. It then describes how the transition changes:
24 |
25 | ```js
26 | tween = this.stream.take(100).map(state => update(state.styleConfig.childKey.height, easeOut(easeOutParams)))
27 | rest = this.steam.drop(100)
28 | .map(state => update(state.config.childKey.height, 0))
29 | .map(state => update(state.children, blaOneLessChild))
30 |
31 | this.stream = tween.concat(rest)
32 | ```
33 |
34 | WIth this we can now do de-unmounting (ok frankly I have no idea why I'm so hooked up on this. It just feels conceptually right, you know?) by overriding the `map`s again.
35 |
36 | ## Mentality (i.e. How This Came to Be)
37 | One important idea is that, while the render function stays a snapshot of props and (current) state, it's called on every frame, just like in game engines. Constantly lifting every state a-la FRP sounds tedious and it's practically hard to express; but modifying a lazy seq (with an array-like API) isn't. `setState(s)` becomes `setStateStream(InfiniteRepeat(s))`.
38 |
39 | For unmounting, we really need to treat it as first-class state stream transformation rather than some hacky afterthought. The system needs to work well when an unmounting item takes infinite time to transition out and but doesn't block anything else.
40 |
41 | When I said first-class, I mean that we need to realize that unmounting transition is not reserved for just animation. With this library (demo 3 specifically) we gain new ways of expressing some UIs:
42 |
43 | - Imagine a photo app where swiping right shows the next picture. If we swipe and hold midway, there are two photos on the screen at that moment. With the transition wrapper, we don't have to take care of that logic in our main view! Picture 1 will infinitely stay in "unmounting" mode and picture 2, in "mounting" mode. In reality we've only specified one photo component in the view.
44 | - If we wrap the whole app in a transition wrapper, we can do portrait-landscape crossfade for free.
45 |
46 | Thanks to this mentality (i.e. animation is really just a state stream), there are very little library-specific code here. Like, 40 lines (to change some existing React behaviors) + some general sequence helpers.
47 |
48 | ## That Layout Problem
49 | During tweening, the layout might be in an invalid state (see demo 3 where the items are moving out). I don't think it's worth the time to design a layout system that accommodates these invalid states (also remember: that invalid state might last indefinitely. See previous bullet point). Fortunately, now that we have support to do [layout in JS](https://github.com/facebook/css-layout), I'm hoping that, under the hood, it places everything in `position: absolute` and that we can easily read/write the values from JS. The layout problem would therefore be solved under this state stream system: the begin/end keyframes (states) are valid layouts, and you tween the values in-between by modifying the absolute position (normally discouraged but legitimate for tweening).
50 |
51 | ## Optimizations
52 | This library is not super performance currently, as I wanted to focus on the API. But there are huge perf boosts to be had. For one, I rAF `setState` each component so the leaf nodes get `log(n)` `setState`s per frame, lol.
53 |
54 | One thing to be careful about is doing an infinite animation like in demo 1. Since we're mapping over an infinite lazy stream, every modification to it (that corresponds to a new `map`) will accumulate until that item gets evaluated. For dealing with infinite animations, we'll expose a few specific helpers in the future.
55 |
56 | For all other terminating animations, make the stream finite. Upon reaching the last cell, the each-frame rendering will halt (so conceptually, [state] of length 1 is the same as current react state). This also conveniently puts an end to all accumulated `map` callback evaluations.
57 |
58 | Laziness is important here, as are persistent collections. As long as these aren't first-class in JS, we'll have to pay the extra cost of converting collections to JS, and vice-versa (unless we use persistent collection and lazy streams in React itself). This library probably runs much faster on ClojureScript right now if I had bothered. Now we sit and wait til lazy seqs and persistent collections become JS native in 20 years.
59 |
--------------------------------------------------------------------------------
/easingTypes.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var easingTypes = {
4 | // t: current time, b: beginning value, c: change in value, d: duration
5 |
6 | // new note: I much prefer specifying the final value rather than the change
7 | // in value this is what the repo's interpolation plugin api will use. Here,
8 | // c will stand for final value
9 |
10 | linear: function(t, b, _c, d) {
11 | var c = _c - b;
12 | return t*c/d + b;
13 | },
14 | easeInQuad: function (t, b, _c, d) {
15 | var c = _c - b;
16 | return c*(t/=d)*t + b;
17 | },
18 | easeOutQuad: function (t, b, _c, d) {
19 | var c = _c - b;
20 | return -c *(t/=d)*(t-2) + b;
21 | },
22 | easeInOutQuad: function (t, b, _c, d) {
23 | var c = _c - b;
24 | if ((t/=d/2) < 1) return c/2*t*t + b;
25 | return -c/2 * ((--t)*(t-2) - 1) + b;
26 | },
27 | easeInElastic: function (t, b, _c, d) {
28 | var c = _c - b;
29 | var s=1.70158;var p=0;var a=c;
30 | if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3;
31 | if (a < Math.abs(c)) { a=c; var s=p/4; }
32 | else var s = p/(2*Math.PI) * Math.asin (c/a);
33 | return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
34 | },
35 | easeOutElastic: function (t, b, _c, d) {
36 | var c = _c - b;
37 | var s=1.70158;var p=0;var a=c;
38 | if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3;
39 | if (a < Math.abs(c)) { a=c; var s=p/4; }
40 | else var s = p/(2*Math.PI) * Math.asin (c/a);
41 | return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b;
42 | },
43 | easeInOutElastic: function (t, b, _c, d) {
44 | var c = _c - b;
45 | var s=1.70158;var p=0;var a=c;
46 | if (t==0) return b; if ((t/=d/2)==2) return b+c; if (!p) p=d*(.3*1.5);
47 | if (a < Math.abs(c)) { a=c; var s=p/4; }
48 | else var s = p/(2*Math.PI) * Math.asin (c/a);
49 | if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
50 | return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b;
51 | },
52 | easeInBack: function (t, b, _c, d, s) {
53 | var c = _c - b;
54 | if (s == undefined) s = 1.70158;
55 | return c*(t/=d)*t*((s+1)*t - s) + b;
56 | },
57 | easeOutBack: function (t, b, _c, d, s) {
58 | var c = _c - b;
59 | if (s == undefined) s = 1.70158;
60 | return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
61 | },
62 | easeInOutBack: function (t, b, _c, d, s) {
63 | var c = _c - b;
64 | if (s == undefined) s = 1.70158;
65 | if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
66 | return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
67 | },
68 | easeInBounce: function (t, b, _c, d) {
69 | var c = _c - b;
70 | return c - easingTypes.easeOutBounce (d-t, 0, c, d) + b;
71 | },
72 | easeOutBounce: function (t, b, _c, d) {
73 | var c = _c - b;
74 | if ((t/=d) < (1/2.75)) {
75 | return c*(7.5625*t*t) + b;
76 | } else if (t < (2/2.75)) {
77 | return c*(7.5625*(t-=(1.5/2.75))*t + .75) + b;
78 | } else if (t < (2.5/2.75)) {
79 | return c*(7.5625*(t-=(2.25/2.75))*t + .9375) + b;
80 | } else {
81 | return c*(7.5625*(t-=(2.625/2.75))*t + .984375) + b;
82 | }
83 | },
84 | easeInOutBounce: function (t, b, _c, d) {
85 | var c = _c - b;
86 | if (t < d/2) return easingTypes.easeInBounce (t*2, 0, c, d) * .5 + b;
87 | return easingTypes.easeOutBounce (t*2-d, 0, c, d) * .5 + c*.5 + b;
88 | }
89 | };
90 |
91 | module.exports = easingTypes;
92 |
93 | /*
94 | *
95 | * TERMS OF USE - EASING EQUATIONS
96 | *
97 | * Open source under the BSD License.
98 | *
99 | * Copyright © 2001 Robert Penner
100 | * All rights reserved.
101 | *
102 | * Redistribution and use in source and binary forms, with or without modification,
103 | * are permitted provided that the following conditions are met:
104 | *
105 | * Redistributions of source code must retain the above copyright notice, this list of
106 | * conditions and the following disclaimer.
107 | * Redistributions in binary form must reproduce the above copyright notice, this list
108 | * of conditions and the following disclaimer in the documentation and/or other materials
109 | * provided with the distribution.
110 | *
111 | * Neither the name of the author nor the names of contributors may be used to endorse
112 | * or promote products derived from this software without specific prior written permission.
113 | *
114 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
115 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
116 | * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
117 | * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
118 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
119 | * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
120 | * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
121 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
122 | * OF THE POSSIBILITY OF SUCH DAMAGE.
123 | *
124 | */
125 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/index.jsx:
--------------------------------------------------------------------------------
1 | /*global -React */
2 | var React = require('react');
3 | var App1 = require('./App1.jsx');
4 | var App2 = require('./App2/App2.jsx');
5 | var App3 = require('./App3.jsx');
6 | var App4 = require('./App4/App4.jsx');
7 |
8 | // React.render(, document.getElementById('content1'));
9 | React.render(, document.getElementById('content2'));
10 | // React.render(, document.getElementById('content3'));
11 | // React.render(, document.getElementById('content4'));
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-set-state-stream",
3 | "version": "0.0.0",
4 | "description": "",
5 | "main": "index.jsx",
6 | "scripts": {
7 | "build": "webpack --module-bind 'jsx=jsx-loader' index.jsx --output-file out.js",
8 | "watch": "webpack -w --module-bind 'jsx=jsx-loader' index.jsx --output-file out.js"
9 | },
10 | "author": "Cheng Lou",
11 | "license": "MIT",
12 | "bugs": {
13 | "url": "https://github.com/chenglou/react-set-state-stream/issues"
14 | },
15 | "homepage": "https://github.com/chenglou/react-set-state-stream",
16 | "devDependencies": {
17 | "jsx-loader": "^0.12.2",
18 | "webpack": "^1.4.13"
19 | },
20 | "dependencies": {
21 | "mori": "^0.2.9",
22 | "react": "^0.12.1"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/stateStream.js:
--------------------------------------------------------------------------------
1 | var M = require('mori');
2 |
3 | function toMs(frame) {
4 | return frame * 1000 / 60;
5 | }
6 |
7 | function toFrameCount(ms) {
8 | return Math.ceil(ms * 60 / 1000);
9 | }
10 |
11 | function requestAnimationFrame2(f) {
12 | setTimeout(function() {
13 | f();
14 | }, 1000/60);
15 | }
16 |
17 | function onlyOneLeft(seq) {
18 | return M.count(M.take(2, seq)) === 1;
19 | }
20 |
21 | // when a stream has size 4 and you want to map a tween of size 10, you extend
22 | // the stream to 10. This helper does it and fill it with the last value of the
23 | // stream. If the stream's already longer than n then just return it
24 | function extendTo(n, seq) {
25 | var s = M.take(n, seq);
26 | var length = M.count(s);
27 | if (length === n) {
28 | return seq;
29 | }
30 |
31 | // TODO: this force evaluates the stream. Change that
32 | return M.concat(s, M.repeat(n - length, M.last(s)));
33 | }
34 |
35 | // mori/cljs mapping over multiple collections stops at the end of the shortest.
36 | // for mapping over stream it's very conveninent to map til the end of the
37 | // longest
38 | function mapAll() {
39 | // TODO: https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments
40 | var f = arguments[0];
41 | var colls = [].slice.call(arguments, 1);
42 |
43 | return M.lazy_seq(function() {
44 | var hasSomeItem = M.some(M.seq, colls);
45 | if (!hasSomeItem) {
46 | return;
47 | }
48 | return M.cons(
49 | M.apply(f, M.map(M.first, colls)),
50 | M.apply(mapAll, f, M.map(M.rest, colls))
51 | );
52 | });
53 | }
54 |
55 | var stateStreamMixin = {
56 | setStateStream: function(stream) {
57 | this.stream = stream;
58 | this.startRaf();
59 | },
60 |
61 | getInitialState: function() {
62 | // TOOD: need to merge mixins getInitialStateStream... bla
63 | var s;
64 | if (this.getInitialStateStream) {
65 | s = M.clj_to_js(M.first(this.getInitialStateStream()));
66 | } else {
67 | s = {};
68 | }
69 |
70 | return s;
71 | },
72 |
73 | componentWillMount: function() {
74 | if (this.getInitialStateStream) {
75 | this.stream = this.getInitialStateStream();
76 | } else {
77 | this.stream = M.repeat(1, M.hash_map());
78 | }
79 | },
80 |
81 | startRaf: function() {
82 | // current implementation of the mixin is basic and doesn't optimize for the
83 | // fact that if a parent and child both include the mixin, there'd be
84 | // useless child updates (since it really should just ride on parent's
85 | // update). That's ok for the purpose of the demo for now
86 | var self = this;
87 | if (self._rafing) {
88 | return;
89 | }
90 | self._rafing = true;
91 |
92 | requestAnimationFrame(function next() {
93 | if (onlyOneLeft(self.stream)) {
94 | // already evaluated stream[0]
95 | self._rafing = false;
96 | return;
97 | }
98 | self.stream = M.rest(self.stream);
99 | var stateI = M.first(self.stream); // check order here
100 | self.replaceState(M.clj_to_js(stateI));
101 |
102 | requestAnimationFrame(next);
103 | });
104 | },
105 |
106 | componentDidMount: function() {
107 | this.startRaf();
108 | },
109 | };
110 |
111 | var stateStream = {
112 | Mixin: stateStreamMixin,
113 | toMs: toMs,
114 | toFrameCount: toFrameCount,
115 | extendTo: extendTo,
116 | mapAll: mapAll,
117 | };
118 |
119 | module.exports = stateStream;
120 |
--------------------------------------------------------------------------------