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

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: fix some quirks Created 3 years, 9 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
1 (function() { 1 (function() {
2 var Utility = { 2
3 distance: function(x1, y1, x2, y2) { 3 var MAX_RADIUS_PX = 300;
4 var xDelta = (x1 - x2); 4 var MIN_DURATION_MS = 800;
5 var yDelta = (y1 - y2); 5
6 6 /**
7 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); 7 * @param {number} x1
8 }, 8 * @param {number} y1
9 9 * @param {number} x2
10 now: window.performance && window.performance.now ? 10 * @param {number} y2
11 window.performance.now.bind(window.performance) : Date.now 11 * @return {number} The distance between (x1, y1) and (x2, y2).
12 }; 12 */
13 13 var distance = function(x1, y1, x2, y2) {
14 /** 14 var xDelta = x1 - x2;
15 * @param {HTMLElement} element 15 var yDelta = y1 - y2;
16 * @constructor 16 return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
17 */ 17 };
18 function ElementMetrics(element) { 18
19 this.element = element; 19 Polymer({
20 this.width = this.boundingRect.width; 20 is: 'paper-ripple',
21 this.height = this.boundingRect.height; 21
22 22 behaviors: [Polymer.IronA11yKeysBehavior],
23 this.size = Math.max(this.width, this.height); 23
24 properties: {
25 center: {type: Boolean, value: false},
26 holdDown: {type: Boolean, value: false, observer: '_holdDownChanged'},
27 recenters: {type: Boolean, value: false},
28 noink: {type: Boolean, value: false},
29 },
30
31 keyBindings: {
32 'enter:keydown': '_onEnterKeydown',
33 'space:keydown': '_onSpaceKeydown',
34 'space:keyup': '_onSpaceKeyup',
35 },
36
37 /** @override */
38 created: function() {
39 /** @type {Array<!Element>} */
40 this.ripples = [];
41 },
42
43 /** @override */
44 attached: function() {
45 this.keyEventTarget = this.parentNode.nodeType == 11 ?
46 Polymer.dom(this).getOwnerRoot().host : this.parentNode;
47 this.keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget);
48 this.listen(this.keyEventTarget, 'up', 'uiUpAction');
49 this.listen(this.keyEventTarget, 'down', 'uiDownAction');
50 },
51
52 /** @override */
53 detached: function() {
54 this.unlisten(this.keyEventTarget, 'up', 'uiUpAction');
55 this.unlisten(this.keyEventTarget, 'down', 'uiDownAction');
56 this.keyEventTarget = null;
57 },
58
59 simulatedRipple: function() {
60 this.downAction();
61 // Using a 1ms delay ensures a macro-task.
62 this.async(function() { this.upAction(); }.bind(this), 1);
63 },
64
65 /** @param {Event=} e */
66 uiDownAction: function(e) {
67 if (!this.noink)
68 this.downAction(e);
69 },
70
71 /** @param {Event=} e */
72 downAction: function(e) {
73 if (this.ripples.length && this.holdDown)
74 return;
75 // TODO(dbeam): some things (i.e. paper-icon-button-light) dynamically
76 // create ripples on 'up', Ripples register an event listener on their
77 // parent (or shadow DOM host) when attached(). This sometimes causes
78 // duplicate events to fire on us.
79 this.debounce('show ripple', function() { this.__showRipple(e); }, 1);
80 },
81
82 /**
83 * @param {Event=} e
84 * @private
85 * @suppress {checkTypes}
86 */
87 __showRipple: function(e) {
88 var rect = this.getBoundingClientRect();
89
90 var roundedCenterX = function() { return Math.round(rect.width / 2); };
91 var roundedCenterY = function() { return Math.round(rect.height / 2); };
92
93 var centered = !e || this.center;
94 if (centered) {
95 var x = roundedCenterX();
96 var y = roundedCenterY();
97 } else {
98 var sourceEvent = e.detail.sourceEvent;
99 var x = Math.round(sourceEvent.clientX - rect.left);
100 var y = Math.round(sourceEvent.clientY - rect.top);
24 } 101 }
25 102
26 ElementMetrics.prototype = { 103 var corners = [
27 get boundingRect () { 104 {x: 0, y: 0},
28 return this.element.getBoundingClientRect(); 105 {x: rect.width, y: 0},
29 }, 106 {x: 0, y: rect.height},
30 107 {x: rect.width, y: rect.height},
31 furthestCornerDistanceFrom: function(x, y) { 108 ];
32 var topLeft = Utility.distance(x, y, 0, 0); 109
33 var topRight = Utility.distance(x, y, this.width, 0); 110 var cornerDistances = corners.map(function(corner) {
34 var bottomLeft = Utility.distance(x, y, 0, this.height); 111 return Math.round(distance(x, y, corner.x, corner.y));
35 var bottomRight = Utility.distance(x, y, this.width, this.height); 112 });
36 113
37 return Math.max(topLeft, topRight, bottomLeft, bottomRight); 114 var radius = Math.min(MAX_RADIUS_PX, Math.max.apply(Math, cornerDistances));
115
116 var startTranslate = (x - radius) + 'px, ' + (y - radius) + 'px';
117 if (this.recenters && !centered) {
118 var endTranslate = (roundedCenterX() - radius) + 'px, ' +
119 (roundedCenterY() - radius) + 'px';
120 } else {
121 var endTranslate = startTranslate;
122 }
123
124 var ripple = document.createElement('div');
125 ripple.classList.add('ripple');
126 ripple.style.height = ripple.style.width = (2 * radius) + 'px';
127
128 this.ripples.push(ripple);
129 this.shadowRoot.appendChild(ripple);
130
131 ripple.animate({
132 // TODO(dbeam): scale to 90% of radius at .75 offset?
133 transform: ['translate(' + startTranslate + ') scale(0)',
134 'translate(' + endTranslate + ') scale(1)'],
135 }, {
136 duration: Math.max(MIN_DURATION_MS, Math.log(radius) * radius) || 0,
137 easing: 'cubic-bezier(.2, .9, .1, .9)',
138 fill: 'forwards',
139 });
140 },
141
142 /** @param {Event=} e */
143 uiUpAction: function(e) {
144 if (!this.noink)
145 this.upAction();
146 },
147
148 /** @param {Event=} e */
149 upAction: function(e) {
150 if (!this.holdDown)
151 this.debounce('hide ripple', function() { this.__hideRipple(); }, 1);
152 },
153
154 /**
155 * @private
156 * @suppress {checkTypes}
157 */
158 __hideRipple: function() {
159 this.ripples.forEach(function(ripple) {
160 var removeRipple = function() { ripple.remove(); };
161 // TODO(dbeam): should we be firing a transitionend event here? Does
162 // anybody actually use it? It's in the original paper-ripple.
163 var opacity = getComputedStyle(ripple).opacity;
164 if (!opacity.length) {
165 removeRipple();
166 } else {
167 var animation = ripple.animate({
168 opacity: [opacity, 0],
169 }, {
170 duration: 150,
171 fill: 'forwards',
172 });
173 animation.addEventListener('finish', removeRipple);
174 animation.addEventListener('cancel', removeRipple);
38 } 175 }
39 };
40
41 /**
42 * @param {HTMLElement} element
43 * @constructor
44 */
45 function Ripple(element) {
46 this.element = element;
47 this.color = window.getComputedStyle(element).color;
48
49 this.wave = document.createElement('div');
50 this.waveContainer = document.createElement('div');
51 this.wave.style.backgroundColor = this.color;
52 this.wave.classList.add('wave');
53 this.waveContainer.classList.add('wave-container');
54 Polymer.dom(this.waveContainer).appendChild(this.wave);
55
56 this.resetInteractionState();
57 }
58
59 Ripple.MAX_RADIUS = 300;
60
61 Ripple.prototype = {
62 get recenters() {
63 return this.element.recenters;
64 },
65
66 get center() {
67 return this.element.center;
68 },
69
70 get mouseDownElapsed() {
71 var elapsed;
72
73 if (!this.mouseDownStart) {
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 }); 176 });
611 })(); 177 this.ripples = [];
178 },
179
180 /** @protected */
181 _onEnterKeydown: function() {
182 this.uiDownAction();
183 this.async(this.uiUpAction, 1);
184 },
185
186 /** @protected */
187 _onSpaceKeydown: function() {
188 this.uiDownAction();
189 },
190
191 /** @protected */
192 _onSpaceKeyup: function() {
193 this.uiUpAction();
194 },
195
196 /** @protected */
197 _holdDownChanged: function(newHoldDown, oldHoldDown) {
198 if (oldHoldDown === undefined)
199 return;
200 if (newHoldDown)
201 this.downAction();
202 else
203 this.upAction();
204 },
205 });
206
207 })();
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698