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

Side by Side Diff: third_party/polymer/v1_0/components-chromium/paper-ripple/paper-ripple-extracted.js

Issue 2691393005: MD WebUI: create a Chrome-only <paper-ripple> that's resistant to JS jank (via web animations API) (Closed)
Patch Set: ownerDocument Created 3 years, 10 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
« no previous file with comments | « third_party/polymer/v1_0/components-chromium/paper-ripple/paper-ripple.html ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 (function() { 1 (function() {
2 var Utility = {
3 distance: function(x1, y1, x2, y2) {
4 var xDelta = (x1 - x2);
5 var yDelta = (y1 - y2);
6 2
7 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); 3 var MAX_RADIUS = 300;
dpapad 2017/02/22 18:48:10 Can we add units (px, ms), either in the variable
Dan Beam 2017/02/22 20:19:14 Done.
8 }, 4 var MIN_DURATION = 800;
9 5
10 now: window.performance && window.performance.now ? 6 Polymer({
dpapad 2017/02/22 18:48:10 Should this file be using JSDoc annotations simila
11 window.performance.now.bind(window.performance) : Date.now 7 is: 'paper-ripple',
8
9 behaviors: [Polymer.IronA11yKeysBehavior],
10
11 properties: {
12 center: {type: Boolean, value: false},
13 holdDown: {type: Boolean, value: false, observer: '_holdDownChanged'},
14 recenters: {type: Boolean, value: false},
15 noink: {type: Boolean, value: false},
16 },
17
18 keyBindings: {
19 'enter:keydown': '_onEnterKeydown',
20 'space:keydown': '_onSpaceKeydown',
21 'space:keyup': '_onSpaceKeyup',
22 },
23
24 created: function() {
25 this.ripples = [];
dpapad 2017/02/22 18:48:10 Should we be declaring properties before we assign
Dan Beam 2017/02/22 20:19:14 there's no functional difference to v8 where is t
dpapad 2017/02/22 21:07:26 That is arguable, 1) see "hidden classes" at http
26 },
27
28 attached: function() {
29 this.keyEventTarget = this.parentNode.nodeType == 11 ?
30 Polymer.dom(this).getOwnerRoot().host : this.parentNode;
31 this.listen(this.keyEventTarget, 'up', 'uiUpAction');
32 this.listen(this.keyEventTarget, 'down', 'uiDownAction');
33 },
34
35 detached: function() {
36 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction');
37 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction');
38 this.keyEventTarget = null;
39 },
40
41 simulatedRipple: function() {
42 this.downAction();
43 this.async(function() { this.upAction(); }.bind(this), 1);
dpapad 2017/02/22 18:48:10 Nit: Can you add a comment explaining why the dela
Dan Beam 2017/02/22 20:19:14 Done.
44 },
45
46 uiDownAction: function(e) {
47 if (!this.noink)
48 this.downAction(e);
49 },
50
51 downAction: function(e) {
52 if (this.ripples.length && this.holdDown)
53 return;
54 // TODO(dbeam): some things (i.e. paper-icon-button-light) dynamically
55 // create ripples on 'up', Ripples register an event listener on their
56 // parent (or shadow DOM host) when attached(). This sometimes causes
57 // duplicate events to fire on us.
58 this.debounce('show ripple', function() { this._showRipple(e); }, 1);
59 },
60
61 _showRipple: function(e) {
62 var rect = this.getBoundingClientRect();
63
64 var roundedCenterX = function() { return Math.round(rect.width / 2); };
65 var roundedCenterY = function() { return Math.round(rect.height / 2); };
66
67 var centered = !e || this.center;
68 if (centered) {
69 var x = roundedCenterX();
70 var y = roundedCenterY();
71 } else {
72 var sourceEvent = e.detail.sourceEvent;
73 var x = Math.round(sourceEvent.clientX - rect.left);
74 var y = Math.round(sourceEvent.clientY - rect.top);
75 }
76
77 var distance = function(x1, y1, x2, y2) {
dpapad 2017/02/22 18:48:10 Can this function be moved outside _showRipple? It
Dan Beam 2017/02/22 20:19:14 Done.
78 var xDelta = x1 - x2;
79 var yDelta = y1 - y2;
80 return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
12 }; 81 };
13 82
14 /** 83 var corners = [
15 * @param {HTMLElement} element 84 {x: 0, y: 0},
16 * @constructor 85 {x: rect.width, y: 0},
17 */ 86 {x: 0, y: rect.height},
18 function ElementMetrics(element) { 87 {x: rect.width, y: rect.height},
19 this.element = element; 88 ];
20 this.width = this.boundingRect.width;
21 this.height = this.boundingRect.height;
22 89
23 this.size = Math.max(this.width, this.height); 90 var cornerDistances = corners.map(function(corner) {
91 return Math.round(distance(x, y, corner.x, corner.y));
92 });
93
94 var radius = Math.min(MAX_RADIUS, Math.max.apply(Math, cornerDistances));
95
96 var startTranslate = (x - radius) + 'px, ' + (y - radius) + 'px';
97 if (this.recenters && !centered) {
98 var endTranslate = (roundedCenterX() - radius) + 'px, ' +
dpapad 2017/02/22 18:48:10 Nit: This usage of var relies on JS's hoisting beh
Dan Beam 2017/02/22 20:19:14 but this doesn't use let and hoisting's not going
dpapad 2017/02/22 21:07:26 Fair enough.
99 (roundedCenterY() - radius) + 'px';
100 } else {
101 var endTranslate = startTranslate;
24 } 102 }
25 103
26 ElementMetrics.prototype = { 104 var ripple = document.createElement('div');
27 get boundingRect () { 105 ripple.classList.add('ripple');
28 return this.element.getBoundingClientRect(); 106 ripple.style.height = ripple.style.width = 2 * radius + 'px';
dpapad 2017/02/22 18:48:10 Nit: Mixing arithmetic operators with numbers and
Dan Beam 2017/02/22 20:19:14 Done.
29 },
30 107
31 furthestCornerDistanceFrom: function(x, y) { 108 this.ripples.push(ripple);
32 var topLeft = Utility.distance(x, y, 0, 0); 109 this.shadowRoot.appendChild(ripple);
33 var topRight = Utility.distance(x, y, this.width, 0);
34 var bottomLeft = Utility.distance(x, y, 0, this.height);
35 var bottomRight = Utility.distance(x, y, this.width, this.height);
36 110
37 return Math.max(topLeft, topRight, bottomLeft, bottomRight); 111 ripple.animate({
38 } 112 // TODO(dbeam): scale to 90% of radius at .75 offset?
39 }; 113 transform: ['translate(' + startTranslate + ') scale(0)',
114 'translate(' + endTranslate + ') scale(1)'],
115 }, {
116 duration: Math.max(MIN_DURATION, Math.log(radius) * radius),
117 easing: 'cubic-bezier(.2, .9, .1, .9)',
118 fill: 'forwards',
119 });
120 },
40 121
41 /** 122 uiUpAction: function(e) {
42 * @param {HTMLElement} element 123 if (!this.noink)
43 * @constructor 124 this.upAction();
44 */ 125 },
45 function Ripple(element) {
46 this.element = element;
47 this.color = window.getComputedStyle(element).color;
48 126
49 this.wave = document.createElement('div'); 127 upAction: function() {
50 this.waveContainer = document.createElement('div'); 128 if (!this.holdDown)
51 this.wave.style.backgroundColor = this.color; 129 this.debounce('hide ripple', function() { this._hideRipple(); }, 1);
52 this.wave.classList.add('wave'); 130 },
53 this.waveContainer.classList.add('wave-container');
54 Polymer.dom(this.waveContainer).appendChild(this.wave);
55 131
56 this.resetInteractionState(); 132 _hideRipple: function() {
57 } 133 this.ripples.forEach(function(ripple) {
134 var animation = ripple.animate({
135 opacity: [getComputedStyle(ripple).opacity, 0],
136 }, {
137 duration: 150,
138 fill: 'forwards',
139 });
140 var removeRipple = function() { ripple.remove(); };
141 animation.addEventListener('finish', removeRipple);
142 animation.addEventListener('cancel', removeRipple);
143 });
144 this.ripples = [];
145 },
58 146
59 Ripple.MAX_RADIUS = 300; 147 _onEnterKeydown: function() {
148 this.uiDownAction();
149 this.async(this.uiUpAction, 1);
150 },
60 151
61 Ripple.prototype = { 152 _onSpaceKeydown: function() {
62 get recenters() { 153 this.uiDownAction();
63 return this.element.recenters; 154 },
64 },
65 155
66 get center() { 156 _onSpaceKeyup: function() {
67 return this.element.center; 157 this.uiUpAction();
68 }, 158 },
69 159
70 get mouseDownElapsed() { 160 _holdDownChanged: function(newHoldDown, oldHoldDown) {
71 var elapsed; 161 if (oldHoldDown === undefined)
162 return;
163 if (newHoldDown)
164 this.downAction();
165 else
166 this.upAction();
167 },
168 });
72 169
73 if (!this.mouseDownStart) { 170 })();
74 return 0;
75 }
76
77 elapsed = Utility.now() - this.mouseDownStart;
78
79 if (this.mouseUpStart) {
80 elapsed -= this.mouseUpElapsed;
81 }
82
83 return elapsed;
84 },
85
86 get mouseUpElapsed() {
87 return this.mouseUpStart ?
88 Utility.now () - this.mouseUpStart : 0;
89 },
90
91 get mouseDownElapsedSeconds() {
92 return this.mouseDownElapsed / 1000;
93 },
94
95 get mouseUpElapsedSeconds() {
96 return this.mouseUpElapsed / 1000;
97 },
98
99 get mouseInteractionSeconds() {
100 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
101 },
102
103 get initialOpacity() {
104 return this.element.initialOpacity;
105 },
106
107 get opacityDecayVelocity() {
108 return this.element.opacityDecayVelocity;
109 },
110
111 get radius() {
112 var width2 = this.containerMetrics.width * this.containerMetrics.width;
113 var height2 = this.containerMetrics.height * this.containerMetrics.heigh t;
114 var waveRadius = Math.min(
115 Math.sqrt(width2 + height2),
116 Ripple.MAX_RADIUS
117 ) * 1.1 + 5;
118
119 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS);
120 var timeNow = this.mouseInteractionSeconds / duration;
121 var size = waveRadius * (1 - Math.pow(80, -timeNow));
122
123 return Math.abs(size);
124 },
125
126 get opacity() {
127 if (!this.mouseUpStart) {
128 return this.initialOpacity;
129 }
130
131 return Math.max(
132 0,
133 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe locity
134 );
135 },
136
137 get outerOpacity() {
138 // Linear increase in background opacity, capped at the opacity
139 // of the wavefront (waveOpacity).
140 var outerOpacity = this.mouseUpElapsedSeconds * 0.3;
141 var waveOpacity = this.opacity;
142
143 return Math.max(
144 0,
145 Math.min(outerOpacity, waveOpacity)
146 );
147 },
148
149 get isOpacityFullyDecayed() {
150 return this.opacity < 0.01 &&
151 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
152 },
153
154 get isRestingAtMaxRadius() {
155 return this.opacity >= this.initialOpacity &&
156 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
157 },
158
159 get isAnimationComplete() {
160 return this.mouseUpStart ?
161 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius;
162 },
163
164 get translationFraction() {
165 return Math.min(
166 1,
167 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2)
168 );
169 },
170
171 get xNow() {
172 if (this.xEnd) {
173 return this.xStart + this.translationFraction * (this.xEnd - this.xSta rt);
174 }
175
176 return this.xStart;
177 },
178
179 get yNow() {
180 if (this.yEnd) {
181 return this.yStart + this.translationFraction * (this.yEnd - this.ySta rt);
182 }
183
184 return this.yStart;
185 },
186
187 get isMouseDown() {
188 return this.mouseDownStart && !this.mouseUpStart;
189 },
190
191 resetInteractionState: function() {
192 this.maxRadius = 0;
193 this.mouseDownStart = 0;
194 this.mouseUpStart = 0;
195
196 this.xStart = 0;
197 this.yStart = 0;
198 this.xEnd = 0;
199 this.yEnd = 0;
200 this.slideDistance = 0;
201
202 this.containerMetrics = new ElementMetrics(this.element);
203 },
204
205 draw: function() {
206 var scale;
207 var translateString;
208 var dx;
209 var dy;
210
211 this.wave.style.opacity = this.opacity;
212
213 scale = this.radius / (this.containerMetrics.size / 2);
214 dx = this.xNow - (this.containerMetrics.width / 2);
215 dy = this.yNow - (this.containerMetrics.height / 2);
216
217
218 // 2d transform for safari because of border-radius and overflow:hidden clipping bug.
219 // https://bugs.webkit.org/show_bug.cgi?id=98538
220 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
221 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + 'px, 0)';
222 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
223 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
224 },
225
226 /** @param {Event=} event */
227 downAction: function(event) {
228 var xCenter = this.containerMetrics.width / 2;
229 var yCenter = this.containerMetrics.height / 2;
230
231 this.resetInteractionState();
232 this.mouseDownStart = Utility.now();
233
234 if (this.center) {
235 this.xStart = xCenter;
236 this.yStart = yCenter;
237 this.slideDistance = Utility.distance(
238 this.xStart, this.yStart, this.xEnd, this.yEnd
239 );
240 } else {
241 this.xStart = event ?
242 event.detail.x - this.containerMetrics.boundingRect.left :
243 this.containerMetrics.width / 2;
244 this.yStart = event ?
245 event.detail.y - this.containerMetrics.boundingRect.top :
246 this.containerMetrics.height / 2;
247 }
248
249 if (this.recenters) {
250 this.xEnd = xCenter;
251 this.yEnd = yCenter;
252 this.slideDistance = Utility.distance(
253 this.xStart, this.yStart, this.xEnd, this.yEnd
254 );
255 }
256
257 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(
258 this.xStart,
259 this.yStart
260 );
261
262 this.waveContainer.style.top =
263 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px' ;
264 this.waveContainer.style.left =
265 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px';
266
267 this.waveContainer.style.width = this.containerMetrics.size + 'px';
268 this.waveContainer.style.height = this.containerMetrics.size + 'px';
269 },
270
271 /** @param {Event=} event */
272 upAction: function(event) {
273 if (!this.isMouseDown) {
274 return;
275 }
276
277 this.mouseUpStart = Utility.now();
278 },
279
280 remove: function() {
281 Polymer.dom(this.waveContainer.parentNode).removeChild(
282 this.waveContainer
283 );
284 }
285 };
286
287 Polymer({
288 is: 'paper-ripple',
289
290 behaviors: [
291 Polymer.IronA11yKeysBehavior
292 ],
293
294 properties: {
295 /**
296 * The initial opacity set on the wave.
297 *
298 * @attribute initialOpacity
299 * @type number
300 * @default 0.25
301 */
302 initialOpacity: {
303 type: Number,
304 value: 0.25
305 },
306
307 /**
308 * How fast (opacity per second) the wave fades out.
309 *
310 * @attribute opacityDecayVelocity
311 * @type number
312 * @default 0.8
313 */
314 opacityDecayVelocity: {
315 type: Number,
316 value: 0.8
317 },
318
319 /**
320 * If true, ripples will exhibit a gravitational pull towards
321 * the center of their container as they fade away.
322 *
323 * @attribute recenters
324 * @type boolean
325 * @default false
326 */
327 recenters: {
328 type: Boolean,
329 value: false
330 },
331
332 /**
333 * If true, ripples will center inside its container
334 *
335 * @attribute recenters
336 * @type boolean
337 * @default false
338 */
339 center: {
340 type: Boolean,
341 value: false
342 },
343
344 /**
345 * A list of the visual ripples.
346 *
347 * @attribute ripples
348 * @type Array
349 * @default []
350 */
351 ripples: {
352 type: Array,
353 value: function() {
354 return [];
355 }
356 },
357
358 /**
359 * True when there are visible ripples animating within the
360 * element.
361 */
362 animating: {
363 type: Boolean,
364 readOnly: true,
365 reflectToAttribute: true,
366 value: false
367 },
368
369 /**
370 * If true, the ripple will remain in the "down" state until `holdDown`
371 * is set to false again.
372 */
373 holdDown: {
374 type: Boolean,
375 value: false,
376 observer: '_holdDownChanged'
377 },
378
379 /**
380 * If true, the ripple will not generate a ripple effect
381 * via pointer interaction.
382 * Calling ripple's imperative api like `simulatedRipple` will
383 * still generate the ripple effect.
384 */
385 noink: {
386 type: Boolean,
387 value: false
388 },
389
390 _animating: {
391 type: Boolean
392 },
393
394 _boundAnimate: {
395 type: Function,
396 value: function() {
397 return this.animate.bind(this);
398 }
399 }
400 },
401
402 get target () {
403 return this.keyEventTarget;
404 },
405
406 keyBindings: {
407 'enter:keydown': '_onEnterKeydown',
408 'space:keydown': '_onSpaceKeydown',
409 'space:keyup': '_onSpaceKeyup'
410 },
411
412 attached: function() {
413 // Set up a11yKeysBehavior to listen to key events on the target,
414 // so that space and enter activate the ripple even if the target doesn' t
415 // handle key events. The key handlers deal with `noink` themselves.
416 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE
417 this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host;
418 } else {
419 this.keyEventTarget = this.parentNode;
420 }
421 var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget);
422 this.listen(keyEventTarget, 'up', 'uiUpAction');
423 this.listen(keyEventTarget, 'down', 'uiDownAction');
424 },
425
426 detached: function() {
427 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction');
428 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction');
429 this.keyEventTarget = null;
430 },
431
432 get shouldKeepAnimating () {
433 for (var index = 0; index < this.ripples.length; ++index) {
434 if (!this.ripples[index].isAnimationComplete) {
435 return true;
436 }
437 }
438
439 return false;
440 },
441
442 simulatedRipple: function() {
443 this.downAction(null);
444
445 // Please see polymer/polymer#1305
446 this.async(function() {
447 this.upAction();
448 }, 1);
449 },
450
451 /**
452 * Provokes a ripple down effect via a UI event,
453 * respecting the `noink` property.
454 * @param {Event=} event
455 */
456 uiDownAction: function(event) {
457 if (!this.noink) {
458 this.downAction(event);
459 }
460 },
461
462 /**
463 * Provokes a ripple down effect via a UI event,
464 * *not* respecting the `noink` property.
465 * @param {Event=} event
466 */
467 downAction: function(event) {
468 if (this.holdDown && this.ripples.length > 0) {
469 return;
470 }
471
472 var ripple = this.addRipple();
473
474 ripple.downAction(event);
475
476 if (!this._animating) {
477 this._animating = true;
478 this.animate();
479 }
480 },
481
482 /**
483 * Provokes a ripple up effect via a UI event,
484 * respecting the `noink` property.
485 * @param {Event=} event
486 */
487 uiUpAction: function(event) {
488 if (!this.noink) {
489 this.upAction(event);
490 }
491 },
492
493 /**
494 * Provokes a ripple up effect via a UI event,
495 * *not* respecting the `noink` property.
496 * @param {Event=} event
497 */
498 upAction: function(event) {
499 if (this.holdDown) {
500 return;
501 }
502
503 this.ripples.forEach(function(ripple) {
504 ripple.upAction(event);
505 });
506
507 this._animating = true;
508 this.animate();
509 },
510
511 onAnimationComplete: function() {
512 this._animating = false;
513 this.$.background.style.backgroundColor = null;
514 this.fire('transitionend');
515 },
516
517 addRipple: function() {
518 var ripple = new Ripple(this);
519
520 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer);
521 this.$.background.style.backgroundColor = ripple.color;
522 this.ripples.push(ripple);
523
524 this._setAnimating(true);
525
526 return ripple;
527 },
528
529 removeRipple: function(ripple) {
530 var rippleIndex = this.ripples.indexOf(ripple);
531
532 if (rippleIndex < 0) {
533 return;
534 }
535
536 this.ripples.splice(rippleIndex, 1);
537
538 ripple.remove();
539
540 if (!this.ripples.length) {
541 this._setAnimating(false);
542 }
543 },
544
545 /**
546 * This conflicts with Element#antimate().
547 * https://developer.mozilla.org/en-US/docs/Web/API/Element/animate
548 * @suppress {checkTypes}
549 */
550 animate: function() {
551 if (!this._animating) {
552 return;
553 }
554 var index;
555 var ripple;
556
557 for (index = 0; index < this.ripples.length; ++index) {
558 ripple = this.ripples[index];
559
560 ripple.draw();
561
562 this.$.background.style.opacity = ripple.outerOpacity;
563
564 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) {
565 this.removeRipple(ripple);
566 }
567 }
568
569 if (!this.shouldKeepAnimating && this.ripples.length === 0) {
570 this.onAnimationComplete();
571 } else {
572 window.requestAnimationFrame(this._boundAnimate);
573 }
574 },
575
576 _onEnterKeydown: function() {
577 this.uiDownAction();
578 this.async(this.uiUpAction, 1);
579 },
580
581 _onSpaceKeydown: function() {
582 this.uiDownAction();
583 },
584
585 _onSpaceKeyup: function() {
586 this.uiUpAction();
587 },
588
589 // note: holdDown does not respect noink since it can be a focus based
590 // effect.
591 _holdDownChanged: function(newVal, oldVal) {
592 if (oldVal === undefined) {
593 return;
594 }
595 if (newVal) {
596 this.downAction();
597 } else {
598 this.upAction();
599 }
600 }
601
602 /**
603 Fired when the animation finishes.
604 This is useful if you want to wait until
605 the ripple animation finishes to perform some action.
606
607 @event transitionend
608 @param {{node: Object}} detail Contains the animated node.
609 */
610 });
611 })();
OLDNEW
« no previous file with comments | « third_party/polymer/v1_0/components-chromium/paper-ripple/paper-ripple.html ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698