• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!--
2Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
3This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
4The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
5The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
6Code distributed by Google as part of the polymer project is also
7subject 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
12use to simulate a rippling effect emanating from the point of contact.  The
13effect can be visualized as a concentric circle with motion.
14
15Example:
16
17    <paper-ripple></paper-ripple>
18
19`paper-ripple` listens to "down" and "up" events so it would display ripple
20effect when touches on it.  You can also defeat the default behavior and
21manually route the down and up actions to the ripple element.  Note that it is
22important if you call downAction() you will have to make sure to call upAction()
23so that `paper-ripple` would end the animation loop.
24
25Example:
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
36Styling 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
47By default, the ripple is centered on the point of contact.  Apply `recenteringTouch`
48class to have the ripple grow toward the center of its container.
49
50    <paper-ripple class="recenteringTouch"></paper-ripple>
51
52Apply `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<!--
62Fired when the animation finishes. This is useful if you want to wait until the ripple
63animation 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