diff options
Diffstat (limited to 'catapult/third_party/polymer/components/paper-ripple/paper-ripple.html')
-rw-r--r-- | catapult/third_party/polymer/components/paper-ripple/paper-ripple.html | 763 |
1 files changed, 763 insertions, 0 deletions
diff --git a/catapult/third_party/polymer/components/paper-ripple/paper-ripple.html b/catapult/third_party/polymer/components/paper-ripple/paper-ripple.html new file mode 100644 index 00000000..3fb18a37 --- /dev/null +++ b/catapult/third_party/polymer/components/paper-ripple/paper-ripple.html @@ -0,0 +1,763 @@ +<!-- +@license +Copyright (c) 2014 The Polymer Project Authors. All rights reserved. +This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt +The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt +The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt +Code distributed by Google as part of the polymer project is also +subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt +--> + +<link rel="import" href="../polymer/polymer.html"> +<link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> + +<!-- +Material design: [Surface reaction](https://www.google.com/design/spec/animation/responsive-interaction.html#responsive-interaction-surface-reaction) + +`paper-ripple` provides a visual effect that other paper elements can +use to simulate a rippling effect emanating from the point of contact. The +effect can be visualized as a concentric circle with motion. + +Example: + + <div style="position:relative"> + <paper-ripple></paper-ripple> + </div> + +Note, it's important that the parent container of the ripple be relative position, otherwise +the ripple will emanate outside of the desired container. + +`paper-ripple` listens to "mousedown" and "mouseup" events so it would display ripple +effect when touches on it. You can also defeat the default behavior and +manually route the down and up actions to the ripple element. Note that it is +important if you call `downAction()` you will have to make sure to call +`upAction()` so that `paper-ripple` would end the animation loop. + +Example: + + <paper-ripple id="ripple" style="pointer-events: none;"></paper-ripple> + ... + downAction: function(e) { + this.$.ripple.downAction({detail: {x: e.x, y: e.y}}); + }, + upAction: function(e) { + this.$.ripple.upAction(); + } + +Styling ripple effect: + + Use CSS color property to style the ripple: + + paper-ripple { + color: #4285f4; + } + + Note that CSS color property is inherited so it is not required to set it on + the `paper-ripple` element directly. + +By default, the ripple is centered on the point of contact. Apply the `recenters` +attribute to have the ripple grow toward the center of its container. + + <paper-ripple recenters></paper-ripple> + +You can also center the ripple inside its container from the start. + + <paper-ripple center></paper-ripple> + +Apply `circle` class to make the rippling effect within a circle. + + <paper-ripple class="circle"></paper-ripple> + +@group Paper Elements +@element paper-ripple +@hero hero.svg +@demo demo/index.html +--> + +<dom-module id="paper-ripple"> + + <template> + <style> + :host { + display: block; + position: absolute; + border-radius: inherit; + overflow: hidden; + top: 0; + left: 0; + right: 0; + bottom: 0; + + /* See PolymerElements/paper-behaviors/issues/34. On non-Chrome browsers, + * creating a node (with a position:absolute) in the middle of an event + * handler "interrupts" that event handler (which happens when the + * ripple is created on demand) */ + pointer-events: none; + } + + :host([animating]) { + /* This resolves a rendering issue in Chrome (as of 40) where the + ripple is not properly clipped by its parent (which may have + rounded corners). See: http://jsbin.com/temexa/4 + + Note: We only apply this style conditionally. Otherwise, the browser + will create a new compositing layer for every ripple element on the + page, and that would be bad. */ + -webkit-transform: translate(0, 0); + transform: translate3d(0, 0, 0); + } + + #background, + #waves, + .wave-container, + .wave { + pointer-events: none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + #background, + .wave { + opacity: 0; + } + + #waves, + .wave { + overflow: hidden; + } + + .wave-container, + .wave { + border-radius: 50%; + } + + :host(.circle) #background, + :host(.circle) #waves { + border-radius: 50%; + } + + :host(.circle) .wave-container { + overflow: hidden; + } + </style> + + <div id="background"></div> + <div id="waves"></div> + </template> +</dom-module> +<script> + (function() { + var Utility = { + distance: function(x1, y1, x2, y2) { + var xDelta = (x1 - x2); + var yDelta = (y1 - y2); + + return Math.sqrt(xDelta * xDelta + yDelta * yDelta); + }, + + now: window.performance && window.performance.now ? + window.performance.now.bind(window.performance) : Date.now + }; + + /** + * @param {HTMLElement} element + * @constructor + */ + function ElementMetrics(element) { + this.element = element; + this.width = this.boundingRect.width; + this.height = this.boundingRect.height; + + this.size = Math.max(this.width, this.height); + } + + ElementMetrics.prototype = { + get boundingRect () { + return this.element.getBoundingClientRect(); + }, + + furthestCornerDistanceFrom: function(x, y) { + var topLeft = Utility.distance(x, y, 0, 0); + var topRight = Utility.distance(x, y, this.width, 0); + var bottomLeft = Utility.distance(x, y, 0, this.height); + var bottomRight = Utility.distance(x, y, this.width, this.height); + + return Math.max(topLeft, topRight, bottomLeft, bottomRight); + } + }; + + /** + * @param {HTMLElement} element + * @constructor + */ + function Ripple(element) { + this.element = element; + this.color = window.getComputedStyle(element).color; + + this.wave = document.createElement('div'); + this.waveContainer = document.createElement('div'); + this.wave.style.backgroundColor = this.color; + this.wave.classList.add('wave'); + this.waveContainer.classList.add('wave-container'); + Polymer.dom(this.waveContainer).appendChild(this.wave); + + this.resetInteractionState(); + } + + Ripple.MAX_RADIUS = 300; + + Ripple.prototype = { + get recenters() { + return this.element.recenters; + }, + + get center() { + return this.element.center; + }, + + get mouseDownElapsed() { + var elapsed; + + if (!this.mouseDownStart) { + return 0; + } + + elapsed = Utility.now() - this.mouseDownStart; + + if (this.mouseUpStart) { + elapsed -= this.mouseUpElapsed; + } + + return elapsed; + }, + + get mouseUpElapsed() { + return this.mouseUpStart ? + Utility.now () - this.mouseUpStart : 0; + }, + + get mouseDownElapsedSeconds() { + return this.mouseDownElapsed / 1000; + }, + + get mouseUpElapsedSeconds() { + return this.mouseUpElapsed / 1000; + }, + + get mouseInteractionSeconds() { + return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; + }, + + get initialOpacity() { + return this.element.initialOpacity; + }, + + get opacityDecayVelocity() { + return this.element.opacityDecayVelocity; + }, + + get radius() { + var width2 = this.containerMetrics.width * this.containerMetrics.width; + var height2 = this.containerMetrics.height * this.containerMetrics.height; + var waveRadius = Math.min( + Math.sqrt(width2 + height2), + Ripple.MAX_RADIUS + ) * 1.1 + 5; + + var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS); + var timeNow = this.mouseInteractionSeconds / duration; + var size = waveRadius * (1 - Math.pow(80, -timeNow)); + + return Math.abs(size); + }, + + get opacity() { + if (!this.mouseUpStart) { + return this.initialOpacity; + } + + return Math.max( + 0, + this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVelocity + ); + }, + + get outerOpacity() { + // Linear increase in background opacity, capped at the opacity + // of the wavefront (waveOpacity). + var outerOpacity = this.mouseUpElapsedSeconds * 0.3; + var waveOpacity = this.opacity; + + return Math.max( + 0, + Math.min(outerOpacity, waveOpacity) + ); + }, + + get isOpacityFullyDecayed() { + return this.opacity < 0.01 && + this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); + }, + + get isRestingAtMaxRadius() { + return this.opacity >= this.initialOpacity && + this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); + }, + + get isAnimationComplete() { + return this.mouseUpStart ? + this.isOpacityFullyDecayed : this.isRestingAtMaxRadius; + }, + + get translationFraction() { + return Math.min( + 1, + this.radius / this.containerMetrics.size * 2 / Math.sqrt(2) + ); + }, + + get xNow() { + if (this.xEnd) { + return this.xStart + this.translationFraction * (this.xEnd - this.xStart); + } + + return this.xStart; + }, + + get yNow() { + if (this.yEnd) { + return this.yStart + this.translationFraction * (this.yEnd - this.yStart); + } + + return this.yStart; + }, + + get isMouseDown() { + return this.mouseDownStart && !this.mouseUpStart; + }, + + resetInteractionState: function() { + this.maxRadius = 0; + this.mouseDownStart = 0; + this.mouseUpStart = 0; + + this.xStart = 0; + this.yStart = 0; + this.xEnd = 0; + this.yEnd = 0; + this.slideDistance = 0; + + this.containerMetrics = new ElementMetrics(this.element); + }, + + draw: function() { + var scale; + var translateString; + var dx; + var dy; + + this.wave.style.opacity = this.opacity; + + scale = this.radius / (this.containerMetrics.size / 2); + dx = this.xNow - (this.containerMetrics.width / 2); + dy = this.yNow - (this.containerMetrics.height / 2); + + + // 2d transform for safari because of border-radius and overflow:hidden clipping bug. + // https://bugs.webkit.org/show_bug.cgi?id=98538 + this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)'; + this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + 'px, 0)'; + this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; + this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; + }, + + /** @param {Event=} event */ + downAction: function(event) { + var xCenter = this.containerMetrics.width / 2; + var yCenter = this.containerMetrics.height / 2; + + this.resetInteractionState(); + this.mouseDownStart = Utility.now(); + + if (this.center) { + this.xStart = xCenter; + this.yStart = yCenter; + this.slideDistance = Utility.distance( + this.xStart, this.yStart, this.xEnd, this.yEnd + ); + } else { + this.xStart = event ? + event.detail.x - this.containerMetrics.boundingRect.left : + this.containerMetrics.width / 2; + this.yStart = event ? + event.detail.y - this.containerMetrics.boundingRect.top : + this.containerMetrics.height / 2; + } + + if (this.recenters) { + this.xEnd = xCenter; + this.yEnd = yCenter; + this.slideDistance = Utility.distance( + this.xStart, this.yStart, this.xEnd, this.yEnd + ); + } + + this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom( + this.xStart, + this.yStart + ); + + this.waveContainer.style.top = + (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px'; + this.waveContainer.style.left = + (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px'; + + this.waveContainer.style.width = this.containerMetrics.size + 'px'; + this.waveContainer.style.height = this.containerMetrics.size + 'px'; + }, + + /** @param {Event=} event */ + upAction: function(event) { + if (!this.isMouseDown) { + return; + } + + this.mouseUpStart = Utility.now(); + }, + + remove: function() { + Polymer.dom(this.waveContainer.parentNode).removeChild( + this.waveContainer + ); + } + }; + + Polymer({ + is: 'paper-ripple', + + behaviors: [ + Polymer.IronA11yKeysBehavior + ], + + properties: { + /** + * The initial opacity set on the wave. + * + * @attribute initialOpacity + * @type number + * @default 0.25 + */ + initialOpacity: { + type: Number, + value: 0.25 + }, + + /** + * How fast (opacity per second) the wave fades out. + * + * @attribute opacityDecayVelocity + * @type number + * @default 0.8 + */ + opacityDecayVelocity: { + type: Number, + value: 0.8 + }, + + /** + * If true, ripples will exhibit a gravitational pull towards + * the center of their container as they fade away. + * + * @attribute recenters + * @type boolean + * @default false + */ + recenters: { + type: Boolean, + value: false + }, + + /** + * If true, ripples will center inside its container + * + * @attribute recenters + * @type boolean + * @default false + */ + center: { + type: Boolean, + value: false + }, + + /** + * A list of the visual ripples. + * + * @attribute ripples + * @type Array + * @default [] + */ + ripples: { + type: Array, + value: function() { + return []; + } + }, + + /** + * True when there are visible ripples animating within the + * element. + */ + animating: { + type: Boolean, + readOnly: true, + reflectToAttribute: true, + value: false + }, + + /** + * If true, the ripple will remain in the "down" state until `holdDown` + * is set to false again. + */ + holdDown: { + type: Boolean, + value: false, + observer: '_holdDownChanged' + }, + + /** + * If true, the ripple will not generate a ripple effect + * via pointer interaction. + * Calling ripple's imperative api like `simulatedRipple` will + * still generate the ripple effect. + */ + noink: { + type: Boolean, + value: false + }, + + _animating: { + type: Boolean + }, + + _boundAnimate: { + type: Function, + value: function() { + return this.animate.bind(this); + } + } + }, + + get target () { + return this.keyEventTarget; + }, + + keyBindings: { + 'enter:keydown': '_onEnterKeydown', + 'space:keydown': '_onSpaceKeydown', + 'space:keyup': '_onSpaceKeyup' + }, + + attached: function() { + // Set up a11yKeysBehavior to listen to key events on the target, + // so that space and enter activate the ripple even if the target doesn't + // handle key events. The key handlers deal with `noink` themselves. + if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE + this.keyEventTarget = Polymer.dom(this).getOwnerRoot().host; + } else { + this.keyEventTarget = this.parentNode; + } + var keyEventTarget = /** @type {!EventTarget} */ (this.keyEventTarget); + this.listen(keyEventTarget, 'up', 'uiUpAction'); + this.listen(keyEventTarget, 'down', 'uiDownAction'); + }, + + detached: function() { + this.unlisten(this.keyEventTarget, 'up', 'uiUpAction'); + this.unlisten(this.keyEventTarget, 'down', 'uiDownAction'); + this.keyEventTarget = null; + }, + + get shouldKeepAnimating () { + for (var index = 0; index < this.ripples.length; ++index) { + if (!this.ripples[index].isAnimationComplete) { + return true; + } + } + + return false; + }, + + simulatedRipple: function() { + this.downAction(null); + + // Please see polymer/polymer#1305 + this.async(function() { + this.upAction(); + }, 1); + }, + + /** + * Provokes a ripple down effect via a UI event, + * respecting the `noink` property. + * @param {Event=} event + */ + uiDownAction: function(event) { + if (!this.noink) { + this.downAction(event); + } + }, + + /** + * Provokes a ripple down effect via a UI event, + * *not* respecting the `noink` property. + * @param {Event=} event + */ + downAction: function(event) { + if (this.holdDown && this.ripples.length > 0) { + return; + } + + var ripple = this.addRipple(); + + ripple.downAction(event); + + if (!this._animating) { + this._animating = true; + this.animate(); + } + }, + + /** + * Provokes a ripple up effect via a UI event, + * respecting the `noink` property. + * @param {Event=} event + */ + uiUpAction: function(event) { + if (!this.noink) { + this.upAction(event); + } + }, + + /** + * Provokes a ripple up effect via a UI event, + * *not* respecting the `noink` property. + * @param {Event=} event + */ + upAction: function(event) { + if (this.holdDown) { + return; + } + + this.ripples.forEach(function(ripple) { + ripple.upAction(event); + }); + + this._animating = true; + this.animate(); + }, + + onAnimationComplete: function() { + this._animating = false; + this.$.background.style.backgroundColor = null; + this.fire('transitionend'); + }, + + addRipple: function() { + var ripple = new Ripple(this); + + Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); + this.$.background.style.backgroundColor = ripple.color; + this.ripples.push(ripple); + + this._setAnimating(true); + + return ripple; + }, + + removeRipple: function(ripple) { + var rippleIndex = this.ripples.indexOf(ripple); + + if (rippleIndex < 0) { + return; + } + + this.ripples.splice(rippleIndex, 1); + + ripple.remove(); + + if (!this.ripples.length) { + this._setAnimating(false); + } + }, + + /** + * This conflicts with Element#antimate(). + * https://developer.mozilla.org/en-US/docs/Web/API/Element/animate + * @suppress {checkTypes} + */ + animate: function() { + if (!this._animating) { + return; + } + var index; + var ripple; + + for (index = 0; index < this.ripples.length; ++index) { + ripple = this.ripples[index]; + + ripple.draw(); + + this.$.background.style.opacity = ripple.outerOpacity; + + if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { + this.removeRipple(ripple); + } + } + + if (!this.shouldKeepAnimating && this.ripples.length === 0) { + this.onAnimationComplete(); + } else { + window.requestAnimationFrame(this._boundAnimate); + } + }, + + _onEnterKeydown: function() { + this.uiDownAction(); + this.async(this.uiUpAction, 1); + }, + + _onSpaceKeydown: function() { + this.uiDownAction(); + }, + + _onSpaceKeyup: function() { + this.uiUpAction(); + }, + + // note: holdDown does not respect noink since it can be a focus based + // effect. + _holdDownChanged: function(newVal, oldVal) { + if (oldVal === undefined) { + return; + } + if (newVal) { + this.downAction(); + } else { + this.upAction(); + } + } + + /** + Fired when the animation finishes. + This is useful if you want to wait until + the ripple animation finishes to perform some action. + + @event transitionend + @param {{node: Object}} detail Contains the animated node. + */ + }); + })(); +</script> |