Thus far we've been hearing about the concept of streams in terms of Transitionables and Samsara inputs. We have yet to precisely describe what we mean by a stream. In Samsara,
EventEmitter that emits
start, update and end events, where zero or more update events come between a start and an end event.That's it really. But when taken from this abstract viewpoint streams are a powerful concept to describe animation, and more generally
anything that changes over time. For example, the entire render tree is also a stream: the request animation frame loop
that is Samsara's internal clock is canceled when the render tree emits an end event, and starts again when
the render tree emits a start event. This ensures that when nothing is changing, no JavaScript is being executed.
Though this opinion on what events need to be emitted to turn your ordinary EventEmitter into a stream is the only
necessary requirement, streams are better described by "stream logic": how to plug them together to build pipelines,
how to transform them, how to combine and split them. For that reason, Samsara streams have methods to accomplish
these tasks.
Transforming streams is the process of converting the data of one stream, into another stream with different data. There are several common methods for doing this:
<Function>)The map method transforms a stream of data into a new stream of mapped data.
var t = new Transitionable(0);
var transform = t.map(function(value){
return Transform.translateX(value);
});
<String>)The pluck method returns a piece of data from a stream, selected by key.
// example of selecting a value of a JSON object
var mouse = new MouseInput();
var delta = mouse.pluck('delta');
If a stream returns an array, you can also pluck by the array index (for JavaScript array[0] and array['0'] are the same anyway).
// example of selecting an index in an array
var t = new Transitionable([0,0]);
var x = t.pluck(0);
var y = t.pluck(1);
<Function>)The filter method takes a function which returns a boolean, and creates a new stream that only emits events if the filter
function is satisfied (returns true).
var t = new Transitionable(0);
var s = t.filter(function(value){
return value < 0.5;
});
The methods above are for transforming a single type of stream into another, but sometimes you want to make a stream
from multiple streams. For combining streams there are two methods on the Stream constructor.
<Function>, <Array>)The lift method takes a function and an array of streams as arguments and produces a new stream whose values are
the return values of the supplied function. The current value of each of the streams in the
array is used as an argument into the function.
var s = new Transitionable(0);
var t = new Transitionable(1);
// this stream will return the sum of the values of `s` and `t`
var liftedStream = Stream.lift(function(s, t){
return s + t;
}, [s, t]);
<Object>)The merge method combines multiple stream sources into one. It takes a JSON object of streams (or static values)
and creates a stream from them whose values are the current values of each of the streams. In fact, every node
in Samsara is simply a merged stream.
var s = new Transitionable(0);
var t = new Transitionable(1);
// this stream will return an object consisting of the current values of `s`, `t` and `a` (which doesn't change)
var mergedStream = Stream.merge({
s : s,
t : t,
a : [1,2,3]
});
Both the lift and merge methods need to handle the case when two or more of their sources are changing simultaneously.
Internally, events will be batched by the request animation frame loop. In the above merge example, if both s and t emit an update event in the same
request animation frame loop. For example, the mergedStream will only emit a single update event, with the most recent values
of both s and t.
Batching when events are not of the same type gets more interesting. To ensure consistency, one needs to define an "event algebra".
update, the combined stream emits an updatestart, the combined stream emits startend, the combined stream emits endstart and the rest emit end the combined stream emits update event.Here are all the cases:
| source event | source event | combined event |
|---|---|---|
start |
start |
start |
start |
update |
update |
start |
end |
update |
update |
update |
update |
update |
end |
update |
end |
end |
end |
If we begin with the premise that any stream that that emits start must eventually emit an end event. It is only with
these combiner rules that we can guarantee that every combined stream that emits a start event also emits an end
event. Because of the particulars of how batching occurs in Samsara, we couldn't use an established stream library like Bacon.js or Rx.js.
Besides Transitionables and inputs, Samsara comes with several other streams that are particularly useful.
An Accumulator is a stream that sums the values it subscribes from. This is often useful
for summing up deltas from other streams to get a total value.
var accumulator = new Accumulator(0);
var mouse = new MouseInput();
accumulator.subscribe(mouse.pluck('delta'));
accumulator.on('update', function(value){
console.log(value);
});
A Differential is a stream that returns the deltas of the streams it subscribes from. This is useful for
getting deltas from a stream that doesn't normally supply them (like a Transitionable).
var differential = new Differential();
var transitionable = new Transitionable(0);
differentiable.subscribe(transitionable);
transitionable.set(100, {duration : 1000});
differentiable.on('update', function(delta){
console.log(delta);
});
Let's have a Surface that can be dragged by the mouse, but springs back to
its original position when the user lets go of the mouse. Since the position of the Surface
is the combination of two sources - the mouse and spring transition - the easiest way to program
this is for the position to be an accumulator of the mouse and spring deltas. MouseInput emits
deltas by default, but a Transitionable must be fed into a Differential to get the deltas. The
following code demonstrates this.
See the Pen transitionable-map-physics by SamsaraJS (@samsaraJS) on CodePen.