UNPKG

tram

Version:

Cross-browser CSS3 transitions in JavaScript

1,222 lines (1,077 loc) 38.7 kB
// -------------------------------------------------- // 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;