3 cssColorWithAlpha: function(cssColor, alpha) {
4 var parts = cssColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
6 if (typeof alpha == 'undefined') {
11 return 'rgba(255, 255, 255, ' + alpha + ')';
14 return 'rgba(' + parts[1] + ', ' + parts[2] + ', ' + parts[3] + ', ' + alpha + ')';
17 distance: function(x1, y1, x2, y2) {
18 var xDelta = (x1 - x2);
19 var yDelta = (y1 - y2);
21 return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
25 if (window.performance && window.performance.now) {
26 return window.performance.now.bind(window.performance);
34 * @param {HTMLElement} element
37 function ElementMetrics(element) {
38 this.element = element;
39 this.width = this.boundingRect.width;
40 this.height = this.boundingRect.height;
42 this.size = Math.max(this.width, this.height);
45 ElementMetrics.prototype = {
47 return this.element.getBoundingClientRect();
50 furthestCornerDistanceFrom: function(x, y) {
51 var topLeft = Utility.distance(x, y, 0, 0);
52 var topRight = Utility.distance(x, y, this.width, 0);
53 var bottomLeft = Utility.distance(x, y, 0, this.height);
54 var bottomRight = Utility.distance(x, y, this.width, this.height);
56 return Math.max(topLeft, topRight, bottomLeft, bottomRight);
61 * @param {HTMLElement} element
64 function Ripple(element) {
65 this.element = element;
66 this.color = window.getComputedStyle(element).color;
68 this.wave = document.createElement('div');
69 this.waveContainer = document.createElement('div');
70 this.wave.style.backgroundColor = this.color;
71 this.wave.classList.add('wave');
72 this.waveContainer.classList.add('wave-container');
73 Polymer.dom(this.waveContainer).appendChild(this.wave);
75 this.resetInteractionState();
78 Ripple.MAX_RADIUS = 300;
82 return this.element.recenters;
86 return this.element.center;
89 get mouseDownElapsed() {
92 if (!this.mouseDownStart) {
96 elapsed = Utility.now() - this.mouseDownStart;
98 if (this.mouseUpStart) {
99 elapsed -= this.mouseUpElapsed;
105 get mouseUpElapsed() {
106 return this.mouseUpStart ?
107 Utility.now () - this.mouseUpStart : 0;
110 get mouseDownElapsedSeconds() {
111 return this.mouseDownElapsed / 1000;
114 get mouseUpElapsedSeconds() {
115 return this.mouseUpElapsed / 1000;
118 get mouseInteractionSeconds() {
119 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
122 get initialOpacity() {
123 return this.element.initialOpacity;
126 get opacityDecayVelocity() {
127 return this.element.opacityDecayVelocity;
131 var width2 = this.containerMetrics.width * this.containerMetrics.width;
132 var height2 = this.containerMetrics.height * this.containerMetrics.height;
133 var waveRadius = Math.min(
134 Math.sqrt(width2 + height2),
138 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS);
139 var timeNow = this.mouseInteractionSeconds / duration;
140 var size = waveRadius * (1 - Math.pow(80, -timeNow));
142 return Math.abs(size);
146 if (!this.mouseUpStart) {
147 return this.initialOpacity;
152 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVelocity
157 // Linear increase in background opacity, capped at the opacity
158 // of the wavefront (waveOpacity).
159 var outerOpacity = this.mouseUpElapsedSeconds * 0.3;
160 var waveOpacity = this.opacity;
164 Math.min(outerOpacity, waveOpacity)
168 get isOpacityFullyDecayed() {
169 return this.opacity < 0.01 &&
170 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
173 get isRestingAtMaxRadius() {
174 return this.opacity >= this.initialOpacity &&
175 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
178 get isAnimationComplete() {
179 return this.mouseUpStart ?
180 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius;
183 get translationFraction() {
186 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2)
192 return this.xStart + this.translationFraction * (this.xEnd - this.xStart);
200 return this.yStart + this.translationFraction * (this.yEnd - this.yStart);
207 return this.mouseDownStart && !this.mouseUpStart;
210 resetInteractionState: function() {
212 this.mouseDownStart = 0;
213 this.mouseUpStart = 0;
219 this.slideDistance = 0;
221 this.containerMetrics = new ElementMetrics(this.element);
230 this.wave.style.opacity = this.opacity;
232 scale = this.radius / (this.containerMetrics.size / 2);
233 dx = this.xNow - (this.containerMetrics.width / 2);
234 dy = this.yNow - (this.containerMetrics.height / 2);
237 // 2d transform for safari because of border-radius and overflow:hidden clipping bug.
238 // https://bugs.webkit.org/show_bug.cgi?id=98538
239 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
240 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + 'px, 0)';
241 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
242 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
245 /** @param {Event=} event */
246 downAction: function(event) {
247 var xCenter = this.containerMetrics.width / 2;
248 var yCenter = this.containerMetrics.height / 2;
250 this.resetInteractionState();
251 this.mouseDownStart = Utility.now();
254 this.xStart = xCenter;
255 this.yStart = yCenter;
256 this.slideDistance = Utility.distance(
257 this.xStart, this.yStart, this.xEnd, this.yEnd
260 this.xStart = event ?
261 event.detail.x - this.containerMetrics.boundingRect.left :
262 this.containerMetrics.width / 2;
263 this.yStart = event ?
264 event.detail.y - this.containerMetrics.boundingRect.top :
265 this.containerMetrics.height / 2;
268 if (this.recenters) {
271 this.slideDistance = Utility.distance(
272 this.xStart, this.yStart, this.xEnd, this.yEnd
276 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(
281 this.waveContainer.style.top =
282 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px';
283 this.waveContainer.style.left =
284 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px';
286 this.waveContainer.style.width = this.containerMetrics.size + 'px';
287 this.waveContainer.style.height = this.containerMetrics.size + 'px';
290 /** @param {Event=} event */
291 upAction: function(event) {
292 if (!this.isMouseDown) {
296 this.mouseUpStart = Utility.now();
300 Polymer.dom(this.waveContainer.parentNode).removeChild(
310 Polymer.IronA11yKeysBehavior
315 * The initial opacity set on the wave.
317 * @attribute initialOpacity
327 * How fast (opacity per second) the wave fades out.
329 * @attribute opacityDecayVelocity
333 opacityDecayVelocity: {
339 * If true, ripples will exhibit a gravitational pull towards
340 * the center of their container as they fade away.
342 * @attribute recenters
352 * If true, ripples will center inside its container
354 * @attribute recenters
364 * A list of the visual ripples.
378 * True when there are visible ripples animating within the
384 reflectToAttribute: true,
389 * If true, the ripple will remain in the "down" state until `holdDown`
390 * is set to false again.
395 observer: '_holdDownChanged'
405 return this.animate.bind(this);
411 var ownerRoot = Polymer.dom(this).getOwnerRoot();
414 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE
415 target = ownerRoot.host;
417 target = this.parentNode;
424 'enter:keydown': '_onEnterKeydown',
425 'space:keydown': '_onSpaceKeydown',
426 'space:keyup': '_onSpaceKeyup'
429 attached: function() {
430 this.listen(this.target, 'up', 'upAction');
431 this.listen(this.target, 'down', 'downAction');
433 if (!this.target.hasAttribute('noink')) {
434 this.keyEventTarget = this.target;
438 get shouldKeepAnimating () {
439 for (var index = 0; index < this.ripples.length; ++index) {
440 if (!this.ripples[index].isAnimationComplete) {
448 simulatedRipple: function() {
449 this.downAction(null);
451 // Please see polymer/polymer#1305
452 this.async(function() {
457 /** @param {Event=} event */
458 downAction: function(event) {
459 if (this.holdDown && this.ripples.length > 0) {
463 var ripple = this.addRipple();
465 ripple.downAction(event);
467 if (!this._animating) {
472 /** @param {Event=} event */
473 upAction: function(event) {
478 this.ripples.forEach(function(ripple) {
479 ripple.upAction(event);
485 onAnimationComplete: function() {
486 this._animating = false;
487 this.$.background.style.backgroundColor = null;
488 this.fire('transitionend');
491 addRipple: function() {
492 var ripple = new Ripple(this);
494 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer);
495 this.$.background.style.backgroundColor = ripple.color;
496 this.ripples.push(ripple);
498 this._setAnimating(true);
503 removeRipple: function(ripple) {
504 var rippleIndex = this.ripples.indexOf(ripple);
506 if (rippleIndex < 0) {
510 this.ripples.splice(rippleIndex, 1);
514 if (!this.ripples.length) {
515 this._setAnimating(false);
519 animate: function() {
523 this._animating = true;
525 for (index = 0; index < this.ripples.length; ++index) {
526 ripple = this.ripples[index];
530 this.$.background.style.opacity = ripple.outerOpacity;
532 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) {
533 this.removeRipple(ripple);
537 if (!this.shouldKeepAnimating && this.ripples.length === 0) {
538 this.onAnimationComplete();
540 window.requestAnimationFrame(this._boundAnimate);
544 _onEnterKeydown: function() {
546 this.async(this.upAction, 1);
549 _onSpaceKeydown: function() {
553 _onSpaceKeyup: function() {
557 _holdDownChanged: function(holdDown) {