OLD | NEW |
(Empty) | |
| 1 |
| 2 |
| 3 // |
| 4 // INK EQUATIONS |
| 5 // |
| 6 |
| 7 // Animation constants. |
| 8 var globalSpeed = 1; |
| 9 var waveOpacityDecayVelocity = 0.8 / globalSpeed; // opacity per second. |
| 10 var waveInitialOpacity = 0.25; |
| 11 var waveLingerOnTouchUp = 0.2; |
| 12 var 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 |
| 19 function 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 |
| 32 function 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 |
| 44 function 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 |
| 57 function 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. |
| 68 function 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 |
| 85 function 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 |
| 95 function drawRipple(canvas, x, y, radius, innerColor, outerColor, innerColorAlph
a, 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 |
| 108 function 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 |
| 128 function 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 |
| 144 function removeWaveFromScope(scope, wave) { |
| 145 if (scope.waves) { |
| 146 var pos = scope.waves.indexOf(wave); |
| 147 scope.waves.splice(pos, 1); |
| 148 } |
| 149 }; |
| 150 |
| 151 |
| 152 function setUpPaperByClass( classname ) { |
| 153 var elems = document.querySelectorAll( classname ); |
| 154 [].forEach.call( elems, function( el ) { |
| 155 setUpPaper(el); |
| 156 }); |
| 157 } |
| 158 |
| 159 function 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.textAlig
n); |
| 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 t
he max dimension of |
| 237 var translateFraction = Math.min(1, radius / wave.containerSize * 2 / Ma
th.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.textAl
ign); |
| 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.offsetL
eft; |
| 296 var touchY = e.clientY - e.target.offsetTop - e.target.offsetParent.offsetTo
p; |
| 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: w
idth, 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. |
| 354 var pow = Math.pow; |
| 355 var now = function() { return new Date().getTime(); }; |
| 356 |
| 357 // Quad beizer where t is between 0 and 1. |
| 358 function 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 |
| 365 function easeIn(t) { |
| 366 return quadBezier(t, 0.4, 0.0, 1, 1); |
| 367 } |
| 368 |
| 369 function 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 |
| 380 function dist(p1, p2) { |
| 381 return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); |
| 382 } |
| 383 |
| 384 function 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 |
| 393 function toggleDialog() { |
| 394 var el = document.getElementById('dialog'); |
| 395 el.classList.toggle("visible"); |
| 396 } |
| 397 |
| 398 function toggleMenu() { |
| 399 var el = document.getElementById('menu'); |
| 400 el.classList.toggle("visible"); |
| 401 } |
| 402 |
| 403 |
| 404 // Initialize |
| 405 |
| 406 function init() { |
| 407 setUpPaperByClass( '.paper' ); |
| 408 } |
| 409 |
| 410 window.addEventListener('DOMContentLoaded', init, false); |
OLD | NEW |