jquery.keyframeanimation.js | |
---|---|
An animation framework based on a CSS animations-like syntax. This project is hosted on GitHub. Annotated source code and usage examples are available online. | |
Dependencies
| |
References | |
Usage | |
Settings
| |
Author | |
| ;(function($, window, undefined) {
|
Public Interface |
|
Call the specified method (passing along subsequent arguments), get
the value of the specified property, or default to calling the
| $.fn.keyframeAnimation = function(propertyOrSettings/* [, ...] */) {
if($.fn.keyframeAnimation[propertyOrSettings]) {
var property = $.fn.keyframeAnimation[propertyOrSettings]; |
Is this property callable? | if(property.apply) {
return property.apply(this, Array.prototype.slice.call(arguments, 1));
} else {
return property;
}
} else {
return $.fn.keyframeAnimation.initialize.apply(this, arguments);
}
};
$.fn.keyframeAnimation.version = '0.3.5';
|
Begin the animation cycle for the jQuery element set ( | $.fn.keyframeAnimation.initialize = function(settings) { |
Every setting has a default value which emulates the CSS defaults as closely as possible. The animation won't do anything if some of the settings are left at their default values. | var defaultSettings = {
keyframes: {},
delays: [],
animationDuration: 0,
animationIterationCount: 1,
animationTimingFunction: 'ease'
};
settings = $.extend({}, defaultSettings, settings);
var elements = this;
|
Create a namespace for element-associated data owned by this plugin. | if(elements.data('keyframeAnimation') === undefined) {
elements.data('keyframeAnimation', {});
}
|
Sort percentages for consistent iteration order. | var orderedKeyframePercentages = (Object.keys ?
Object.keys(settings.keyframes) :
$.map(settings.keyframes, function(styles, percentage) { return percentage; })
).sort();
var currentIteration = 0; |
This function is called repeatedly to perform the animation cycle. | var iterateAnimation = function() {
currentIteration++;
|
If there are more animation iterations: | if(settings.animationIterationCount === 'infinite' || currentIteration <= settings.animationIterationCount) {
elements.data('keyframeAnimation').timeouts = [];
|
Animate each element and tell the next iteration to run
after the delay specified by | elements.each(animateElement);
elements.data('keyframeAnimation').timeouts.push(window.setTimeout(iterateAnimation, settings.animationDuration));
} else { |
Otherwise, animation iteration is complete. Do some cleanup. | delete elements.data('keyframeAnimation').timeouts;
}
};
|
To animate a single element, iterate over each keyframe and set up an animation with the appropriate delay (as determined by the element's delay and the keyframe percentage) and duration (calculated using the difference between keyframe percentages). | var animateElement = function(elementIndex, element) {
var elementDelay = settings.delays[elementIndex] || 0;
|
Set up transitions between each keyframe. | $.each(orderedKeyframePercentages, function(keyframeIndex, percentage) {
var styles = settings.keyframes[percentage];
|
Figure out the duration and offset for this keyframe. | /* TODO: Move this out of the loop so it only needs to be
computed once (or memoize). */
var percentageBetweenKeyframes, keyframeOffset;
if(percentage != 0) { |
These | |
Percentage of total time that this particular keyframe transition should take; the interval between this keyframe and the previous one. | percentageBetweenKeyframes = (percentage - orderedKeyframePercentages[keyframeIndex - 1]);
|
Offset transition animation start times based on the percentage of the previous keyframe. | keyframeOffset = settings.animationDuration * orderedKeyframePercentages[keyframeIndex - 1];
} else { |
The 0% keyframe's styles are applied instantly. | percentageBetweenKeyframes = 0;
keyframeOffset = 0;
}
|
How long the transition to this keyframe should last. | var interpolationDuration = settings.animationDuration * percentageBetweenKeyframes;
|
Delay transition start as needed. | elements.data('keyframeAnimation').timeouts.push(window.setTimeout(function() { |
Complete any previous animations immediately before
beginning the next animation. At first glance this
may appear to be a complicated way of doing
| /* FIXME? Will calling `.stop()` thwart attempts to
resolve the "skipped styles" bug? (Since the
solution to that will probably need multiple
simultaneous animations.) Maybe there should be
one queue per keyframe? */
$(element).stop('keyframeAnimation', false, true).animate(styles, {
duration: interpolationDuration,
queue: 'keyframeAnimation',
easing: getEasingFunction(settings.animationTimingFunction)
}).dequeue('keyframeAnimation');
}, keyframeOffset + elementDelay));
});
};
|
Start the animation cycle (it will repeat itself). | iterateAnimation();
|
Allow chaining. | return this;
};
|
Immediately clear all animation timers; note that in-progress
| $.fn.keyframeAnimation.abort = function() {
if(this.data('keyframeAnimation').timeouts) {
$.each(this.data('keyframeAnimation').timeouts, function(index, timeoutID) {
window.clearTimeout(timeoutID);
});
delete this.data('keyframeAnimation').timeouts;
}
|
Allow chaining. | return this;
};
|
Private Helpers/Variables |
|
var cubicBezierTimingFunctionPoints = {
ease: [
{ x: 0.25, y: 0.1 },
{ x: 0.25, y: 1 }
],
linear: [
{ x: 0, y: 0 },
{ x: 1, y: 1 }
],
easeIn: [
{ x: 0.42, y: 0 },
{ x: 1, y: 1 }
],
easeOut: [
{ x: 0, y: 0 },
{ x: 0.58, y: 1 }
],
easeInOut: [
{ x: 0.42, y: 0 },
{ x: 0.58, y: 1 }
]
};
| |
| /* FIXME? Is the extra complexity of creating these on-the-fly worth the
small performance savings and the smaller `$.easing` namespace
footprint? Since `$.easing` is externally-visible, will it be
confusing to not always have all css* easing functions present?
Something like this would definitely still be necessary for custom
timing functions (e.g. 'cubic-bezier(x1, y1, x2, y2)'). */
var getEasingFunction = function(timingFunctionName) { |
The | var easingName = 'css' + timingFunctionName.charAt(0).toUpperCase() + timingFunctionName.slice(1);
if($.easing[easingName] === undefined) { |
Create a new | var points = cubicBezierTimingFunctionPoints[timingFunctionName];
$.easing[easingName] = function(percentComplete, millisecondsSince, startValue, endValue, totalDuration) {
var totalDurationSeconds = totalDuration / 1000;
return cubicBezierAtTime(percentComplete, points[0].x, points[0].y, points[1].x, points[1].y, totalDurationSeconds);
};
}
return easingName;
};
|
Used to create cubic bezier timing functions. This code is adapted from Christian Effenberger's public domain implementation of WebKit algorithms in WebCore/page/animation/AnimationBase.cpp. | /* TODO: more descriptive variable names. */
var cubicBezierAtTime = function(t, p1x, p1y, p2x, p2y, duration) { |
Calculate the polynomial coefficients, implicit first and last control points are (0,0) and (1,1). | var cx = 3 * p1x;
var bx = 3 * (p2x - p1x) - cx;
var ax = 1 - cx - bx;
var cy = 3 * p1y;
var by = 3 * (p2y - p1y) - cy;
var ay = 1 - cy - by;
|
'ax t^3 + bx t^2 + cx t' expanded using Horner's rule. | var sampleCurveX = function(t) {
return ((ax * t + bx) * t + cx) * t;
};
var sampleCurveY = function(t) {
return ((ay * t + by) * t + cy) * t;
};
var sampleCurveDerivativeX = function(t) {
return (3 * ax * t + 2 * bx) * t + cx;
};
|
Given an x value, find a parametric value it came from. | var solveCurveX = function(x, epsilon) {
var t0, t1, t2, x2, d2, i;
|
First try a few iterations of Newton's method, which is normally very quick. | for(t2 = x, i = 0; i < 8; i++) {
x2 = sampleCurveX(t2) - x;
if(Math.abs(x2) < epsilon) {
return t2;
}
d2 = sampleCurveDerivativeX(t2);
if(Math.abs(d2) < 1e-6) {
break;
}
t2 = t2 - x2 / d2;
}
|
Fall back to the bisection method for reliability. | t0 = 0;
t1 = 1;
t2 = x;
if(t2 < t0) {
return t0;
}
if(t2 > t1) {
return t1;
}
while(t0 < t1) {
x2 = sampleCurveX(t2);
if(Math.abs(x2 - x) < epsilon) {
return t2;
}
if(x > x2) {
t0 = t2;
} else {
t1 = t2;
}
t2 = (t1 - t0) * 0.5 + t0;
}
return t2; // Failure. /* FIXME: What does this imply? */
};
|
The epsilon value to pass given that the animation is going to
run over | var epsilon = 1 / (200 * duration);
|
Convert from input time to parametric value in curve, then from that to output time. | return sampleCurveY(solveCurveX(t, epsilon));
};
}(jQuery, window));
|