wilderness-core
Version:
The SVG animation engine behind Wilderness
795 lines (664 loc) • 25.1 kB
JavaScript
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
/* globals __DEV__ */
import clone from './clone';
import config from './config';
import { input } from './middleware';
import { event } from './events';
/**
* The position of an object on a Timeline
* where 0 is Timeline start and 1 is Timeline finish.
*
* @typedef {Object} TimelinePosition
*
* @property {Position} start
* @property {Position} finish
*/
/**
* A Shape positioned on a Timeline.
*
* @typedef {Object} TimelineShape
*
* @property {Shape} shape
* @property {TimelinePosition} timelinePosition
*/
/**
* The position of an object on a Timeline in milliseconds.
*
* @typedef {Object} MsTimelinePosition
*
* @property {number} start.
* @property {number} finish.
*/
/**
* A Shape positioned on a Timeline (position set in milliseconds).
*
* @typedef {Object} MsTimelineShape
*
* @property {Shape} shape
* @property {MsTimelinePosition} timelinePosition
*/
/**
* A TimelineShape array and their total duration.
*
* @typedef {Object} TimelineShapesAndDuration
*
* @property {TimelineShape[]} timelineShapes
* @property {number} duration
*/
/**
* The options required to calculate the current playback Position.
*
* @typedef {Object} PlaybackOptions
*
* @property {boolean} alternate - Should the next iteration reverse current direction?
* @property {number} duration - Milliseconds that each iteration lasts.
* @property {number} initialIterations - The starting number of iterations.
* @property {number} iterations - The number of playback interations (additional to initialIterations).
* @property {boolean} reverse - Should the first iteration start in a reverse direction?
* @property {number} [started] - The UNIX timestamp of playback start.
*/
/**
* PlaybackOptions and tween middleware.
*
* @typedef {Object} TimelineOptions
*
* @extends PlaybackOptions
* @property {Middleware[]} middleware
*/
/**
* A Shape and timeline related options.
*
* @typedef {Object} ShapeWithOptions
*
* @property {(string|number)} [after] - The name of the Shape to queue after (in sequence).
* @property {(string|number)} [at] - The name of the Shape to queue at (in parallel).
* @property {(string|number)} name - A unique reference.
* @property {number} offset - The offset in milliseconds to adjust the queuing of this shape.
* @property {Shape} shape
*/
/**
* An object containing Middlware, PlaybackOptions and ShapesWithOptions.
*
* @typedef {Object} SortedTimelineProps
*
* @property {Middleware[]} middleware
* @property {PlaybackOptions} playbackOptions
* @property {ShapeWithOptions[]} shapesWithOptions
*/
/**
* A sequence of Shapes.
*
* @typedef {Object} Timeline
*
* @property {Middleware[]} middleware
* @property {PlaybackOptions} playbackOptions
* @property {Object} state - Holds the last known state of the timeline.
* @property {TimelineShape[]} timelineShapes
*/
/**
* Runs each Middleware input function on every Keyframe's FrameShape.
*
* @param {Shape} shape
* @param {Middleware[]} middleware
*
* @example
* apply(shape, middleware)
*/
var apply = function apply(_ref, middleware) {
var keyframes = _ref.keyframes;
for (var i = 0, l = keyframes.length; i < l; i++) {
var keyframe = keyframes[i];
keyframe.frameShape = input(keyframe.frameShape, middleware);
}
};
/**
* Is playback currently in reverse?
*
* @param {PlaybackOptions} playbackOptions
* @param {number} complete - The number of iterations complete.
*
* @example
* currentReverse(playbackOptions, complete)
*/
var currentReverse = function currentReverse(playbackOptions, complete) {
var reverse = playbackOptions.reverse;
if (complete === 0) {
return reverse;
}
var alternate = playbackOptions.alternate;
var initialIterations = playbackOptions.initialIterations;
var initialReverse = sameDirection(alternate, initialIterations) ? reverse : !reverse;
return sameDirection(alternate, initialIterations + complete) ? initialReverse : !initialReverse;
};
/**
* The number of iterations a Timeline has completed.
*
* @param {PlaybackOptions} playbackOptions
* @param {number} opts.at
*
* @returns {number}
*
* @example
* iterationsComplete(playbackOptions, 1000)
*/
var iterationsComplete = function iterationsComplete(playbackOptions, at) {
var duration = playbackOptions.duration;
var iterations = playbackOptions.iterations;
var started = playbackOptions.started;
if (typeof started === 'undefined' || at <= started) {
return 0;
}
var ms = at - started;
var maxDuration = duration * iterations;
if (ms >= maxDuration) {
return iterations;
}
return ms / duration;
};
/**
* Stops playback of a Timeline.
*
* @param {Timeline} timeline
* @param {PlaybackOptions} playbackOptions
* @param {number} [at]
*
* @example
* pause(timeline)
*/
var pause = function pause(timeline) {
var playbackOptions = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var at = arguments[2];
timeline.playbackOptions = updatePlaybackOptions({ at: at, timeline: timeline, pause: true, playbackOptions: playbackOptions });
updateState(timeline, at);
};
/**
* Starts playback of a Timeline.
*
* @param {Timeline} timeline
* @param {PlaybackOptions} playbackOptions
* @param {number} [at]
*
* @example
* play(timeline, { initialIterations: 0 })
*/
var play = function play(timeline) {
var playbackOptions = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var at = arguments[2];
timeline.playbackOptions = updatePlaybackOptions({ at: at, timeline: timeline, playbackOptions: playbackOptions });
updateState(timeline, at);
};
/**
* Calculate the Timeline Position.
*
* @param {number} totalIterations - initialIterations + iterationsComplete.
* @param {boolean} reverse - Is the Timeline currently in reverse?
*
* @returns {Position}
*
* @example
* position(5.43, true)
*/
var position = function position(totalIterations, reverse) {
var i = totalIterations >= 1 && totalIterations % 1 === 0 ? 1 : totalIterations % 1;
return reverse ? 1 - i : i;
};
/**
* Is the direction same as initial direction?
*
* @param {boolean} alternate - Is iteration direction alternating?
* @param {number} iterations - The number of iterations complete.
*
* @return {boolean}
*
* @example
* sameDirection(true, 3.25)
*/
var sameDirection = function sameDirection(alternate, iterations) {
var x = iterations % 2;
return !alternate || iterations === 0 || x <= 1 && x % 2 > 0;
};
/**
* Calculate the start position of a Shape on the Timeline.
*
* @param {Object} props
* @param {(string|number)} [props.after]
* @param {(string|number)} [props.at]
* @param {MsTimelineShape[]} props.msTimelineShapes
* @param {number} props.offset
* @param {number} props.timelineFinish - The current finish of the timeline.
*
* @returns {number}
*
* @example
* shapeStart({ 'foo', msTimelineShapes, 200, 2000 })
*/
var shapeStart = function shapeStart(_ref2) {
var after = _ref2.after,
at = _ref2.at,
msTimelineShapes = _ref2.msTimelineShapes,
offset = _ref2.offset,
timelineFinish = _ref2.timelineFinish;
if (typeof after !== 'undefined' || typeof at !== 'undefined') {
var reference = typeof after !== 'undefined' ? after : at;
for (var i = 0; i < msTimelineShapes.length; i++) {
var s = msTimelineShapes[i];
if (reference === s.shape.name) {
return (typeof at !== 'undefined' ? s.timelinePosition.start : s.timelinePosition.finish) + offset;
}
}
for (var _i = 0; _i < msTimelineShapes.length; _i++) {
var _s = msTimelineShapes[_i];
for (var j = 0; j < _s.shape.keyframes.length; j++) {
var keyframe = _s.shape.keyframes[j];
if (reference === keyframe.name) {
return _s.timelinePosition.start + _s.shape.duration * keyframe.position + offset;
}
}
}
if (process.env.NODE_ENV !== 'production') {
throw new Error('No Shape or Keyframe matching name \'' + reference + '\'');
}
}
return timelineFinish + offset;
};
/**
* Create a ShapeWithOptions from an array.
*
* @param {Object[]} arr
* @param {Shape} arr.0
* @param {Object} arr.1
*
* @returns {ShapeWithOptions}
*
* @example
* shapeWithOptionsFromArray(arr, i)
*/
var shapeWithOptionsFromArray = function shapeWithOptionsFromArray(_ref3, i) {
var _ref4 = _slicedToArray(_ref3, 2),
shape = _ref4[0],
options = _ref4[1];
if (process.env.NODE_ENV !== 'production' && ((typeof shape === 'undefined' ? 'undefined' : _typeof(shape)) !== 'object' || !shape.keyframes)) {
throw new TypeError('When an array is passed to the timeline function the first item must be a Shape');
}
if (process.env.NODE_ENV !== 'production' && (typeof options === 'undefined' ? 'undefined' : _typeof(options)) !== 'object') {
throw new TypeError('When an array is passed to the timeline function the second item must be an object');
}
var _options$name = options.name,
name = _options$name === undefined ? i : _options$name,
_options$queue = options.queue,
queue = _options$queue === undefined ? config.defaults.timeline.queue : _options$queue;
if (process.env.NODE_ENV !== 'production' && typeof name !== 'string' && typeof name !== 'number') {
throw new TypeError('The name prop must be of type string or number');
}
if ((typeof queue === 'undefined' ? 'undefined' : _typeof(queue)) === 'object' && !Array.isArray(queue) && queue !== null) {
var after = queue.after,
at = queue.at,
_queue$offset = queue.offset,
offset = _queue$offset === undefined ? 0 : _queue$offset;
if (process.env.NODE_ENV !== 'production' && typeof offset !== 'undefined' && typeof offset !== 'number') {
throw new TypeError('The queue.offset prop must be of type number');
}
if (process.env.NODE_ENV !== 'production' && typeof at !== 'undefined' && typeof after !== 'undefined') {
throw new TypeError('You cannot pass both queue.at and queue.after props');
}
if (process.env.NODE_ENV !== 'production' && typeof at !== 'undefined' && typeof at !== 'string' && typeof at !== 'number') {
throw new TypeError('The queue.at prop must be of type string or number');
}
if (process.env.NODE_ENV !== 'production' && typeof after !== 'undefined' && typeof after !== 'string' && typeof after !== 'number') {
throw new TypeError('The queue.after prop must be of type string or number');
}
if (typeof at !== 'undefined') {
return { at: at, name: name, offset: offset, shape: shape };
}
if (typeof after !== 'undefined') {
return { after: after, name: name, offset: offset, shape: shape };
}
return { name: name, offset: offset, shape: shape };
} else if (typeof queue === 'number') {
return { name: name, offset: queue, shape: shape };
} else if (typeof queue === 'string') {
return { after: queue, name: name, offset: 0, shape: shape };
}
if (process.env.NODE_ENV !== 'production') {
throw new TypeError('The queue prop must be of type number, string or object');
}
return { name: name, offset: 0, shape: shape };
};
/**
* Sorts an array of Shapes, ShapesWithOptions and TimelineOptions.
*
* @param {(Shape|Object[]|TimelineOptions)[]} props
*
* @returns {SortedTimelineProps}
*
* @example
* sort(props)
*/
var sort = function sort(props) {
if (process.env.NODE_ENV !== 'production' && props.length === 0) {
throw new TypeError('The timeline function must be passed at least one Shape');
}
var options = {};
var shapesWithOptions = [];
for (var i = 0, l = props.length; i < l; i++) {
var prop = props[i];
if (Array.isArray(prop)) {
shapesWithOptions.push(shapeWithOptionsFromArray(prop, i));
} else {
if (process.env.NODE_ENV !== 'production' && (typeof prop === 'undefined' ? 'undefined' : _typeof(prop)) !== 'object') {
throw new TypeError('The timeline function must only be passed objects and arrays');
}
if (prop.keyframes) {
shapesWithOptions.push({
name: i,
offset: config.defaults.timeline.queue,
shape: prop
});
} else {
if (process.env.NODE_ENV !== 'production') {
if (i === 0) {
throw new TypeError('The timeline function must receive a Shape as the first argument');
} else if (i !== props.length - 1) {
throw new TypeError('The timeline function must receive options as the final argument');
}
}
options = clone(prop);
}
}
}
return {
middleware: validMiddleware(options),
playbackOptions: validPlaybackOptions(options),
shapesWithOptions: shapesWithOptions
};
};
/**
* Creates a Timeline from one or more Shape.
* Optionally can take an options object as the last argument,
* as well as options for each Shape if passed in as an array.
*
* @param {...(Shape|Object[]|TimelineOptions)} props
*
* @returns {Timeline}
*
* @example
* timeline(circle, [ square, { queue: -200 } ], { duration: 5000 })
*/
var timeline = function timeline() {
for (var _len = arguments.length, props = Array(_len), _key = 0; _key < _len; _key++) {
props[_key] = arguments[_key];
}
var _sort = sort(props),
middleware = _sort.middleware,
playbackOptions = _sort.playbackOptions,
shapesWithOptions = _sort.shapesWithOptions;
var _timelineShapesAndDur = timelineShapesAndDuration(shapesWithOptions, middleware),
duration = _timelineShapesAndDur.duration,
timelineShapes = _timelineShapesAndDur.timelineShapes;
if (typeof playbackOptions.duration === 'undefined') {
playbackOptions.duration = duration;
}
var t = { middleware: middleware, playbackOptions: playbackOptions, state: {}, timelineShapes: timelineShapes };
for (var i = 0, l = timelineShapes.length; i < l; i++) {
var shape = timelineShapes[i].shape;
shape.timeline = t;
shape.timelineIndex = i;
}
updateState(t);
t.event = event(t);
return t;
};
/**
* Converts a set of MsTimelineShapes to a set of TimelineShapes
* given the Timeline start and total duration values.
*
* @param {Object} props
* @param {number} props.duration
* @param {msTimelineShape[]} props.msTimelineShapes
* @param {number} props.start
*
* @returns {TimelineShape[]}
*
* @example
* timelineShapes()
*/
var timelineShapes = function timelineShapes(_ref5) {
var duration = _ref5.duration,
msTimelineShapes = _ref5.msTimelineShapes,
start = _ref5.start;
var s = [];
for (var i = 0, l = msTimelineShapes.length; i < l; i++) {
var msTimelineShape = msTimelineShapes[i];
var timelinePosition = msTimelineShape.timelinePosition;
s.push({
shape: msTimelineShape.shape,
timelinePosition: {
start: (timelinePosition.start - start) / duration,
finish: (timelinePosition.finish - start) / duration
}
});
}
return s;
};
/**
* Converts an array of ShapesWithOptions into TimelineShapes
* and their total duration.
*
* @param {ShapeWithOptions[]} shapesWithOptions
* @param {Middleware[]} middleware
*
* @returns {TimelineShapesAndDuration}
*
* @example
* timelineShapes(shapesWithOptions)
*/
var timelineShapesAndDuration = function timelineShapesAndDuration(shapesWithOptions, middleware) {
var timelineStart = 0;
var timelineFinish = 0;
var msTimelineShapes = [];
for (var i = 0, l = shapesWithOptions.length; i < l; i++) {
var _shapesWithOptions$i = shapesWithOptions[i],
after = _shapesWithOptions$i.after,
at = _shapesWithOptions$i.at,
name = _shapesWithOptions$i.name,
offset = _shapesWithOptions$i.offset,
shape = _shapesWithOptions$i.shape;
if (process.env.NODE_ENV !== 'production' && typeof shape.timeline !== 'undefined') {
throw new Error('A Shape can only be added to one timeline');
}
shape.name = name;
apply(shape, middleware);
var start = shapeStart({
after: after,
at: at,
msTimelineShapes: msTimelineShapes,
offset: offset,
timelineFinish: timelineFinish
});
var finish = start + shape.duration;
timelineStart = Math.min(timelineStart, start);
timelineFinish = Math.max(timelineFinish, finish);
msTimelineShapes.push({ shape: shape, timelinePosition: { start: start, finish: finish } });
}
var timelineDuration = Math.abs(timelineStart - timelineFinish);
return {
duration: timelineDuration,
timelineShapes: timelineShapes({
duration: timelineDuration,
msTimelineShapes: msTimelineShapes,
start: timelineStart
})
};
};
/**
* Updates the PlaybackOptions of a Timeline.
*
* @param {Object} opts
* @param {number} [opts.at]
* @param {PlaybackOptions} opts.playbackOptions
* @param {Timeline} opts.timeline
*
* @example
* updatePlaybackOptions({ timeline, playbackOptions })
*/
var updatePlaybackOptions = function updatePlaybackOptions(_ref6) {
var at = _ref6.at,
_ref6$pause = _ref6.pause,
pause = _ref6$pause === undefined ? false : _ref6$pause,
playbackOptions = _ref6.playbackOptions,
timeline = _ref6.timeline;
if (process.env.NODE_ENV !== 'production' && ((typeof timeline === 'undefined' ? 'undefined' : _typeof(timeline)) !== 'object' || !timeline.timelineShapes || !timeline.playbackOptions)) {
throw new TypeError('The updatePlaybackOptions function must be passed a Timeline');
}
if (process.env.NODE_ENV !== 'production' && typeof at !== 'undefined' && typeof at !== 'number') {
throw new TypeError('The updatePlaybackOptions function at property must be of type number');
}
var previous = timeline.playbackOptions;
var next = validPlaybackOptions(_extends({}, previous, playbackOptions, {
started: typeof at !== 'undefined' ? at : Date.now()
}));
if (typeof playbackOptions.initialIterations !== 'undefined') {
if (typeof playbackOptions.reverse === 'undefined') {
next.reverse = currentReverse(previous, next.initialIterations - previous.initialIterations);
}
if (typeof playbackOptions.iterations === 'undefined' && previous.iterations !== Infinity) {
next.iterations = Math.max(0, previous.initialIterations + previous.iterations - next.initialIterations);
}
} else {
var complete = iterationsComplete(previous, next.started);
var reverse = currentReverse(previous, complete);
next.initialIterations = previous.initialIterations + complete;
if (typeof playbackOptions.iterations === 'undefined') {
next.iterations = previous.iterations - complete;
if (typeof playbackOptions.reverse !== 'undefined' && next.reverse !== previous.reverse && next.iterations !== Infinity) {
var nextIterations = next.initialIterations;
next.initialIterations = next.iterations;
next.iterations = nextIterations;
}
} else {
if (typeof playbackOptions.reverse !== 'undefined' && playbackOptions.reverse !== reverse && next.iterations !== Infinity) {
next.initialIterations = previous.iterations - complete;
}
}
if (typeof playbackOptions.reverse === 'undefined') {
next.reverse = reverse;
} else if (next.iterations === Infinity) {
next.initialIterations = playbackOptions.reverse === reverse ? next.initialIterations % 1 : 1 - next.initialIterations % 1;
}
}
if (pause) {
delete next.started;
}
return next;
};
/**
* Updates the Timeline state.
*
* @param {Timeline} timeline
* @param {number} at
*
* @example
* updateState(timeline, Date.now())
*/
var updateState = function updateState(t, at) {
var playbackOptions = t.playbackOptions;
var state = t.state;
state.started = typeof playbackOptions.started !== 'undefined';
state.iterationsComplete = iterationsComplete(playbackOptions, at);
state.totalIterations = playbackOptions.initialIterations + state.iterationsComplete;
state.reverse = currentReverse(playbackOptions, state.iterationsComplete);
state.finished = playbackOptions.iterations - state.iterationsComplete === 0;
state.position = position(state.totalIterations, state.reverse);
};
/**
* Extracts and validates Middlware from an object.
*
* @param {Object} opts
*
* @returns {Middleware[]}
*
* @example
* validMiddleware(opts)
*/
var validMiddleware = function validMiddleware(_ref7) {
var _ref7$middleware = _ref7.middleware,
middleware = _ref7$middleware === undefined ? config.defaults.timeline.middleware : _ref7$middleware;
if (!Array.isArray(middleware)) {
throw new TypeError('The timeline function middleware option must be of type array');
}
for (var i = 0, l = middleware.length; i < l; i++) {
var _middleware$i = middleware[i],
name = _middleware$i.name,
_input = _middleware$i.input,
output = _middleware$i.output;
if (typeof name !== 'string') {
throw new TypeError('A middleware must have a name prop');
}
if (typeof _input !== 'function') {
throw new TypeError('The ' + name + ' middleware must have an input method');
}
if (typeof output !== 'function') {
throw new TypeError('The ' + name + ' middleware must have an output method');
}
}
return middleware;
};
/**
* Extracts and validates PlaybackOptions from an object.
*
* @param {Object} opts
*
* @returns {PlaybackOptions}
*
* @example
* validPlaybackOptions(opts)
*/
var validPlaybackOptions = function validPlaybackOptions(_ref8) {
var _ref8$alternate = _ref8.alternate,
alternate = _ref8$alternate === undefined ? config.defaults.timeline.alternate : _ref8$alternate,
duration = _ref8.duration,
_ref8$initialIteratio = _ref8.initialIterations,
initialIterations = _ref8$initialIteratio === undefined ? config.defaults.timeline.initialIterations : _ref8$initialIteratio,
_ref8$iterations = _ref8.iterations,
iterations = _ref8$iterations === undefined ? config.defaults.timeline.iterations : _ref8$iterations,
_ref8$reverse = _ref8.reverse,
reverse = _ref8$reverse === undefined ? config.defaults.timeline.reverse : _ref8$reverse,
started = _ref8.started;
var playbackOptions = {};
if (typeof duration !== 'undefined') {
if (process.env.NODE_ENV !== 'production' && (typeof duration !== 'number' || duration < 0)) {
throw new TypeError('The timeline function duration option must be a positive number or zero');
}
playbackOptions.duration = duration;
}
if (process.env.NODE_ENV !== 'production') {
if (typeof alternate !== 'boolean') {
throw new TypeError('The timeline function alternate option must be true or false');
}
if (typeof initialIterations !== 'number' || initialIterations < 0) {
throw new TypeError('The timeline function initialIterations option must be a positive number or zero');
}
if (typeof iterations !== 'number' || iterations < 0) {
throw new TypeError('The timeline function iterations option must be a positive number or zero');
}
if (typeof reverse !== 'boolean') {
throw new TypeError('The timeline function reverse option must be true or false');
}
}
if (typeof started !== 'undefined') {
if (process.env.NODE_ENV !== 'production' && (typeof started !== 'number' || started < 0)) {
throw new TypeError('The timeline function started option must be a positive number or zero');
}
playbackOptions.started = started;
}
return _extends({}, playbackOptions, {
alternate: alternate,
initialIterations: initialIterations,
iterations: iterations,
reverse: reverse
});
};
export { currentReverse, iterationsComplete, pause, play, position, sameDirection, updateState };
export default timeline;