Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(70)

Side by Side Diff: third_party/WebKit/Source/core/page/scrolling/snap/SnapManager.js

Issue 1333323003: SnapManager implementation using V8 Extras - {WIP} Base URL: https://chromium.googlesource.com/chromium/blink.git@master
Patch Set: Update with latest master Created 4 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 (function(global, binding, v8) {
6 'use strict';
7
8 // V8 "Imports":
9 // - %CreatePrivateOwnSymbol
10 // - %_CallFunction
11 // - %AddNamedProperty
12 // - %HasOwnProperty
13 // - $promiseThen, $promiseCreate, $promiseResolve, $promiseReject
14 // - InternalPackedArray
15
16
17 const Number = global.Number;
18 const Math = global.Math;
19 const Symbol = global.Symbol;
20 const Date = global.Date;
21 const Map = global.Map;
22 // const performance = window.performance;
23 // const console = window.console;
24
25 const undefined = void 0;
26 const DONT_ENUM = 2;
27 const FLING_VELOCITY_THRESHOLD = 400; // dip/s
28
29 var snapper = Symbol('[[Snapper]]');
30
31 // A reference to the window which is initialized by SnapManager constructor
32 // This is needed because {request,cancel}AnimationFrame are not exposed in gl obal
33 let window;
34
35 function log() {
36 console.log.apply(console, arguments);
37 var message = "";
38 for (var k in arguments) {
39 message += arguments[k];
40 }
41
42 //%DebugPrint(message);
43 }
44
45 /*
46 * Responsible for receiving updates from Blink about changes to snap containe rs
47 */
48 class SnapManager {
49 constructor(win) {
50 log('Initialized snap manager');
51 log(win);
52 log(win.requestAnimationFrame);
53 window = win;
54 }
55
56 updateSnapContainer(element, horizontalOffsets, verticalOffsets) {
57 log(element);
58 if (!element[snapper]) {
59 // TODO(majidvp): I have put the snap related functionality in snap cont roller but perhaps
60 // they can be in the elements prototype?
61 element[snapper] = new Snapper(element);
62 // Customize scrolling by override native scroll handler
63 if (element.setApplyScroll) {
64 element.setApplyScroll(element[snapper].applyScroll, "perform-before-n ative-scroll");
65 } else {
66 log("Element::setApplyScroll is missing. Make sure ScrollCustomization feature is enabled.");
67 }
68 }
69
70 element[snapper].setSnapOffsets(horizontalOffsets, verticalOffsets);
71 }
72 }
73
74 /* Encapsulates the logic for determining when and where to snap for each snap container */
75 class Snapper {
76 constructor(element) {
77 this.element = element;
78 // Make applyScroll bounded as it is called using function invocation patt ern
79 this.applyScroll = this.applyScroll.bind(this);
80 }
81
82 setSnapOffsets(h, v) {
83 this.snapOffsets = [h, v];
84 // TODO: perhaps update inflight snaps?
85 }
86
87 // custom apply scroll handler
88 applyScroll(scrollState) {
89 log("scrollState", scrollState);
90 if (scrollState.isBeginning) {
91 // A new scroll clears all existing snap animations for this element
92 this.clearSnapState();
93 AnimationController.instance().remove(this);
94 return;
95 } else if (!(scrollState.inInertialPhase || scrollState.isEnding)) {
96 // Active user scrolling
97 return;
98 }
99 let consumedDelta = this.snap(scrollState);
100 scrollState.consumeDelta(consumedDelta[0], consumedDelta[1]);
101 }
102
103
104 // Make snapping decision separate on each dimension and creates an independ ent
105 // 1D snap animations for each. This allows us to fling in one direction whi le
106 // snap in another.
107 snap(scrollState) {
108 let consumedDelta = [0, 0];
109
110 for (let i of[0, 1]) {
111 let property = i == 0 ? 'scrollLeft' : 'scrollTop';
112 let delta = i == 0 ? scrollState.deltaX : scrollState.deltaY;
113 let velocity = i == 0 ? scrollState.velocityX : scrollState.velocityY;
114 let position = this.scrollPosition()[i];
115
116 if (this.snapOffsets[i].length == 0)
117 continue;
118
119 // consume all scroll delta while snap is in progress preventing native scroll
120 if (this.isSnapping[i]) {
121 consumedDelta[i] = delta;
122 continue;
123 }
124
125 // Wait for fling to slow down before starting the snap
126 // TODO(majidvp): Handle overscroll cases as well. One option is to clam p the velocity when overscrolling
127 if (scrollState.inInertialPhase && Math.abs(velocity) > FLING_VELOCITY_T HRESHOLD)
128 continue;
129
130 if (scrollState.inInertialPhase && Math.abs(velocity) > 0 && !this.fling Curves[i])
131 this.flingCurves[i] = this.createFlingCurve(position, velocity);
132
133 let td = this.snapTargetAndDuration(position, velocity, this.snapOffsets [i], this.flingCurves[i]),
134 target = td.target,
135 duration = td.duration;
136
137 if (position == target)
138 continue;
139
140 // TODO(majidvp): Perhaps stay away from creating new animation objects to reduce
141 // V8 GC pressure
142 let animation = new Animation({
143 element: this.element,
144 property: property,
145 begin: position,
146 end: target,
147 duration: duration,
148 v0: velocity,
149 curveType: 'motion' /* linear | motion */
150 });
151
152 AnimationController.instance().add(this, property, animation);
153 this.isSnapping[i] = true;
154 log('horizontal:' + this.snapOffsets[0] + ' vertical:' + this.snapOffs ets[1]);
155 }
156
157 return consumedDelta;
158 }
159
160 clearSnapState() {
161 this.flingCurves = [];
162 this.isSnapping = [false, false];
163 }
164
165 createFlingCurve(position, velocity) {
166 let now = Utils.now();
167 return new FlingCurve(position, -1 * velocity, now);
168 }
169
170 snapTargetAndDuration(position, velocity, snapOffsets, flingCurve) {
171 let target = flingCurve ? flingCurve.getFinalPosition() : position;
172
173 //let snapTarget = Utils.snapWithInterval(target, velocity, this.repeat(), size);
174 let snapTarget = Utils.snapWithOffsets(target, velocity, snapOffsets);
175 let snapDuration = this.estimateDuration(position, snapTarget, velocity, f lingCurve);
176
177 log('position:' + position + ' velocity:' + velocity + ' => target:' + tar get + ' snapped-target: ' + snapTarget + ' @(' + snapDuration + ')');
178 return {
179 target: snapTarget,
180 duration: snapDuration
181 };
182 }
183
184 estimateDuration(current, target, velocity, flingCurve) {
185 // If there is a fling curve then use its estimated duration otherwise
186 // assume a constant snap speed to calculate duration.
187 let SPEED = 2; // 2px/ms
188 return flingCurve ? flingCurve.getDuration() * 1000 : Math.abs(target - cu rrent) / SPEED;
189 }
190
191 scrollSize() {
192 return [this.element.scrollWidth, this.element.scrollHeight];
193 }
194
195 scrollPosition() {
196 return [this.element.scrollLeft, this.element.scrollTop];
197 }
198 }
199
200 class Utils {
201 static snapWithInterval(value, velocity, interval, max) {
202 let roundingFn = velocity[i] == 0 ? Math.round : (velocity[i] > 0 ? Math.c eil : Math.floor);
203 return Utils.clamp(roundingFn(value[i] / interval) * interval, 0, max[i]);
204 }
205
206 // Find closest match in values
207 static snapWithOffsets(value, velocity, offsets) {
208 let snappedOffset = Utils.binarySearchClosest(value, offsets);
209 return snappedOffset;
210 }
211
212 // Find the value in array that is closes to v
213 static binarySearchClosest(v, array) {
214 if (array.length == 0)
215 return v;
216
217 let left = 0,
218 right = array.length;
219 // Clamp value to ensure it falls in range range
220 v = Utils.clamp(v, array[0], array[right - 1]);
221
222 while (right - left > 1) {
223 let mid = Math.ceil((right + left) / 2);
224 if (v < array[mid])
225 right = mid;
226 else
227 left = mid;
228 }
229
230 // min <= v <= max
231 let min = array[left],
232 max = array[right];
233 return max - v <= v - min ? max : min;
234 }
235
236 static clamp(num, min, max) {
237 return Math.min(Math.max(num, min), max);
238 }
239
240 static now() {
241 return Date.now() / 1000; // performance.now() / 1000
242 }
243 }
244
245 var controllerSingleton;
246 /* Animation controller is responsible to controlling all active snap animatio ns and properly abort or resume them upon user touch. */
247 class AnimationController {
248
249 static instance() {
250 if (!controllerSingleton)
251 controllerSingleton = new AnimationController();
252
253 return controllerSingleton;
254 }
255
256 // TODO(majidvp): make this private
257 constructor() {
258 this.active = new Map();
259 }
260
261 add(snapper, property, animation) {
262 if (!this.active.has(snapper))
263 this.active.set(snapper, {});
264
265 let snapAnimations = this.active.get(snapper)
266 snapAnimations[property] = animation;
267
268 log('start animation');
269 snapper.element.addEventListener('touchstart', AnimationController.onTouch Start);
270 snapper.element.addEventListener('touchend', AnimationController.onTouchEn d);
271
272 animation.start();
273 }
274
275 remove(snapper) {
276 let snapAnimations = this.active.get(snapper);
277 if (snapAnimations)
278 for (let key in snapAnimations)
279 snapAnimations[key].stop();
280 this.active.delete(snapper);
281
282 snapper.isOnhold = false;
283 snapper.element.removeEventListener('touchstart', AnimationController.onTo uchStart);
284 snapper.element.removeEventListener('touchend', AnimationController.onTouc hEnd);
285 }
286
287 // aborts snap animation for snapper and puts it on hold
288 abort(snapper) {
289 let snapAnimations = this.active.get(snapper);
290 if (!snapAnimations || snapAnimations.length == 0)
291 return;
292
293 snapper.clearSnapState();
294 snapper.isOnhold = true;
295 for (let key in snapAnimations)
296 snapAnimations[key].stop();
297
298 this.active.delete(snapper);
299 }
300
301 // resume any aborted snap animation for this snapper
302 resume(snapper) {
303 if (!snapper || !snapper.isOnhold)
304 return;
305 // resuming snap animation is equivalent to snapping at the end of a scrol l gesture with zero delta, and velocity
306 snapper.snap(new ScrollState(0, 0, 0, 0, 0, false, false, true));
307 }
308
309 static onTouchStart() {
310 log("touchstart: abort snap animations");
311 AnimationController.instance().abort(this[snapper]);
312 }
313
314 static onTouchEnd() {
315 log("touchend: re-snap");
316 AnimationController.instance().resume(this[snapper]);
317 }
318 }
319
320 class Animation {
321 /*
322 * Setup necessary RAF loop for snap animation to reach snap destination
323 */
324 constructor(params) {
325 for (let attr of['element', 'property', 'begin', 'end', 'duration', 'dista nce'])
326 this[attr] = params[attr];
327
328 this.distance = this.end - this.begin;
329 switch (params.curveType) {
330 case 'motion':
331 this.curve = SnapCurve.motion(-1 * params.v0, this.distance);
332 break;
333 case 'linear':
334 default:
335 this.curve = SnapCurve.linear(this.distance);
336 }
337
338 // bind step
339 this.step = this.step.bind(this);
340 }
341
342 start() {
343 log('Animate to scroll position ' + this.end + ' in ' + this.duration + ' ms');
344 if (this.duration <= 0) {
345 log("WARNING: duration must be positive");
346 return;
347 }
348
349 this.startTime = undefined;
350 this.rAF = window.requestAnimationFrame(this.step);
351 }
352
353 stop() {
354 window.cancelAnimationFrame(this.rAF);
355 }
356
357 step(now) {
358 log("animation step");
359 this.startTime = this.startTime || now;
360
361 let progress = Math.min(now - this.startTime, this.duration) / this.durati on;
362 let delta = this.curve(progress);
363 let next = Math.floor(this.begin + delta);
364 log(this.property + '=' + next + ' (delta:' + delta + ' progress:' + prog ress + ' now:' + now + ')');
365
366 this.element[this.property] = next;
367
368 if (progress == 1) {
369 this.stop();
370 // TODO: callback and let AnimationController know that this is complete
371 return;
372 }
373
374 this.rAF = window.requestAnimationFrame(this.step);
375 }
376
377 }
378
379 class SnapCurve {
380 static linear(distance) {
381 return function(progress) {
382 return progress * distance;
383 };
384 }
385
386 /**
387 * Constructs a motion curve D(t) which satisfies these conditions:
388 * - D(0) = 0 and D(duration)= distance.
389 * - Velocity (dD/dt) is continuous
390 * - Velocity is v0 at start, t=0, and 0 at the end, t=1.
391 */
392 static motion(v0, distance) {
393 let a = 3 * v0 - 6 * distance,
394 b = 6 * distance - 4 * v0;
395
396 //let formula = '0.33*' + a + '*t^3+0.5*' + b + '*t^2+' + v0 + '*t' + ', y =' + distance;
397 //log('Motion Curve: ' + 'https://www.wolframalpha.com/input/?i=' + encode URIComponent('plot ' + formula + ' for t=0 to 1'));
398 return function curve(t) {
399 // to ensure we always end up at distance at the end.
400 if (t === 1) {
401 return distance;
402 }
403
404 let t2 = t * t,
405 t3 = t * t * t;
406 return 0.33 * a * t3 + 0.5 * b * t2 + v0 * t;
407 };
408 }
409 }
410
411 // From src/content/public/common/renderer_preferences.cc
412 const p = [-5707.62, 172, 3.7];
413
414 // Times are in second
415 class FlingCurve {
416 constructor(initPosition, initVelocity, startTime) {
417
418 this.sign = initVelocity >= 0 ? 1 : -1;
419
420 initVelocity = Math.abs(initVelocity);
421 initVelocity = Math.min(initVelocity, this.velocity(0));
422 initVelocity = Math.max(initVelocity, 0);
423
424 this.localStartTime = this.timeAtVelocity(initVelocity);
425 this.localEndTime = this.timeAtVelocity(0);
426 this.endTime = startTime + this.localEndTime - this.localStartTime;
427 this.positionOffset = initPosition - this.sign * this.position(this.localS tartTime);
428 this.timeOffset = this.localStartTime - startTime;
429
430 // let formula = -p[0] + "*" + p[2] + "*" + " e^(" + (-p[2]) + " * t) + " + (-p[1]) + ", y=" + initial_velocity;
431 //log('Fling Velocity Curve: ' + 'https://www.wolframalpha.com/input/?i=' + encodeURIComponent('plot ' + formula + " for t=0 to 2"));
432 //log('Duration:' + this.getDuration() * 1000 + " Distance:" + this.getDis tance());
433 }
434
435 // From src/content/child/touch_fling_gesture_curve.cc.
436 // Uses local time and return local position
437 position(localTime) {
438 let t = localTime;
439 return p[0] * Math.exp(-p[2] * t) - p[1] * t - p[0];
440 }
441
442 velocity(localTime) {
443 let t = localTime;
444 return -p[0] * p[2] * Math.exp(-p[2] * t) - p[1];
445 }
446
447 timeAtVelocity(velocity) {
448 return (-Math.log((velocity + p[1]) / (-p[0] * p[2])) / p[2]);
449 }
450
451 // take global time and return global position
452 getPositionAtTime(time) {
453 time = Math.max(time, this.endTime);
454 return this.positionOffset + this.sign * this.position(time + this.timeOff set);
455 };
456
457 getFinalPosition() {
458 return this.positionOffset + this.sign * this.position(this.localEndTime);
459 };
460 getDuration() {
461 return this.localEndTime - this.localStartTime;
462 };
463 getDistance() {
464 return this.position(this.localEndTime) - this.position(this.localStartTim e);
465 }
466 }
467
468 //
469 // Additions to the global
470 //
471
472 //%AddNamedProperty(global, 'SnapManager', SnapManager, DONT_ENUM);
473
474 //
475 // Exports for Blink to use
476 //
477
478 binding.CreateSnapManager = function(window) {
479 return new SnapManager(window);
480 }
481
482 binding.UpdateSnapContainer = function(snapManager, element, horizontalOffsets , verticalOffsets) {
483 snapManager.updateSnapContainer(element, horizontalOffsets, verticalOffsets) ;
484 }
485
486 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698