1 | /*
|
2 | * Scroller
|
3 | * http://github.com/zynga/scroller
|
4 | *
|
5 | * Copyright 2011, Zynga Inc.
|
6 | * Licensed under the MIT License.
|
7 | * https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
|
8 | *
|
9 | * Based on the work of: Unify Project (unify-project.org)
|
10 | * http://unify-project.org
|
11 | * Copyright 2011, Deutsche Telekom AG
|
12 | * License: MIT + Apache (V2)
|
13 | */
|
14 |
|
15 | /**
|
16 | * Generic animation class with support for dropped frames both optional easing and duration.
|
17 | *
|
18 | * Optional duration is useful when the lifetime is defined by another condition than time
|
19 | * e.g. speed of an animating object, etc.
|
20 | *
|
21 | * Dropped frame logic allows to keep using the same updater logic independent from the actual
|
22 | * rendering. This eases a lot of cases where it might be pretty complex to break down a state
|
23 | * based on the pure time difference.
|
24 | */
|
25 | (function(global) {
|
26 | var time = Date.now || function() {
|
27 | return +new Date();
|
28 | };
|
29 | var desiredFrames = 60;
|
30 | var millisecondsPerSecond = 1000;
|
31 | var running = {};
|
32 | var counter = 1;
|
33 |
|
34 | // Create namespaces
|
35 | if (!global.core) {
|
36 | global.core = { effect : {} };
|
37 |
|
38 | } else if (!core.effect) {
|
39 | core.effect = {};
|
40 | }
|
41 |
|
42 | core.effect.Animate = {
|
43 |
|
44 | /**
|
45 | * A requestAnimationFrame wrapper / polyfill.
|
46 | *
|
47 | * @param callback {Function} The callback to be invoked before the next repaint.
|
48 | * @param root {HTMLElement} The root element for the repaint
|
49 | */
|
50 | requestAnimationFrame: (function() {
|
51 |
|
52 | // Check for request animation Frame support
|
53 | var requestFrame = global.requestAnimationFrame || global.webkitRequestAnimationFrame || global.mozRequestAnimationFrame || global.oRequestAnimationFrame;
|
54 | var isNative = !!requestFrame;
|
55 |
|
56 | if (requestFrame && !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(requestFrame.toString())) {
|
57 | isNative = false;
|
58 | }
|
59 |
|
60 | if (isNative) {
|
61 | return function(callback, root) {
|
62 | requestFrame(callback, root)
|
63 | };
|
64 | }
|
65 |
|
66 | var TARGET_FPS = 60;
|
67 | var requests = {};
|
68 | var requestCount = 0;
|
69 | var rafHandle = 1;
|
70 | var intervalHandle = null;
|
71 | var lastActive = +new Date();
|
72 |
|
73 | return function(callback, root) {
|
74 | var callbackHandle = rafHandle++;
|
75 |
|
76 | // Store callback
|
77 | requests[callbackHandle] = callback;
|
78 | requestCount++;
|
79 |
|
80 | // Create timeout at first request
|
81 | if (intervalHandle === null) {
|
82 |
|
83 | intervalHandle = setInterval(function() {
|
84 |
|
85 | var time = +new Date();
|
86 | var currentRequests = requests;
|
87 |
|
88 | // Reset data structure before executing callbacks
|
89 | requests = {};
|
90 | requestCount = 0;
|
91 |
|
92 | for(var key in currentRequests) {
|
93 | if (currentRequests.hasOwnProperty(key)) {
|
94 | currentRequests[key](time);
|
95 | lastActive = time;
|
96 | }
|
97 | }
|
98 |
|
99 | // Disable the timeout when nothing happens for a certain
|
100 | // period of time
|
101 | if (time - lastActive > 2500) {
|
102 | clearInterval(intervalHandle);
|
103 | intervalHandle = null;
|
104 | }
|
105 |
|
106 | }, 1000 / TARGET_FPS);
|
107 | }
|
108 |
|
109 | return callbackHandle;
|
110 | };
|
111 |
|
112 | })(),
|
113 |
|
114 |
|
115 | /**
|
116 | * Stops the given animation.
|
117 | *
|
118 | * @param id {Integer} Unique animation ID
|
119 | * @return {Boolean} Whether the animation was stopped (aka, was running before)
|
120 | */
|
121 | stop: function(id) {
|
122 | var cleared = running[id] != null;
|
123 | if (cleared) {
|
124 | running[id] = null;
|
125 | }
|
126 |
|
127 | return cleared;
|
128 | },
|
129 |
|
130 |
|
131 | /**
|
132 | * Whether the given animation is still running.
|
133 | *
|
134 | * @param id {Integer} Unique animation ID
|
135 | * @return {Boolean} Whether the animation is still running
|
136 | */
|
137 | isRunning: function(id) {
|
138 | return running[id] != null;
|
139 | },
|
140 |
|
141 |
|
142 | /**
|
143 | * Start the animation.
|
144 | *
|
145 | * @param stepCallback {Function} Pointer to function which is executed on every step.
|
146 | * Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }`
|
147 | * @param verifyCallback {Function} Executed before every animation step.
|
148 | * Signature of the method should be `function() { return continueWithAnimation; }`
|
149 | * @param completedCallback {Function}
|
150 | * Signature of the method should be `function(droppedFrames, finishedAnimation) {}`
|
151 | * @param duration {Integer} Milliseconds to run the animation
|
152 | * @param easingMethod {Function} Pointer to easing function
|
153 | * Signature of the method should be `function(percent) { return modifiedValue; }`
|
154 | * @param root {Element ? document.body} Render root, when available. Used for internal
|
155 | * usage of requestAnimationFrame.
|
156 | * @return {Integer} Identifier of animation. Can be used to stop it any time.
|
157 | */
|
158 | start: function(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) {
|
159 |
|
160 | var start = time();
|
161 | var lastFrame = start;
|
162 | var percent = 0;
|
163 | var dropCounter = 0;
|
164 | var id = counter++;
|
165 |
|
166 | if (!root) {
|
167 | root = document.body;
|
168 | }
|
169 |
|
170 | // Compacting running db automatically every few new animations
|
171 | if (id % 20 === 0) {
|
172 | var newRunning = {};
|
173 | for (var usedId in running) {
|
174 | newRunning[usedId] = true;
|
175 | }
|
176 | running = newRunning;
|
177 | }
|
178 |
|
179 | // This is the internal step method which is called every few milliseconds
|
180 | var step = function(virtual) {
|
181 |
|
182 | // Normalize virtual value
|
183 | var render = virtual !== true;
|
184 |
|
185 | // Get current time
|
186 | var now = time();
|
187 |
|
188 | // Verification is executed before next animation step
|
189 | if (!running[id] || (verifyCallback && !verifyCallback(id))) {
|
190 |
|
191 | running[id] = null;
|
192 | completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, false);
|
193 | return;
|
194 |
|
195 | }
|
196 |
|
197 | // For the current rendering to apply let's update omitted steps in memory.
|
198 | // This is important to bring internal state variables up-to-date with progress in time.
|
199 | if (render) {
|
200 |
|
201 | var droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1;
|
202 | for (var j = 0; j < Math.min(droppedFrames, 4); j++) {
|
203 | step(true);
|
204 | dropCounter++;
|
205 | }
|
206 |
|
207 | }
|
208 |
|
209 | // Compute percent value
|
210 | if (duration) {
|
211 | percent = (now - start) / duration;
|
212 | if (percent > 1) {
|
213 | percent = 1;
|
214 | }
|
215 | }
|
216 |
|
217 | // Execute step callback, then...
|
218 | var value = easingMethod ? easingMethod(percent) : percent;
|
219 | if ((stepCallback(value, now, render) === false || percent === 1) && render) {
|
220 | running[id] = null;
|
221 | completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, percent === 1 || duration == null);
|
222 | } else if (render) {
|
223 | lastFrame = now;
|
224 | core.effect.Animate.requestAnimationFrame(step, root);
|
225 | }
|
226 | };
|
227 |
|
228 | // Mark as running
|
229 | running[id] = true;
|
230 |
|
231 | // Init first step
|
232 | core.effect.Animate.requestAnimationFrame(step, root);
|
233 |
|
234 | // Return unique animation ID
|
235 | return id;
|
236 | }
|
237 | };
|
238 | })(global);
|
239 |
|