diff --git a/morpheus.js b/morpheus.js index b97fc59..996859b 100644 --- a/morpheus.js +++ b/morpheus.js @@ -16,13 +16,6 @@ , now = perfNow ? function () { return perfNow.call(perf) } : function () { return +new Date() } , 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} @@ -42,6 +35,7 @@ }() // initial style is determined by the elements themselves + // TODO: redundant camelize, but getStyle is exposed to the outside world, so removing this might break compatibility var getStyle = doc.defaultView && doc.defaultView.getComputedStyle ? function (el, property) { property = property == 'transform' ? transform : property @@ -121,35 +115,6 @@ } } - 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) { @@ -236,43 +201,182 @@ 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) + + // Takes a CSS "used value" and returns a parsed object + function parseCSS1(k, s, i) { + var x, y, a = [], old = i + // It's a number + if (/[+\-0-9]/.test(s[i])) { + x = { type: 'number' } + if (/[+\-]/.test(s[i])) { + if (s[i + 1] === '=') { + x.relative = s[i] + i += 2 + } else { + a.push(s[i]) + i += 1 + } + } + while (s[i] && /[0-9\.]/.test(s[i])) { + a.push(s[i]) + i += 1 + } + x.value = +a.join('') + + // This is for the optional unit + a = [] + while (s[i] && /[a-zA-Z%]/.test(s[i])) { + a.push(s[i]) + i += 1 + } + x.unit = a.join('') + + // TODO: pretty hacky + // some css properties don't require a unit (like zIndex, lineHeight, opacity) + if (x.unit === '' && !(k in unitless)) { + x.unit = 'px' + } + } else { + while (s[i] && /[^\( ]/.test(s[i])) { + a.push(s[i]) + i += 1 + } + // It's a function + // TODO: doesn't currently handle stuff like `foo(bar qux, corge)` + if (s[i] === '(') { + x = { type: 'function', value: a.join(''), args: [] } + i += 1 + while (s[i]) { + while (s[i] === ' ') { + i += 1 + } + y = parseCSS1('opacity', s, i) + x.args.push(y.value) + i = y.index + if (s[i] === ')') { + i += 1 + break + } else if (s[i] === ',') { + i += 1 + } + } + // It's a normal value + } else { + x = { type: 'normal', value: a.join('') } + } + } + if (i === old) { + throw new Error('invalid CSS value ' + s) } - return '#' + r.join('') + return { value: x, index: i } } - // 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]) + function parseCSS(k, s) { + s = '' + s // convert to string + var i = 0 + , a = [] + , x + while (s[i]) { + x = parseCSS1(k, s, 0) + a.push(x.value) + i = x.index + } + if (a.length === 1) { + return a[0] } 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 + return { type: 'multi', args: a } + } + } + + /** + * makeTweener: + * @param x: parsed CSS object which represents the start of the animation + * @param y: parsed CSS object which represents the end of the animation + * @returns a function which when given a position returns the interpolated style + */ + function makeTweener(x, y) { + switch (x.type) { + case 'number': + var diff = y.value - x.value + return function (pos) { + // round so we don't get crazy long floats + return Math.round((x.value + (diff * pos)) * thousand) / thousand + x.unit + } + case 'multi': + x.args = x.args.map(function (x, i) { + return makeTweener(x, y.args[i]) + }) + return function (pos) { + return x.args.map(function (f) { return f(pos) }).join(' ') + } + case 'function': + // Normalize to rgba + if (x.value === 'rgb') { + x.args.push({ type: 'number', value: 1, unit: '' }) + x.value = 'rgba' + } + if (y.value === 'rgb') { + y.args.push({ type: 'number', value: 1, unit: '' }) + x.value = 'rgba' + } + x.args = x.args.map(function (x, i) { + return makeTweener(x, y.args[i]) + }) + if (x.value === 'rgba') { + return function (pos) { + return x.value + '(' + Math.round(x.args[0](pos)) + ', ' + + Math.round(x.args[1](pos)) + ', ' + + Math.round(x.args[2](pos)) + ', ' + + x.args[3](pos) + ')' + } + } else { + return function (pos) { + return x.value + '(' + x.args.map(function (f) { return f(pos) }).join(', ') + ')' + } + } + break // TODO: useless break but JSHint complained about it + case 'normal': + return function (pos) { + return pos > 0.5 ? y.value : x.value + } } } - // 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) + function getRelative(x, y) { + switch (y.type) { + case 'number': + // Relative values with += and -= + if (y.relative === '+') { + y.value = x.value + y.value + } else if (y.relative === '-') { + y.value = x.value - y.value + } + return y.value + y.unit + case 'multi': + y.args = y.args.map(function (y, i) { + return getRelative(x.args[i], y) + }) + return y.args.join(' ') + // TODO Should this normalize to rgba? + case 'function': + y.args = y.args.map(function (y, i) { + return getRelative(x.args[i], y) + }) + return y.value + '(' + y.args.join(', ') + ')' + case 'normal': + return y.value + } + } + + function opacity(f) { + return function (pos) { + // TODO: this coerces from a string to a number + return 'alpha(opacity=' + f(pos) * 100 + ')' + } + } + + function getOption(v, el) { + return fun(v) ? v(el) : v } /** @@ -290,102 +394,104 @@ * - 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 + var 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; - } + , ease = options.easing + , points = options.bezier + , a = [] + , els = (elements + ? (els = isFinite(elements.length) + ? elements + : [elements]) + : []) + , old + , begin + , end + , camel + , bez + , i + , k 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([ + , top = getStyle(els[i], 'top') + , xy = [] + + // TODO: pretty ridiculous; split it into multiple statements, or simplify it? + xy.push(parseFloat(getRelative(parseCSS('left', left), + parseCSS('left', options.left + ? getOption(options.left, els[i]) + : 0)))) + xy.push(parseFloat(getRelative(parseCSS('top', top), + parseCSS('top', options.top + ? getOption(options.top, els[i]) + : 0)))) + + bez = fun(points) ? points(els[i], xy) : points + bez.push(xy) + bez.unshift([ parseInt(left, 10), parseInt(top, 10) ]) + a.push({ element: els[i], bezier: bez }) } - for (var k in options) { + for (k in options) { switch (k) { case 'complete': case 'duration': case 'easing': case 'bezier': - continue; - break + 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) + + if (points) { + switch (k) { + case 'left': + case 'top': + case 'bottom': + case 'right': + continue + } + } + + if (k === 'transform') { + camel = transform + } else { + camel = camelize(k) } - 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]) + old = els[i].style[camel] + begin = parseCSS(camel, getStyle(els[i], camel)) + // TODO: inefficient and very hacky, but I don't see a better way to make relative units work + els[i].style[camel] = getRelative(begin, parseCSS(camel, getOption(options[k], els[i]))) + end = parseCSS(camel, getStyle(els[i], camel)) + els[i].style[camel] = old + + if (camel === 'opacity' && !opasity) { + a.push({ element: els[i] + , style: 'filter' + , value: opacity(makeTweener(begin, end)) }) + } else { + a.push({ element: els[i] + , style: camel + , value: makeTweener(begin, end) }) + } } } // 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) + return tween.call(els, duration, function (pos) { + for (i = a.length; i--;) { + if (a[i].bezier) { + var xy = bezier(a[i].bezier, pos) + a[i].element.style.left = xy[0] + 'px' + a[i].element.style.top = xy[1] + 'px' + } else { + a[i].element.style[a[i].style] = a[i].value(pos) } } - }, complete, ease]) + }, complete, ease) } // expose useful methods @@ -393,8 +499,6 @@ morpheus.getStyle = getStyle morpheus.bezier = bezier morpheus.transform = transform - morpheus.parseTransform = parseTransform - morpheus.formatTransform = formatTransform morpheus.easings = {} return morpheus diff --git a/morpheus.min.js b/morpheus.min.js index c0c9458..609b1a6 100644 --- a/morpheus.min.js +++ b/morpheus.min.js @@ -3,4 +3,4 @@ * https://github.com/ded/morpheus - (c) Dustin Diaz 2011 * License MIT */ -!function(e,t){typeof define=="function"?define(t):typeof module!="undefined"?module.exports=t():this[e]=t()}("morpheus",function(){function w(e,t,n){if(Array.prototype.indexOf)return e.indexOf(t);for(n=0;n1e12&&(e=i());for(t=n;t--;)b[t](e);b.length&&y(E)}function S(e){b.push(e)===1&&y(E)}function x(e){var t,n=w(b,e);n>=0&&(t=b.slice(n+1),b.length=n,b=b.concat(t))}function T(e,t){var n={},r;if(r=e.match(l))n.rotate=H(r[1],t?t.rotate:null);if(r=e.match(c))n.scale=H(r[1],t?t.scale:null);if(r=e.match(h))n.skewx=H(r[1],t?t.skewx:null),n.skewy=H(r[3],t?t.skewy:null);if(r=e.match(p))n.translatex=H(r[1],t?t.translatex:null),n.translatey=H(r[3],t?t.translatey:null);return n}function N(e){var t="";return"rotate"in e&&(t+="rotate("+e.rotate+"deg) "),"scale"in e&&(t+="scale("+e.scale+") "),"translatex"in e&&(t+="translate("+e.translatex+"px,"+e.translatey+"px) "),"skewx"in e&&(t+="skew("+e.skewx+"deg,"+e.skewy+"deg)"),t}function C(e,t,n){return"#"+(1<<24|e<<16|t<<8|n).toString(16).slice(1)}function k(e){var t=e.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);return(t?C(t[1],t[2],t[3]):e).replace(/#(\w)(\w)(\w)$/,"#$1$1$2$2$3$3")}function L(e){return e.replace(/-(.)/g,function(e,t){return t.toUpperCase()})}function A(e){return typeof e=="function"}function O(e){return Math.sin(e*Math.PI/2)}function M(e,t,n,r,s,u){function d(e){var i=e-c;if(i>a||h)return u=isFinite(u)?u:1,h?p&&t(u):t(u),x(d),n&&n.apply(f);isFinite(u)?t(l*r(i/a)+s):t(r(i/a))}r=A(r)?r:B.easings[r]||O;var a=e||o,f=this,l=u-s,c=i(),h=0,p=0;return S(d),{stop:function(e){h=1,p=e,e||(n=null)}}}function _(e,t){var n=e.length,r=[],i,s;for(i=0;i15?15:s<0?0:s,r[i]=s.toString(16);return"#"+r.join("")}function P(e,t,n,r,i,s,u){if(i=="transform"){u={};for(var a in n[s][i])u[a]=a in r[s][i]?Math.round(((r[s][i][a]-n[s][i][a])*e+n[s][i][a])*o)/o:n[s][i][a];return u}return typeof n[s][i]=="string"?D(e,n[s][i],r[s][i]):(u=Math.round(((r[s][i]-n[s][i])*e+n[s][i])*o)/o,i in d||(u+=t[s][i]||"px"),u)}function H(e,t,n,r,i){return(n=a.exec(e))?(i=parseFloat(n[2]))&&t+(n[1]=="+"?1:-1)*i:parseFloat(e)}function B(e,t){var n=e?n=isFinite(e.length)?e:[e]:[],r,i=t.complete,s=t.duration,o=t.easing,a=t.bezier,l=[],c=[],h=[],p=[],d,y;a&&(d=t.left,y=t.top,delete t.right,delete t.bottom,delete t.left,delete t.top);for(r=n.length;r--;){l[r]={},c[r]={},h[r]={};if(a){var b=g(n[r],"left"),w=g(n[r],"top"),E=[H(A(d)?d(n[r]):d||0,parseFloat(b)),H(A(y)?y(n[r]):y||0,parseFloat(w))];p[r]=A(a)?a(n[r],E):a,p[r].push(E),p[r].unshift([parseInt(b,10),parseInt(w,10)])}for(var S in t){switch(S){case"complete":case"duration":case"easing":case"bezier":continue}var x=g(n[r],S),C,O=A(t[S])?t[S](n[r]):t[S];if(typeof O=="string"&&u.test(O)&&!u.test(x)){delete t[S];continue}l[r][S]=S=="transform"?T(x):typeof O=="string"&&u.test(O)?k(x).slice(1):parseFloat(x),c[r][S]=S=="transform"?T(O,l[r][S]):typeof O=="string"&&O.charAt(0)=="#"?k(O).slice(1):H(O,parseFloat(x)),typeof O=="string"&&(C=O.match(f))&&(h[r][S]=C[1])}}return M.apply(n,[s,function(e,i,s){for(r=n.length;r--;){a&&(s=_(p[r],e),n[r].style.left=s[0]+"px",n[r].style.top=s[1]+"px");for(var o in t)i=P(e,h,l,c,o,r),o=="transform"?n[r].style[v]=N(i):o=="opacity"&&!m?n[r].style.filter="alpha(opacity="+i*100+")":n[r].style[L(o)]=i}},i,o])}var e=document,t=window,n=t.performance,r=n&&(n.now||n.webkitNow||n.msNow||n.mozNow),i=r?function(){return r.call(n)}:function(){return+(new Date)},s=e.documentElement,o=1e3,u=/^rgb\(|#/,a=/^([+\-])=([\d\.]+)/,f=/^(?:[\+\-]=)?\d+(?:\.\d+)?(%|in|cm|mm|em|ex|pt|pc|px)$/,l=/rotate\(((?:[+\-]=)?([\-\d\.]+))deg\)/,c=/scale\(((?:[+\-]=)?([\d\.]+))\)/,h=/skew\(((?:[+\-]=)?([\-\d\.]+))deg, ?((?:[+\-]=)?([\-\d\.]+))deg\)/,p=/translate\(((?:[+\-]=)?([\-\d\.]+))px, ?((?:[+\-]=)?([\-\d\.]+))px\)/,d={lineHeight:1,zoom:1,zIndex:1,opacity:1,transform:1},v=function(){var t=e.createElement("a").style,n=["webkitTransform","MozTransform","OTransform","msTransform","Transform"],r;for(r=0;r1e12&&(e=i());for(t=n;t--;)h[t](e);h.length&&c(d)}function v(e){h.push(e)===1&&c(d)}function m(e){var t,n=p(h,e);n>=0&&(t=h.slice(n+1),h.length=n,h=h.concat(t))}function g(e){return e.replace(/-(.)/g,function(e,t){return t.toUpperCase()})}function y(e){return typeof e=="function"}function b(e){return Math.sin(e*Math.PI/2)}function w(e,t,n,r,s,u){function d(e){var i=e-c;if(i>a||h)return u=isFinite(u)?u:1,h?p&&t(u):t(u),m(d),n&&n.apply(f);isFinite(u)?t(l*r(i/a)+s):t(r(i/a))}r=y(r)?r:L.easings[r]||b;var a=e||o,f=this,l=u-s,c=i(),h=0,p=0;return v(d),{stop:function(e){h=1,p=e,e||(n=null)}}}function E(e,t){var n=e.length,r=[],i,s;for(i=0;i.5?t.value:e.value}}}function N(e,t){switch(t.type){case"number":return t.relative==="+"?t.value=e.value+t.value:t.relative==="-"&&(t.value=e.value-t.value),t.value+t.unit;case"multi":return t.args=t.args.map(function(t,n){return N(e.args[n],t)}),t.args.join(" ");case"function":return t.args=t.args.map(function(t,n){return N(e.args[n],t)}),t.value+"("+t.args.join(", ")+")";case"normal":return t.value}}function C(e){return function(t){return"alpha(opacity="+e(t)*100+")"}}function k(e,t){return y(e)?e(t):e}function L(e,t){var n=t.complete,r=t.duration,i=t.easing,s=t.bezier,o=[],u=e?u=isFinite(e.length)?e:[e]:[],c,h,p,d,v,m,b;for(m=u.length;m--;){if(s){var S=l(u[m],"left"),L=l(u[m],"top"),A=[];A.push(parseFloat(N(x("left",S),x("left",t.left?k(t.left,u[m]):0)))),A.push(parseFloat(N(x("top",L),x("top",t.top?k(t.top,u[m]):0)))),v=y(s)?s(u[m],A):s,v.push(A),v.unshift([parseInt(S,10),parseInt(L,10)]),o.push({element:u[m],bezier:v})}for(b in t){switch(b){case"complete":case"duration":case"easing":case"bezier":continue}if(s)switch(b){case"left":case"top":case"bottom":case"right":continue}b==="transform"?d=a:d=g(b),c=u[m].style[d],h=x(d,l(u[m],d)),u[m].style[d]=N(h,x(d,k(t[b],u[m]))),p=x(d,l(u[m],d)),u[m].style[d]=c,d==="opacity"&&!f?o.push({element:u[m],style:"filter",value:C(T(h,p))}):o.push({element:u[m],style:d,value:T(h,p)})}}return w.call(u,r,function(e){for(m=o.length;m--;)if(o[m].bezier){var t=E(o[m].bezier,e);o[m].element.style.left=t[0]+"px",o[m].element.style.top=t[1]+"px"}else o[m].element.style[o[m].style]=o[m].value(e)},n,i)}var e=document,t=window,n=t.performance,r=n&&(n.now||n.webkitNow||n.msNow||n.mozNow),i=r?function(){return r.call(n)}:function(){return+(new Date)},s=e.documentElement,o=1e3,u={lineHeight:1,zoom:1,zIndex:1,opacity:1,transform:1},a=function(){var t=e.createElement("a").style,n=["webkitTransform","MozTransform","OTransform","msTransform","Transform"],r;for(r=0;r fontSize etc. function camelize(s) { return s.replace(/-(.)/g, function (m, m1) { @@ -231,43 +196,182 @@ 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) + + // Takes a CSS "used value" and returns a parsed object + function parseCSS1(k, s, i) { + var x, y, a = [], old = i + // It's a number + if (/[+\-0-9]/.test(s[i])) { + x = { type: 'number' } + if (/[+\-]/.test(s[i])) { + if (s[i + 1] === '=') { + x.relative = s[i] + i += 2 + } else { + a.push(s[i]) + i += 1 + } + } + while (s[i] && /[0-9\.]/.test(s[i])) { + a.push(s[i]) + i += 1 + } + x.value = +a.join('') + + // This is for the optional unit + a = [] + while (s[i] && /[a-zA-Z%]/.test(s[i])) { + a.push(s[i]) + i += 1 + } + x.unit = a.join('') + + // TODO: pretty hacky + // some css properties don't require a unit (like zIndex, lineHeight, opacity) + if (x.unit === '' && !(k in unitless)) { + x.unit = 'px' + } + } else { + while (s[i] && /[^\( ]/.test(s[i])) { + a.push(s[i]) + i += 1 + } + // It's a function + // TODO: doesn't currently handle stuff like `foo(bar qux, corge)` + if (s[i] === '(') { + x = { type: 'function', value: a.join(''), args: [] } + i += 1 + while (s[i]) { + while (s[i] === ' ') { + i += 1 + } + y = parseCSS1('opacity', s, i) + x.args.push(y.value) + i = y.index + if (s[i] === ')') { + i += 1 + break + } else if (s[i] === ',') { + i += 1 + } + } + // It's a normal value + } else { + x = { type: 'normal', value: a.join('') } + } + } + if (i === old) { + throw new Error('invalid CSS value ' + s) } - return '#' + r.join('') + return { value: x, index: i } } - // 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]) + function parseCSS(k, s) { + s = '' + s // convert to string + var i = 0 + , a = [] + , x + while (s[i]) { + x = parseCSS1(k, s, 0) + a.push(x.value) + i = x.index + } + if (a.length === 1) { + return a[0] } 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 + return { type: 'multi', args: a } + } + } + + /** + * makeTweener: + * @param x: parsed CSS object which represents the start of the animation + * @param y: parsed CSS object which represents the end of the animation + * @returns a function which when given a position returns the interpolated style + */ + function makeTweener(x, y) { + switch (x.type) { + case 'number': + var diff = y.value - x.value + return function (pos) { + // round so we don't get crazy long floats + return Math.round((x.value + (diff * pos)) * thousand) / thousand + x.unit + } + case 'multi': + x.args = x.args.map(function (x, i) { + return makeTweener(x, y.args[i]) + }) + return function (pos) { + return x.args.map(function (f) { return f(pos) }).join(' ') + } + case 'function': + // Normalize to rgba + if (x.value === 'rgb') { + x.args.push({ type: 'number', value: 1, unit: '' }) + x.value = 'rgba' + } + if (y.value === 'rgb') { + y.args.push({ type: 'number', value: 1, unit: '' }) + x.value = 'rgba' + } + x.args = x.args.map(function (x, i) { + return makeTweener(x, y.args[i]) + }) + if (x.value === 'rgba') { + return function (pos) { + return x.value + '(' + Math.round(x.args[0](pos)) + ', ' + + Math.round(x.args[1](pos)) + ', ' + + Math.round(x.args[2](pos)) + ', ' + + x.args[3](pos) + ')' + } + } else { + return function (pos) { + return x.value + '(' + x.args.map(function (f) { return f(pos) }).join(', ') + ')' + } + } + break // TODO: useless break but JSHint complained about it + case 'normal': + return function (pos) { + return pos > 0.5 ? y.value : x.value + } } } - // 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) + function getRelative(x, y) { + switch (y.type) { + case 'number': + // Relative values with += and -= + if (y.relative === '+') { + y.value = x.value + y.value + } else if (y.relative === '-') { + y.value = x.value - y.value + } + return y.value + y.unit + case 'multi': + y.args = y.args.map(function (y, i) { + return getRelative(x.args[i], y) + }) + return y.args.join(' ') + // TODO Should this normalize to rgba? + case 'function': + y.args = y.args.map(function (y, i) { + return getRelative(x.args[i], y) + }) + return y.value + '(' + y.args.join(', ') + ')' + case 'normal': + return y.value + } + } + + function opacity(f) { + return function (pos) { + // TODO: this coerces from a string to a number + return 'alpha(opacity=' + f(pos) * 100 + ')' + } + } + + function getOption(v, el) { + return fun(v) ? v(el) : v } /** @@ -285,102 +389,104 @@ * - 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 + var 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; - } + , ease = options.easing + , points = options.bezier + , a = [] + , els = (elements + ? (els = isFinite(elements.length) + ? elements + : [elements]) + : []) + , old + , begin + , end + , camel + , bez + , i + , k 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([ + , top = getStyle(els[i], 'top') + , xy = [] + + // TODO: pretty ridiculous; split it into multiple statements, or simplify it? + xy.push(parseFloat(getRelative(parseCSS('left', left), + parseCSS('left', options.left + ? getOption(options.left, els[i]) + : 0)))) + xy.push(parseFloat(getRelative(parseCSS('top', top), + parseCSS('top', options.top + ? getOption(options.top, els[i]) + : 0)))) + + bez = fun(points) ? points(els[i], xy) : points + bez.push(xy) + bez.unshift([ parseInt(left, 10), parseInt(top, 10) ]) + a.push({ element: els[i], bezier: bez }) } - for (var k in options) { + for (k in options) { switch (k) { case 'complete': case 'duration': case 'easing': case 'bezier': - continue; - break + 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) + + if (points) { + switch (k) { + case 'left': + case 'top': + case 'bottom': + case 'right': + continue + } + } + + if (k === 'transform') { + camel = transform + } else { + camel = camelize(k) } - 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]) + old = els[i].style[camel] + begin = parseCSS(camel, getStyle(els[i], camel)) + // TODO: inefficient and very hacky, but I don't see a better way to make relative units work + els[i].style[camel] = getRelative(begin, parseCSS(camel, getOption(options[k], els[i]))) + end = parseCSS(camel, getStyle(els[i], camel)) + els[i].style[camel] = old + + if (camel === 'opacity' && !opasity) { + a.push({ element: els[i] + , style: 'filter' + , value: opacity(makeTweener(begin, end)) }) + } else { + a.push({ element: els[i] + , style: camel + , value: makeTweener(begin, end) }) + } } } // 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) + return tween.call(els, duration, function (pos) { + for (i = a.length; i--;) { + if (a[i].bezier) { + var xy = bezier(a[i].bezier, pos) + a[i].element.style.left = xy[0] + 'px' + a[i].element.style.top = xy[1] + 'px' + } else { + a[i].element.style[a[i].style] = a[i].value(pos) } } - }, complete, ease]) + }, complete, ease) } // expose useful methods @@ -388,8 +494,6 @@ morpheus.getStyle = getStyle morpheus.bezier = bezier morpheus.transform = transform - morpheus.parseTransform = parseTransform - morpheus.formatTransform = formatTransform morpheus.easings = {} return morpheus diff --git a/tests/tests.html b/tests/tests.html index e024c24..f316230 100644 --- a/tests/tests.html +++ b/tests/tests.html @@ -146,6 +146,37 @@

Morpheus Tests

}) }) + test('should accept rgb function', 1, function () { + morpheus(el, { + color: 'rgb(10, 20, 30)', + duration: 10, + complete: function () { + ok(el.style.color == 'rgb(10, 20, 30)', 'color is rgb(10, 20, 30)') + } + }) + }) + + test('should accept hsl function', 1, function () { + morpheus(el, { + color: 'hsl(50, 75%, 50%)', + duration: 10, + complete: function () { + ok(el.style.color == 'rgb(223, 191, 31)', 'color is rgb(223, 191, 31)') + } + }) + }) + + test('should work with transparent colors', 1, function () { + el.style.color = 'hsla(10, 100%, 25%, 0.75)' + morpheus(el, { + color: 'hsla(50, 75%, 50%, 0.2)', + duration: 10, + complete: function () { + ok(el.style.color == 'rgba(223, 191, 31, 0.2)', 'color is rgb(223, 191, 31, 0.2)') + } + }) + }) + }); sink('Syntax', function (test, ok) { @@ -175,32 +206,32 @@

Morpheus Tests

}) var transformTest = function(test, ok, title, start, transform, verify) { - verify = typeof verify == 'string' ? [verify] : verify + if (verify == null) { + verify = transform + } var el = document.getElementById('test') - test(title, verify.length, function() { + test(title, 1, function() { el.style[morpheus.transform] = start morpheus(el, { transform: transform, duration: 10, complete: function () { - var v = morpheus.parseTransform(el.style[morpheus.transform]) - for (var i = 0; i < verify.length; i++) { - ok(eval(verify[i]), verify[i]) - } + ok(el.style[morpheus.transform] == verify, verify) } }) }) } sink('Transform', function(test, ok, before, after) { - transformTest(test, ok, 'should accept absolute rotate','rotate(30deg)','rotate(40deg)','v.rotate == 40') - transformTest(test, ok, 'should accept relative rotate','rotate(10deg)','rotate(-=20deg)','v.rotate == -10 || v.rotate == 350') - transformTest(test, ok, 'should accept absolute scale','scale(1)','scale(3)','v.scale == 3') - transformTest(test, ok, 'should accept relative scale','scale(1)','scale(+=3)','v.scale == 4') - transformTest(test, ok, 'should accept absolute skew','skew(10deg,10deg)','skew(30deg,90deg)',['v.skewx == 30','v.skewy == 90']) - transformTest(test, ok, 'should accept relative skew','skew(10deg,10deg)','skew(+=30deg,-=6deg)',['v.skewx == 40','v.skewy == 4']) - transformTest(test, ok, 'should accept absolute translate','translate(-10px,0px)','translate(30px,99px)',['v.translatex == 30','v.translatey == 99']) - transformTest(test, ok, 'should accept relative translate','translate(100px,100px)','translate(+=30px,-=106px)',['v.translatex == 130','v.translatey == -6']) + transformTest(test, ok, 'should accept absolute rotate','rotate(30deg)','rotate(40deg)') + transformTest(test, ok, 'should accept relative rotate','rotate(10deg)','rotate(-=20deg)', 'rotate(-10deg)') + transformTest(test, ok, 'should accept absolute scale','scale(1)','scale(3)') + transformTest(test, ok, 'should accept relative scale','scale(1)','scale(+=3)', 'scale(4)') + transformTest(test, ok, 'should accept absolute skew','skew(10deg,10deg)','skew(30deg, 90deg)') + transformTest(test, ok, 'should accept relative skew','skew(10deg,10deg)','skew(+=30deg,-=6deg)', 'skew(40deg, 4deg)') + transformTest(test, ok, 'should accept absolute translate','translate(-10px,0px)','translate(30px, 99px)') + transformTest(test, ok, 'should accept relative translate','translate(100px,100px)','translate(+=30px,-=106px)', 'translate(130px, -6px)') + transformTest(test, ok, 'should accept scaleY','scaleY(0)','scaleY(0.5)') }) sink('Out', function (test, ok) {