Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / polymer / v1_0 / components-chromium / paper-ripple / paper-ripple-extracted.js
blob655f301aad988dd956379d6ca47a109c99a75210
2   (function() {
3     var Utility = {
4       distance: function(x1, y1, x2, y2) {
5         var xDelta = (x1 - x2);
6         var yDelta = (y1 - y2);
8         return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
9       },
11       now: window.performance && window.performance.now ?
12           window.performance.now.bind(window.performance) : Date.now
13     };
15     /**
16      * @param {HTMLElement} element
17      * @constructor
18      */
19     function ElementMetrics(element) {
20       this.element = element;
21       this.width = this.boundingRect.width;
22       this.height = this.boundingRect.height;
24       this.size = Math.max(this.width, this.height);
25     }
27     ElementMetrics.prototype = {
28       get boundingRect () {
29         return this.element.getBoundingClientRect();
30       },
32       furthestCornerDistanceFrom: function(x, y) {
33         var topLeft = Utility.distance(x, y, 0, 0);
34         var topRight = Utility.distance(x, y, this.width, 0);
35         var bottomLeft = Utility.distance(x, y, 0, this.height);
36         var bottomRight = Utility.distance(x, y, this.width, this.height);
38         return Math.max(topLeft, topRight, bottomLeft, bottomRight);
39       }
40     };
42     /**
43      * @param {HTMLElement} element
44      * @constructor
45      */
46     function Ripple(element) {
47       this.element = element;
48       this.color = window.getComputedStyle(element).color;
50       this.wave = document.createElement('div');
51       this.waveContainer = document.createElement('div');
52       this.wave.style.backgroundColor = this.color;
53       this.wave.classList.add('wave');
54       this.waveContainer.classList.add('wave-container');
55       Polymer.dom(this.waveContainer).appendChild(this.wave);
57       this.resetInteractionState();
58     }
60     Ripple.MAX_RADIUS = 300;
62     Ripple.prototype = {
63       get recenters() {
64         return this.element.recenters;
65       },
67       get center() {
68         return this.element.center;
69       },
71       get mouseDownElapsed() {
72         var elapsed;
74         if (!this.mouseDownStart) {
75           return 0;
76         }
78         elapsed = Utility.now() - this.mouseDownStart;
80         if (this.mouseUpStart) {
81           elapsed -= this.mouseUpElapsed;
82         }
84         return elapsed;
85       },
87       get mouseUpElapsed() {
88         return this.mouseUpStart ?
89           Utility.now () - this.mouseUpStart : 0;
90       },
92       get mouseDownElapsedSeconds() {
93         return this.mouseDownElapsed / 1000;
94       },
96       get mouseUpElapsedSeconds() {
97         return this.mouseUpElapsed / 1000;
98       },
100       get mouseInteractionSeconds() {
101         return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
102       },
104       get initialOpacity() {
105         return this.element.initialOpacity;
106       },
108       get opacityDecayVelocity() {
109         return this.element.opacityDecayVelocity;
110       },
112       get radius() {
113         var width2 = this.containerMetrics.width * this.containerMetrics.width;
114         var height2 = this.containerMetrics.height * this.containerMetrics.height;
115         var waveRadius = Math.min(
116           Math.sqrt(width2 + height2),
117           Ripple.MAX_RADIUS
118         ) * 1.1 + 5;
120         var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS);
121         var timeNow = this.mouseInteractionSeconds / duration;
122         var size = waveRadius * (1 - Math.pow(80, -timeNow));
124         return Math.abs(size);
125       },
127       get opacity() {
128         if (!this.mouseUpStart) {
129           return this.initialOpacity;
130         }
132         return Math.max(
133           0,
134           this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVelocity
135         );
136       },
138       get outerOpacity() {
139         // Linear increase in background opacity, capped at the opacity
140         // of the wavefront (waveOpacity).
141         var outerOpacity = this.mouseUpElapsedSeconds * 0.3;
142         var waveOpacity = this.opacity;
144         return Math.max(
145           0,
146           Math.min(outerOpacity, waveOpacity)
147         );
148       },
150       get isOpacityFullyDecayed() {
151         return this.opacity < 0.01 &&
152           this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
153       },
155       get isRestingAtMaxRadius() {
156         return this.opacity >= this.initialOpacity &&
157           this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
158       },
160       get isAnimationComplete() {
161         return this.mouseUpStart ?
162           this.isOpacityFullyDecayed : this.isRestingAtMaxRadius;
163       },
165       get translationFraction() {
166         return Math.min(
167           1,
168           this.radius / this.containerMetrics.size * 2 / Math.sqrt(2)
169         );
170       },
172       get xNow() {
173         if (this.xEnd) {
174           return this.xStart + this.translationFraction * (this.xEnd - this.xStart);
175         }
177         return this.xStart;
178       },
180       get yNow() {
181         if (this.yEnd) {
182           return this.yStart + this.translationFraction * (this.yEnd - this.yStart);
183         }
185         return this.yStart;
186       },
188       get isMouseDown() {
189         return this.mouseDownStart && !this.mouseUpStart;
190       },
192       resetInteractionState: function() {
193         this.maxRadius = 0;
194         this.mouseDownStart = 0;
195         this.mouseUpStart = 0;
197         this.xStart = 0;
198         this.yStart = 0;
199         this.xEnd = 0;
200         this.yEnd = 0;
201         this.slideDistance = 0;
203         this.containerMetrics = new ElementMetrics(this.element);
204       },
206       draw: function() {
207         var scale;
208         var translateString;
209         var dx;
210         var dy;
212         this.wave.style.opacity = this.opacity;
214         scale = this.radius / (this.containerMetrics.size / 2);
215         dx = this.xNow - (this.containerMetrics.width / 2);
216         dy = this.yNow - (this.containerMetrics.height / 2);
219         // 2d transform for safari because of border-radius and overflow:hidden clipping bug.
220         // https://bugs.webkit.org/show_bug.cgi?id=98538
221         this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
222         this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + 'px, 0)';
223         this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
224         this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
225       },
227       /** @param {Event=} event */
228       downAction: function(event) {
229         var xCenter = this.containerMetrics.width / 2;
230         var yCenter = this.containerMetrics.height / 2;
232         this.resetInteractionState();
233         this.mouseDownStart = Utility.now();
235         if (this.center) {
236           this.xStart = xCenter;
237           this.yStart = yCenter;
238           this.slideDistance = Utility.distance(
239             this.xStart, this.yStart, this.xEnd, this.yEnd
240           );
241         } else {
242           this.xStart = event ?
243               event.detail.x - this.containerMetrics.boundingRect.left :
244               this.containerMetrics.width / 2;
245           this.yStart = event ?
246               event.detail.y - this.containerMetrics.boundingRect.top :
247               this.containerMetrics.height / 2;
248         }
250         if (this.recenters) {
251           this.xEnd = xCenter;
252           this.yEnd = yCenter;
253           this.slideDistance = Utility.distance(
254             this.xStart, this.yStart, this.xEnd, this.yEnd
255           );
256         }
258         this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(
259           this.xStart,
260           this.yStart
261         );
263         this.waveContainer.style.top =
264           (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px';
265         this.waveContainer.style.left =
266           (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px';
268         this.waveContainer.style.width = this.containerMetrics.size + 'px';
269         this.waveContainer.style.height = this.containerMetrics.size + 'px';
270       },
272       /** @param {Event=} event */
273       upAction: function(event) {
274         if (!this.isMouseDown) {
275           return;
276         }
278         this.mouseUpStart = Utility.now();
279       },
281       remove: function() {
282         Polymer.dom(this.waveContainer.parentNode).removeChild(
283           this.waveContainer
284         );
285       }
286     };
288     Polymer({
289       is: 'paper-ripple',
291       behaviors: [
292         Polymer.IronA11yKeysBehavior
293       ],
295       properties: {
296         /**
297          * The initial opacity set on the wave.
298          *
299          * @attribute initialOpacity
300          * @type number
301          * @default 0.25
302          */
303         initialOpacity: {
304           type: Number,
305           value: 0.25
306         },
308         /**
309          * How fast (opacity per second) the wave fades out.
310          *
311          * @attribute opacityDecayVelocity
312          * @type number
313          * @default 0.8
314          */
315         opacityDecayVelocity: {
316           type: Number,
317           value: 0.8
318         },
320         /**
321          * If true, ripples will exhibit a gravitational pull towards
322          * the center of their container as they fade away.
323          *
324          * @attribute recenters
325          * @type boolean
326          * @default false
327          */
328         recenters: {
329           type: Boolean,
330           value: false
331         },
333         /**
334          * If true, ripples will center inside its container
335          *
336          * @attribute recenters
337          * @type boolean
338          * @default false
339          */
340         center: {
341           type: Boolean,
342           value: false
343         },
345         /**
346          * A list of the visual ripples.
347          *
348          * @attribute ripples
349          * @type Array
350          * @default []
351          */
352         ripples: {
353           type: Array,
354           value: function() {
355             return [];
356           }
357         },
359         /**
360          * True when there are visible ripples animating within the
361          * element.
362          */
363         animating: {
364           type: Boolean,
365           readOnly: true,
366           reflectToAttribute: true,
367           value: false
368         },
370         /**
371          * If true, the ripple will remain in the "down" state until `holdDown`
372          * is set to false again.
373          */
374         holdDown: {
375           type: Boolean,
376           value: false,
377           observer: '_holdDownChanged'
378         },
380         _animating: {
381           type: Boolean
382         },
384         _boundAnimate: {
385           type: Function,
386           value: function() {
387             return this.animate.bind(this);
388           }
389         }
390       },
392       get target () {
393         var ownerRoot = Polymer.dom(this).getOwnerRoot();
394         var target;
396         if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE
397           target = ownerRoot.host;
398         } else {
399           target = this.parentNode;
400         }
402         return target;
403       },
405       keyBindings: {
406         'enter:keydown': '_onEnterKeydown',
407         'space:keydown': '_onSpaceKeydown',
408         'space:keyup': '_onSpaceKeyup'
409       },
411       attached: function() {
412         this.listen(this.target, 'up', 'upAction');
413         this.listen(this.target, 'down', 'downAction');
415         if (!this.target.hasAttribute('noink')) {
416           this.keyEventTarget = this.target;
417         }
418       },
420       get shouldKeepAnimating () {
421         for (var index = 0; index < this.ripples.length; ++index) {
422           if (!this.ripples[index].isAnimationComplete) {
423             return true;
424           }
425         }
427         return false;
428       },
430       simulatedRipple: function() {
431         this.downAction(null);
433         // Please see polymer/polymer#1305
434         this.async(function() {
435           this.upAction();
436         }, 1);
437       },
439       /** @param {Event=} event */
440       downAction: function(event) {
441         if (this.holdDown && this.ripples.length > 0) {
442           return;
443         }
445         var ripple = this.addRipple();
447         ripple.downAction(event);
449         if (!this._animating) {
450           this.animate();
451         }
452       },
454       /** @param {Event=} event */
455       upAction: function(event) {
456         if (this.holdDown) {
457           return;
458         }
460         this.ripples.forEach(function(ripple) {
461           ripple.upAction(event);
462         });
464         this.animate();
465       },
467       onAnimationComplete: function() {
468         this._animating = false;
469         this.$.background.style.backgroundColor = null;
470         this.fire('transitionend');
471       },
473       addRipple: function() {
474         var ripple = new Ripple(this);
476         Polymer.dom(this.$.waves).appendChild(ripple.waveContainer);
477         this.$.background.style.backgroundColor = ripple.color;
478         this.ripples.push(ripple);
480         this._setAnimating(true);
482         return ripple;
483       },
485       removeRipple: function(ripple) {
486         var rippleIndex = this.ripples.indexOf(ripple);
488         if (rippleIndex < 0) {
489           return;
490         }
492         this.ripples.splice(rippleIndex, 1);
494         ripple.remove();
496         if (!this.ripples.length) {
497           this._setAnimating(false);
498         }
499       },
501       animate: function() {
502         var index;
503         var ripple;
505         this._animating = true;
507         for (index = 0; index < this.ripples.length; ++index) {
508           ripple = this.ripples[index];
510           ripple.draw();
512           this.$.background.style.opacity = ripple.outerOpacity;
514           if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) {
515             this.removeRipple(ripple);
516           }
517         }
519         if (!this.shouldKeepAnimating && this.ripples.length === 0) {
520           this.onAnimationComplete();
521         } else {
522           window.requestAnimationFrame(this._boundAnimate);
523         }
524       },
526       _onEnterKeydown: function() {
527         this.downAction();
528         this.async(this.upAction, 1);
529       },
531       _onSpaceKeydown: function() {
532         this.downAction();
533       },
535       _onSpaceKeyup: function() {
536         this.upAction();
537       },
539       _holdDownChanged: function(holdDown) {
540         if (holdDown) {
541           this.downAction();
542         } else {
543           this.upAction();
544         }
545       }
546     });
547   })();