1 2 3// 4// INK EQUATIONS 5// 6 7// Animation constants. 8var globalSpeed = 1; 9var waveOpacityDecayVelocity = 0.8 / globalSpeed; // opacity per second. 10var waveInitialOpacity = 0.25; 11var waveLingerOnTouchUp = 0.2; 12var waveMaxRadius = 150; 13 14// TODOs: 15// - rather than max distance to corner, use hypotenuos(sp) (diag) 16// - use quadratic for the fall off, move fast at the beginning, 17// - on cancel, immediately fade out, reverse the direction 18 19function waveRadiusFn(touchDownMs, touchUpMs, ww, hh) { 20 // Convert from ms to s. 21 var touchDown = touchDownMs / 1000; 22 var touchUp = touchUpMs / 1000; 23 var totalElapsed = touchDown + touchUp; 24 var waveRadius = Math.min(Math.max(ww, hh), waveMaxRadius) * 1.1 + 5; 25 var dduration = 1.1 - .2 * (waveRadius / waveMaxRadius); 26 var tt = (totalElapsed / dduration); 27 28 var ssize = waveRadius * (1 - Math.pow(80, -tt)); 29 return Math.abs(ssize); 30} 31 32function waveOpacityFn(td, tu) { 33 // Convert from ms to s. 34 var touchDown = td / 1000; 35 var touchUp = tu / 1000; 36 var totalElapsed = touchDown + touchUp; 37 38 if (tu <= 0) { // before touch up 39 return waveInitialOpacity; 40 } 41 return Math.max(0, waveInitialOpacity - touchUp * waveOpacityDecayVelocity); 42} 43 44function waveOuterOpacityFn(td, tu) { 45 // Convert from ms to s. 46 var touchDown = td / 1000; 47 var touchUp = tu / 1000; 48 49 // Linear increase in background opacity, capped at the opacity 50 // of the wavefront (waveOpacity). 51 var outerOpacity = touchDown * 0.3; 52 var waveOpacity = waveOpacityFn(td, tu); 53 return Math.max(0, Math.min(outerOpacity, waveOpacity)); 54 55} 56 57function waveGravityToCenterPercentageFn(td, tu, r) { 58 // Convert from ms to s. 59 var touchDown = td / 1000; 60 var touchUp = tu / 1000; 61 var totalElapsed = touchDown + touchUp; 62 63 return Math.min(1.0, touchUp * 6); 64} 65 66 67// Determines whether the wave should be completely removed. 68function waveDidFinish(wave, radius) { 69 var waveOpacity = waveOpacityFn(wave.tDown, wave.tUp); 70 // Does not linger any more. 71 // var lingerTimeMs = waveLingerOnTouchUp * 1000; 72 73 // If the wave opacity is 0 and the radius exceeds the bounds 74 // of the element, then this is finished. 75 if (waveOpacity < 0.01 && radius >= wave.maxRadius) { 76 return true; 77 } 78 return false; 79}; 80 81// 82// DRAWING 83// 84 85function animateIcon() { 86 var el = document.getElementById('button_toolbar0'); 87 el.classList.add('animate'); 88 setTimeout(function(){ 89 el.classList.remove('animate'); 90 el.classList.toggle('selected'); 91 }, 500); 92} 93 94 95function drawRipple(canvas, x, y, radius, innerColor, outerColor, innerColorAlpha, outerColorAlpha) { 96 var ctx = canvas.getContext('2d'); 97 if (outerColor) { 98 ctx.fillStyle = outerColor; 99 ctx.fillRect(0,0,canvas.width, canvas.height); 100 } 101 102 ctx.beginPath(); 103 ctx.arc(x, y, radius, 0, 2 * Math.PI, false); 104 ctx.fillStyle = innerColor; 105 ctx.fill(); 106} 107 108function drawLabel(canvas, label, fontSize, color, alignment) { 109 var ctx = canvas.getContext('2d'); 110 ctx.font= fontSize + 'px Helvetica'; 111 112 var metrics = ctx.measureText(label); 113 var width = metrics.width; 114 var height = metrics.height; 115 ctx.fillStyle = color; 116 117 var xPos = (canvas.width/2 - width)/2; 118 119 if (alignment === 'left') { xPos = 16; } 120 121 ctx.fillText(label, xPos, canvas.height/2 - (canvas.height/2 - fontSize +2) / 2); 122} 123 124// 125// BUTTON SETUP 126// 127 128function createWave(elem) { 129 var elementStyle = window.getComputedStyle(elem); 130 var fgColor = elementStyle.color; 131 132 var wave = { 133 waveColor: fgColor, 134 maxRadius: 0, 135 isMouseDown: false, 136 mouseDownStart: 0.0, 137 mouseUpStart: 0.0, 138 tDown: 0, 139 tUp: 0 140 }; 141 return wave; 142} 143 144function removeWaveFromScope(scope, wave) { 145 if (scope.waves) { 146 var pos = scope.waves.indexOf(wave); 147 scope.waves.splice(pos, 1); 148 } 149}; 150 151 152function setUpPaperByClass( classname ) { 153 var elems = document.querySelectorAll( classname ); 154 [].forEach.call( elems, function( el ) { 155 setUpPaper(el); 156 }); 157} 158 159function setUpPaper(elem) { 160 var pixelDensity = 2; 161 162 var elementStyle = window.getComputedStyle(elem); 163 var fgColor = elementStyle.color; 164 var bgColor = elementStyle.backgroundColor; 165 elem.width = elem.clientWidth; 166 elem.setAttribute('width', elem.clientWidth * pixelDensity + "px"); 167 elem.setAttribute('height', elem.clientHeight * pixelDensity + "px"); 168 169 var isButton = elem.classList.contains( 'button' ) || elem.classList.contains( 'button_floating' ) | elem.classList.contains( 'button_menu' ); 170 var isToolbarButton = elem.classList.contains( 'button_toolbar' ); 171 172 elem.getContext('2d').scale(pixelDensity, pixelDensity) 173 174 var scope = { 175 backgroundFill: true, 176 element: elem, 177 label: 'Button', 178 waves: [], 179 }; 180 181 182 scope.label = elem.getAttribute('value') || elementStyle.content; 183 scope.labelFontSize = elementStyle.fontSize.split("px")[0]; 184 185 drawLabel(elem, scope.label, scope.labelFontSize, fgColor, elem.style.textAlign); 186 187 188 // 189 // RENDER FOR EACH FRAME 190 // 191 var onFrame = function() { 192 var shouldRenderNextFrame = false; 193 194 // Clear the canvas 195 var ctx = elem.getContext('2d'); 196 ctx.clearRect(0, 0, elem.width, elem.height); 197 198 var deleteTheseWaves = []; 199 // The oldest wave's touch down duration 200 var longestTouchDownDuration = 0; 201 var longestTouchUpDuration = 0; 202 // Save the last known wave color 203 var lastWaveColor = null; 204 205 for (var i = 0; i < scope.waves.length; i++) { 206 var wave = scope.waves[i]; 207 208 if (wave.mouseDownStart > 0) { 209 wave.tDown = now() - wave.mouseDownStart; 210 } 211 if (wave.mouseUpStart > 0) { 212 wave.tUp = now() - wave.mouseUpStart; 213 } 214 215 // Determine how long the touch has been up or down. 216 var tUp = wave.tUp; 217 var tDown = wave.tDown; 218 longestTouchDownDuration = Math.max(longestTouchDownDuration, tDown); 219 longestTouchUpDuration = Math.max(longestTouchUpDuration, tUp); 220 221 // Obtain the instantenous size and alpha of the ripple. 222 var radius = waveRadiusFn(tDown, tUp, elem.width, elem.height); 223 var waveAlpha = waveOpacityFn(tDown, tUp); 224 var waveColor = cssColorWithAlpha(wave.waveColor, waveAlpha); 225 lastWaveColor = wave.waveColor; 226 227 // Position of the ripple. 228 var x = wave.startPosition.x; 229 var y = wave.startPosition.y; 230 231 // Ripple gravitational pull to the center of the canvas. 232 if (wave.endPosition) { 233 234 var translateFraction = waveGravityToCenterPercentageFn(tDown, tUp, wave.maxRadius); 235 236 // This translates from the origin to the center of the view based on the max dimension of 237 var translateFraction = Math.min(1, radius / wave.containerSize * 2 / Math.sqrt(2) ); 238 239 x += translateFraction * (wave.endPosition.x - wave.startPosition.x); 240 y += translateFraction * (wave.endPosition.y - wave.startPosition.y); 241 } 242 243 // If we do a background fill fade too, work out the correct color. 244 var bgFillColor = null; 245 if (scope.backgroundFill) { 246 var bgFillAlpha = waveOuterOpacityFn(tDown, tUp); 247 bgFillColor = cssColorWithAlpha(wave.waveColor, bgFillAlpha); 248 } 249 250 // Draw the ripple. 251 drawRipple(elem, x, y, radius, waveColor, bgFillColor); 252 253 // Determine whether there is any more rendering to be done. 254 var shouldRenderWaveAgain = !waveDidFinish(wave, radius); 255 shouldRenderNextFrame = shouldRenderNextFrame || shouldRenderWaveAgain; 256 if (!shouldRenderWaveAgain) { 257 deleteTheseWaves.push(wave); 258 } 259 } 260 261 if (shouldRenderNextFrame) { 262 window.requestAnimationFrame(onFrame); 263 } else { 264 // If there is nothing to draw, clear any drawn waves now because 265 // we're not going to get another requestAnimationFrame any more. 266 var ctx = elem.getContext('2d'); 267 ctx.clearRect(0, 0, elem.width, elem.height); 268 } 269 270 // Draw the label at the very last point so it is on top of everything. 271 drawLabel(elem, scope.label, scope.labelFontSize, fgColor, elem.style.textAlign); 272 273 for (var i = 0; i < deleteTheseWaves.length; ++i) { 274 var wave = deleteTheseWaves[i]; 275 removeWaveFromScope(scope, wave); 276 } 277 }; 278 279 // 280 // MOUSE DOWN HANDLER 281 // 282 283 elem.addEventListener('mousedown', function(e) { 284 var wave = createWave(e.target); 285 var elem = scope.element; 286 287 wave.isMouseDown = true; 288 wave.tDown = 0.0; 289 wave.tUp = 0.0; 290 wave.mouseUpStart = 0.0; 291 wave.mouseDownStart = now(); 292 293 var width = e.target.width / 2; // Retina canvas 294 var height = e.target.height / 2; 295 var touchX = e.clientX - e.target.offsetLeft - e.target.offsetParent.offsetLeft; 296 var touchY = e.clientY - e.target.offsetTop - e.target.offsetParent.offsetTop; 297 wave.startPosition = {x:touchX, y:touchY}; 298 299 if (elem.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 elem.classList.add("activated"); 306 scope.waves.push(wave); 307 window.requestAnimationFrame(onFrame); 308 return false; 309 }); 310 311 // 312 // MOUSE UP HANDLER 313 // 314 315 elem.addEventListener('mouseup', function(e) { 316 elem.classList.remove("activated"); 317 318 for (var i = 0; i < scope.waves.length; i++) { 319 // Declare the next wave that has mouse down to be mouse'ed up. 320 var wave = scope.waves[i]; 321 if (wave.isMouseDown) { 322 wave.isMouseDown = false 323 wave.mouseUpStart = now(); 324 wave.mouseDownStart = 0; 325 wave.tUp = 0.0; 326 break; 327 } 328 } 329 return false; 330 }); 331 332 elem.addEventListener('mouseout', function(e) { 333 elem.classList.remove("activated"); 334 335 for (var i = 0; i < scope.waves.length; i++) { 336 // Declare the next wave that has mouse down to be mouse'ed up. 337 var wave = scope.waves[i]; 338 if (wave.isMouseDown) { 339 wave.isMouseDown = false 340 wave.mouseUpStart = now(); 341 wave.mouseDownStart = 0; 342 wave.tUp = 0.0; 343 wave.cancelled = true; 344 break; 345 } 346 } 347 return false; 348 }); 349 350 return scope; 351}; 352 353// Shortcuts. 354var pow = Math.pow; 355var now = function() { return new Date().getTime(); }; 356 357// Quad beizer where t is between 0 and 1. 358function quadBezier(t, p0, p1, p2, p3) { 359 return pow(1 - t, 3) * p0 + 360 3 * pow(1 - t, 2) * t * p1 + 361 (1 - t) * pow(t, 2) * p2 + 362 pow(t, 3) * p3; 363} 364 365function easeIn(t) { 366 return quadBezier(t, 0.4, 0.0, 1, 1); 367} 368 369function cssColorWithAlpha(cssColor, alpha) { 370 var parts = cssColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); 371 if (typeof alpha == 'undefined') { 372 alpha = 1; 373 } 374 if (!parts) { 375 return 'rgba(255, 255, 255, ' + alpha + ')'; 376 } 377 return 'rgba(' + parts[1] + ', ' + parts[2] + ', ' + parts[3] + ', ' + alpha + ')'; 378} 379 380function dist(p1, p2) { 381 return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); 382} 383 384function distanceFromPointToFurthestCorner(point, size) { 385 var tl_d = dist(point, {x: 0, y: 0}); 386 var tr_d = dist(point, {x: size.w, y: 0}); 387 var bl_d = dist(point, {x: 0, y: size.h}); 388 var br_d = dist(point, {x: size.w, y: size.h}); 389 return Math.max(Math.max(tl_d, tr_d), Math.max(bl_d, br_d)); 390} 391 392 393function toggleDialog() { 394 var el = document.getElementById('dialog'); 395 el.classList.toggle("visible"); 396} 397 398function toggleMenu() { 399 var el = document.getElementById('menu'); 400 el.classList.toggle("visible"); 401} 402 403 404// Initialize 405 406function init() { 407 setUpPaperByClass( '.paper' ); 408} 409 410window.addEventListener('DOMContentLoaded', init, false); 411