Home | History | Annotate | Download | only in paper-ripple
      1 <!--
      2 Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
      3 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
      4 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
      5 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
      6 Code distributed by Google as part of the polymer project is also
      7 subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
      8 -->
      9 
     10 <!--
     11 `paper-ripple` provides a visual effect that other paper elements can
     12 use to simulate a rippling effect emanating from the point of contact.  The
     13 effect can be visualized as a concentric circle with motion.
     14 
     15 Example:
     16 
     17     <paper-ripple></paper-ripple>
     18 
     19 `paper-ripple` listens to "down" and "up" events so it would display ripple
     20 effect when touches on it.  You can also defeat the default behavior and
     21 manually route the down and up actions to the ripple element.  Note that it is
     22 important if you call downAction() you will have to make sure to call upAction()
     23 so that `paper-ripple` would end the animation loop.
     24 
     25 Example:
     26 
     27     <paper-ripple id="ripple" style="pointer-events: none;"></paper-ripple>
     28     ...
     29     downAction: function(e) {
     30       this.$.ripple.downAction({x: e.x, y: e.y});
     31     },
     32     upAction: function(e) {
     33       this.$.ripple.upAction();
     34     }
     35 
     36 Styling ripple effect:
     37 
     38   Use CSS color property to style the ripple:
     39 
     40     paper-ripple {
     41       color: #4285f4;
     42     }
     43 
     44   Note that CSS color property is inherited so it is not required to set it on
     45   the `paper-ripple` element directly.
     46 
     47 By default, the ripple is centered on the point of contact.  Apply `recenteringTouch` 
     48 class to have the ripple grow toward the center of its container.
     49 
     50     <paper-ripple class="recenteringTouch"></paper-ripple>
     51 
     52 Apply `circle` class to make the rippling effect within a circle.
     53 
     54     <paper-ripple class="circle"></paper-ripple>
     55 
     56 @group Paper Elements
     57 @element paper-ripple
     58 @homepage github.io
     59 -->
     60 
     61 <!--
     62 Fired when the animation finishes. This is useful if you want to wait until the ripple
     63 animation finishes to perform some action.
     64 
     65 @event core-transitionend
     66 @param {Object} detail
     67 @param {Object} detail.node The animated node
     68 -->
     69 
     70 <link rel="import" href="../polymer/polymer.html" >
     71 
     72 <polymer-element name="paper-ripple" attributes="initialOpacity opacityDecayVelocity">
     73 <template>
     74 
     75   <style>
     76 
     77     :host {
     78       display: block;
     79       position: relative;
     80       border-radius: inherit;
     81       overflow: hidden;
     82     }
     83 
     84     :host-context([noink]) {
     85       pointer-events: none;
     86     }
     87 
     88     #bg, #waves, .wave-container, .wave {
     89       pointer-events: none;
     90       position: absolute;
     91       top: 0;
     92       left: 0;
     93       width: 100%;
     94       height: 100%;
     95     }
     96 
     97     #bg, .wave {
     98       opacity: 0;
     99     }
    100 
    101     #waves, .wave {
    102       overflow: hidden;
    103     }
    104 
    105     .wave-container, .wave {
    106       border-radius: 50%;
    107     }
    108 
    109     :host(.circle) #bg,
    110     :host(.circle) #waves {
    111       border-radius: 50%;
    112     }
    113 
    114     :host(.circle) .wave-container {
    115       overflow: hidden;
    116     }
    117 
    118   </style>
    119 
    120   <div id="bg"></div>
    121   <div id="waves">
    122   </div>
    123 
    124 </template>
    125 <script>
    126 
    127   (function() {
    128 
    129     var waveMaxRadius = 150;
    130     //
    131     // INK EQUATIONS
    132     //
    133     function waveRadiusFn(touchDownMs, touchUpMs, anim) {
    134       // Convert from ms to s
    135       var touchDown = touchDownMs / 1000;
    136       var touchUp = touchUpMs / 1000;
    137       var totalElapsed = touchDown + touchUp;
    138       var ww = anim.width, hh = anim.height;
    139       // use diagonal size of container to avoid floating point math sadness
    140       var waveRadius = Math.min(Math.sqrt(ww * ww + hh * hh), waveMaxRadius) * 1.1 + 5;
    141       var duration = 1.1 - .2 * (waveRadius / waveMaxRadius);
    142       var tt = (totalElapsed / duration);
    143 
    144       var size = waveRadius * (1 - Math.pow(80, -tt));
    145       return Math.abs(size);
    146     }
    147 
    148     function waveOpacityFn(td, tu, anim) {
    149       // Convert from ms to s.
    150       var touchDown = td / 1000;
    151       var touchUp = tu / 1000;
    152       var totalElapsed = touchDown + touchUp;
    153 
    154       if (tu <= 0) {  // before touch up
    155         return anim.initialOpacity;
    156       }
    157       return Math.max(0, anim.initialOpacity - touchUp * anim.opacityDecayVelocity);
    158     }
    159 
    160     function waveOuterOpacityFn(td, tu, anim) {
    161       // Convert from ms to s.
    162       var touchDown = td / 1000;
    163       var touchUp = tu / 1000;
    164 
    165       // Linear increase in background opacity, capped at the opacity
    166       // of the wavefront (waveOpacity).
    167       var outerOpacity = touchDown * 0.3;
    168       var waveOpacity = waveOpacityFn(td, tu, anim);
    169       return Math.max(0, Math.min(outerOpacity, waveOpacity));
    170     }
    171 
    172     // Determines whether the wave should be completely removed.
    173     function waveDidFinish(wave, radius, anim) {
    174       var waveOpacity = waveOpacityFn(wave.tDown, wave.tUp, anim);
    175 
    176       // If the wave opacity is 0 and the radius exceeds the bounds
    177       // of the element, then this is finished.
    178       return waveOpacity < 0.01 && radius >= Math.min(wave.maxRadius, waveMaxRadius);
    179     };
    180 
    181     function waveAtMaximum(wave, radius, anim) {
    182       var waveOpacity = waveOpacityFn(wave.tDown, wave.tUp, anim);
    183 
    184       return waveOpacity >= anim.initialOpacity && radius >= Math.min(wave.maxRadius, waveMaxRadius);
    185     }
    186 
    187     //
    188     // DRAWING
    189     //
    190     function drawRipple(ctx, x, y, radius, innerAlpha, outerAlpha) {
    191       // Only animate opacity and transform
    192       if (outerAlpha !== undefined) {
    193         ctx.bg.style.opacity = outerAlpha;
    194       }
    195       ctx.wave.style.opacity = innerAlpha;
    196 
    197       var s = radius / (ctx.containerSize / 2);
    198       var dx = x - (ctx.containerWidth / 2);
    199       var dy = y - (ctx.containerHeight / 2);
    200 
    201       ctx.wc.style.webkitTransform = 'translate3d(' + dx + 'px,' + dy + 'px,0)';
    202       ctx.wc.style.transform = 'translate3d(' + dx + 'px,' + dy + 'px,0)';
    203 
    204       // 2d transform for safari because of border-radius and overflow:hidden clipping bug.
    205       // https://bugs.webkit.org/show_bug.cgi?id=98538
    206       ctx.wave.style.webkitTransform = 'scale(' + s + ',' + s + ')';
    207       ctx.wave.style.transform = 'scale3d(' + s + ',' + s + ',1)';
    208     }
    209 
    210     //
    211     // SETUP
    212     //
    213     function createWave(elem) {
    214       var elementStyle = window.getComputedStyle(elem);
    215       var fgColor = elementStyle.color;
    216 
    217       var inner = document.createElement('div');
    218       inner.style.backgroundColor = fgColor;
    219       inner.classList.add('wave');
    220 
    221       var outer = document.createElement('div');
    222       outer.classList.add('wave-container');
    223       outer.appendChild(inner);
    224 
    225       var container = elem.$.waves;
    226       container.appendChild(outer);
    227 
    228       elem.$.bg.style.backgroundColor = fgColor;
    229 
    230       var wave = {
    231         bg: elem.$.bg,
    232         wc: outer,
    233         wave: inner,
    234         waveColor: fgColor,
    235         maxRadius: 0,
    236         isMouseDown: false,
    237         mouseDownStart: 0.0,
    238         mouseUpStart: 0.0,
    239         tDown: 0,
    240         tUp: 0
    241       };
    242       return wave;
    243     }
    244 
    245     function removeWaveFromScope(scope, wave) {
    246       if (scope.waves) {
    247         var pos = scope.waves.indexOf(wave);
    248         scope.waves.splice(pos, 1);
    249         // FIXME cache nodes
    250         wave.wc.remove();
    251       }
    252     };
    253 
    254     // Shortcuts.
    255     var pow = Math.pow;
    256     var now = Date.now;
    257     if (window.performance && performance.now) {
    258       now = performance.now.bind(performance);
    259     }
    260 
    261     function cssColorWithAlpha(cssColor, alpha) {
    262         var parts = cssColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
    263         if (typeof alpha == 'undefined') {
    264             alpha = 1;
    265         }
    266         if (!parts) {
    267           return 'rgba(255, 255, 255, ' + alpha + ')';
    268         }
    269         return 'rgba(' + parts[1] + ', ' + parts[2] + ', ' + parts[3] + ', ' + alpha + ')';
    270     }
    271 
    272     function dist(p1, p2) {
    273       return Math.sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));
    274     }
    275 
    276     function distanceFromPointToFurthestCorner(point, size) {
    277       var tl_d = dist(point, {x: 0, y: 0});
    278       var tr_d = dist(point, {x: size.w, y: 0});
    279       var bl_d = dist(point, {x: 0, y: size.h});
    280       var br_d = dist(point, {x: size.w, y: size.h});
    281       return Math.max(tl_d, tr_d, bl_d, br_d);
    282     }
    283 
    284     Polymer('paper-ripple', {
    285 
    286       /**
    287        * The initial opacity set on the wave.
    288        *
    289        * @attribute initialOpacity
    290        * @type number
    291        * @default 0.25
    292        */
    293       initialOpacity: 0.25,
    294 
    295       /**
    296        * How fast (opacity per second) the wave fades out.
    297        *
    298        * @attribute opacityDecayVelocity
    299        * @type number
    300        * @default 0.8
    301        */
    302       opacityDecayVelocity: 0.8,
    303 
    304       backgroundFill: true,
    305       pixelDensity: 2,
    306 
    307       eventDelegates: {
    308         down: 'downAction',
    309         up: 'upAction'
    310       },
    311 
    312       ready: function() {
    313         this.waves = [];
    314       },
    315 
    316       downAction: function(e) {
    317         var wave = createWave(this);
    318 
    319         this.cancelled = false;
    320         wave.isMouseDown = true;
    321         wave.tDown = 0.0;
    322         wave.tUp = 0.0;
    323         wave.mouseUpStart = 0.0;
    324         wave.mouseDownStart = now();
    325 
    326         var rect = this.getBoundingClientRect();
    327         var width = rect.width;
    328         var height = rect.height;
    329         var touchX = e.x - rect.left;
    330         var touchY = e.y - rect.top;
    331 
    332         wave.startPosition = {x:touchX, y:touchY};
    333 
    334         if (this.classList.contains("recenteringTouch")) {
    335           wave.endPosition = {x: width / 2,  y: height / 2};
    336           wave.slideDistance = dist(wave.startPosition, wave.endPosition);
    337         }
    338         wave.containerSize = Math.max(width, height);
    339         wave.containerWidth = width;
    340         wave.containerHeight = height;
    341         wave.maxRadius = distanceFromPointToFurthestCorner(wave.startPosition, {w: width, h: height});
    342 
    343         // The wave is circular so constrain its container to 1:1
    344         wave.wc.style.top = (wave.containerHeight - wave.containerSize) / 2 + 'px';
    345         wave.wc.style.left = (wave.containerWidth - wave.containerSize) / 2 + 'px';
    346         wave.wc.style.width = wave.containerSize + 'px';
    347         wave.wc.style.height = wave.containerSize + 'px';
    348 
    349         this.waves.push(wave);
    350 
    351         if (!this._loop) {
    352           this._loop = this.animate.bind(this, {
    353             width: width,
    354             height: height
    355           });
    356           requestAnimationFrame(this._loop);
    357         }
    358         // else there is already a rAF
    359       },
    360 
    361       upAction: function() {
    362         for (var i = 0; i < this.waves.length; i++) {
    363           // Declare the next wave that has mouse down to be mouse'ed up.
    364           var wave = this.waves[i];
    365           if (wave.isMouseDown) {
    366             wave.isMouseDown = false;
    367             wave.mouseUpStart = now();
    368             wave.mouseDownStart = 0;
    369             wave.tUp = 0.0;
    370             break;
    371           }
    372         }
    373         this._loop && requestAnimationFrame(this._loop);
    374       },
    375 
    376       cancel: function() {
    377         this.cancelled = true;
    378       },
    379 
    380       animate: function(ctx) {
    381         var shouldRenderNextFrame = false;
    382 
    383         var deleteTheseWaves = [];
    384         // The oldest wave's touch down duration
    385         var longestTouchDownDuration = 0;
    386         var longestTouchUpDuration = 0;
    387         // Save the last known wave color
    388         var lastWaveColor = null;
    389         // wave animation values
    390         var anim = {
    391           initialOpacity: this.initialOpacity,
    392           opacityDecayVelocity: this.opacityDecayVelocity,
    393           height: ctx.height,
    394           width: ctx.width
    395         }
    396 
    397         for (var i = 0; i < this.waves.length; i++) {
    398           var wave = this.waves[i];
    399 
    400           if (wave.mouseDownStart > 0) {
    401             wave.tDown = now() - wave.mouseDownStart;
    402           }
    403           if (wave.mouseUpStart > 0) {
    404             wave.tUp = now() - wave.mouseUpStart;
    405           }
    406 
    407           // Determine how long the touch has been up or down.
    408           var tUp = wave.tUp;
    409           var tDown = wave.tDown;
    410           longestTouchDownDuration = Math.max(longestTouchDownDuration, tDown);
    411           longestTouchUpDuration = Math.max(longestTouchUpDuration, tUp);
    412 
    413           // Obtain the instantenous size and alpha of the ripple.
    414           var radius = waveRadiusFn(tDown, tUp, anim);
    415           var waveAlpha =  waveOpacityFn(tDown, tUp, anim);
    416           var waveColor = cssColorWithAlpha(wave.waveColor, waveAlpha);
    417           lastWaveColor = wave.waveColor;
    418 
    419           // Position of the ripple.
    420           var x = wave.startPosition.x;
    421           var y = wave.startPosition.y;
    422 
    423           // Ripple gravitational pull to the center of the canvas.
    424           if (wave.endPosition) {
    425 
    426             // This translates from the origin to the center of the view  based on the max dimension of
    427             var translateFraction = Math.min(1, radius / wave.containerSize * 2 / Math.sqrt(2) );
    428 
    429             x += translateFraction * (wave.endPosition.x - wave.startPosition.x);
    430             y += translateFraction * (wave.endPosition.y - wave.startPosition.y);
    431           }
    432 
    433           // If we do a background fill fade too, work out the correct color.
    434           var bgFillColor = null;
    435           if (this.backgroundFill) {
    436             var bgFillAlpha = waveOuterOpacityFn(tDown, tUp, anim);
    437             bgFillColor = cssColorWithAlpha(wave.waveColor, bgFillAlpha);
    438           }
    439 
    440           // Draw the ripple.
    441           drawRipple(wave, x, y, radius, waveAlpha, bgFillAlpha);
    442 
    443           // Determine whether there is any more rendering to be done.
    444           var maximumWave = waveAtMaximum(wave, radius, anim);
    445           var waveDissipated = waveDidFinish(wave, radius, anim);
    446           var shouldKeepWave = !waveDissipated || maximumWave;
    447           // keep rendering dissipating wave when at maximum radius on upAction
    448           var shouldRenderWaveAgain = wave.mouseUpStart ? !waveDissipated : !maximumWave;
    449           shouldRenderNextFrame = shouldRenderNextFrame || shouldRenderWaveAgain;
    450           if (!shouldKeepWave || this.cancelled) {
    451             deleteTheseWaves.push(wave);
    452           }
    453        }
    454 
    455         if (shouldRenderNextFrame) {
    456           requestAnimationFrame(this._loop);
    457         }
    458 
    459         for (var i = 0; i < deleteTheseWaves.length; ++i) {
    460           var wave = deleteTheseWaves[i];
    461           removeWaveFromScope(this, wave);
    462         }
    463 
    464         if (!this.waves.length && this._loop) {
    465           // clear the background color
    466           this.$.bg.style.backgroundColor = null;
    467           this._loop = null;
    468           this.fire('core-transitionend');
    469         }
    470       }
    471 
    472     });
    473 
    474   })();
    475 
    476 </script>
    477 </polymer-element>
    478