redshift
Version:
A JavaScript UX framework. Handles animation, UI physics and user input tracking.
516 lines (398 loc) • 12.7 kB
JavaScript
"use strict";
var parseArgs = require('./parse-args.js'),
Value = require('../types/value.js'),
Queue = require('./queue.js'),
Process = require('../process/process.js'),
processor = require('./processor.js'),
routes = require('./routes.js'),
defaultProps = require('../defaults/action-props.js'),
defaultState = require('../defaults/action-state.js'),
utils = require('../utils/utils.js'),
styler = require('../routes/css/styler.js'),
namespace = function (key, space) {
return (space && space !== routes.defaultRoute) ? key + '.' + space : key;
},
Action = function () {
var self = this;
// Create value repo
self.values = {};
this.scope = this;
self.setProp(defaultState);
self.resetProps();
// Register process wth cycl
self.process = new Process(function (framestamp, frameDuration) {
if (self.isActive()) {
processor(self, framestamp, frameDuration);
}
});
self.queue = new Queue();
self.output = {};
self.set(parseArgs.generic.apply(self, arguments));
};
Action.prototype = {
/*
Play the provided actions as animations
Syntax
.play(playlist, [override])
@param [string]: Playlist of presets
@param [object]: (optional) Override object
.play(params)
@param [object]: Action properties
.play(params, [duration, easing, onEnd])
@param [object]: Action props
@param [number]: Duration in ms
@param [string]: Easing function to apply
@param [function]: Function to run on end
@return [Action]
*/
play: function () {
var props = parseArgs.play.apply(this, arguments);
if (!this.isActive()) {
this.set(props, 'to');
this.start('play');
} else {
this.queue.add.apply(this.queue, arguments);
}
return this;
},
/*
Set Action values and properties
Syntax
.set(params)
@param [object]: Action properties
@return [Action]
*/
set: function (props, defaultProp) {
var self = this;
// Reset properties to defaults
this.resetProps();
// Remove current values from order list
this.clearOrder();
// Update current properties
this.setProp(props);
// Set default property to current if it isn't set
defaultProp = defaultProp || 'current';
// Loop over values and update
routes.shard(function (route, routeValues) {
var preprocessedValues = {},
valueBase = {},
value,
base = {
route: route.name
};
for (var key in routeValues) {
if (routeValues.hasOwnProperty(key)) {
value = routeValues[key];
if (!utils.isObj(value)) {
valueBase = { name: key };
valueBase[defaultProp] = value;
} else {
valueBase = value;
valueBase.name = key;
}
valueBase = utils.merge(base, valueBase);
// If no preprocess step, assign directly
if (!route.preprocess) {
self.setValue(key, valueBase, props, route.name, true);
// Else preprocess and add each returned value
} else {
preprocessedValues = route.preprocess(key, valueBase, self, props);
for (var subKey in preprocessedValues) {
self.setValue(subKey, preprocessedValues[subKey], props, route.name, true);
}
}
}
}
}, props);
self.resetOrigins();
return self;
},
/*
Loop through all values and create origin points
*/
resetOrigins: function () {
var values = this.values,
key = '';
for (key in values) {
if (values.hasOwnProperty(key)) {
values[key].origin = values[key].current;
}
}
},
/*
Start Action
@param [string]: Name of processing type to use
@return [Action]
*/
start: function (processType) {
var input = this.input;
this.resetProgress();
if (processType) {
this.rubix = processType;
}
if (processType !== 'track' && input && input.stop) {
input.stop();
}
this.activate();
return this;
},
/*
Stop current Action process
*/
stop: function () {
this.queue.clear();
this.pause();
return this;
},
/*
Pause current Action
*/
pause: function () {
var self = this,
input = this.input;
self.isActive(false);
self.process.stop();
if (input && input.stop) {
input.stop();
}
return self;
},
/*
Move playhead to a specific location
@param [number]: 0-1
*/
seek: function (seekTo) {
this.elapsed = this.duration * seekTo;
if (!this.isActive()) {
this.rubix = 'seek';
this.activate();
}
return this;
},
activate: function () {
this.isActive(true);
this.started = utils.currentTime() + this.delay;
this.framestamp = this.started;
this.firstFrame = true;
this.process.start();
},
/*
Resume a paused Action
*/
resume: function () {
var self = this;
self.started = utils.currentTime();
self.framestamp = self.started;
self.isActive(true);
self.process.start();
return self;
},
/*
Reset Action progress and values
*/
reset: function () {
var self = this,
values = self.values;
self.resetProgress();
for (var key in values) {
values[key].reset();
}
return self;
},
/*
Reset Action progress
*/
resetProgress: function () {
this.elapsed = (this.playDirection === 1) ? 0 : this.duration;
this.started = utils.currentTime();
return this;
},
/*
Reverse Action progress and values
*/
reverse: function () {
var values = this.values;
this.playDirection = this.playDirection * -1;
for (var key in values) {
if (values.hasOwnProperty(key)) {
values[key].retarget();
}
}
return this;
},
/*
Swap value origins and to
*/
flipValues: function () {
var values = this.values;
this.elapsed = this.duration - this.elapsed;
for (var key in values) {
values[key].flip();
}
return this;
},
toggle: function () {
if (this.isActive()) {
this.pause();
} else {
this.resume();
}
return this;
},
/*
Check for next steps and perform, stop if not
*/
next: function () {
var self = this,
nexts = [{
key: 'loop',
callback: self.reset
}, {
key: 'yoyo',
callback: self.reverse
}, {
key: 'flip',
callback: self.flipValues
}],
possibles = nexts.length,
hasNext = false;
for (var i = 0; i < possibles; ++i) {
if (self.checkNextStep(nexts[i].key, nexts[i].callback)) {
hasNext = true;
break;
}
}
if (!hasNext && !self.playNext()) {
self.stop();
} else {
self.isActive(true);
}
return self;
},
/*
Check next step
@param [string]: Name of step ('yoyo' or 'loop')
@param [callback]: Function to run if we take this step
*/
checkNextStep: function (key, callback) {
var COUNT = 'Count',
stepTaken = false,
step = this[key],
count = this[key + COUNT],
forever = (step === true);
if (forever || utils.isNum(step)) {
++count;
this[key + COUNT] = count;
if (forever || count <= step) {
callback.call(this);
stepTaken = true;
}
}
return stepTaken;
},
/*
Next in playlist
*/
playNext: function () {
var stepTaken = false,
nextInQueue = this.queue.next(this.playDirection);
if (utils.isArray(nextInQueue)) {
this.set(parseArgs.generic.apply(this, nextInQueue), 'to')
.reset();
stepTaken = true;
}
return stepTaken;
},
setValue: function (key, value, inherit, space, reset) {
var existing = this.getValue(key, space);
key = namespace(key, space);
// Update if value exists
if (existing) {
// Overwrite with defaults
if (reset) {
existing.resetProps();
}
existing.set(value, inherit);
// Or create new if it doesn't
} else {
this.values[key] = new Value(key, value, inherit, this);
}
return this;
},
getValue: function (key, space) {
key = namespace(key, space);
return this.values[key];
},
setProp: function (data, prop) {
var multiArg = (arguments.length > 1),
defaultRoute = routes.getName(),
toSet = multiArg ? {} : data,
key = '';
// If this is a key/value setter, add to toSet
if (multiArg) {
toSet[data] = prop;
}
// Loop over toSet and assign to our data store
for (key in toSet) {
if (toSet.hasOwnProperty(key) && key != defaultRoute) {
this[key] = toSet[key];
}
}
return this;
},
resetProps: function () {
this.setProp(defaultProps);
return this;
},
/*
Is Action active?
@param [boolean] (optional): If provided, will set action to active/inactive
@return [boolean]: Active status
*/
isActive: function (active) {
var isActive = (active !== undefined) ? active : this.active;
if (active === true) {
this.hasChanged = active;
}
this.active = isActive;
return isActive;
},
/*
Update order of value keys
@param [string]: Key of value
@param [boolean]: Whether to move value to back
@param [string] (optional): Name of order array (if not default)
*/
updateOrder: function (key, moveToBack, orderName) {
var pos, order;
orderName = orderName || 'order';
order = this[orderName] = this[orderName] || [];
pos = order.indexOf(key);
if (pos === -1 || moveToBack) {
order.push(key);
if (pos !== -1) {
order.splice(pos, 1);
}
}
},
clearOrder: function () {
this.order = [];
},
/*
Style our dom element
Becomes get if props is string, set if object
*/
style: function (name, props) {
var elementIsDefined = (arguments.length === 2),
dom,
returnVal;
props = elementIsDefined ? props : name;
name = elementIsDefined ? name : 'dom';
dom = this[name];
if (dom) {
returnVal = styler(dom, props);
}
return (returnVal === false) ? this : returnVal;
}
};
module.exports = Action;