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