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