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
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.
17 <paper-ripple></paper-ripple>
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.
27 <paper-ripple id="ripple" style="pointer-events: none;"></paper-ripple>
29 downAction: function(e) {
30 this.$.ripple.downAction({x: e.x, y: e.y});
32 upAction: function(e) {
33 this.$.ripple.upAction();
36 Styling ripple effect:
38 Use CSS color property to style the ripple:
44 Note that CSS color property is inherited so it is not required to set it on
45 the `paper-ripple` element directly.
47 Apply `recenteringTouch` class to make the recentering rippling effect.
49 <paper-ripple class="recenteringTouch"></paper-ripple>
51 Apply `circle` class to make the rippling effect within a circle.
53 <paper-ripple class="circle"></paper-ripple>
60 <link rel="import" href="../polymer/polymer.html" >
62 <polymer-element name="paper-ripple" attributes="initialOpacity opacityDecayVelocity">
81 :host(.circle) #canvas {
92 var waveMaxRadius = 150;
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);
107 var size = waveRadius * (1 - Math.pow(80, -tt));
108 return Math.abs(size);
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;
117 if (tu <= 0) { // before touch up
118 return anim.initialOpacity;
120 return Math.max(0, anim.initialOpacity - touchUp * anim.opacityDecayVelocity);
123 function waveOuterOpacityFn(td, tu, anim) {
124 // Convert from ms to s.
125 var touchDown = td / 1000;
126 var touchUp = tu / 1000;
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));
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)) {
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)) {
157 function drawRipple(ctx, x, y, radius, innerColor, outerColor) {
159 ctx.fillStyle = outerColor;
160 ctx.fillRect(0,0,ctx.canvas.width, ctx.canvas.height);
163 ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
164 ctx.fillStyle = innerColor;
171 function createWave(elem) {
172 var elementStyle = window.getComputedStyle(elem);
173 var fgColor = elementStyle.color;
187 function removeWaveFromScope(scope, wave) {
189 var pos = scope.waves.indexOf(wave);
190 scope.waves.splice(pos, 1);
197 if (window.performance && performance.now) {
198 now = performance.now.bind(performance);
201 function cssColorWithAlpha(cssColor, alpha) {
202 var parts = cssColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
203 if (typeof alpha == 'undefined') {
207 return 'rgba(255, 255, 255, ' + alpha + ')';
209 return 'rgba(' + parts[1] + ', ' + parts[2] + ', ' + parts[3] + ', ' + alpha + ')';
212 function dist(p1, p2) {
213 return Math.sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));
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);
224 Polymer('paper-ripple', {
227 * The initial opacity set on the wave.
229 * @attribute initialOpacity
233 initialOpacity: 0.25,
236 * How fast (opacity per second) the wave fades out.
238 * @attribute opacityDecayVelocity
242 opacityDecayVelocity: 0.8,
244 backgroundFill: true,
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;
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);
276 this._loop = this.animate.bind(this, ctx);
280 downAction: function(e) {
282 var wave = createWave(this.$.canvas);
284 this.cancelled = false;
285 wave.isMouseDown = true;
288 wave.mouseUpStart = 0.0;
289 wave.mouseDownStart = now();
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;
297 wave.startPosition = {x:touchX, y:touchY};
299 if (this.classList.contains("recenteringTouch")) {
300 wave.endPosition = {x: width / 2, y: height / 2};
301 wave.slideDistance = dist(wave.startPosition, wave.endPosition);
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);
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;
321 this._loop && requestAnimationFrame(this._loop);
325 this.cancelled = true;
328 animate: function(ctx) {
329 var shouldRenderNextFrame = false;
332 ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
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
342 initialOpacity: this.initialOpacity,
343 opacityDecayVelocity: this.opacityDecayVelocity,
344 height: ctx.canvas.height,
345 width: ctx.canvas.width
348 for (var i = 0; i < this.waves.length; i++) {
349 var wave = this.waves[i];
351 if (wave.mouseDownStart > 0) {
352 wave.tDown = now() - wave.mouseDownStart;
354 if (wave.mouseUpStart > 0) {
355 wave.tUp = now() - wave.mouseUpStart;
358 // Determine how long the touch has been up or down.
360 var tDown = wave.tDown;
361 longestTouchDownDuration = Math.max(longestTouchDownDuration, tDown);
362 longestTouchUpDuration = Math.max(longestTouchUpDuration, tUp);
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;
370 // Position of the ripple.
371 var x = wave.startPosition.x;
372 var y = wave.startPosition.y;
374 // Ripple gravitational pull to the center of the canvas.
375 if (wave.endPosition) {
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) );
380 x += translateFraction * (wave.endPosition.x - wave.startPosition.x);
381 y += translateFraction * (wave.endPosition.y - wave.startPosition.y);
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);
392 drawRipple(ctx, x, y, radius, waveColor, bgFillColor);
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);
405 if (shouldRenderNextFrame) {
406 requestAnimationFrame(this._loop);
409 for (var i = 0; i < deleteTheseWaves.length; ++i) {
410 var wave = deleteTheseWaves[i];
411 removeWaveFromScope(this, wave);
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);