paper-ripple.html 13.7 KB
Newer Older
Pascal Hartig's avatar
Pascal Hartig committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<!--
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:

17
    <paper-ripple></paper-ripple>
Pascal Hartig's avatar
Pascal Hartig committed
18

19 20
`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 
Pascal Hartig's avatar
Pascal Hartig committed
21
manually route the down and up actions to the ripple element.  Note that it is
22 23
important if you call downAction() you will have to make sure to call upAction()
so that `paper-ripple` would end the animation loop.
Pascal Hartig's avatar
Pascal Hartig committed
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46

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.

47
Apply `recenteringTouch` class to make the recentering rippling effect.
Pascal Hartig's avatar
Pascal Hartig committed
48

49
    <paper-ripple class="recenteringTouch"></paper-ripple>
Pascal Hartig's avatar
Pascal Hartig committed
50 51 52 53 54 55 56

Apply `circle` class to make the rippling effect within a circle.

    <paper-ripple class="circle"></paper-ripple>

@group Paper Elements
@element paper-ripple
57
@homepage github.io
Pascal Hartig's avatar
Pascal Hartig committed
58 59
-->

60
<link rel="import" href="../polymer/polymer.html" >
Pascal Hartig's avatar
Pascal Hartig committed
61

62 63
<polymer-element name="paper-ripple" attributes="initialOpacity opacityDecayVelocity">
<template>
Pascal Hartig's avatar
Pascal Hartig committed
64

65
  <style>
Pascal Hartig's avatar
Pascal Hartig committed
66

67 68 69
    :host {
      display: block;
      position: relative;
Pascal Hartig's avatar
Pascal Hartig committed
70 71
    }

72 73
    :host-context([noink]) {
      pointer-events: none;
Pascal Hartig's avatar
Pascal Hartig committed
74 75
    }

76 77 78 79 80 81 82 83
    #bg, #waves, .wave-container, .wave {
      pointer-events: none;
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }
Pascal Hartig's avatar
Pascal Hartig committed
84

85 86 87
    #bg, .wave {
      opacity: 0;
    }
Pascal Hartig's avatar
Pascal Hartig committed
88

89 90 91
    #waves, .wave {
      overflow: hidden;
    }
Pascal Hartig's avatar
Pascal Hartig committed
92

93 94 95
    .wave-container, .wave {
      border-radius: 50%;
    }
Pascal Hartig's avatar
Pascal Hartig committed
96

97 98 99 100
    :host(.circle) #bg,
    :host(.circle) #waves {
      border-radius: 50%;
    }
Pascal Hartig's avatar
Pascal Hartig committed
101

102 103 104
    :host(.circle) .wave-container {
      overflow: hidden;
    }
Pascal Hartig's avatar
Pascal Hartig committed
105

106
  </style>
Pascal Hartig's avatar
Pascal Hartig committed
107

108 109 110
  <div id="bg"></div>
  <div id="waves">
  </div>
Pascal Hartig's avatar
Pascal Hartig committed
111

112 113
</template>
<script>
Pascal Hartig's avatar
Pascal Hartig committed
114

115
  (function() {
Pascal Hartig's avatar
Pascal Hartig committed
116

117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
    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);
    }
Pascal Hartig's avatar
Pascal Hartig committed
135

136 137 138 139 140
    function waveOpacityFn(td, tu, anim) {
      // Convert from ms to s.
      var touchDown = td / 1000;
      var touchUp = tu / 1000;
      var totalElapsed = touchDown + touchUp;
Pascal Hartig's avatar
Pascal Hartig committed
141

142 143 144 145 146
      if (tu <= 0) {  // before touch up
        return anim.initialOpacity;
      }
      return Math.max(0, anim.initialOpacity - touchUp * anim.opacityDecayVelocity);
    }
Pascal Hartig's avatar
Pascal Hartig committed
147

148 149 150 151
    function waveOuterOpacityFn(td, tu, anim) {
      // Convert from ms to s.
      var touchDown = td / 1000;
      var touchUp = tu / 1000;
Pascal Hartig's avatar
Pascal Hartig committed
152

153 154 155 156 157 158
      // 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));
    }
Pascal Hartig's avatar
Pascal Hartig committed
159

160 161 162
    // Determines whether the wave should be completely removed.
    function waveDidFinish(wave, radius, anim) {
      var waveOpacity = waveOpacityFn(wave.tDown, wave.tUp, anim);
Pascal Hartig's avatar
Pascal Hartig committed
163

164 165 166 167
      // 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);
    };
Pascal Hartig's avatar
Pascal Hartig committed
168

169 170
    function waveAtMaximum(wave, radius, anim) {
      var waveOpacity = waveOpacityFn(wave.tDown, wave.tUp, anim);
Pascal Hartig's avatar
Pascal Hartig committed
171

172 173
      return waveOpacity >= anim.initialOpacity && radius >= Math.min(wave.maxRadius, waveMaxRadius);
    }
Pascal Hartig's avatar
Pascal Hartig committed
174

175 176 177 178 179 180 181 182 183
    //
    // 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;
Pascal Hartig's avatar
Pascal Hartig committed
184

185 186 187
      var s = radius / (ctx.containerSize / 2);
      var dx = x - (ctx.containerWidth / 2);
      var dy = y - (ctx.containerHeight / 2);
Pascal Hartig's avatar
Pascal Hartig committed
188

189 190
      ctx.wc.style.webkitTransform = 'translate3d(' + dx + 'px,' + dy + 'px,0)';
      ctx.wc.style.transform = 'translate3d(' + dx + 'px,' + dy + 'px,0)';
Pascal Hartig's avatar
Pascal Hartig committed
191

192 193 194 195 196
      // 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)';
    }
Pascal Hartig's avatar
Pascal Hartig committed
197

198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
    //
    // 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;
    }
Pascal Hartig's avatar
Pascal Hartig committed
232

233 234 235 236 237 238
    function removeWaveFromScope(scope, wave) {
      if (scope.waves) {
        var pos = scope.waves.indexOf(wave);
        scope.waves.splice(pos, 1);
        // FIXME cache nodes
        wave.wc.remove();
Pascal Hartig's avatar
Pascal Hartig committed
239 240 241
      }
    };

242 243 244 245 246 247
    // Shortcuts.
    var pow = Math.pow;
    var now = Date.now;
    if (window.performance && performance.now) {
      now = performance.now.bind(performance);
    }
Pascal Hartig's avatar
Pascal Hartig committed
248

249 250 251 252
    function cssColorWithAlpha(cssColor, alpha) {
        var parts = cssColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
        if (typeof alpha == 'undefined') {
            alpha = 1;
Pascal Hartig's avatar
Pascal Hartig committed
253
        }
254 255
        if (!parts) {
          return 'rgba(255, 255, 255, ' + alpha + ')';
Pascal Hartig's avatar
Pascal Hartig committed
256
        }
257 258
        return 'rgba(' + parts[1] + ', ' + parts[2] + ', ' + parts[3] + ', ' + alpha + ')';
    }
Pascal Hartig's avatar
Pascal Hartig committed
259

260 261 262
    function dist(p1, p2) {
      return Math.sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));
    }
Pascal Hartig's avatar
Pascal Hartig committed
263

264 265 266 267 268 269 270
    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);
    }
Pascal Hartig's avatar
Pascal Hartig committed
271

272
    Polymer('paper-ripple', {
Pascal Hartig's avatar
Pascal Hartig committed
273

Pascal Hartig's avatar
Pascal Hartig committed
274
      /**
275 276 277 278 279
       * The initial opacity set on the wave.
       *
       * @attribute initialOpacity
       * @type number
       * @default 0.25
Pascal Hartig's avatar
Pascal Hartig committed
280
       */
281
      initialOpacity: 0.25,
Pascal Hartig's avatar
Pascal Hartig committed
282

Pascal Hartig's avatar
Pascal Hartig committed
283
      /**
284 285 286 287 288
       * How fast (opacity per second) the wave fades out.
       *
       * @attribute opacityDecayVelocity
       * @type number
       * @default 0.8
Pascal Hartig's avatar
Pascal Hartig committed
289
       */
290
      opacityDecayVelocity: 0.8,
Pascal Hartig's avatar
Pascal Hartig committed
291

292 293
      backgroundFill: true,
      pixelDensity: 2,
Pascal Hartig's avatar
Pascal Hartig committed
294

295 296 297
      eventDelegates: {
        down: 'downAction',
        up: 'upAction'
Pascal Hartig's avatar
Pascal Hartig committed
298
      },
Pascal Hartig's avatar
Pascal Hartig committed
299

300 301
      ready: function() {
        this.waves = [];
Pascal Hartig's avatar
Pascal Hartig committed
302
      },
Pascal Hartig's avatar
Pascal Hartig committed
303

304 305
      downAction: function(e) {
        var wave = createWave(this);
Pascal Hartig's avatar
Pascal Hartig committed
306

307 308 309 310 311 312
        this.cancelled = false;
        wave.isMouseDown = true;
        wave.tDown = 0.0;
        wave.tUp = 0.0;
        wave.mouseUpStart = 0.0;
        wave.mouseDownStart = now();
Pascal Hartig's avatar
Pascal Hartig committed
313

314 315 316 317 318
        var rect = this.getBoundingClientRect();
        var width = rect.width;
        var height = rect.height;
        var touchX = e.x - rect.left;
        var touchY = e.y - rect.top;
Pascal Hartig's avatar
Pascal Hartig committed
319

320
        wave.startPosition = {x:touchX, y:touchY};
Pascal Hartig's avatar
Pascal Hartig committed
321

322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
        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);
      },
Pascal Hartig's avatar
Pascal Hartig committed
363

364 365
      cancel: function() {
        this.cancelled = true;
Pascal Hartig's avatar
Pascal Hartig committed
366 367
      },

368 369
      animate: function(ctx) {
        var shouldRenderNextFrame = false;
Pascal Hartig's avatar
Pascal Hartig committed
370

371 372 373 374 375 376 377 378 379 380 381 382
        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
Pascal Hartig's avatar
Pascal Hartig committed
383 384
        }

385 386
        for (var i = 0; i < this.waves.length; i++) {
          var wave = this.waves[i];
Pascal Hartig's avatar
Pascal Hartig committed
387

388 389 390 391 392 393
          if (wave.mouseDownStart > 0) {
            wave.tDown = now() - wave.mouseDownStart;
          }
          if (wave.mouseUpStart > 0) {
            wave.tUp = now() - wave.mouseUpStart;
          }
Pascal Hartig's avatar
Pascal Hartig committed
394

395 396 397 398 399
          // 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);
Pascal Hartig's avatar
Pascal Hartig committed
400

401 402 403 404 405
          // 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;
Pascal Hartig's avatar
Pascal Hartig committed
406

407 408 409
          // Position of the ripple.
          var x = wave.startPosition.x;
          var y = wave.startPosition.y;
Pascal Hartig's avatar
Pascal Hartig committed
410

411 412
          // Ripple gravitational pull to the center of the canvas.
          if (wave.endPosition) {
Pascal Hartig's avatar
Pascal Hartig committed
413

414 415
            // 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) );
Pascal Hartig's avatar
Pascal Hartig committed
416

417 418
            x += translateFraction * (wave.endPosition.x - wave.startPosition.x);
            y += translateFraction * (wave.endPosition.y - wave.startPosition.y);
Pascal Hartig's avatar
Pascal Hartig committed
419
          }
Pascal Hartig's avatar
Pascal Hartig committed
420

421 422 423 424 425 426
          // 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);
          }
Pascal Hartig's avatar
Pascal Hartig committed
427

428 429 430 431 432 433 434 435 436 437 438 439 440
          // 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;
          var shouldRenderWaveAgain = !waveDissipated && !maximumWave;
          shouldRenderNextFrame = shouldRenderNextFrame || shouldRenderWaveAgain;
          if (!shouldKeepWave || this.cancelled) {
            deleteTheseWaves.push(wave);
          }
       }
Pascal Hartig's avatar
Pascal Hartig committed
441

442 443
        if (shouldRenderNextFrame) {
          requestAnimationFrame(this._loop);
Pascal Hartig's avatar
Pascal Hartig committed
444
        }
445 446 447 448

        for (var i = 0; i < deleteTheseWaves.length; ++i) {
          var wave = deleteTheseWaves[i];
          removeWaveFromScope(this, wave);
Pascal Hartig's avatar
Pascal Hartig committed
449
        }
Pascal Hartig's avatar
Pascal Hartig committed
450

451 452 453 454 455
        if (!this.waves.length && this._loop) {
          // clear the background color
          this.$.bg.style.backgroundColor = null;
          this._loop = null;
          this.fire('core-transitionend');
Pascal Hartig's avatar
Pascal Hartig committed
456 457
        }
      }
458

Pascal Hartig's avatar
Pascal Hartig committed
459
    });
460

Pascal Hartig's avatar
Pascal Hartig committed
461
  })();
462

Pascal Hartig's avatar
Pascal Hartig committed
463
</script>
464
</polymer-element>