Home | History | Annotate | Download | only in paper-ripple
      1 
      2 
      3   (function() {
      4 
      5     var waveMaxRadius = 150;
      6     //
      7     // INK EQUATIONS
      8     //
      9     function waveRadiusFn(touchDownMs, touchUpMs, anim) {
     10       // Convert from ms to s.
     11       var touchDown = touchDownMs / 1000;
     12       var touchUp = touchUpMs / 1000;
     13       var totalElapsed = touchDown + touchUp;
     14       var ww = anim.width, hh = anim.height;
     15       // use diagonal size of container to avoid floating point math sadness
     16       var waveRadius = Math.min(Math.sqrt(ww * ww + hh * hh), waveMaxRadius) * 1.1 + 5;
     17       var duration = 1.1 - .2 * (waveRadius / waveMaxRadius);
     18       var tt = (totalElapsed / duration);
     19 
     20       var size = waveRadius * (1 - Math.pow(80, -tt));
     21       return Math.abs(size);
     22     }
     23 
     24     function waveOpacityFn(td, tu, anim) {
     25       // Convert from ms to s.
     26       var touchDown = td / 1000;
     27       var touchUp = tu / 1000;
     28       var totalElapsed = touchDown + touchUp;
     29 
     30       if (tu <= 0) {  // before touch up
     31         return anim.initialOpacity;
     32       }
     33       return Math.max(0, anim.initialOpacity - touchUp * anim.opacityDecayVelocity);
     34     }
     35 
     36     function waveOuterOpacityFn(td, tu, anim) {
     37       // Convert from ms to s.
     38       var touchDown = td / 1000;
     39       var touchUp = tu / 1000;
     40 
     41       // Linear increase in background opacity, capped at the opacity
     42       // of the wavefront (waveOpacity).
     43       var outerOpacity = touchDown * 0.3;
     44       var waveOpacity = waveOpacityFn(td, tu, anim);
     45       return Math.max(0, Math.min(outerOpacity, waveOpacity));
     46     }
     47 
     48     // Determines whether the wave should be completely removed.
     49     function waveDidFinish(wave, radius, anim) {
     50       var waveOpacity = waveOpacityFn(wave.tDown, wave.tUp, anim);
     51       // If the wave opacity is 0 and the radius exceeds the bounds
     52       // of the element, then this is finished.
     53       if (waveOpacity < 0.01 && radius >= Math.min(wave.maxRadius, waveMaxRadius)) {
     54         return true;
     55       }
     56       return false;
     57     };
     58 
     59     function waveAtMaximum(wave, radius, anim) {
     60       var waveOpacity = waveOpacityFn(wave.tDown, wave.tUp, anim);
     61       if (waveOpacity >= anim.initialOpacity && radius >= Math.min(wave.maxRadius, waveMaxRadius)) {
     62         return true;
     63       }
     64       return false;
     65     }
     66 
     67     //
     68     // DRAWING
     69     //
     70     function drawRipple(ctx, x, y, radius, innerColor, outerColor) {
     71       if (outerColor) {
     72         ctx.fillStyle = outerColor;
     73         ctx.fillRect(0,0,ctx.canvas.width, ctx.canvas.height);
     74       }
     75       ctx.beginPath();
     76       ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
     77       ctx.fillStyle = innerColor;
     78       ctx.fill();
     79     }
     80 
     81     //
     82     // SETUP
     83     //
     84     function createWave(elem) {
     85       var elementStyle = window.getComputedStyle(elem);
     86       var fgColor = elementStyle.color;
     87 
     88       var wave = {
     89         waveColor: fgColor,
     90         maxRadius: 0,
     91         isMouseDown: false,
     92         mouseDownStart: 0.0,
     93         mouseUpStart: 0.0,
     94         tDown: 0,
     95         tUp: 0
     96       };
     97       return wave;
     98     }
     99 
    100     function removeWaveFromScope(scope, wave) {
    101       if (scope.waves) {
    102         var pos = scope.waves.indexOf(wave);
    103         scope.waves.splice(pos, 1);
    104       }
    105     };
    106 
    107     // Shortcuts.
    108     var pow = Math.pow;
    109     var now = Date.now;
    110     if (window.performance && performance.now) {
    111       now = performance.now.bind(performance);
    112     }
    113 
    114     function cssColorWithAlpha(cssColor, alpha) {
    115         var parts = cssColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
    116         if (typeof alpha == 'undefined') {
    117             alpha = 1;
    118         }
    119         if (!parts) {
    120           return 'rgba(255, 255, 255, ' + alpha + ')';
    121         }
    122         return 'rgba(' + parts[1] + ', ' + parts[2] + ', ' + parts[3] + ', ' + alpha + ')';
    123     }
    124 
    125     function dist(p1, p2) {
    126       return Math.sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));
    127     }
    128 
    129     function distanceFromPointToFurthestCorner(point, size) {
    130       var tl_d = dist(point, {x: 0, y: 0});
    131       var tr_d = dist(point, {x: size.w, y: 0});
    132       var bl_d = dist(point, {x: 0, y: size.h});
    133       var br_d = dist(point, {x: size.w, y: size.h});
    134       return Math.max(tl_d, tr_d, bl_d, br_d);
    135     }
    136 
    137     Polymer('paper-ripple', {
    138 
    139       /**
    140        * The initial opacity set on the wave.
    141        *
    142        * @attribute initialOpacity
    143        * @type number
    144        * @default 0.25
    145        */
    146       initialOpacity: 0.25,
    147 
    148       /**
    149        * How fast (opacity per second) the wave fades out.
    150        *
    151        * @attribute opacityDecayVelocity
    152        * @type number
    153        * @default 0.8
    154        */
    155       opacityDecayVelocity: 0.8,
    156 
    157       backgroundFill: true,
    158       pixelDensity: 2,
    159 
    160       eventDelegates: {
    161         down: 'downAction',
    162         up: 'upAction'
    163       },
    164 
    165       attached: function() {
    166         // create the canvas element manually becase ios
    167         // does not render the canvas element if it is not created in the
    168         // main document (component templates are created in a
    169         // different document). See:
    170         // https://bugs.webkit.org/show_bug.cgi?id=109073.
    171         if (!this.$.canvas) {
    172           var canvas = document.createElement('canvas');
    173           canvas.id = 'canvas';
    174           this.shadowRoot.appendChild(canvas);
    175           this.$.canvas = canvas;
    176         }
    177       },
    178 
    179       ready: function() {
    180         this.waves = [];
    181       },
    182 
    183       setupCanvas: function() {
    184         this.$.canvas.setAttribute('width', this.$.canvas.clientWidth * this.pixelDensity + "px");
    185         this.$.canvas.setAttribute('height', this.$.canvas.clientHeight * this.pixelDensity + "px");
    186         var ctx = this.$.canvas.getContext('2d');
    187         ctx.scale(this.pixelDensity, this.pixelDensity);
    188         if (!this._loop) {
    189           this._loop = this.animate.bind(this, ctx);
    190         }
    191       },
    192 
    193       downAction: function(e) {
    194         this.setupCanvas();
    195         var wave = createWave(this.$.canvas);
    196 
    197         this.cancelled = false;
    198         wave.isMouseDown = true;
    199         wave.tDown = 0.0;
    200         wave.tUp = 0.0;
    201         wave.mouseUpStart = 0.0;
    202         wave.mouseDownStart = now();
    203 
    204         var width = this.$.canvas.width / 2; // Retina canvas
    205         var height = this.$.canvas.height / 2;
    206         var rect = this.getBoundingClientRect();
    207         var touchX = e.x - rect.left;
    208         var touchY = e.y - rect.top;
    209 
    210         wave.startPosition = {x:touchX, y:touchY};
    211 
    212         if (this.classList.contains("recenteringTouch")) {
    213           wave.endPosition = {x: width / 2,  y: height / 2};
    214           wave.slideDistance = dist(wave.startPosition, wave.endPosition);
    215         }
    216         wave.containerSize = Math.max(width, height);
    217         wave.maxRadius = distanceFromPointToFurthestCorner(wave.startPosition, {w: width, h: height});
    218         this.waves.push(wave);
    219         requestAnimationFrame(this._loop);
    220       },
    221 
    222       upAction: function() {
    223         for (var i = 0; i < this.waves.length; i++) {
    224           // Declare the next wave that has mouse down to be mouse'ed up.
    225           var wave = this.waves[i];
    226           if (wave.isMouseDown) {
    227             wave.isMouseDown = false
    228             wave.mouseUpStart = now();
    229             wave.mouseDownStart = 0;
    230             wave.tUp = 0.0;
    231             break;
    232           }
    233         }
    234         this._loop && requestAnimationFrame(this._loop);
    235       },
    236 
    237       cancel: function() {
    238         this.cancelled = true;
    239       },
    240 
    241       animate: function(ctx) {
    242         var shouldRenderNextFrame = false;
    243 
    244         // Clear the canvas
    245         ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    246 
    247         var deleteTheseWaves = [];
    248         // The oldest wave's touch down duration
    249         var longestTouchDownDuration = 0;
    250         var longestTouchUpDuration = 0;
    251         // Save the last known wave color
    252         var lastWaveColor = null;
    253         // wave animation values
    254         var anim = {
    255           initialOpacity: this.initialOpacity,
    256           opacityDecayVelocity: this.opacityDecayVelocity,
    257           height: ctx.canvas.height,
    258           width: ctx.canvas.width
    259         }
    260 
    261         for (var i = 0; i < this.waves.length; i++) {
    262           var wave = this.waves[i];
    263 
    264           if (wave.mouseDownStart > 0) {
    265             wave.tDown = now() - wave.mouseDownStart;
    266           }
    267           if (wave.mouseUpStart > 0) {
    268             wave.tUp = now() - wave.mouseUpStart;
    269           }
    270 
    271           // Determine how long the touch has been up or down.
    272           var tUp = wave.tUp;
    273           var tDown = wave.tDown;
    274           longestTouchDownDuration = Math.max(longestTouchDownDuration, tDown);
    275           longestTouchUpDuration = Math.max(longestTouchUpDuration, tUp);
    276 
    277           // Obtain the instantenous size and alpha of the ripple.
    278           var radius = waveRadiusFn(tDown, tUp, anim);
    279           var waveAlpha =  waveOpacityFn(tDown, tUp, anim);
    280           var waveColor = cssColorWithAlpha(wave.waveColor, waveAlpha);
    281           lastWaveColor = wave.waveColor;
    282 
    283           // Position of the ripple.
    284           var x = wave.startPosition.x;
    285           var y = wave.startPosition.y;
    286 
    287           // Ripple gravitational pull to the center of the canvas.
    288           if (wave.endPosition) {
    289 
    290             // This translates from the origin to the center of the view  based on the max dimension of  
    291             var translateFraction = Math.min(1, radius / wave.containerSize * 2 / Math.sqrt(2) );
    292 
    293             x += translateFraction * (wave.endPosition.x - wave.startPosition.x);
    294             y += translateFraction * (wave.endPosition.y - wave.startPosition.y);
    295           }
    296 
    297           // If we do a background fill fade too, work out the correct color.
    298           var bgFillColor = null;
    299           if (this.backgroundFill) {
    300             var bgFillAlpha = waveOuterOpacityFn(tDown, tUp, anim);
    301             bgFillColor = cssColorWithAlpha(wave.waveColor, bgFillAlpha);
    302           }
    303 
    304           // Draw the ripple.
    305           drawRipple(ctx, x, y, radius, waveColor, bgFillColor);
    306 
    307           // Determine whether there is any more rendering to be done.
    308           var maximumWave = waveAtMaximum(wave, radius, anim);
    309           var waveDissipated = waveDidFinish(wave, radius, anim);
    310           var shouldKeepWave = !waveDissipated || maximumWave;
    311           var shouldRenderWaveAgain = !waveDissipated && !maximumWave;
    312           shouldRenderNextFrame = shouldRenderNextFrame || shouldRenderWaveAgain;
    313           if (!shouldKeepWave || this.cancelled) {
    314             deleteTheseWaves.push(wave);
    315           }
    316        }
    317 
    318         if (shouldRenderNextFrame) {
    319           requestAnimationFrame(this._loop);
    320         }
    321 
    322         for (var i = 0; i < deleteTheseWaves.length; ++i) {
    323           var wave = deleteTheseWaves[i];
    324           removeWaveFromScope(this, wave);
    325         }
    326 
    327         if (!this.waves.length) {
    328           // If there is nothing to draw, clear any drawn waves now because
    329           // we're not going to get another requestAnimationFrame any more.
    330           ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    331           this._loop = null;
    332         }
    333       }
    334 
    335     });
    336 
    337   })();
    338 
    339