478 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			478 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
<!--
 | 
						|
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
 | 
						|
-->
 | 
						|
 | 
						|
<!--
 | 
						|
`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:
 | 
						|
 | 
						|
    <paper-ripple></paper-ripple>
 | 
						|
 | 
						|
`paper-ripple` listens to "down" and "up" 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({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 `recenteringTouch` 
 | 
						|
class to have the ripple grow toward the center of its container.
 | 
						|
 | 
						|
    <paper-ripple class="recenteringTouch"></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
 | 
						|
@homepage github.io
 | 
						|
-->
 | 
						|
 | 
						|
<!--
 | 
						|
Fired when the animation finishes. This is useful if you want to wait until the ripple
 | 
						|
animation finishes to perform some action.
 | 
						|
 | 
						|
@event core-transitionend
 | 
						|
@param {Object} detail
 | 
						|
@param {Object} detail.node The animated node
 | 
						|
-->
 | 
						|
 | 
						|
<link rel="import" href="../polymer/polymer.html" >
 | 
						|
 | 
						|
<polymer-element name="paper-ripple" attributes="initialOpacity opacityDecayVelocity">
 | 
						|
<template>
 | 
						|
 | 
						|
  <style>
 | 
						|
 | 
						|
    :host {
 | 
						|
      display: block;
 | 
						|
      position: relative;
 | 
						|
      border-radius: inherit;
 | 
						|
      overflow: hidden;
 | 
						|
    }
 | 
						|
 | 
						|
    :host-context([noink]) {
 | 
						|
      pointer-events: none;
 | 
						|
    }
 | 
						|
 | 
						|
    #bg, #waves, .wave-container, .wave {
 | 
						|
      pointer-events: none;
 | 
						|
      position: absolute;
 | 
						|
      top: 0;
 | 
						|
      left: 0;
 | 
						|
      width: 100%;
 | 
						|
      height: 100%;
 | 
						|
    }
 | 
						|
 | 
						|
    #bg, .wave {
 | 
						|
      opacity: 0;
 | 
						|
    }
 | 
						|
 | 
						|
    #waves, .wave {
 | 
						|
      overflow: hidden;
 | 
						|
    }
 | 
						|
 | 
						|
    .wave-container, .wave {
 | 
						|
      border-radius: 50%;
 | 
						|
    }
 | 
						|
 | 
						|
    :host(.circle) #bg,
 | 
						|
    :host(.circle) #waves {
 | 
						|
      border-radius: 50%;
 | 
						|
    }
 | 
						|
 | 
						|
    :host(.circle) .wave-container {
 | 
						|
      overflow: hidden;
 | 
						|
    }
 | 
						|
 | 
						|
  </style>
 | 
						|
 | 
						|
  <div id="bg"></div>
 | 
						|
  <div id="waves">
 | 
						|
  </div>
 | 
						|
 | 
						|
</template>
 | 
						|
<script>
 | 
						|
 | 
						|
  (function() {
 | 
						|
 | 
						|
    var waveMaxRadius = 150;
 | 
						|
    //
 | 
						|
    // INK EQUATIONS
 | 
						|
    //
 | 
						|
    function waveRadiusFn(touchDownMs, touchUpMs, anim) {
 | 
						|
      // Convert from ms to s
 | 
						|
      var touchDown = touchDownMs / 1000;
 | 
						|
      var touchUp = touchUpMs / 1000;
 | 
						|
      var totalElapsed = touchDown + touchUp;
 | 
						|
      var ww = anim.width, hh = anim.height;
 | 
						|
      // use diagonal size of container to avoid floating point math sadness
 | 
						|
      var waveRadius = Math.min(Math.sqrt(ww * ww + hh * hh), waveMaxRadius) * 1.1 + 5;
 | 
						|
      var duration = 1.1 - .2 * (waveRadius / waveMaxRadius);
 | 
						|
      var tt = (totalElapsed / duration);
 | 
						|
 | 
						|
      var size = waveRadius * (1 - Math.pow(80, -tt));
 | 
						|
      return Math.abs(size);
 | 
						|
    }
 | 
						|
 | 
						|
    function waveOpacityFn(td, tu, anim) {
 | 
						|
      // Convert from ms to s.
 | 
						|
      var touchDown = td / 1000;
 | 
						|
      var touchUp = tu / 1000;
 | 
						|
      var totalElapsed = touchDown + touchUp;
 | 
						|
 | 
						|
      if (tu <= 0) {  // before touch up
 | 
						|
        return anim.initialOpacity;
 | 
						|
      }
 | 
						|
      return Math.max(0, anim.initialOpacity - touchUp * anim.opacityDecayVelocity);
 | 
						|
    }
 | 
						|
 | 
						|
    function waveOuterOpacityFn(td, tu, anim) {
 | 
						|
      // Convert from ms to s.
 | 
						|
      var touchDown = td / 1000;
 | 
						|
      var touchUp = tu / 1000;
 | 
						|
 | 
						|
      // Linear increase in background opacity, capped at the opacity
 | 
						|
      // of the wavefront (waveOpacity).
 | 
						|
      var outerOpacity = touchDown * 0.3;
 | 
						|
      var waveOpacity = waveOpacityFn(td, tu, anim);
 | 
						|
      return Math.max(0, Math.min(outerOpacity, waveOpacity));
 | 
						|
    }
 | 
						|
 | 
						|
    // Determines whether the wave should be completely removed.
 | 
						|
    function waveDidFinish(wave, radius, anim) {
 | 
						|
      var waveOpacity = waveOpacityFn(wave.tDown, wave.tUp, anim);
 | 
						|
 | 
						|
      // If the wave opacity is 0 and the radius exceeds the bounds
 | 
						|
      // of the element, then this is finished.
 | 
						|
      return waveOpacity < 0.01 && radius >= Math.min(wave.maxRadius, waveMaxRadius);
 | 
						|
    };
 | 
						|
 | 
						|
    function waveAtMaximum(wave, radius, anim) {
 | 
						|
      var waveOpacity = waveOpacityFn(wave.tDown, wave.tUp, anim);
 | 
						|
 | 
						|
      return waveOpacity >= anim.initialOpacity && radius >= Math.min(wave.maxRadius, waveMaxRadius);
 | 
						|
    }
 | 
						|
 | 
						|
    //
 | 
						|
    // DRAWING
 | 
						|
    //
 | 
						|
    function drawRipple(ctx, x, y, radius, innerAlpha, outerAlpha) {
 | 
						|
      // Only animate opacity and transform
 | 
						|
      if (outerAlpha !== undefined) {
 | 
						|
        ctx.bg.style.opacity = outerAlpha;
 | 
						|
      }
 | 
						|
      ctx.wave.style.opacity = innerAlpha;
 | 
						|
 | 
						|
      var s = radius / (ctx.containerSize / 2);
 | 
						|
      var dx = x - (ctx.containerWidth / 2);
 | 
						|
      var dy = y - (ctx.containerHeight / 2);
 | 
						|
 | 
						|
      ctx.wc.style.webkitTransform = 'translate3d(' + dx + 'px,' + dy + 'px,0)';
 | 
						|
      ctx.wc.style.transform = 'translate3d(' + dx + 'px,' + dy + 'px,0)';
 | 
						|
 | 
						|
      // 2d transform for safari because of border-radius and overflow:hidden clipping bug.
 | 
						|
      // https://bugs.webkit.org/show_bug.cgi?id=98538
 | 
						|
      ctx.wave.style.webkitTransform = 'scale(' + s + ',' + s + ')';
 | 
						|
      ctx.wave.style.transform = 'scale3d(' + s + ',' + s + ',1)';
 | 
						|
    }
 | 
						|
 | 
						|
    //
 | 
						|
    // SETUP
 | 
						|
    //
 | 
						|
    function createWave(elem) {
 | 
						|
      var elementStyle = window.getComputedStyle(elem);
 | 
						|
      var fgColor = elementStyle.color;
 | 
						|
 | 
						|
      var inner = document.createElement('div');
 | 
						|
      inner.style.backgroundColor = fgColor;
 | 
						|
      inner.classList.add('wave');
 | 
						|
 | 
						|
      var outer = document.createElement('div');
 | 
						|
      outer.classList.add('wave-container');
 | 
						|
      outer.appendChild(inner);
 | 
						|
 | 
						|
      var container = elem.$.waves;
 | 
						|
      container.appendChild(outer);
 | 
						|
 | 
						|
      elem.$.bg.style.backgroundColor = fgColor;
 | 
						|
 | 
						|
      var wave = {
 | 
						|
        bg: elem.$.bg,
 | 
						|
        wc: outer,
 | 
						|
        wave: inner,
 | 
						|
        waveColor: fgColor,
 | 
						|
        maxRadius: 0,
 | 
						|
        isMouseDown: false,
 | 
						|
        mouseDownStart: 0.0,
 | 
						|
        mouseUpStart: 0.0,
 | 
						|
        tDown: 0,
 | 
						|
        tUp: 0
 | 
						|
      };
 | 
						|
      return wave;
 | 
						|
    }
 | 
						|
 | 
						|
    function removeWaveFromScope(scope, wave) {
 | 
						|
      if (scope.waves) {
 | 
						|
        var pos = scope.waves.indexOf(wave);
 | 
						|
        scope.waves.splice(pos, 1);
 | 
						|
        // FIXME cache nodes
 | 
						|
        wave.wc.remove();
 | 
						|
      }
 | 
						|
    };
 | 
						|
 | 
						|
    // Shortcuts.
 | 
						|
    var pow = Math.pow;
 | 
						|
    var now = Date.now;
 | 
						|
    if (window.performance && performance.now) {
 | 
						|
      now = performance.now.bind(performance);
 | 
						|
    }
 | 
						|
 | 
						|
    function cssColorWithAlpha(cssColor, alpha) {
 | 
						|
        var parts = cssColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
 | 
						|
        if (typeof alpha == 'undefined') {
 | 
						|
            alpha = 1;
 | 
						|
        }
 | 
						|
        if (!parts) {
 | 
						|
          return 'rgba(255, 255, 255, ' + alpha + ')';
 | 
						|
        }
 | 
						|
        return 'rgba(' + parts[1] + ', ' + parts[2] + ', ' + parts[3] + ', ' + alpha + ')';
 | 
						|
    }
 | 
						|
 | 
						|
    function dist(p1, p2) {
 | 
						|
      return Math.sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));
 | 
						|
    }
 | 
						|
 | 
						|
    function distanceFromPointToFurthestCorner(point, size) {
 | 
						|
      var tl_d = dist(point, {x: 0, y: 0});
 | 
						|
      var tr_d = dist(point, {x: size.w, y: 0});
 | 
						|
      var bl_d = dist(point, {x: 0, y: size.h});
 | 
						|
      var br_d = dist(point, {x: size.w, y: size.h});
 | 
						|
      return Math.max(tl_d, tr_d, bl_d, br_d);
 | 
						|
    }
 | 
						|
 | 
						|
    Polymer('paper-ripple', {
 | 
						|
 | 
						|
      /**
 | 
						|
       * The initial opacity set on the wave.
 | 
						|
       *
 | 
						|
       * @attribute initialOpacity
 | 
						|
       * @type number
 | 
						|
       * @default 0.25
 | 
						|
       */
 | 
						|
      initialOpacity: 0.25,
 | 
						|
 | 
						|
      /**
 | 
						|
       * How fast (opacity per second) the wave fades out.
 | 
						|
       *
 | 
						|
       * @attribute opacityDecayVelocity
 | 
						|
       * @type number
 | 
						|
       * @default 0.8
 | 
						|
       */
 | 
						|
      opacityDecayVelocity: 0.8,
 | 
						|
 | 
						|
      backgroundFill: true,
 | 
						|
      pixelDensity: 2,
 | 
						|
 | 
						|
      eventDelegates: {
 | 
						|
        down: 'downAction',
 | 
						|
        up: 'upAction'
 | 
						|
      },
 | 
						|
 | 
						|
      ready: function() {
 | 
						|
        this.waves = [];
 | 
						|
      },
 | 
						|
 | 
						|
      downAction: function(e) {
 | 
						|
        var wave = createWave(this);
 | 
						|
 | 
						|
        this.cancelled = false;
 | 
						|
        wave.isMouseDown = true;
 | 
						|
        wave.tDown = 0.0;
 | 
						|
        wave.tUp = 0.0;
 | 
						|
        wave.mouseUpStart = 0.0;
 | 
						|
        wave.mouseDownStart = now();
 | 
						|
 | 
						|
        var rect = this.getBoundingClientRect();
 | 
						|
        var width = rect.width;
 | 
						|
        var height = rect.height;
 | 
						|
        var touchX = e.x - rect.left;
 | 
						|
        var touchY = e.y - rect.top;
 | 
						|
 | 
						|
        wave.startPosition = {x:touchX, y:touchY};
 | 
						|
 | 
						|
        if (this.classList.contains("recenteringTouch")) {
 | 
						|
          wave.endPosition = {x: width / 2,  y: height / 2};
 | 
						|
          wave.slideDistance = dist(wave.startPosition, wave.endPosition);
 | 
						|
        }
 | 
						|
        wave.containerSize = Math.max(width, height);
 | 
						|
        wave.containerWidth = width;
 | 
						|
        wave.containerHeight = height;
 | 
						|
        wave.maxRadius = distanceFromPointToFurthestCorner(wave.startPosition, {w: width, h: height});
 | 
						|
 | 
						|
        // The wave is circular so constrain its container to 1:1
 | 
						|
        wave.wc.style.top = (wave.containerHeight - wave.containerSize) / 2 + 'px';
 | 
						|
        wave.wc.style.left = (wave.containerWidth - wave.containerSize) / 2 + 'px';
 | 
						|
        wave.wc.style.width = wave.containerSize + 'px';
 | 
						|
        wave.wc.style.height = wave.containerSize + 'px';
 | 
						|
 | 
						|
        this.waves.push(wave);
 | 
						|
 | 
						|
        if (!this._loop) {
 | 
						|
          this._loop = this.animate.bind(this, {
 | 
						|
            width: width,
 | 
						|
            height: height
 | 
						|
          });
 | 
						|
          requestAnimationFrame(this._loop);
 | 
						|
        }
 | 
						|
        // else there is already a rAF
 | 
						|
      },
 | 
						|
 | 
						|
      upAction: function() {
 | 
						|
        for (var i = 0; i < this.waves.length; i++) {
 | 
						|
          // Declare the next wave that has mouse down to be mouse'ed up.
 | 
						|
          var wave = this.waves[i];
 | 
						|
          if (wave.isMouseDown) {
 | 
						|
            wave.isMouseDown = false
 | 
						|
            wave.mouseUpStart = now();
 | 
						|
            wave.mouseDownStart = 0;
 | 
						|
            wave.tUp = 0.0;
 | 
						|
            break;
 | 
						|
          }
 | 
						|
        }
 | 
						|
        this._loop && requestAnimationFrame(this._loop);
 | 
						|
      },
 | 
						|
 | 
						|
      cancel: function() {
 | 
						|
        this.cancelled = true;
 | 
						|
      },
 | 
						|
 | 
						|
      animate: function(ctx) {
 | 
						|
        var shouldRenderNextFrame = false;
 | 
						|
 | 
						|
        var deleteTheseWaves = [];
 | 
						|
        // The oldest wave's touch down duration
 | 
						|
        var longestTouchDownDuration = 0;
 | 
						|
        var longestTouchUpDuration = 0;
 | 
						|
        // Save the last known wave color
 | 
						|
        var lastWaveColor = null;
 | 
						|
        // wave animation values
 | 
						|
        var anim = {
 | 
						|
          initialOpacity: this.initialOpacity,
 | 
						|
          opacityDecayVelocity: this.opacityDecayVelocity,
 | 
						|
          height: ctx.height,
 | 
						|
          width: ctx.width
 | 
						|
        }
 | 
						|
 | 
						|
        for (var i = 0; i < this.waves.length; i++) {
 | 
						|
          var wave = this.waves[i];
 | 
						|
 | 
						|
          if (wave.mouseDownStart > 0) {
 | 
						|
            wave.tDown = now() - wave.mouseDownStart;
 | 
						|
          }
 | 
						|
          if (wave.mouseUpStart > 0) {
 | 
						|
            wave.tUp = now() - wave.mouseUpStart;
 | 
						|
          }
 | 
						|
 | 
						|
          // Determine how long the touch has been up or down.
 | 
						|
          var tUp = wave.tUp;
 | 
						|
          var tDown = wave.tDown;
 | 
						|
          longestTouchDownDuration = Math.max(longestTouchDownDuration, tDown);
 | 
						|
          longestTouchUpDuration = Math.max(longestTouchUpDuration, tUp);
 | 
						|
 | 
						|
          // Obtain the instantenous size and alpha of the ripple.
 | 
						|
          var radius = waveRadiusFn(tDown, tUp, anim);
 | 
						|
          var waveAlpha =  waveOpacityFn(tDown, tUp, anim);
 | 
						|
          var waveColor = cssColorWithAlpha(wave.waveColor, waveAlpha);
 | 
						|
          lastWaveColor = wave.waveColor;
 | 
						|
 | 
						|
          // Position of the ripple.
 | 
						|
          var x = wave.startPosition.x;
 | 
						|
          var y = wave.startPosition.y;
 | 
						|
 | 
						|
          // Ripple gravitational pull to the center of the canvas.
 | 
						|
          if (wave.endPosition) {
 | 
						|
 | 
						|
            // This translates from the origin to the center of the view  based on the max dimension of
 | 
						|
            var translateFraction = Math.min(1, radius / wave.containerSize * 2 / Math.sqrt(2) );
 | 
						|
 | 
						|
            x += translateFraction * (wave.endPosition.x - wave.startPosition.x);
 | 
						|
            y += translateFraction * (wave.endPosition.y - wave.startPosition.y);
 | 
						|
          }
 | 
						|
 | 
						|
          // If we do a background fill fade too, work out the correct color.
 | 
						|
          var bgFillColor = null;
 | 
						|
          if (this.backgroundFill) {
 | 
						|
            var bgFillAlpha = waveOuterOpacityFn(tDown, tUp, anim);
 | 
						|
            bgFillColor = cssColorWithAlpha(wave.waveColor, bgFillAlpha);
 | 
						|
          }
 | 
						|
 | 
						|
          // Draw the ripple.
 | 
						|
          drawRipple(wave, x, y, radius, waveAlpha, bgFillAlpha);
 | 
						|
 | 
						|
          // Determine whether there is any more rendering to be done.
 | 
						|
          var maximumWave = waveAtMaximum(wave, radius, anim);
 | 
						|
          var waveDissipated = waveDidFinish(wave, radius, anim);
 | 
						|
          var shouldKeepWave = !waveDissipated || maximumWave;
 | 
						|
          // keep rendering dissipating wave when at maximum radius on upAction
 | 
						|
          var shouldRenderWaveAgain = wave.mouseUpStart ? !waveDissipated : !maximumWave;
 | 
						|
          shouldRenderNextFrame = shouldRenderNextFrame || shouldRenderWaveAgain;
 | 
						|
          if (!shouldKeepWave || this.cancelled) {
 | 
						|
            deleteTheseWaves.push(wave);
 | 
						|
          }
 | 
						|
       }
 | 
						|
 | 
						|
        if (shouldRenderNextFrame) {
 | 
						|
          requestAnimationFrame(this._loop);
 | 
						|
        }
 | 
						|
 | 
						|
        for (var i = 0; i < deleteTheseWaves.length; ++i) {
 | 
						|
          var wave = deleteTheseWaves[i];
 | 
						|
          removeWaveFromScope(this, wave);
 | 
						|
        }
 | 
						|
 | 
						|
        if (!this.waves.length && this._loop) {
 | 
						|
          // clear the background color
 | 
						|
          this.$.bg.style.backgroundColor = null;
 | 
						|
          this._loop = null;
 | 
						|
          this.fire('core-transitionend');
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
    });
 | 
						|
 | 
						|
  })();
 | 
						|
 | 
						|
</script>
 | 
						|
</polymer-element>
 |