UNPKG

6.72 kBJavaScriptView Raw
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