morpheus
Version:
A Brilliant Animator
404 lines (363 loc) • 13.7 kB
JavaScript
!function (name, definition) {
if (typeof define == 'function') define(definition)
else if (typeof module != 'undefined') module.exports = definition()
else this[name] = definition()
}('morpheus', function () {
var doc = document
, win = window
, perf = win.performance
, perfNow = perf && (perf.now || perf.webkitNow || perf.msNow || perf.mozNow)
, now = perfNow ? function () { return perfNow.call(perf) } : function () { return +new Date() }
, fixTs = false // feature detected below
, html = doc.documentElement
, thousand = 1000
, rgbOhex = /^rgb\(|#/
, relVal = /^([+\-])=([\d\.]+)/
, numUnit = /^(?:[\+\-]=?)?\d+(?:\.\d+)?(%|in|cm|mm|em|ex|pt|pc|px)$/
, rotate = /rotate\(((?:[+\-]=)?([\-\d\.]+))deg\)/
, scale = /scale\(((?:[+\-]=)?([\d\.]+))\)/
, skew = /skew\(((?:[+\-]=)?([\-\d\.]+))deg, ?((?:[+\-]=)?([\-\d\.]+))deg\)/
, translate = /translate\(((?:[+\-]=)?([\-\d\.]+))px, ?((?:[+\-]=)?([\-\d\.]+))px\)/
// these elements do not require 'px'
, unitless = { lineHeight: 1, zoom: 1, zIndex: 1, opacity: 1, transform: 1}
// which property name does this browser use for transform
var transform = function () {
var styles = doc.createElement('a').style
, props = ['webkitTransform', 'MozTransform', 'OTransform', 'msTransform', 'Transform']
, i
for (i = 0; i < props.length; i++) {
if (props[i] in styles) return props[i]
}
}()
// does this browser support the opacity property?
var opasity = function () {
return typeof doc.createElement('a').style.opacity !== 'undefined'
}()
// initial style is determined by the elements themselves
var getStyle = doc.defaultView && doc.defaultView.getComputedStyle ?
function (el, property) {
property = property == 'transform' ? transform : property
property = camelize(property)
var value = null
, computed = doc.defaultView.getComputedStyle(el, '')
computed && (value = computed[property])
return el.style[property] || value
} : html.currentStyle ?
function (el, property) {
property = camelize(property)
if (property == 'opacity') {
var val = 100
try {
val = el.filters['DXImageTransform.Microsoft.Alpha'].opacity
} catch (e1) {
try {
val = el.filters('alpha').opacity
} catch (e2) {}
}
return val / 100
}
var value = el.currentStyle ? el.currentStyle[property] : null
return el.style[property] || value
} :
function (el, property) {
return el.style[camelize(property)]
}
var frame = function () {
// native animation frames
// http://webstuff.nfshost.com/anim-timing/Overview.html
// http://dev.chromium.org/developers/design-documents/requestanimationframe-implementation
return win.requestAnimationFrame ||
win.webkitRequestAnimationFrame ||
win.mozRequestAnimationFrame ||
win.msRequestAnimationFrame ||
win.oRequestAnimationFrame ||
function (callback) {
win.setTimeout(function () {
callback(+new Date())
}, 17) // when I was 17..
}
}()
frame(function(timestamp) {
// feature-detect if rAF and now() are of the same scale (epoch or high-res),
// if not, we have to do a timestamp fix on each frame
fixTs = timestamp > 1e12 != now() > 1e12
})
var children = []
function has(array, elem, i) {
if (Array.prototype.indexOf) return array.indexOf(elem)
for (i = 0; i < array.length; ++i) {
if (array[i] === elem) return i
}
}
function render(timestamp) {
var i, count = children.length
if (fixTs) timestamp = now()
for (i = count; i--;) {
children[i](timestamp)
}
children.length && frame(render)
}
function live(f) {
if (children.push(f) === 1) frame(render)
}
function die(f) {
var rest, index = has(children, f)
if (index >= 0) {
rest = children.slice(index + 1)
children.length = index
children = children.concat(rest)
}
}
function parseTransform(style, base) {
var values = {}, m
if (m = style.match(rotate)) values.rotate = by(m[1], base ? base.rotate : null)
if (m = style.match(scale)) values.scale = by(m[1], base ? base.scale : null)
if (m = style.match(skew)) {values.skewx = by(m[1], base ? base.skewx : null); values.skewy = by(m[3], base ? base.skewy : null)}
if (m = style.match(translate)) {values.translatex = by(m[1], base ? base.translatex : null); values.translatey = by(m[3], base ? base.translatey : null)}
return values
}
function formatTransform(v) {
var s = ''
if ('rotate' in v) s += 'rotate(' + v.rotate + 'deg) '
if ('scale' in v) s += 'scale(' + v.scale + ') '
if ('translatex' in v) s += 'translate(' + v.translatex + 'px,' + v.translatey + 'px) '
if ('skewx' in v) s += 'skew(' + v.skewx + 'deg,' + v.skewy + 'deg)'
return s
}
function rgb(r, g, b) {
return '#' + (1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)
}
// convert rgb and short hex to long hex
function toHex(c) {
var m = c.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/)
return (m ? rgb(m[1], m[2], m[3]) : c)
.replace(/#(\w)(\w)(\w)$/, '#$1$1$2$2$3$3') // short skirt to long jacket
}
// change font-size => fontSize etc.
function camelize(s) {
return s.replace(/-(.)/g, function (m, m1) {
return m1.toUpperCase()
})
}
// aren't we having it?
function fun(f) {
return typeof f == 'function'
}
function nativeTween(t) {
// default to a pleasant-to-the-eye easeOut (like native animations)
return Math.sin(t * Math.PI / 2)
}
/**
* Core tween method that requests each frame
* @param duration: time in milliseconds. defaults to 1000
* @param fn: tween frame callback function receiving 'position'
* @param done {optional}: complete callback function
* @param ease {optional}: easing method. defaults to easeOut
* @param from {optional}: integer to start from
* @param to {optional}: integer to end at
* @returns method to stop the animation
*/
function tween(duration, fn, done, ease, from, to) {
ease = fun(ease) ? ease : morpheus.easings[ease] || nativeTween
var time = duration || thousand
, self = this
, diff = to - from
, start = now()
, stop = 0
, end = 0
function run(t) {
var delta = t - start
if (delta > time || stop) {
to = isFinite(to) ? to : 1
stop ? end && fn(to) : fn(to)
die(run)
return done && done.apply(self)
}
// if you don't specify a 'to' you can use tween as a generic delta tweener
// cool, eh?
isFinite(to) ?
fn((diff * ease(delta / time)) + from) :
fn(ease(delta / time))
}
live(run)
return {
stop: function (jump) {
stop = 1
end = jump // jump to end of animation?
if (!jump) done = null // remove callback if not jumping to end
}
}
}
/**
* generic bezier method for animating x|y coordinates
* minimum of 2 points required (start and end).
* first point start, last point end
* additional control points are optional (but why else would you use this anyway ;)
* @param points: array containing control points
[[0, 0], [100, 200], [200, 100]]
* @param pos: current be(tween) position represented as float 0 - 1
* @return [x, y]
*/
function bezier(points, pos) {
var n = points.length, r = [], i, j
for (i = 0; i < n; ++i) {
r[i] = [points[i][0], points[i][1]]
}
for (j = 1; j < n; ++j) {
for (i = 0; i < n - j; ++i) {
r[i][0] = (1 - pos) * r[i][0] + pos * r[parseInt(i + 1, 10)][0]
r[i][1] = (1 - pos) * r[i][1] + pos * r[parseInt(i + 1, 10)][1]
}
}
return [r[0][0], r[0][1]]
}
// this gets you the next hex in line according to a 'position'
function nextColor(pos, start, finish) {
var r = [], i, e, from, to
for (i = 0; i < 6; i++) {
from = Math.min(15, parseInt(start.charAt(i), 16))
to = Math.min(15, parseInt(finish.charAt(i), 16))
e = Math.floor((to - from) * pos + from)
e = e > 15 ? 15 : e < 0 ? 0 : e
r[i] = e.toString(16)
}
return '#' + r.join('')
}
// this retreives the frame value within a sequence
function getTweenVal(pos, units, begin, end, k, i, v) {
if (k == 'transform') {
v = {}
for (var t in begin[i][k]) {
v[t] = (t in end[i][k]) ? Math.round(((end[i][k][t] - begin[i][k][t]) * pos + begin[i][k][t]) * thousand) / thousand : begin[i][k][t]
}
return v
} else if (typeof begin[i][k] == 'string') {
return nextColor(pos, begin[i][k], end[i][k])
} else {
// round so we don't get crazy long floats
v = Math.round(((end[i][k] - begin[i][k]) * pos + begin[i][k]) * thousand) / thousand
// some css properties don't require a unit (like zIndex, lineHeight, opacity)
if (!(k in unitless)) v += units[i][k] || 'px'
return v
}
}
// support for relative movement via '+=n' or '-=n'
function by(val, start, m, r, i) {
return (m = relVal.exec(val)) ?
(i = parseFloat(m[2])) && (start + (m[1] == '+' ? 1 : -1) * i) :
parseFloat(val)
}
/**
* morpheus:
* @param element(s): HTMLElement(s)
* @param options: mixed bag between CSS Style properties & animation options
* - {n} CSS properties|values
* - value can be strings, integers,
* - or callback function that receives element to be animated. method must return value to be tweened
* - relative animations start with += or -= followed by integer
* - duration: time in ms - defaults to 1000(ms)
* - easing: a transition method - defaults to an 'easeOut' algorithm
* - complete: a callback method for when all elements have finished
* - bezier: array of arrays containing x|y coordinates that define the bezier points. defaults to none
* - this may also be a function that receives element to be animated. it must return a value
*/
function morpheus(elements, options) {
var els = elements ? (els = isFinite(elements.length) ? elements : [elements]) : [], i
, complete = options.complete
, duration = options.duration
, ease = options.easing
, points = options.bezier
, begin = []
, end = []
, units = []
, bez = []
, originalLeft
, originalTop
if (points) {
// remember the original values for top|left
originalLeft = options.left;
originalTop = options.top;
delete options.right;
delete options.bottom;
delete options.left;
delete options.top;
}
for (i = els.length; i--;) {
// record beginning and end states to calculate positions
begin[i] = {}
end[i] = {}
units[i] = {}
// are we 'moving'?
if (points) {
var left = getStyle(els[i], 'left')
, top = getStyle(els[i], 'top')
, xy = [by(fun(originalLeft) ? originalLeft(els[i]) : originalLeft || 0, parseFloat(left)),
by(fun(originalTop) ? originalTop(els[i]) : originalTop || 0, parseFloat(top))]
bez[i] = fun(points) ? points(els[i], xy) : points
bez[i].push(xy)
bez[i].unshift([
parseInt(left, 10),
parseInt(top, 10)
])
}
for (var k in options) {
switch (k) {
case 'complete':
case 'duration':
case 'easing':
case 'bezier':
continue
}
var v = getStyle(els[i], k), unit
, tmp = fun(options[k]) ? options[k](els[i]) : options[k]
if (typeof tmp == 'string' &&
rgbOhex.test(tmp) &&
!rgbOhex.test(v)) {
delete options[k]; // remove key :(
continue; // cannot animate colors like 'orange' or 'transparent'
// only #xxx, #xxxxxx, rgb(n,n,n)
}
begin[i][k] = k == 'transform' ? parseTransform(v) :
typeof tmp == 'string' && rgbOhex.test(tmp) ?
toHex(v).slice(1) :
parseFloat(v)
end[i][k] = k == 'transform' ? parseTransform(tmp, begin[i][k]) :
typeof tmp == 'string' && tmp.charAt(0) == '#' ?
toHex(tmp).slice(1) :
by(tmp, parseFloat(v));
// record original unit
(typeof tmp == 'string') && (unit = tmp.match(numUnit)) && (units[i][k] = unit[1])
}
}
// ONE TWEEN TO RULE THEM ALL
return tween.apply(els, [duration, function (pos, v, xy) {
// normally not a fan of optimizing for() loops, but we want something
// fast for animating
for (i = els.length; i--;) {
if (points) {
xy = bezier(bez[i], pos)
els[i].style.left = xy[0] + 'px'
els[i].style.top = xy[1] + 'px'
}
for (var k in options) {
v = getTweenVal(pos, units, begin, end, k, i)
k == 'transform' ?
els[i].style[transform] = formatTransform(v) :
k == 'opacity' && !opasity ?
(els[i].style.filter = 'alpha(opacity=' + (v * 100) + ')') :
(els[i].style[camelize(k)] = v)
}
}
}, complete, ease])
}
// expose useful methods
morpheus.tween = tween
morpheus.getStyle = getStyle
morpheus.bezier = bezier
morpheus.transform = transform
morpheus.parseTransform = parseTransform
morpheus.formatTransform = formatTransform
morpheus.animationFrame = frame
morpheus.easings = {}
return morpheus
});