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 Apply `recenteringTouch` class to make the recentering rippling effect.
     48 
     49     <paper-ripple class="recenteringTouch"></paper-ripple>
     50 
     51 Apply `circle` class to make the rippling effect within a circle.
     52 
     53     <paper-ripple class="circle"></paper-ripple>
     54 
     55 @group Paper Elements
     56 @element paper-ripple
     57 @homepage github.io
     58 -->
     59 
     60 <link rel="import" href="../polymer/polymer.html" >
     61 
     62 <polymer-element name="paper-ripple" attributes="initialOpacity opacityDecayVelocity">
     63 <template>
     64 
     65   <style>
     66 
     67     :host {
     68       display: block;
     69       position: relative;
     70     }
     71 
     72     #canvas {
     73       pointer-events: none;
     74       position: absolute;
     75       top: 0;
     76       left: 0;
     77       width: 100%;
     78       height: 100%;
     79     }
     80 
     81     :host(.circle) #canvas {
     82       border-radius: 50%;
     83     }
     84 
     85   </style>
     86 
     87 </template>
     88 <script>
     89 
     90   (function() {
     91 
     92     var waveMaxRadius = 150;
     93     //
     94     // INK EQUATIONS
     95     //
     96     function waveRadiusFn(touchDownMs, touchUpMs, anim) {
     97       // Convert from ms to s.
     98       var touchDown = touchDownMs / 1000;
     99       var touchUp = touchUpMs / 1000;
    100       var totalElapsed = touchDown + touchUp;
    101       var ww = anim.width, hh = anim.height;
    102       // use diagonal size of container to avoid floating point math sadness
    103       var waveRadius = Math.min(Math.sqrt(ww * ww + hh * hh), waveMaxRadius) * 1.1 + 5;
    104       var duration = 1.1 - .2 * (waveRadius / waveMaxRadius);
    105       var tt = (totalElapsed / duration);
    106 
    107       var size = waveRadius * (1 - Math.pow(80, -tt));
    108       return Math.abs(size);
    109     }
    110 
    111     function waveOpacityFn(td, tu, anim) {
    112       // Convert from ms to s.
    113       var touchDown = td / 1000;
    114       var touchUp = tu / 1000;
    115       var totalElapsed = touchDown + touchUp;
    116 
    117       if (tu <= 0) {  // before touch up
    118         return anim.initialOpacity;
    119       }
    120       return Math.max(0, anim.initialOpacity - touchUp * anim.opacityDecayVelocity);
    121     }
    122 
    123     function waveOuterOpacityFn(td, tu, anim) {
    124       // Convert from ms to s.
    125       var touchDown = td / 1000;
    126       var touchUp = tu / 1000;
    127 
    128       // Linear increase in background opacity, capped at the opacity
    129       // of the wavefront (waveOpacity).
    130       var outerOpacity = touchDown * 0.3;
    131       var waveOpacity = waveOpacityFn(td, tu, anim);
    132       return Math.max(0, Math.min(outerOpacity, waveOpacity));
    133     }
    134 
    135     // Determines whether the wave should be completely removed.
    136     function waveDidFinish(wave, radius, anim) {
    137       var waveOpacity = waveOpacityFn(wave.tDown, wave.tUp, anim);
    138       // If the wave opacity is 0 and the radius exceeds the bounds
    139       // of the element, then this is finished.
    140       if (waveOpacity < 0.01 && radius >= Math.min(wave.maxRadius, waveMaxRadius)) {
    141         return true;
    142       }
    143       return false;
    144     };
    145 
    146     function waveAtMaximum(wave, radius, anim) {
    147       var waveOpacity = waveOpacityFn(wave.tDown, wave.tUp, anim);
    148       if (waveOpacity >= anim.initialOpacity && radius >= Math.min(wave.maxRadius, waveMaxRadius)) {
    149         return true;
    150       }
    151       return false;
    152     }
    153 
    154     //
    155     // DRAWING
    156     //
    157     function drawRipple(ctx, x, y, radius, innerColor, outerColor) {
    158       if (outerColor) {
    159         ctx.fillStyle = outerColor;
    160         ctx.fillRect(0,0,ctx.canvas.width, ctx.canvas.height);
    161       }
    162       ctx.beginPath();
    163       ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
    164       ctx.fillStyle = innerColor;
    165       ctx.fill();
    166     }
    167 
    168     //
    169     // SETUP
    170     //
    171     function createWave(elem) {
    172       var elementStyle = window.getComputedStyle(elem);
    173       var fgColor = elementStyle.color;
    174 
    175       var wave = {
    176         waveColor: fgColor,
    177         maxRadius: 0,
    178         isMouseDown: false,
    179         mouseDownStart: 0.0,
    180         mouseUpStart: 0.0,
    181         tDown: 0,
    182         tUp: 0
    183       };
    184       return wave;
    185     }
    186 
    187     function removeWaveFromScope(scope, wave) {
    188       if (scope.waves) {
    189         var pos = scope.waves.indexOf(wave);
    190         scope.waves.splice(pos, 1);
    191       }
    192     };
    193 
    194     // Shortcuts.
    195     var pow = Math.pow;
    196     var now = Date.now;
    197     if (window.performance && performance.now) {
    198       now = performance.now.bind(performance);
    199     }
    200 
    201     function cssColorWithAlpha(cssColor, alpha) {
    202         var parts = cssColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
    203         if (typeof alpha == 'undefined') {
    204             alpha = 1;
    205         }
    206         if (!parts) {
    207           return 'rgba(255, 255, 255, ' + alpha + ')';
    208         }
    209         return 'rgba(' + parts[1] + ', ' + parts[2] + ', ' + parts[3] + ', ' + alpha + ')';
    210     }
    211 
    212     function dist(p1, p2) {
    213       return Math.sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));
    214     }
    215 
    216     function distanceFromPointToFurthestCorner(point, size) {
    217       var tl_d = dist(point, {x: 0, y: 0});
    218       var tr_d = dist(point, {x: size.w, y: 0});
    219       var bl_d = dist(point, {x: 0, y: size.h});
    220       var br_d = dist(point, {x: size.w, y: size.h});
    221       return Math.max(tl_d, tr_d, bl_d, br_d);
    222     }
    223 
    224     Polymer('paper-ripple', {
    225 
    226       /**
    227        * The initial opacity set on the wave.
    228        *
    229        * @attribute initialOpacity
    230        * @type number
    231        * @default 0.25
    232        */
    233       initialOpacity: 0.25,
    234 
    235       /**
    236        * How fast (opacity per second) the wave fades out.
    237        *
    238        * @attribute opacityDecayVelocity
    239        * @type number
    240        * @default 0.8
    241        */
    242       opacityDecayVelocity: 0.8,
    243 
    244       backgroundFill: true,
    245       pixelDensity: 2,
    246 
    247       eventDelegates: {
    248         down: 'downAction',
    249         up: 'upAction'
    250       },
    251 
    252       attached: function() {
    253         // create the canvas element manually becase ios
    254         // does not render the canvas element if it is not created in the
    255         // main document (component templates are created in a
    256         // different document). See:
    257         // https://bugs.webkit.org/show_bug.cgi?id=109073.
    258         if (!this.$.canvas) {
    259           var canvas = document.createElement('canvas');
    260           canvas.id = 'canvas';
    261           this.shadowRoot.appendChild(canvas);
    262           this.$.canvas = canvas;
    263         }
    264       },
    265 
    266       ready: function() {
    267         this.waves = [];
    268       },
    269 
    270       setupCanvas: function() {
    271         this.$.canvas.setAttribute('width', this.$.canvas.clientWidth * this.pixelDensity + "px");
    272         this.$.canvas.setAttribute('height', this.$.canvas.clientHeight * this.pixelDensity + "px");
    273         var ctx = this.$.canvas.getContext('2d');
    274         ctx.scale(this.pixelDensity, this.pixelDensity);
    275         if (!this._loop) {
    276           this._loop = this.animate.bind(this, ctx);
    277         }
    278       },
    279 
    280       downAction: function(e) {
    281         this.setupCanvas();
    282         var wave = createWave(this.$.canvas);
    283 
    284         this.cancelled = false;
    285         wave.isMouseDown = true;
    286         wave.tDown = 0.0;
    287         wave.tUp = 0.0;
    288         wave.mouseUpStart = 0.0;
    289         wave.mouseDownStart = now();
    290 
    291         var width = this.$.canvas.width / 2; // Retina canvas
    292         var height = this.$.canvas.height / 2;
    293         var rect = this.getBoundingClientRect();
    294         var touchX = e.x - rect.left;
    295         var touchY = e.y - rect.top;
    296 
    297         wave.startPosition = {x:touchX, y:touchY};
    298 
    299         if (this.classList.contains("recenteringTouch")) {
    300           wave.endPosition = {x: width / 2,  y: height / 2};
    301           wave.slideDistance = dist(wave.startPosition, wave.endPosition);
    302         }
    303         wave.containerSize = Math.max(width, height);
    304         wave.maxRadius = distanceFromPointToFurthestCorner(wave.startPosition, {w: width, h: height});
    305         this.waves.push(wave);
    306         requestAnimationFrame(this._loop);
    307       },
    308 
    309       upAction: function() {
    310         for (var i = 0; i < this.waves.length; i++) {
    311           // Declare the next wave that has mouse down to be mouse'ed up.
    312           var wave = this.waves[i];
    313           if (wave.isMouseDown) {
    314             wave.isMouseDown = false
    315             wave.mouseUpStart = now();
    316             wave.mouseDownStart = 0;
    317             wave.tUp = 0.0;
    318             break;
    319           }
    320         }
    321         this._loop && requestAnimationFrame(this._loop);
    322       },
    323 
    324       cancel: function() {
    325         this.cancelled = true;
    326       },
    327 
    328       animate: function(ctx) {
    329         var shouldRenderNextFrame = false;
    330 
    331         // Clear the canvas
    332         ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    333 
    334         var deleteTheseWaves = [];
    335         // The oldest wave's touch down duration
    336         var longestTouchDownDuration = 0;
    337         var longestTouchUpDuration = 0;
    338         // Save the last known wave color
    339         var lastWaveColor = null;
    340         // wave animation values
    341         var anim = {
    342           initialOpacity: this.initialOpacity,
    343           opacityDecayVelocity: this.opacityDecayVelocity,
    344           height: ctx.canvas.height,
    345           width: ctx.canvas.width
    346         }
    347 
    348         for (var i = 0; i < this.waves.length; i++) {
    349           var wave = this.waves[i];
    350 
    351           if (wave.mouseDownStart > 0) {
    352             wave.tDown = now() - wave.mouseDownStart;
    353           }
    354           if (wave.mouseUpStart > 0) {
    355             wave.tUp = now() - wave.mouseUpStart;
    356           }
    357 
    358           // Determine how long the touch has been up or down.
    359           var tUp = wave.tUp;
    360           var tDown = wave.tDown;
    361           longestTouchDownDuration = Math.max(longestTouchDownDuration, tDown);
    362           longestTouchUpDuration = Math.max(longestTouchUpDuration, tUp);
    363 
    364           // Obtain the instantenous size and alpha of the ripple.
    365           var radius = waveRadiusFn(tDown, tUp, anim);
    366           var waveAlpha =  waveOpacityFn(tDown, tUp, anim);
    367           var waveColor = cssColorWithAlpha(wave.waveColor, waveAlpha);
    368           lastWaveColor = wave.waveColor;
    369 
    370           // Position of the ripple.
    371           var x = wave.startPosition.x;
    372           var y = wave.startPosition.y;
    373 
    374           // Ripple gravitational pull to the center of the canvas.
    375           if (wave.endPosition) {
    376 
    377             // This translates from the origin to the center of the view  based on the max dimension of  
    378             var translateFraction = Math.min(1, radius / wave.containerSize * 2 / Math.sqrt(2) );
    379 
    380             x += translateFraction * (wave.endPosition.x - wave.startPosition.x);
    381             y += translateFraction * (wave.endPosition.y - wave.startPosition.y);
    382           }
    383 
    384           // If we do a background fill fade too, work out the correct color.
    385           var bgFillColor = null;
    386           if (this.backgroundFill) {
    387             var bgFillAlpha = waveOuterOpacityFn(tDown, tUp, anim);
    388             bgFillColor = cssColorWithAlpha(wave.waveColor, bgFillAlpha);
    389           }
    390 
    391           // Draw the ripple.
    392           drawRipple(ctx, x, y, radius, waveColor, bgFillColor);
    393 
    394           // Determine whether there is any more rendering to be done.
    395           var maximumWave = waveAtMaximum(wave, radius, anim);
    396           var waveDissipated = waveDidFinish(wave, radius, anim);
    397           var shouldKeepWave = !waveDissipated || maximumWave;
    398           var shouldRenderWaveAgain = !waveDissipated && !maximumWave;
    399           shouldRenderNextFrame = shouldRenderNextFrame || shouldRenderWaveAgain;
    400           if (!shouldKeepWave || this.cancelled) {
    401             deleteTheseWaves.push(wave);
    402           }
    403        }
    404 
    405         if (shouldRenderNextFrame) {
    406           requestAnimationFrame(this._loop);
    407         }
    408 
    409         for (var i = 0; i < deleteTheseWaves.length; ++i) {
    410           var wave = deleteTheseWaves[i];
    411           removeWaveFromScope(this, wave);
    412         }
    413 
    414         if (!this.waves.length) {
    415           // If there is nothing to draw, clear any drawn waves now because
    416           // we're not going to get another requestAnimationFrame any more.
    417           ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    418           this._loop = null;
    419         }
    420       }
    421 
    422     });
    423 
    424   })();
    425 
    426 
    427 
    428