3 distance: function(x1
, y1
, x2
, y2
) {
4 var xDelta
= (x1
- x2
);
5 var yDelta
= (y1
- y2
);
7 return Math
.sqrt(xDelta
* xDelta
+ yDelta
* yDelta
);
10 now
: window
.performance
&& window
.performance
.now
?
11 window
.performance
.now
.bind(window
.performance
) : Date
.now
15 * @param {HTMLElement} element
18 function ElementMetrics(element
) {
19 this.element
= element
;
20 this.width
= this.boundingRect
.width
;
21 this.height
= this.boundingRect
.height
;
23 this.size
= Math
.max(this.width
, this.height
);
26 ElementMetrics
.prototype = {
28 return this.element
.getBoundingClientRect();
31 furthestCornerDistanceFrom: function(x
, y
) {
32 var topLeft
= Utility
.distance(x
, y
, 0, 0);
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
);
37 return Math
.max(topLeft
, topRight
, bottomLeft
, bottomRight
);
42 * @param {HTMLElement} element
45 function Ripple(element
) {
46 this.element
= element
;
47 this.color
= window
.getComputedStyle(element
).color
;
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
);
56 this.resetInteractionState();
59 Ripple
.MAX_RADIUS
= 300;
63 return this.element
.recenters
;
67 return this.element
.center
;
70 get mouseDownElapsed() {
73 if (!this.mouseDownStart
) {
77 elapsed
= Utility
.now() - this.mouseDownStart
;
79 if (this.mouseUpStart
) {
80 elapsed
-= this.mouseUpElapsed
;
86 get mouseUpElapsed() {
87 return this.mouseUpStart
?
88 Utility
.now () - this.mouseUpStart
: 0;
91 get mouseDownElapsedSeconds() {
92 return this.mouseDownElapsed
/ 1000;
95 get mouseUpElapsedSeconds() {
96 return this.mouseUpElapsed
/ 1000;
99 get mouseInteractionSeconds() {
100 return this.mouseDownElapsedSeconds
+ this.mouseUpElapsedSeconds
;
103 get initialOpacity() {
104 return this.element
.initialOpacity
;
107 get opacityDecayVelocity() {
108 return this.element
.opacityDecayVelocity
;
112 var width2
= this.containerMetrics
.width
* this.containerMetrics
.width
;
113 var height2
= this.containerMetrics
.height
* this.containerMetrics
.height
;
114 var waveRadius
= Math
.min(
115 Math
.sqrt(width2
+ height2
),
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
));
123 return Math
.abs(size
);
127 if (!this.mouseUpStart
) {
128 return this.initialOpacity
;
133 this.initialOpacity
- this.mouseUpElapsedSeconds
* this.opacityDecayVelocity
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
;
145 Math
.min(outerOpacity
, waveOpacity
)
149 get isOpacityFullyDecayed() {
150 return this.opacity
< 0.01 &&
151 this.radius
>= Math
.min(this.maxRadius
, Ripple
.MAX_RADIUS
);
154 get isRestingAtMaxRadius() {
155 return this.opacity
>= this.initialOpacity
&&
156 this.radius
>= Math
.min(this.maxRadius
, Ripple
.MAX_RADIUS
);
159 get isAnimationComplete() {
160 return this.mouseUpStart
?
161 this.isOpacityFullyDecayed
: this.isRestingAtMaxRadius
;
164 get translationFraction() {
167 this.radius
/ this.containerMetrics
.size
* 2 / Math
.sqrt(2)
173 return this.xStart
+ this.translationFraction
* (this.xEnd
- this.xStart
);
181 return this.yStart
+ this.translationFraction
* (this.yEnd
- this.yStart
);
188 return this.mouseDownStart
&& !this.mouseUpStart
;
191 resetInteractionState: function() {
193 this.mouseDownStart
= 0;
194 this.mouseUpStart
= 0;
200 this.slideDistance
= 0;
202 this.containerMetrics
= new ElementMetrics(this.element
);
211 this.wave
.style
.opacity
= this.opacity
;
213 scale
= this.radius
/ (this.containerMetrics
.size
/ 2);
214 dx
= this.xNow
- (this.containerMetrics
.width
/ 2);
215 dy
= this.yNow
- (this.containerMetrics
.height
/ 2);
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)';
226 /** @param {Event=} event */
227 downAction: function(event
) {
228 var xCenter
= this.containerMetrics
.width
/ 2;
229 var yCenter
= this.containerMetrics
.height
/ 2;
231 this.resetInteractionState();
232 this.mouseDownStart
= Utility
.now();
235 this.xStart
= xCenter
;
236 this.yStart
= yCenter
;
237 this.slideDistance
= Utility
.distance(
238 this.xStart
, this.yStart
, this.xEnd
, this.yEnd
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;
249 if (this.recenters
) {
252 this.slideDistance
= Utility
.distance(
253 this.xStart
, this.yStart
, this.xEnd
, this.yEnd
257 this.maxRadius
= this.containerMetrics
.furthestCornerDistanceFrom(
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';
267 this.waveContainer
.style
.width
= this.containerMetrics
.size
+ 'px';
268 this.waveContainer
.style
.height
= this.containerMetrics
.size
+ 'px';
271 /** @param {Event=} event */
272 upAction: function(event
) {
273 if (!this.isMouseDown
) {
277 this.mouseUpStart
= Utility
.now();
281 Polymer
.dom(this.waveContainer
.parentNode
).removeChild(
291 Polymer
.IronA11yKeysBehavior
296 * The initial opacity set on the wave.
298 * @attribute initialOpacity
308 * How fast (opacity per second) the wave fades out.
310 * @attribute opacityDecayVelocity
314 opacityDecayVelocity
: {
320 * If true, ripples will exhibit a gravitational pull towards
321 * the center of their container as they fade away.
323 * @attribute recenters
333 * If true, ripples will center inside its container
335 * @attribute recenters
345 * A list of the visual ripples.
359 * True when there are visible ripples animating within the
365 reflectToAttribute
: true,
370 * If true, the ripple will remain in the "down" state until `holdDown`
371 * is set to false again.
376 observer
: '_holdDownChanged'
386 return this.animate
.bind(this);
392 var ownerRoot
= Polymer
.dom(this).getOwnerRoot();
395 if (this.parentNode
.nodeType
== 11) { // DOCUMENT_FRAGMENT_NODE
396 target
= ownerRoot
.host
;
398 target
= this.parentNode
;
405 'enter:keydown': '_onEnterKeydown',
406 'space:keydown': '_onSpaceKeydown',
407 'space:keyup': '_onSpaceKeyup'
410 attached: function() {
411 this.listen(this.target
, 'up', 'upAction');
412 this.listen(this.target
, 'down', 'downAction');
414 if (!this.target
.hasAttribute('noink')) {
415 this.keyEventTarget
= this.target
;
419 get shouldKeepAnimating () {
420 for (var index
= 0; index
< this.ripples
.length
; ++index
) {
421 if (!this.ripples
[index
].isAnimationComplete
) {
429 simulatedRipple: function() {
430 this.downAction(null);
432 // Please see polymer/polymer#1305
433 this.async(function() {
438 /** @param {Event=} event */
439 downAction: function(event
) {
440 if (this.holdDown
&& this.ripples
.length
> 0) {
444 var ripple
= this.addRipple();
446 ripple
.downAction(event
);
448 if (!this._animating
) {
453 /** @param {Event=} event */
454 upAction: function(event
) {
459 this.ripples
.forEach(function(ripple
) {
460 ripple
.upAction(event
);
466 onAnimationComplete: function() {
467 this._animating
= false;
468 this.$.background
.style
.backgroundColor
= null;
469 this.fire('transitionend');
472 addRipple: function() {
473 var ripple
= new Ripple(this);
475 Polymer
.dom(this.$.waves
).appendChild(ripple
.waveContainer
);
476 this.$.background
.style
.backgroundColor
= ripple
.color
;
477 this.ripples
.push(ripple
);
479 this._setAnimating(true);
484 removeRipple: function(ripple
) {
485 var rippleIndex
= this.ripples
.indexOf(ripple
);
487 if (rippleIndex
< 0) {
491 this.ripples
.splice(rippleIndex
, 1);
495 if (!this.ripples
.length
) {
496 this._setAnimating(false);
500 animate: function() {
504 this._animating
= true;
506 for (index
= 0; index
< this.ripples
.length
; ++index
) {
507 ripple
= this.ripples
[index
];
511 this.$.background
.style
.opacity
= ripple
.outerOpacity
;
513 if (ripple
.isOpacityFullyDecayed
&& !ripple
.isRestingAtMaxRadius
) {
514 this.removeRipple(ripple
);
518 if (!this.shouldKeepAnimating
&& this.ripples
.length
=== 0) {
519 this.onAnimationComplete();
521 window
.requestAnimationFrame(this._boundAnimate
);
525 _onEnterKeydown: function() {
527 this.async(this.upAction
, 1);
530 _onSpaceKeydown: function() {
534 _onSpaceKeyup: function() {
538 _holdDownChanged: function(holdDown
) {