Permalink
Browse files

Beginning of CSS3 transitions implementation. Added parser for transi…

…tion property, implementations for cubic bezier and step timing functions, and basic color interpolation.
  • Loading branch information...
1 parent e7d3630 commit 59a89eded52906af2a99a63d978c8194e05d2202 @lojjic committed May 7, 2012
Showing with 242 additions and 4 deletions.
  1. +4 −0 build.xml
  2. +31 −0 sources/Color.js
  3. +6 −2 sources/Element.js
  4. +4 −2 sources/Tokenizer.js
  5. +119 −0 sources/TransitionStyleInfo.js
  6. +78 −0 sources/TransitionTimingFunction.js
View
@@ -32,6 +32,7 @@
<fileset file="${src_dir}/BgSize.js" />
<fileset file="${src_dir}/Angle.js" />
<fileset file="${src_dir}/Color.js" />
+ <fileset file="${src_dir}/TransitionTimingFunction.js" />
<fileset file="${src_dir}/Tokenizer.js" />
<fileset file="${src_dir}/BoundsInfo.js" />
<fileset file="${src_dir}/StyleInfoBase.js" />
@@ -41,6 +42,7 @@
<fileset file="${src_dir}/BorderImageStyleInfo.js" />
<fileset file="${src_dir}/BoxShadowStyleInfo.js" />
<fileset file="${src_dir}/PaddingStyleInfo.js" />
+ <fileset file="${src_dir}/TransitionStyleInfo.js" />
<fileset file="${src_dir}/VisibilityStyleInfo.js" />
<fileset file="${src_dir}/VmlShape.js" />
<fileset file="${src_dir}/RendererBase.js" />
@@ -79,13 +81,15 @@
<fileset file="${src_dir}/BgSize.js" />
<fileset file="${src_dir}/Angle.js" />
<fileset file="${src_dir}/Color.js" />
+ <fileset file="${src_dir}/TransitionTimingFunction.js" />
<fileset file="${src_dir}/Tokenizer.js" />
<fileset file="${src_dir}/BoundsInfo.js" />
<fileset file="${src_dir}/StyleInfoBase.js" />
<fileset file="${src_dir}/BackgroundStyleInfo.js" />
<fileset file="${src_dir}/BorderStyleInfo.js" />
<fileset file="${src_dir}/BorderImageStyleInfo.js" />
<fileset file="${src_dir}/PaddingStyleInfo.js" />
+ <fileset file="${src_dir}/TransitionStyleInfo.js" />
<fileset file="${src_dir}/RendererBase.js" />
<fileset file="${src_dir}/IE9RootRenderer.js" />
<fileset file="${src_dir}/IE9BackgroundRenderer.js" />
View
@@ -189,6 +189,37 @@ PIE.Color = (function() {
alpha: function() {
this.parse();
return this._alpha;
+ },
+
+
+ /**
+ * Given another target color, calculate and return an intermediate color at a given
+ * percentage between the two.
+ * @param {PIE.Color} toColor The other color being transitioned to
+ * @param {number} percent The percent (number from 0 to 1) between the two colors to calculate
+ * @param {Element} el
+ * @return {PIE.Color} The interpolated color
+ */
+ interpolate: function( toColor, percent, el ) {
+ var hex1 = this.hexValue( el ),
+ hex2 = toColor.hexValue( el ),
+ parse = parseInt,
+ r1 = parse( hex1.substr(1, 2), 16 ),
+ g1 = parse( hex1.substr(3, 2), 16 ),
+ b1 = parse( hex1.substr(5, 2), 16 ),
+ r2 = parse( hex2.substr(1, 2), 16 ),
+ g2 = parse( hex2.substr(3, 2), 16 ),
+ b2 = parse( hex2.substr(5, 2), 16 ),
+ a1 = this.alpha(),
+ a2 = toColor.alpha(),
+ newColor = new Color();
+
+ // Set the internal color/alpha cached values directly so it doesn't have to re-parse
+ newColor._alpha = ( a2 - a1 ) * percent + a1;
+ newColor._color = '#' + ( ( r2 - r1 ) * percent + r1 ).toString( 16 ) +
+ ( ( g2 - g1 ) * percent + g1 ).toString( 16 ) +
+ ( ( b2 - b1 ) * percent + b1 ).toString( 16 );
+ return newColor;
}
};
View
@@ -112,13 +112,15 @@ PIE.Element = (function() {
backgroundInfo: new PIE.BackgroundStyleInfo( el ),
borderImageInfo: new PIE.BorderImageStyleInfo( el ),
borderInfo: new PIE.BorderStyleInfo( el ),
- paddingInfo: new PIE.PaddingStyleInfo( el )
+ paddingInfo: new PIE.PaddingStyleInfo( el ),
+ transitionInfo: new PIE.TransitionStyleInfo( el )
};
styleInfosArr = [
styleInfos.backgroundInfo,
styleInfos.borderInfo,
styleInfos.borderImageInfo,
- styleInfos.paddingInfo
+ styleInfos.paddingInfo,
+ styleInfos.transitionInfo
];
rootRenderer = new PIE.IE9RootRenderer( el, boundsInfo, styleInfos );
childRenderers = [
@@ -134,6 +136,7 @@ PIE.Element = (function() {
borderRadiusInfo: new PIE.BorderRadiusStyleInfo( el ),
boxShadowInfo: new PIE.BoxShadowStyleInfo( el ),
paddingInfo: new PIE.PaddingStyleInfo( el ),
+ transitionInfo: new PIE.TransitionStyleInfo( el ),
visibilityInfo: new PIE.VisibilityStyleInfo( el )
};
styleInfosArr = [
@@ -143,6 +146,7 @@ PIE.Element = (function() {
styleInfos.borderRadiusInfo,
styleInfos.boxShadowInfo,
styleInfos.paddingInfo,
+ styleInfos.transitionInfo,
styleInfos.visibilityInfo
];
rootRenderer = new PIE.RootRenderer( el, boundsInfo, styleInfos );
View
@@ -27,7 +27,8 @@ PIE.Tokenizer = (function() {
OPERATOR: 256,
PERCENT: 512,
STRING: 1024,
- URL: 2048
+ URL: 2048,
+ TIME: 4096
};
/**
@@ -63,7 +64,8 @@ PIE.Tokenizer = (function() {
'px': Type.LENGTH, 'em': Type.LENGTH, 'ex': Type.LENGTH,
'mm': Type.LENGTH, 'cm': Type.LENGTH, 'in': Type.LENGTH,
'pt': Type.LENGTH, 'pc': Type.LENGTH,
- 'deg': Type.ANGLE, 'rad': Type.ANGLE, 'grad': Type.ANGLE
+ 'deg': Type.ANGLE, 'rad': Type.ANGLE, 'grad': Type.ANGLE,
+ 's': Type.TIME, 'ms': Type.TIME
},
colorFunctions: {
@@ -0,0 +1,119 @@
+/**
+ * Handles parsing, caching, and detecting changes to the 'transition' CSS property
+ * @constructor
+ * @param {Element} el the target element
+ */
+PIE.TransitionStyleInfo = PIE.StyleInfoBase.newStyleInfo( {
+
+ cssProperty: 'transition',
+ styleProperty: 'transition',
+
+ parseCss: function( css ) {
+ var props,
+ Type = PIE.Tokenizer.Type,
+ tokenizer, token, tokenType, tokenValue, currentTransition,
+ a, b, c, d;
+
+ function tokenIsChar( token, ch ) {
+ return token && token.tokenValue === ch;
+ }
+
+ function tokenToNumber( token ) {
+ return ( token && ( token.tokenType & Type.NUMBER ) ) ? parseFloat( token.tokenValue, 10 ) : NaN;
+ }
+
+ function flushCurrentTransition() {
+ // Fill in defaults
+ if (!currentTransition.property) {
+ currentTransition.property = 'all';
+ }
+ if (!currentTransition.timingFunction) {
+ currentTransition.timingFunction = 'ease';
+ }
+ if (!currentTransition.delay) {
+ currentTransition.delay = '0';
+ }
+ props.push(currentTransition);
+ currentTransition = 0;
+ }
+
+ if( css ) {
+ tokenizer = new PIE.Tokenizer( css );
+ props = [];
+
+ while( token = tokenizer.next() ) {
+ tokenType = token.tokenType;
+ tokenValue = token.tokenValue;
+ if (!currentTransition) {
+ currentTransition = {
+ // property: 'all',
+ // duration: '0ms',
+ // timingFunction: <PIE.TransitionTimingFunction.*>,
+ // delay: '0ms'
+ };
+ }
+
+ // Time values: first one is duration and second one is delay
+ if (tokenType & Type.TIME && !currentTransition.duration) {
+ currentTransition.duration = tokenValue;
+ }
+ else if (tokenType & Type.TIME && !currentTransition.delay) {
+ currentTransition.delay = tokenValue;
+ }
+
+ // Timing function
+ else if (tokenType & Type.IDENT && !currentTransition.timingFunction && tokenValue in PIE.TransitionTimingFunction.namedEasings) {
+ currentTransition.timingFunction = PIE.TransitionTimingFunction.namedEasings[tokenValue];
+ }
+ else if (tokenType & Type.FUNCTION && !currentTransition.timingFunction) {
+ if ( tokenValue === 'step' &&
+ !isNaN( a = tokenToNumber( tokenizer.next() ) ) && a > 0 && Math.floor( a ) === a &&
+ tokenIsChar( tokenizer.next(), ',' ) &&
+ ( b = tokenizer.next() ) && ( b.tokenType & Type.IDENT ) && ( b.tokenValue === 'start' || b.tokenValue === 'end' ) &&
+ tokenIsChar( tokenizer.next(), ')' )
+ ) {
+ currentTransition.timingFunction = PIE.TransitionTimingFunction.getStepsTimingFunction( a, b );
+ }
+ else if (tokenValue === 'cubic-bezier' &&
+ !isNaN( a = tokenToNumber( tokenizer.next() ) ) && a >= 0 && a <= 1 &&
+ tokenIsChar( tokenizer.next(), ',' ) &&
+ !isNaN( b = tokenToNumber( tokenizer.next() ) ) &&
+ tokenIsChar( tokenizer.next(), ',' ) &&
+ !isNaN( c = tokenToNumber( tokenizer.next() ) ) && c >= 0 && c <= 1 &&
+ tokenIsChar( tokenizer.next(), ',' ) &&
+ !isNaN( d = tokenToNumber( tokenizer.next() ) ) &&
+ tokenIsChar( tokenizer.next(), ')' )
+ ) {
+ currentTransition.timingFunction = PIE.TransitionTimingFunction.getCubicBezierTimingFunction( a, b, c, d );
+ }
+ else {
+ return null;
+ }
+ }
+
+ // Property name
+ else if (tokenType & Type.IDENT && !currentTransition.property) {
+ currentTransition.property = tokenValue;
+ }
+
+ // Comma: start a new value. Only duration is required so fail if it wasn't found.
+ else if (tokenType & Type.OPERATOR && tokenValue === ',' && currentTransition.duration) {
+ flushCurrentTransition();
+ }
+
+ // Something unrecognized - fail!
+ else {
+ return null;
+ }
+ }
+
+ // leftovers
+ if( currentTransition && currentTransition.duration ) {
+ flushCurrentTransition();
+ }
+
+ }
+
+ return props && props.length ? props : null;
+ }
+} );
@@ -0,0 +1,78 @@
+PIE.TransitionTimingFunction = {
+
+ /**
+ * Cubic bezier easing utility by Gaëtan Renaudeau, originally "KeySpline.js", used with
+ * permission of the author. From the writeup at:
+ * http://blog.greweb.fr/2012/02/bezier-curve-based-easing-functions-from-concept-to-implementation/
+ *
+ * Modified to be a factory that returns a Function, and to make the internal utility
+ * functions shared across instances.
+ *
+ * @param {Number} x1
+ * @param {Number} y1
+ * @param {Number} x2
+ * @param {Number} y2
+ * @return Function
+ */
+ getCubicBezierTimingFunction: (function() {
+ function A(aA1, aA2) { return 1.0 - 3.0 * aA2 + 3.0 * aA1; }
+ function B(aA1, aA2) { return 3.0 * aA2 - 6.0 * aA1; }
+ function C(aA1) { return 3.0 * aA1; }
+
+ // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
+ function CalcBezier(aT, aA1, aA2) {
+ return ((A(aA1, aA2)*aT + B(aA1, aA2))*aT + C(aA1))*aT;
+ }
+
+ // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
+ function GetSlope(aT, aA1, aA2) {
+ return 3.0 * A(aA1, aA2)*aT*aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
+ }
+
+ function GetTForX(aX, mX1, mX2) {
+ // Newton raphson iteration
+ var aGuessT = aX,
+ i = 0,
+ currentSlope, currentX;
+ for (; i < 4; ++i) {
+ currentSlope = GetSlope(aGuessT, mX1, mX2);
+ if (currentSlope == 0.0) return aGuessT;
+ currentX = CalcBezier(aGuessT, mX1, mX2) - aX;
+ aGuessT -= currentX / currentSlope;
+ }
+ return aGuessT;
+ }
+
+ return function(mX1, mY1, mX2, mY2) {
+ return function(time) {
+ if (mX1 == mY1 && mX2 == mY2) return time; // linear
+ return CalcBezier(GetTForX(time, mX1, mX2), mY1, mY2);
+ };
+ }
+ })(),
+
+ /**
+ * Stepping function - see http://www.w3.org/TR/css3-transitions/#transition-timing-function-property
+ * @param numSteps The number of steps in the transition
+ * @param changeAt Whether the step up occurs at the 'start' or 'end' of each step interval. Defaults to 'end'.
+ * @return Function
+ */
+ getStepsTimingFunction: function(numSteps, changeAt) {
+ return function(time) {
+ return Math[ changeAt === 'start' ? 'ceil' : 'floor' ]( time * numSteps ) / numSteps;
+ };
+ }
+};
+
+
+PIE.TransitionTimingFunction.namedEasings = {
+ 'ease': PIE.TransitionTimingFunction.getCubicBezierTimingFunction( 0.25, 0.1, 0.25, 1 ),
+ 'linear': PIE.TransitionTimingFunction.getCubicBezierTimingFunction( 0, 0, 1, 1 ),
+ 'ease-in': PIE.TransitionTimingFunction.getCubicBezierTimingFunction( 0.42, 0, 1, 1 ),
+ 'ease-out': PIE.TransitionTimingFunction.getCubicBezierTimingFunction( 0, 0, 0.58, 1 ),
+ 'ease-in-out': PIE.TransitionTimingFunction.getCubicBezierTimingFunction( 0.42, 0, 0.58, 1 ),
+ 'step-start': PIE.TransitionTimingFunction.getStepsTimingFunction( 1, 'start' ),
+ 'step-end': PIE.TransitionTimingFunction.getStepsTimingFunction( 1, 'end' )
+};
+
+

0 comments on commit 59a89ed

Please sign in to comment.