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 update
start
, the combined stream emits start
end
, the combined stream emits end
start
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.