tram
Version:
Cross-browser CSS3 transitions in JavaScript
1,522 lines (1,323 loc) • 48 kB
JavaScript
/*!
* tram.js v0.8.3-global
* Cross-browser CSS3 transitions in JavaScript
* https://github.com/bkwld/tram
* MIT License
*/
window.tram = (function (jQuery) {
/*!
* P.js
* A lightweight class system. It's just prototypes!
* http:// github.com/jayferd/pjs
* MIT license
*/
var P = (function(prototype, ownProperty, undefined) {
// helper functions that also help minification
function isObject(o) { return typeof o === 'object'; }
function isFunction(f) { return typeof f === 'function'; }
// a function that gets reused to make uninitialized objects
function BareConstructor() {}
function P(_superclass /* = Object */, definition) {
// handle the case where no superclass is given
if (definition === undefined) {
definition = _superclass;
_superclass = Object;
}
// C is the class to be returned.
//
// It delegates to instantiating an instance of `Bare`, so that it
// will always return a new instance regardless of the calling
// context.
//
// TODO: the Chrome inspector shows all created objects as `C`
// rather than `Object`. Setting the .name property seems to
// have no effect. Is there a way to override this behavior?
function C() {
var self = new Bare;
if (isFunction(self.init)) self.init.apply(self, arguments);
return self;
}
// C.Bare is a class with a noop constructor. Its prototype is the
// same as C, so that instances of C.Bare are also instances of C.
// New objects can be allocated without initialization by calling
// `new MyClass.Bare`.
function Bare() {}
C.Bare = Bare;
// Set up the prototype of the new class.
var _super = BareConstructor[prototype] = _superclass[prototype];
var proto = Bare[prototype] = C[prototype] = new BareConstructor;
// other variables, as a minifier optimization
var extensions;
// set the constructor property on the prototype, for convenience
proto.constructor = C;
C.mixin = function(def) {
Bare[prototype] = C[prototype] = P(C, def)[prototype];
return C;
};
C.open = function(def) {
extensions = {};
if (isFunction(def)) {
// call the defining function with all the arguments you need
// extensions captures the return value.
extensions = def.call(C, proto, _super, C, _superclass);
}
else if (isObject(def)) {
// if you passed an object instead, we'll take it
extensions = def;
}
// ...and extend it
if (isObject(extensions)) {
for (var ext in extensions) {
if (ownProperty.call(extensions, ext)) {
proto[ext] = extensions[ext];
}
}
}
// if there's no init, we assume we're inheriting a non-pjs class, so
// we default to applying the superclass's constructor.
if (!isFunction(proto.init)) {
proto.init = _superclass;
}
return C;
};
return C.open(definition);
}
// ship it
return P;
// as a minifier optimization, we've closured in a few helper functions
// and the string 'prototype' (C[p] is much shorter than C.prototype)
})('prototype', ({}).hasOwnProperty);
// --------------------------------------------------
// Easing methods { id: [ cssString, jsFunction ] }
var easing = {
// --------------------------------------------------
// CSS easings, converted to functions using Timothee Groleau's generator.
// http://www.timotheegroleau.com/Flash/experiments/easing_function_generator.htm
'ease': ['ease', function (t, b, c, d) {
var ts=(t /= d)*t;
var tc=ts*t;
return b+c*(-2.75*tc*ts + 11*ts*ts + -15.5*tc + 8*ts + 0.25*t);
}]
, 'ease-in': ['ease-in', function (t, b, c, d) {
var ts=(t /= d)*t;
var tc=ts*t;
return b+c*(-1*tc*ts + 3*ts*ts + -3*tc + 2*ts);
}]
, 'ease-out': ['ease-out', function (t, b, c, d) {
var ts=(t /= d)*t;
var tc=ts*t;
return b+c*(0.3*tc*ts + -1.6*ts*ts + 2.2*tc + -1.8*ts + 1.9*t);
}]
, 'ease-in-out': ['ease-in-out', function (t, b, c, d) {
var ts=(t /= d)*t;
var tc=ts*t;
return b+c*(2*tc*ts + -5*ts*ts + 2*tc + 2*ts);
}]
// --------------------------------------------------
// Robert Penner easing equations and their CSS equivalents.
// http://www.robertpenner.com/easing_terms_of_use.html
, 'linear': ['linear', function (t, b, c, d) {
return c*t/d + b;
}]
// Quad
, 'ease-in-quad':
['cubic-bezier(0.550, 0.085, 0.680, 0.530)', function (t, b, c, d) {
return c*(t /= d)*t + b;
}]
, 'ease-out-quad':
['cubic-bezier(0.250, 0.460, 0.450, 0.940)', function (t, b, c, d) {
return -c *(t /= d)*(t-2) + b;
}]
, 'ease-in-out-quad':
['cubic-bezier(0.455, 0.030, 0.515, 0.955)', function (t, b, c, d) {
if ((t /= d/2) < 1) return c/2*t*t + b;
return -c/2 * ((--t)*(t-2) - 1) + b;
}]
// Cubic
, 'ease-in-cubic':
['cubic-bezier(0.550, 0.055, 0.675, 0.190)', function (t, b, c, d) {
return c*(t /= d)*t*t + b;
}]
, 'ease-out-cubic':
['cubic-bezier(0.215, 0.610, 0.355, 1)', function (t, b, c, d) {
return c*((t=t/d-1)*t*t + 1) + b;
}]
, 'ease-in-out-cubic':
['cubic-bezier(0.645, 0.045, 0.355, 1)', function (t, b, c, d) {
if ((t /= d/2) < 1) return c/2*t*t*t + b;
return c/2*((t-=2)*t*t + 2) + b;
}]
// Quart
, 'ease-in-quart':
['cubic-bezier(0.895, 0.030, 0.685, 0.220)', function (t, b, c, d) {
return c*(t /= d)*t*t*t + b;
}]
, 'ease-out-quart':
['cubic-bezier(0.165, 0.840, 0.440, 1)', function (t, b, c, d) {
return -c * ((t=t/d-1)*t*t*t - 1) + b;
}]
, 'ease-in-out-quart':
['cubic-bezier(0.770, 0, 0.175, 1)', function (t, b, c, d) {
if ((t /= d/2) < 1) return c/2*t*t*t*t + b;
return -c/2 * ((t-=2)*t*t*t - 2) + b;
}]
// Quint
, 'ease-in-quint':
['cubic-bezier(0.755, 0.050, 0.855, 0.060)', function (t, b, c, d) {
return c*(t /= d)*t*t*t*t + b;
}]
, 'ease-out-quint':
['cubic-bezier(0.230, 1, 0.320, 1)', function (t, b, c, d) {
return c*((t=t/d-1)*t*t*t*t + 1) + b;
}]
, 'ease-in-out-quint':
['cubic-bezier(0.860, 0, 0.070, 1)', function (t, b, c, d) {
if ((t /= d/2) < 1) return c/2*t*t*t*t*t + b;
return c/2*((t-=2)*t*t*t*t + 2) + b;
}]
// Sine
, 'ease-in-sine':
['cubic-bezier(0.470, 0, 0.745, 0.715)', function (t, b, c, d) {
return -c * Math.cos(t/d * (Math.PI/2)) + c + b;
}]
, 'ease-out-sine':
['cubic-bezier(0.390, 0.575, 0.565, 1)', function (t, b, c, d) {
return c * Math.sin(t/d * (Math.PI/2)) + b;
}]
, 'ease-in-out-sine':
['cubic-bezier(0.445, 0.050, 0.550, 0.950)', function (t, b, c, d) {
return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b;
}]
// Expo
, 'ease-in-expo':
['cubic-bezier(0.950, 0.050, 0.795, 0.035)', function (t, b, c, d) {
return (t === 0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b;
}]
, 'ease-out-expo':
['cubic-bezier(0.190, 1, 0.220, 1)', function (t, b, c, d) {
return (t === d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
}]
, 'ease-in-out-expo':
['cubic-bezier(1, 0, 0, 1)', function (t, b, c, d) {
if (t === 0) return b;
if (t === d) return b+c;
if ((t /= d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b;
return c/2 * (-Math.pow(2, -10 * --t) + 2) + b;
}]
// Circ
, 'ease-in-circ':
['cubic-bezier(0.600, 0.040, 0.980, 0.335)', function (t, b, c, d) {
return -c * (Math.sqrt(1 - (t /= d)*t) - 1) + b;
}]
, 'ease-out-circ':
['cubic-bezier(0.075, 0.820, 0.165, 1)', function (t, b, c, d) {
return c * Math.sqrt(1 - (t=t/d-1)*t) + b;
}]
, 'ease-in-out-circ':
['cubic-bezier(0.785, 0.135, 0.150, 0.860)', function (t, b, c, d) {
if ((t /= d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b;
return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b;
}]
// Back
, 'ease-in-back':
['cubic-bezier(0.600, -0.280, 0.735, 0.045)', function (t, b, c, d, s) {
if (s === undefined) s = 1.70158;
return c*(t /= d)*t*((s+1)*t - s) + b;
}]
, 'ease-out-back':
['cubic-bezier(0.175, 0.885, 0.320, 1.275)', function (t, b, c, d, s) {
if (s === undefined) s = 1.70158;
return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
}]
, 'ease-in-out-back':
['cubic-bezier(0.680, -0.550, 0.265, 1.550)', function (t, b, c, d, s) {
if (s === undefined) s = 1.70158;
if ((t /= d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
}]
};
// Clamp cubic-bezier values for webkit bug
// https://bugs.webkit.org/show_bug.cgi?id=45761
var clamped = {
'ease-in-back': 'cubic-bezier(0.600, 0, 0.735, 0.045)'
, 'ease-out-back': 'cubic-bezier(0.175, 0.885, 0.320, 1)'
, 'ease-in-out-back': 'cubic-bezier(0.680, 0, 0.265, 1)'
};
// --------------------------------------------------
// Private vars
/*global jQuery, P, easing, clamped */
var doc = document
, win = window
, dataKey = 'bkwld-tram'
, unitRegex = /[\-\.0-9]/g
, capsRegex = /[A-Z]/
, typeNumber = 'number'
, typeColor = /^(rgb|#)/
, typeLength = /(em|cm|mm|in|pt|pc|px)$/
, typeLenPerc = /(em|cm|mm|in|pt|pc|px|%)$/
, typeAngle = /(deg|rad|turn)$/
, typeFancy = 'unitless'
, emptyTrans = /(all|none) 0s ease 0s/
, allowAuto = /^(width|height)$/
, space = ' '
;
// --------------------------------------------------
// Private functions
// Simple feature detect, returns both dom + css prefixed names
var testDiv = doc.createElement('a')
, domPrefixes = ['Webkit', 'Moz', 'O', 'ms']
, cssPrefixes = ['-webkit-', '-moz-', '-o-', '-ms-']
;
var testFeature = function (prop) {
// unprefixed case
if (prop in testDiv.style) return { dom: prop, css: prop };
// test all prefixes
var i, domProp, domSuffix = '', words = prop.split('-');
for (i = 0; i < words.length; i++) {
domSuffix += words[i].charAt(0).toUpperCase() + words[i].slice(1);
}
for (i = 0; i < domPrefixes.length; i++) {
domProp = domPrefixes[i] + domSuffix;
if (domProp in testDiv.style) return { dom: domProp, css: cssPrefixes[i] + prop };
}
};
// Run feature tests
var support = tram.support = {
bind: Function.prototype.bind
, transform: testFeature('transform')
, transition: testFeature('transition')
, backface: testFeature('backface-visibility')
, timing: testFeature('transition-timing-function')
};
// Modify easing variants for webkit clamp bug
if (support.transition) {
var timingProp = support.timing.dom;
testDiv.style[timingProp] = easing['ease-in-back'][0];
if (!testDiv.style[timingProp]) {
// style invalid, use clamped versions instead
for (var x in clamped) easing[x][0] = clamped[x];
}
}
// Animation timer shim with setTimeout fallback
var enterFrame = tram.frame = function () {
var raf = win.requestAnimationFrame ||
win.webkitRequestAnimationFrame ||
win.mozRequestAnimationFrame ||
win.oRequestAnimationFrame ||
win.msRequestAnimationFrame;
if (raf && support.bind) return raf.bind(win);
return function (callback) {
win.setTimeout(callback, 16);
};
}();
// Timestamp shim with fallback
var timeNow = tram.now = function () {
// use high-res timer if available
var perf = win.performance,
perfNow = perf && (perf.now || perf.webkitNow || perf.msNow || perf.mozNow);
if (perfNow && support.bind) return perfNow.bind(perf);
// fallback to epoch-based timestamp
return Date.now || function () {
return +(new Date);
};
}();
// --------------------------------------------------
// Transition class - public API returned from the tram() wrapper.
var Transition = P(function(proto) {
proto.init = function (el) {
this.$el = jQuery(el);
this.el = this.$el[0];
this.props = {};
this.queue = [];
this.style = '';
this.active = false;
// store inherited transitions from css styles
if (config.keepInherited && !config.fallback) {
var upstream = getStyle(this.el, 'transition');
if (upstream && !emptyTrans.test(upstream)) this.upstream = upstream;
}
// hide backface if supported, for better perf
if (support.backface && config.hideBackface) {
setStyle(this.el, support.backface.css, 'hidden');
}
};
// Public chainable methods
chain('add', add);
chain('start', start);
chain('wait', wait);
chain('then', then);
chain('next', next);
chain('stop', stop);
chain('set', set);
chain('show', show);
chain('hide', hide);
chain('redraw', redraw);
chain('destroy', destroy);
// Public add() - chainable
function add(transition, options) {
// Parse transition settings
var settings = compact(('' + transition).split(space));
var name = settings[0];
options = options || {};
// Get property definition from map
var definition = propertyMap[name];
if (!definition) return warn('Unsupported property: ' + name);
// Ignore weak property additions
if (options.weak && this.props[name]) return;
// Init property instance
var Class = definition[0];
var prop = this.props[name];
if (!prop) prop = this.props[name] = new Class.Bare();
// Init settings + type + options
prop.init(this.$el, settings, definition, options);
return prop; // return for internal use
}
// Public start() - chainable
function start(options, fromQueue, queueArgs) {
if (!options) return;
var optionType = typeof options;
// Clear queue unless start was called from it
if (!fromQueue) {
this.timer && this.timer.destroy();
this.queue = [];
this.active = false;
}
// If options is a number, wait for a delay and continue queue.
if (optionType == 'number' && fromQueue) {
this.timer = new Delay({ duration: options, context: this, complete: next });
this.active = true;
return;
}
// If options is a string, invoke add() to modify transition settings
if (optionType == 'string' && fromQueue) {
switch (options) {
case 'hide': hide.call(this); break;
case 'stop': stop.call(this); break;
case 'redraw': redraw.call(this); break;
default: add.call(this, options, (queueArgs && queueArgs[1]));
}
return next.call(this);
}
// If options is a function, invoke it.
if (optionType == 'function') {
options.call(this, this);
return;
}
// If options is an object, start property tweens.
if (optionType == 'object') {
// loop through each valid property
var timespan = 0;
eachProp.call(this, options, function (prop, value) {
// determine the longest time span (duration + delay)
if (prop.span > timespan) timespan = prop.span;
// stop current, then begin animation
prop.stop();
prop.animate(value);
}, function (extras) {
// look for wait property and use it to override timespan
if ('wait' in extras) timespan = validTime(extras.wait, 0);
});
// update main transition styles for active props
updateStyles.call(this);
// start timer for total transition timespan
if (timespan > 0) {
this.timer = new Delay({ duration: timespan, context: this });
this.active = true;
if (fromQueue) this.timer.complete = next;
}
// apply deferred styles after a single frame delay
var self = this, found = false, styles = {};
enterFrame(function () {
eachProp.call(self, options, function (prop) {
if (!prop.active) return;
found = true;
styles[prop.name] = prop.nextStyle;
});
found && self.$el.css(styles); // set styles object
});
}
}
// Public wait() - chainable
function wait(time) {
time = validTime(time, 0);
// if start() has ocurred, simply push wait into queue
if (this.active) {
this.queue.push({ options: time });
} else {
// otherwise, start a timer. wait() is starting the sequence.
this.timer = new Delay({ duration: time, context: this, complete: next });
this.active = true;
}
}
// Public then() - chainable
function then(options) {
if (!this.active) {
return warn('No active transition timer. Use start() or wait() before then().');
}
// push options into queue
this.queue.push({ options: options, args: arguments });
// set timer complete callback
this.timer.complete = next;
}
// Public next() - chainable
function next() {
// stop current timer in case next() was called early
this.timer && this.timer.destroy();
this.active = false;
// if the queue is empty do nothing
if (!this.queue.length) return;
// start next item in queue
var queued = this.queue.shift();
start.call(this, queued.options, true, queued.args);
}
// Public stop() - chainable
function stop(options) {
this.timer && this.timer.destroy();
this.queue = [];
this.active = false;
var values;
if (typeof options == 'string') {
values = {};
values[options] = 1;
} else if (typeof options == 'object' && options != null) {
values = options;
} else {
values = this.props;
}
eachProp.call(this, values, stopProp);
updateStyles.call(this);
}
// Public set() - chainable
function set(values) {
stop.call(this, values);
eachProp.call(this, values, setProp, setExtras);
}
// Public show() - chainable
function show(display) {
// Show an element by setting its display
if (typeof display != 'string') display = 'block';
this.el.style.display = display;
}
// Public hide() - chainable
function hide() {
// Stop all transitions before hiding the element
stop.call(this);
this.el.style.display = 'none';
}
// Public redraw() - chainable
function redraw() {
this.el.offsetHeight;
}
// Public destroy() - chainable
function destroy() {
stop.call(this);
jQuery.removeData(this.el, dataKey);
this.$el = this.el = null;
}
// Update transition styles
function updateStyles() {
// build transition string from active props
var p, prop, result = [];
if (this.upstream) result.push(this.upstream);
for (p in this.props) {
prop = this.props[p];
if (!prop.active) continue;
result.push(prop.string);
}
// set transition style property on dom element
result = result.join(',');
if (this.style === result) return;
this.style = result;
this.el.style[support.transition.dom] = result;
}
// Loop through valid properties, auto-create them, and run iterator callback
function eachProp(collection, iterator, ejector) {
// skip auto-add during stop()
var autoAdd = iterator !== stopProp;
var name;
var prop;
var value;
var matches = {};
var extras;
// find valid properties in collection
for (name in collection) {
value = collection[name];
// match transform sub-properties
if (name in transformMap) {
if (!matches.transform) matches.transform = {};
matches.transform[name] = value;
continue;
}
// convert camelCase to dashed
if (capsRegex.test(name)) name = toDashed(name);
// match base properties
if (name in propertyMap) {
matches[name] = value;
continue;
}
// otherwise, add property to extras
if (!extras) extras = {};
extras[name] = value;
}
// iterate on each matched property, auto-adding them
for (name in matches) {
value = matches[name];
prop = this.props[name];
if (!prop) {
// skip auto-add during stop()
if (!autoAdd) continue;
// auto-add property instances
prop = add.call(this, name);
}
// iterate on each property
iterator.call(this, prop, value);
}
// finally, eject the extras into space
if (ejector && extras) ejector.call(this, extras);
}
// Loop iterators
function stopProp(prop) { prop.stop(); }
function setProp(prop, value) { prop.set(value); }
function setExtras(extras) { this.$el.css(extras); }
// Define a chainable method that takes children into account
function chain(name, method) {
proto[name] = function () {
if (this.children) return eachChild.call(this, method, arguments);
this.el && method.apply(this, arguments);
return this;
};
}
// Iterate through children and apply the method, return for chaining
function eachChild(method, args) {
var i, count = this.children.length;
for (i = 0; i < count; i++) {
method.apply(this.children[i], args);
}
return this;
}
});
// Tram class - extends Transition + wraps child instances for chaining.
var Tram = P(Transition, function (proto) {
proto.init = function (element, options) {
var $elems = jQuery(element);
// Invalid selector, do nothing.
if (!$elems.length) return this;
// Single case - return single Transition instance
if ($elems.length === 1) return factory($elems[0], options);
// Store multiple instances for chaining
var children = [];
$elems.each(function (index, el) {
children.push(factory(el, options));
});
this.children = children;
return this;
};
// Retrieve instance from data or create a new one.
function factory(el, options) {
var t = jQuery.data(el, dataKey) || jQuery.data(el, dataKey, new Transition.Bare());
if (!t.el) t.init(el);
if (options) return t.start(options);
return t;
}
});
// --------------------------------------------------
// Property class - get/set property values
var Property = P(function (proto) {
var defaults = {
duration: 500
, ease: 'ease'
, delay: 0
};
proto.init = function ($el, settings, definition, options) {
// Initialize or extend settings
this.$el = $el;
this.el = $el[0];
var name = settings[0];
if (definition[2]) name = definition[2]; // expand name
if (prefixed[name]) name = prefixed[name];
this.name = name;
this.type = definition[1];
this.duration = validTime(settings[1], this.duration, defaults.duration);
this.ease = validEase(settings[2], this.ease, defaults.ease);
this.delay = validTime(settings[3], this.delay, defaults.delay);
this.span = this.duration + this.delay;
this.active = false;
this.nextStyle = null;
this.auto = allowAuto.test(this.name);
this.unit = options.unit || this.unit || config.defaultUnit;
this.angle = options.angle || this.angle || config.defaultAngle;
// Animate using tween fallback if necessary, otherwise use transition.
if (config.fallback || options.fallback) {
this.animate = this.fallback;
} else {
this.animate = this.transition;
this.string = this.name + space + this.duration + 'ms' +
(this.ease != 'ease' ? space + easing[this.ease][0] : '') +
(this.delay ? space + this.delay + 'ms' : '');
}
};
// Set value immediately
proto.set = function (value) {
value = this.convert(value, this.type);
this.update(value);
this.redraw();
};
// CSS transition
proto.transition = function (value) {
// set new value to start transition
this.active = true;
value = this.convert(value, this.type);
if (this.auto) {
// when transitioning from 'auto', we must reset to computed
if (this.el.style[this.name] == 'auto') {
this.update(this.get());
this.redraw();
}
if (value == 'auto') value = getAuto.call(this);
}
this.nextStyle = value;
};
// Fallback tweening
proto.fallback = function (value) {
var from = this.el.style[this.name] || this.convert(this.get(), this.type);
value = this.convert(value, this.type);
if (this.auto) {
if (from == 'auto') from = this.convert(this.get(), this.type);
if (value == 'auto') value = getAuto.call(this);
}
this.tween = new Tween({
from: from
, to: value
, duration: this.duration
, delay: this.delay
, ease: this.ease
, update: this.update
, context: this
});
};
// Get current element style
proto.get = function () {
return getStyle(this.el, this.name);
};
// Update element style value
proto.update = function (value) {
setStyle(this.el, this.name, value);
};
// Stop animation
proto.stop = function () {
// Stop CSS transition
if (this.active || this.nextStyle) {
this.active = false;
this.nextStyle = null;
setStyle(this.el, this.name, this.get());
}
// Stop fallback tween
var tween = this.tween;
if (tween && tween.context) tween.destroy();
};
// Convert value to expected type
proto.convert = function (value, type) {
if (value == 'auto' && this.auto) return value;
var warnType
, number = typeof value == 'number'
, string = typeof value == 'string'
;
switch(type) {
case typeNumber:
if (number) return value;
if (string && value.replace(unitRegex, '') === '') return +value;
warnType = 'number(unitless)';
break;
case typeColor:
if (string) {
if (value === '' && this.original) {
return this.original;
}
if (type.test(value)) {
if (value.charAt(0) == '#' && value.length == 7) return value;
return cssToHex(value);
}
}
warnType = 'hex or rgb string';
break;
case typeLength:
if (number) return value + this.unit;
if (string && type.test(value)) return value;
warnType = 'number(px) or string(unit)';
break;
case typeLenPerc:
if (number) return value + this.unit;
if (string && type.test(value)) return value;
warnType = 'number(px) or string(unit or %)';
break;
case typeAngle:
if (number) return value + this.angle;
if (string && type.test(value)) return value;
warnType = 'number(deg) or string(angle)';
break;
case typeFancy:
if (number) return value;
if (string && typeLenPerc.test(value)) return value;
warnType = 'number(unitless) or string(unit or %)';
break;
}
// Type must be invalid, warn people.
typeWarning(warnType, value);
return value;
};
proto.redraw = function () {
this.el.offsetHeight;
};
// Calculate expected value for animating towards 'auto'
function getAuto() {
var oldVal = this.get();
this.update('auto');
var newVal = this.get();
this.update(oldVal);
return newVal;
}
// Make sure ease exists
function validEase(ease, current, safe) {
if (current !== undefined) safe = current;
return ease in easing ? ease : safe;
}
// Convert rgb and short hex to long hex
function cssToHex(c) {
var m = /rgba?\((\d+),\s*(\d+),\s*(\d+)/.exec(c);
return (m ? rgbToHex(m[1], m[2], m[3]) : c)
.replace(/#(\w)(\w)(\w)$/, '#$1$1$2$2$3$3');
}
});
// --------------------------------------------------
// Color prop
var Color = P(Property, function (proto, supr) {
proto.init = function () {
supr.init.apply(this, arguments);
// Store original computed value to allow tweening to ''
if (this.original) return;
this.original = this.convert(this.get(), typeColor);
};
});
// --------------------------------------------------
// Scroll prop
var Scroll = P(Property, function (proto, supr) {
proto.init = function () {
supr.init.apply(this, arguments);
this.animate = this.fallback;
};
proto.get = function () {
return this.$el[this.name]();
};
proto.update = function (value) {
this.$el[this.name](value);
};
});
// --------------------------------------------------
// Transform prop w/ sub-properties
var Transform = P(Property, function (proto, supr) {
proto.init = function () {
supr.init.apply(this, arguments);
// If a current state exists, return here
if (this.current) return;
// Store transform state
this.current = {};
// Set default perspective, if specified
if (transformMap.perspective && config.perspective) {
this.current.perspective = config.perspective;
setStyle(this.el, this.name, this.style(this.current));
this.redraw();
}
};
proto.set = function (props) {
// convert new props and store current values
convertEach.call(this, props, function (name, value) {
this.current[name] = value;
});
// set element style immediately
setStyle(this.el, this.name, this.style(this.current));
this.redraw();
};
proto.transition = function (props) {
// convert new prop values and set defaults
var values = this.values(props);
// create MultiTween to track values over time
this.tween = new MultiTween({
current: this.current
, values: values
, duration: this.duration
, delay: this.delay
, ease: this.ease
});
// build temp object for final transition values
var p, temp = {};
for (p in this.current) {
temp[p] = p in values ? values[p] : this.current[p];
}
// set new value to start transition
this.active = true;
this.nextStyle = this.style(temp);
};
proto.fallback = function (props) {
// convert new prop values and set defaults
var values = this.values(props);
// create MultiTween to track values over time
this.tween = new MultiTween({
current: this.current
, values: values
, duration: this.duration
, delay: this.delay
, ease: this.ease
, update: this.update
, context: this
});
};
// Update current values (called from MultiTween)
proto.update = function () {
setStyle(this.el, this.name, this.style(this.current));
};
// Get combined style string from props
proto.style = function (props) {
var p, out = '';
for (p in props) {
out += p + '(' + props[p] + ') ';
}
return out;
};
// Build values object and set defaults
proto.values = function (props) {
var values = {}, def;
convertEach.call(this, props, function (name, value, type) {
values[name] = value;
// set default value if current property does not exist
if (this.current[name] === undefined) {
def = 0; // default prop value
if (~name.indexOf('scale')) def = 1;
this.current[name] = this.convert(def, type);
}
});
return values;
};
// Loop through each prop and output name + converted value
function convertEach(props, iterator) {
var p, name, type, definition, value;
for (p in props) {
definition = transformMap[p];
type = definition[0];
name = definition[1] || p;
value = this.convert(props[p], type);
iterator.call(this, name, value, type);
}
}
});
// --------------------------------------------------
// Tween class - tweens values over time, based on frame timers.
var Tween = P(function (proto) {
// Private vars
var defaults = {
ease: easing.ease[1]
, from: 0
, to: 1
};
proto.init = function (options) {
// Init timing props
this.duration = options.duration || 0;
this.delay = options.delay || 0;
// Use ease function or key value from easing map
var ease = options.ease || defaults.ease;
if (easing[ease]) ease = easing[ease][1];
if (typeof ease != 'function') ease = defaults.ease;
this.ease = ease;
this.update = options.update || noop;
this.complete = options.complete || noop;
this.context = options.context || this;
this.name = options.name;
// Format value and determine units
var from = options.from;
var to = options.to;
if (from === undefined) from = defaults.from;
if (to === undefined) to = defaults.to;
this.unit = options.unit || '';
if (typeof from == 'number' && typeof to == 'number') {
this.begin = from;
this.change = to - from;
} else {
this.format(to, from);
}
// Store value + unit in case it's accessed before delay is up
this.value = this.begin + this.unit;
// Set start time for all Tween instances
this.start = timeNow();
// Start tween (unless autoplay disabled)
if (options.autoplay !== false) {
this.play();
}
};
proto.play = function () {
if (this.active) return;
if (!this.start) this.start = timeNow();
this.active = true;
addRender(this);
};
proto.stop = function () {
if (!this.active) return;
this.active = false;
removeRender(this);
};
proto.render = function (now) {
var value, delta = now - this.start;
// skip render during delay
if (this.delay) {
if (delta <= this.delay) return;
// after delay, reduce delta
delta -= this.delay;
}
if (delta < this.duration) {
// calculate eased position
var position = this.ease(delta, 0, 1, this.duration);
value = this.startRGB ? interpolate(this.startRGB, this.endRGB, position)
: round(this.begin + (position * this.change));
this.value = value + this.unit;
this.update.call(this.context, this.value);
return;
}
// we're done, set final value and destroy
value = this.endHex || this.begin + this.change;
this.value = value + this.unit;
this.update.call(this.context, this.value);
this.complete.call(this.context);
this.destroy();
};
// Format string values for tween
proto.format = function (to, from) {
// cast strings
from += '';
to += '';
// hex colors
if (to.charAt(0) == '#') {
this.startRGB = hexToRgb(from);
this.endRGB = hexToRgb(to);
this.endHex = to;
this.begin = 0;
this.change = 1;
return;
}
// determine unit suffix
if (!this.unit) {
var fromUnit = from.replace(unitRegex, '');
var toUnit = to.replace(unitRegex, '');
if (fromUnit !== toUnit) unitWarning('tween', from, to);
this.unit = fromUnit;
}
from = parseFloat(from);
to = parseFloat(to);
this.begin = this.value = from;
this.change = to - from;
};
// Clean up for garbage collection
proto.destroy = function () {
this.stop();
this.context = null;
this.ease = this.update = this.complete = noop;
};
// Add a tween to the render list
var tweenList = [];
function addRender(tween) {
// if this is the first item, start the render loop
if (tweenList.push(tween) === 1) enterFrame(renderLoop);
}
// Loop through all tweens on each frame
function renderLoop() {
var i, now, tween, count = tweenList.length;
if (!count) return;
enterFrame(renderLoop);
now = timeNow();
for (i = count; i--;) {
tween = tweenList[i];
tween && tween.render(now);
}
}
// Remove tween from render list
function removeRender(tween) {
var rest, index = jQuery.inArray(tween, tweenList);
if (index >= 0) {
rest = tweenList.slice(index + 1);
tweenList.length = index;
if (rest.length) tweenList = tweenList.concat(rest);
}
}
// Round number to limit decimals
var factor = 1000;
function round(value) {
return Math.round(value * factor) / factor;
}
// Interpolate rgb colors based on `position`, returns hex string
function interpolate(start, end, position) {
return rgbToHex(
start[0] + position * (end[0] - start[0]),
start[1] + position * (end[1] - start[1]),
start[2] + position * (end[2] - start[2])
);
}
});
// Delay - simple delay timer that hooks into frame loop
var Delay = P(Tween, function (proto) {
proto.init = function (options) {
this.duration = options.duration || 0;
this.complete = options.complete || noop;
this.context = options.context;
this.play();
};
proto.render = function (now) {
var delta = now - this.start;
if (delta < this.duration) return;
this.complete.call(this.context);
this.destroy();
};
});
// MultiTween - tween multiple properties on a single frame loop
var MultiTween = P(Tween, function (proto, supr) {
proto.init = function (options) {
// configure basic options
this.context = options.context;
this.update = options.update;
// create child tweens for each changed property
this.tweens = [];
this.current = options.current; // store direct reference
var name, value;
for (name in options.values) {
value = options.values[name];
if (this.current[name] === value) continue;
this.tweens.push(new Tween({
name: name
, from: this.current[name]
, to: value
, duration: options.duration
, delay: options.delay
, ease: options.ease
, autoplay: false
}));
}
// begin MultiTween render
this.play();
};
proto.render = function (now) {
// render each child tween
var i, tween, count = this.tweens.length;
var alive = false;
for (i = count; i--;) {
tween = this.tweens[i];
if (tween.context) {
tween.render(now);
// store current value directly on object
this.current[tween.name] = tween.value;
alive = true;
}
}
// destroy and stop render if no longer alive
if (!alive) return this.destroy();
// call update method
this.update && this.update.call(this.context);
};
proto.destroy = function () {
supr.destroy.call(this);
if (!this.tweens) return;
// Destroy all child tweens
var i, count = this.tweens.length;
for (i = count; i--;) {
this.tweens[i].destroy();
}
this.tweens = null;
this.current = null;
};
});
// --------------------------------------------------
// Main wrapper - returns a Tram instance with public chaining API.
function tram(element, options) {
// Chain on the result of Tram.init() to optimize single case.
var wrap = new Tram.Bare();
return wrap.init(element, options);
}
// Global tram config
var config = tram.config = {
debug: false // debug mode with console warnings
, defaultUnit: 'px' // default unit added to <length> types
, defaultAngle: 'deg' // default unit added to <angle> types
, keepInherited: false // optionally keep inherited CSS transitions
, hideBackface: false // optionally hide backface on all elements
, perspective: '' // optional default perspective value e.g. '1000px'
, fallback: !support.transition // boolean to globally set fallback mode
, agentTests: [] // array of userAgent test strings for sniffing
};
// fallback() static - browser sniff to force fallback mode
// Example: tram.fallback('firefox');
// Would match Firefox along with previously sniffed browsers.
tram.fallback = function (testString) {
// if no transition support, fallback is always true
if (!support.transition) return config.fallback = true;
config.agentTests.push('(' + testString + ')');
var pattern = new RegExp(config.agentTests.join('|'), 'i');
config.fallback = pattern.test(navigator.userAgent);
};
// Default sniffs for browsers that support transitions badly ;_;
tram.fallback('6.0.[2-5] Safari');
// tram.tween() static method
tram.tween = function (options) {
return new Tween(options);
};
// tram.delay() static method
tram.delay = function (duration, callback, context) {
return new Delay({ complete: callback, duration: duration, context: context });
};
// --------------------------------------------------
// jQuery methods
// jQuery plugin method, diverts chain to Tram object.
jQuery.fn.tram = function (options) {
return tram.call(null, this, options);
};
// Shortcuts for internal jQuery style getter / setter
var setStyle = jQuery.style;
var getStyle = jQuery.css;
// --------------------------------------------------
// Property maps + unit values
// Prefixed property names
var prefixed = {
'transform': support.transform && support.transform.css
};
// Main Property map { name: [ Class, valueType, expand ]}
var propertyMap = {
'color' : [ Color, typeColor ]
, 'background' : [ Color, typeColor, 'background-color' ]
, 'outline-color' : [ Color, typeColor ]
, 'border-color' : [ Color, typeColor ]
, 'border-top-color' : [ Color, typeColor ]
, 'border-right-color' : [ Color, typeColor ]
, 'border-bottom-color' : [ Color, typeColor ]
, 'border-left-color' : [ Color, typeColor ]
, 'border-width' : [ Property, typeLength ]
, 'border-top-width' : [ Property, typeLength ]
, 'border-right-width' : [ Property, typeLength ]
, 'border-bottom-width' : [ Property, typeLength ]
, 'border-left-width' : [ Property, typeLength ]
, 'border-spacing' : [ Property, typeLength ]
, 'letter-spacing' : [ Property, typeLength ]
, 'margin' : [ Property, typeLength ]
, 'margin-top' : [ Property, typeLength ]
, 'margin-right' : [ Property, typeLength ]
, 'margin-bottom' : [ Property, typeLength ]
, 'margin-left' : [ Property, typeLength ]
, 'padding' : [ Property, typeLength ]
, 'padding-top' : [ Property, typeLength ]
, 'padding-right' : [ Property, typeLength ]
, 'padding-bottom' : [ Property, typeLength ]
, 'padding-left' : [ Property, typeLength ]
, 'outline-width' : [ Property, typeLength ]
, 'opacity' : [ Property, typeNumber ]
, 'top' : [ Property, typeLenPerc ]
, 'right' : [ Property, typeLenPerc ]
, 'bottom' : [ Property, typeLenPerc ]
, 'left' : [ Property, typeLenPerc ]
, 'font-size' : [ Property, typeLenPerc ]
, 'text-indent' : [ Property, typeLenPerc ]
, 'word-spacing' : [ Property, typeLenPerc ]
, 'width' : [ Property, typeLenPerc ]
, 'min-width' : [ Property, typeLenPerc ]
, 'max-width' : [ Property, typeLenPerc ]
, 'height' : [ Property, typeLenPerc ]
, 'min-height' : [ Property, typeLenPerc ]
, 'max-height' : [ Property, typeLenPerc ]
, 'line-height' : [ Property, typeFancy ]
, 'scroll-top' : [ Scroll, typeNumber, 'scrollTop' ]
, 'scroll-left' : [ Scroll, typeNumber, 'scrollLeft' ]
// , 'background-position' : [ Property, typeLenPerc ]
};
// Transform property maps
var transformMap = {};
if (support.transform) {
// Add base properties if supported
propertyMap['transform'] = [ Transform ];
// propertyMap['transform-origin'] = [ Transform ];
// Transform sub-property map { name: [ valueType, expand ]}
transformMap = {
x: [ typeLenPerc, 'translateX' ]
, y: [ typeLenPerc, 'translateY' ]
, rotate: [ typeAngle ]
, rotateX: [ typeAngle ]
, rotateY: [ typeAngle ]
, scale: [ typeNumber ]
, scaleX: [ typeNumber ]
, scaleY: [ typeNumber ]
, skew: [ typeAngle ]
, skewX: [ typeAngle ]
, skewY: [ typeAngle ]
};
}
// Add 3D transform props if supported
if (support.transform && support.backface) {
transformMap.z = [ typeLenPerc, 'translateZ' ];
transformMap.rotateZ = [ typeAngle ];
transformMap.scaleZ = [ typeNumber ];
transformMap.perspective = [ typeLength ];
}
// --------------------------------------------------
// Utils
function toDashed(string) {
return string.replace(/[A-Z]/g, function (letter) {
return '-' + letter.toLowerCase();
});
}
function hexToRgb(hex) {
var n = parseInt(hex.slice(1), 16);
var r = (n >> 16) & 255;
var g = (n >> 8) & 255;
var b = n & 255;
return [r,g,b];
}
function rgbToHex(r, g, b) {
return '#' + (1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1);
}
function noop() {}
function typeWarning(exp, val) {
warn('Type warning: Expected: [' + exp + '] Got: [' + typeof val + '] ' + val);
}
function unitWarning(name, from, to) {
warn('Units do not match [' + name + ']: ' + from + ', ' + to);
}
// Normalize time values
var milli = /ms/, seconds = /s|\./;
function validTime(string, current, safe) {
if (current !== undefined) safe = current;
if (string === undefined) return safe;
var n = safe;
// if string contains 'ms' or contains no suffix
if (milli.test(string) || !seconds.test(string)) {
n = parseInt(string, 10);
// otherwise if string contains 's' or a decimal point
} else if (seconds.test(string)) {
n = parseFloat(string) * 1000;
}
if (n < 0) n = 0; // no negative times
return n === n ? n : safe; // protect from NaNs
}
// Log warning message if supported
function warn(msg) {
config.debug && window && window.console.warn(msg);
}
// Lo-Dash compact()
// MIT license <http://lodash.com/license>
// Creates an array with all falsey values of `array` removed
function compact(array) {
var index = -1,
length = array ? array.length : 0,
result = [];
while (++index < length) {
var value = array[index];
if (value) {
result.push(value);
}
}
return result;
}
// --------------------------------------------------
// Export public module.
return jQuery.tram = tram;
}(window.jQuery));