| OLD | NEW |
| (Empty) |
| 1 | |
| 2 (function() { | |
| 3 var Utility = { | |
| 4 cssColorWithAlpha: function(cssColor, alpha) { | |
| 5 var parts = cssColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); | |
| 6 | |
| 7 if (typeof alpha == 'undefined') { | |
| 8 alpha = 1; | |
| 9 } | |
| 10 | |
| 11 if (!parts) { | |
| 12 return 'rgba(255, 255, 255, ' + alpha + ')'; | |
| 13 } | |
| 14 | |
| 15 return 'rgba(' + parts[1] + ', ' + parts[2] + ', ' + parts[3] + ', ' + a
lpha + ')'; | |
| 16 }, | |
| 17 | |
| 18 distance: function(x1, y1, x2, y2) { | |
| 19 var xDelta = (x1 - x2); | |
| 20 var yDelta = (y1 - y2); | |
| 21 | |
| 22 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); | |
| 23 }, | |
| 24 | |
| 25 now: (function() { | |
| 26 if (window.performance && window.performance.now) { | |
| 27 return window.performance.now.bind(window.performance); | |
| 28 } | |
| 29 | |
| 30 return Date.now; | |
| 31 })() | |
| 32 }; | |
| 33 | |
| 34 /** | |
| 35 * @param {HTMLElement} element | |
| 36 * @constructor | |
| 37 */ | |
| 38 function ElementMetrics(element) { | |
| 39 this.element = element; | |
| 40 this.width = this.boundingRect.width; | |
| 41 this.height = this.boundingRect.height; | |
| 42 | |
| 43 this.size = Math.max(this.width, this.height); | |
| 44 } | |
| 45 | |
| 46 ElementMetrics.prototype = { | |
| 47 get boundingRect () { | |
| 48 return this.element.getBoundingClientRect(); | |
| 49 }, | |
| 50 | |
| 51 furthestCornerDistanceFrom: function(x, y) { | |
| 52 var topLeft = Utility.distance(x, y, 0, 0); | |
| 53 var topRight = Utility.distance(x, y, this.width, 0); | |
| 54 var bottomLeft = Utility.distance(x, y, 0, this.height); | |
| 55 var bottomRight = Utility.distance(x, y, this.width, this.height); | |
| 56 | |
| 57 return Math.max(topLeft, topRight, bottomLeft, bottomRight); | |
| 58 } | |
| 59 }; | |
| 60 | |
| 61 /** | |
| 62 * @param {HTMLElement} element | |
| 63 * @constructor | |
| 64 */ | |
| 65 function Ripple(element) { | |
| 66 this.element = element; | |
| 67 this.color = window.getComputedStyle(element).color; | |
| 68 | |
| 69 this.wave = document.createElement('div'); | |
| 70 this.waveContainer = document.createElement('div'); | |
| 71 this.wave.style.backgroundColor = this.color; | |
| 72 this.wave.classList.add('wave'); | |
| 73 this.waveContainer.classList.add('wave-container'); | |
| 74 Polymer.dom(this.waveContainer).appendChild(this.wave); | |
| 75 | |
| 76 this.resetInteractionState(); | |
| 77 } | |
| 78 | |
| 79 Ripple.MAX_RADIUS = 300; | |
| 80 | |
| 81 Ripple.prototype = { | |
| 82 get recenters() { | |
| 83 return this.element.recenters; | |
| 84 }, | |
| 85 | |
| 86 get center() { | |
| 87 return this.element.center; | |
| 88 }, | |
| 89 | |
| 90 get mouseDownElapsed() { | |
| 91 var elapsed; | |
| 92 | |
| 93 if (!this.mouseDownStart) { | |
| 94 return 0; | |
| 95 } | |
| 96 | |
| 97 elapsed = Utility.now() - this.mouseDownStart; | |
| 98 | |
| 99 if (this.mouseUpStart) { | |
| 100 elapsed -= this.mouseUpElapsed; | |
| 101 } | |
| 102 | |
| 103 return elapsed; | |
| 104 }, | |
| 105 | |
| 106 get mouseUpElapsed() { | |
| 107 return this.mouseUpStart ? | |
| 108 Utility.now () - this.mouseUpStart : 0; | |
| 109 }, | |
| 110 | |
| 111 get mouseDownElapsedSeconds() { | |
| 112 return this.mouseDownElapsed / 1000; | |
| 113 }, | |
| 114 | |
| 115 get mouseUpElapsedSeconds() { | |
| 116 return this.mouseUpElapsed / 1000; | |
| 117 }, | |
| 118 | |
| 119 get mouseInteractionSeconds() { | |
| 120 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; | |
| 121 }, | |
| 122 | |
| 123 get initialOpacity() { | |
| 124 return this.element.initialOpacity; | |
| 125 }, | |
| 126 | |
| 127 get opacityDecayVelocity() { | |
| 128 return this.element.opacityDecayVelocity; | |
| 129 }, | |
| 130 | |
| 131 get radius() { | |
| 132 var width2 = this.containerMetrics.width * this.containerMetrics.width; | |
| 133 var height2 = this.containerMetrics.height * this.containerMetrics.heigh
t; | |
| 134 var waveRadius = Math.min( | |
| 135 Math.sqrt(width2 + height2), | |
| 136 Ripple.MAX_RADIUS | |
| 137 ) * 1.1 + 5; | |
| 138 | |
| 139 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS); | |
| 140 var timeNow = this.mouseInteractionSeconds / duration; | |
| 141 var size = waveRadius * (1 - Math.pow(80, -timeNow)); | |
| 142 | |
| 143 return Math.abs(size); | |
| 144 }, | |
| 145 | |
| 146 get opacity() { | |
| 147 if (!this.mouseUpStart) { | |
| 148 return this.initialOpacity; | |
| 149 } | |
| 150 | |
| 151 return Math.max( | |
| 152 0, | |
| 153 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe
locity | |
| 154 ); | |
| 155 }, | |
| 156 | |
| 157 get outerOpacity() { | |
| 158 // Linear increase in background opacity, capped at the opacity | |
| 159 // of the wavefront (waveOpacity). | |
| 160 var outerOpacity = this.mouseUpElapsedSeconds * 0.3; | |
| 161 var waveOpacity = this.opacity; | |
| 162 | |
| 163 return Math.max( | |
| 164 0, | |
| 165 Math.min(outerOpacity, waveOpacity) | |
| 166 ); | |
| 167 }, | |
| 168 | |
| 169 get isOpacityFullyDecayed() { | |
| 170 return this.opacity < 0.01 && | |
| 171 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
| 172 }, | |
| 173 | |
| 174 get isRestingAtMaxRadius() { | |
| 175 return this.opacity >= this.initialOpacity && | |
| 176 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
| 177 }, | |
| 178 | |
| 179 get isAnimationComplete() { | |
| 180 return this.mouseUpStart ? | |
| 181 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius; | |
| 182 }, | |
| 183 | |
| 184 get translationFraction() { | |
| 185 return Math.min( | |
| 186 1, | |
| 187 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2) | |
| 188 ); | |
| 189 }, | |
| 190 | |
| 191 get xNow() { | |
| 192 if (this.xEnd) { | |
| 193 return this.xStart + this.translationFraction * (this.xEnd - this.xSta
rt); | |
| 194 } | |
| 195 | |
| 196 return this.xStart; | |
| 197 }, | |
| 198 | |
| 199 get yNow() { | |
| 200 if (this.yEnd) { | |
| 201 return this.yStart + this.translationFraction * (this.yEnd - this.ySta
rt); | |
| 202 } | |
| 203 | |
| 204 return this.yStart; | |
| 205 }, | |
| 206 | |
| 207 get isMouseDown() { | |
| 208 return this.mouseDownStart && !this.mouseUpStart; | |
| 209 }, | |
| 210 | |
| 211 resetInteractionState: function() { | |
| 212 this.maxRadius = 0; | |
| 213 this.mouseDownStart = 0; | |
| 214 this.mouseUpStart = 0; | |
| 215 | |
| 216 this.xStart = 0; | |
| 217 this.yStart = 0; | |
| 218 this.xEnd = 0; | |
| 219 this.yEnd = 0; | |
| 220 this.slideDistance = 0; | |
| 221 | |
| 222 this.containerMetrics = new ElementMetrics(this.element); | |
| 223 }, | |
| 224 | |
| 225 draw: function() { | |
| 226 var scale; | |
| 227 var translateString; | |
| 228 var dx; | |
| 229 var dy; | |
| 230 | |
| 231 this.wave.style.opacity = this.opacity; | |
| 232 | |
| 233 scale = this.radius / (this.containerMetrics.size / 2); | |
| 234 dx = this.xNow - (this.containerMetrics.width / 2); | |
| 235 dy = this.yNow - (this.containerMetrics.height / 2); | |
| 236 | |
| 237 | |
| 238 // 2d transform for safari because of border-radius and overflow:hidden
clipping bug. | |
| 239 // https://bugs.webkit.org/show_bug.cgi?id=98538 | |
| 240 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' +
dy + 'px)'; | |
| 241 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy +
'px, 0)'; | |
| 242 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; | |
| 243 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; | |
| 244 }, | |
| 245 | |
| 246 downAction: function(event) { | |
| 247 var xCenter = this.containerMetrics.width / 2; | |
| 248 var yCenter = this.containerMetrics.height / 2; | |
| 249 | |
| 250 this.resetInteractionState(); | |
| 251 this.mouseDownStart = Utility.now(); | |
| 252 | |
| 253 if (this.center) { | |
| 254 this.xStart = xCenter; | |
| 255 this.yStart = yCenter; | |
| 256 this.slideDistance = Utility.distance( | |
| 257 this.xStart, this.yStart, this.xEnd, this.yEnd | |
| 258 ); | |
| 259 } else { | |
| 260 this.xStart = event ? | |
| 261 event.detail.x - this.containerMetrics.boundingRect.left : | |
| 262 this.containerMetrics.width / 2; | |
| 263 this.yStart = event ? | |
| 264 event.detail.y - this.containerMetrics.boundingRect.top : | |
| 265 this.containerMetrics.height / 2; | |
| 266 } | |
| 267 | |
| 268 if (this.recenters) { | |
| 269 this.xEnd = xCenter; | |
| 270 this.yEnd = yCenter; | |
| 271 this.slideDistance = Utility.distance( | |
| 272 this.xStart, this.yStart, this.xEnd, this.yEnd | |
| 273 ); | |
| 274 } | |
| 275 | |
| 276 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom( | |
| 277 this.xStart, | |
| 278 this.yStart | |
| 279 ); | |
| 280 | |
| 281 this.waveContainer.style.top = | |
| 282 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px'
; | |
| 283 this.waveContainer.style.left = | |
| 284 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px'; | |
| 285 | |
| 286 this.waveContainer.style.width = this.containerMetrics.size + 'px'; | |
| 287 this.waveContainer.style.height = this.containerMetrics.size + 'px'; | |
| 288 }, | |
| 289 | |
| 290 upAction: function(event) { | |
| 291 if (!this.isMouseDown) { | |
| 292 return; | |
| 293 } | |
| 294 | |
| 295 this.mouseUpStart = Utility.now(); | |
| 296 }, | |
| 297 | |
| 298 remove: function() { | |
| 299 Polymer.dom(this.waveContainer.parentNode).removeChild( | |
| 300 this.waveContainer | |
| 301 ); | |
| 302 } | |
| 303 }; | |
| 304 | |
| 305 Polymer({ | |
| 306 is: 'paper-ripple', | |
| 307 | |
| 308 behaviors: [ | |
| 309 Polymer.IronA11yKeysBehavior | |
| 310 ], | |
| 311 | |
| 312 properties: { | |
| 313 /** | |
| 314 * The initial opacity set on the wave. | |
| 315 * | |
| 316 * @attribute initialOpacity | |
| 317 * @type number | |
| 318 * @default 0.25 | |
| 319 */ | |
| 320 initialOpacity: { | |
| 321 type: Number, | |
| 322 value: 0.25 | |
| 323 }, | |
| 324 | |
| 325 /** | |
| 326 * How fast (opacity per second) the wave fades out. | |
| 327 * | |
| 328 * @attribute opacityDecayVelocity | |
| 329 * @type number | |
| 330 * @default 0.8 | |
| 331 */ | |
| 332 opacityDecayVelocity: { | |
| 333 type: Number, | |
| 334 value: 0.8 | |
| 335 }, | |
| 336 | |
| 337 /** | |
| 338 * If true, ripples will exhibit a gravitational pull towards | |
| 339 * the center of their container as they fade away. | |
| 340 * | |
| 341 * @attribute recenters | |
| 342 * @type boolean | |
| 343 * @default false | |
| 344 */ | |
| 345 recenters: { | |
| 346 type: Boolean, | |
| 347 value: false | |
| 348 }, | |
| 349 | |
| 350 /** | |
| 351 * If true, ripples will center inside its container | |
| 352 * | |
| 353 * @attribute recenters | |
| 354 * @type boolean | |
| 355 * @default false | |
| 356 */ | |
| 357 center: { | |
| 358 type: Boolean, | |
| 359 value: false | |
| 360 }, | |
| 361 | |
| 362 /** | |
| 363 * A list of the visual ripples. | |
| 364 * | |
| 365 * @attribute ripples | |
| 366 * @type Array | |
| 367 * @default [] | |
| 368 */ | |
| 369 ripples: { | |
| 370 type: Array, | |
| 371 value: function() { | |
| 372 return []; | |
| 373 } | |
| 374 }, | |
| 375 | |
| 376 /** | |
| 377 * True when there are visible ripples animating within the | |
| 378 * element. | |
| 379 */ | |
| 380 animating: { | |
| 381 type: Boolean, | |
| 382 readOnly: true, | |
| 383 reflectToAttribute: true, | |
| 384 value: false | |
| 385 }, | |
| 386 | |
| 387 /** | |
| 388 * If true, the ripple will remain in the "down" state until `holdDown` | |
| 389 * is set to false again. | |
| 390 */ | |
| 391 holdDown: { | |
| 392 type: Boolean, | |
| 393 value: false, | |
| 394 observer: '_holdDownChanged' | |
| 395 }, | |
| 396 | |
| 397 _animating: { | |
| 398 type: Boolean | |
| 399 }, | |
| 400 | |
| 401 _boundAnimate: { | |
| 402 type: Function, | |
| 403 value: function() { | |
| 404 return this.animate.bind(this); | |
| 405 } | |
| 406 } | |
| 407 }, | |
| 408 | |
| 409 get target () { | |
| 410 var ownerRoot = Polymer.dom(this).getOwnerRoot(); | |
| 411 var target; | |
| 412 | |
| 413 if (ownerRoot) { | |
| 414 target = ownerRoot.host; | |
| 415 } | |
| 416 | |
| 417 if (!target) { | |
| 418 target = this.parentNode; | |
| 419 } | |
| 420 | |
| 421 return target; | |
| 422 }, | |
| 423 | |
| 424 keyBindings: { | |
| 425 'enter:keydown': '_onEnterKeydown', | |
| 426 'space:keydown': '_onSpaceKeydown', | |
| 427 'space:keyup': '_onSpaceKeyup' | |
| 428 }, | |
| 429 | |
| 430 attached: function() { | |
| 431 this._listen(this.target, 'up', this.upAction.bind(this)); | |
| 432 this._listen(this.target, 'down', this.downAction.bind(this)); | |
| 433 | |
| 434 if (!this.target.hasAttribute('noink')) { | |
| 435 this.keyEventTarget = this.target; | |
| 436 } | |
| 437 }, | |
| 438 | |
| 439 get shouldKeepAnimating () { | |
| 440 for (var index = 0; index < this.ripples.length; ++index) { | |
| 441 if (!this.ripples[index].isAnimationComplete) { | |
| 442 return true; | |
| 443 } | |
| 444 } | |
| 445 | |
| 446 return false; | |
| 447 }, | |
| 448 | |
| 449 simulatedRipple: function() { | |
| 450 this.downAction(null); | |
| 451 | |
| 452 // Please see polymer/polymer#1305 | |
| 453 this.async(function() { | |
| 454 this.upAction(); | |
| 455 }, 1); | |
| 456 }, | |
| 457 | |
| 458 downAction: function(event) { | |
| 459 if (this.holdDown && this.ripples.length > 0) { | |
| 460 return; | |
| 461 } | |
| 462 | |
| 463 var ripple = this.addRipple(); | |
| 464 | |
| 465 ripple.downAction(event); | |
| 466 | |
| 467 if (!this._animating) { | |
| 468 this.animate(); | |
| 469 } | |
| 470 }, | |
| 471 | |
| 472 upAction: function(event) { | |
| 473 if (this.holdDown) { | |
| 474 return; | |
| 475 } | |
| 476 | |
| 477 this.ripples.forEach(function(ripple) { | |
| 478 ripple.upAction(event); | |
| 479 }); | |
| 480 | |
| 481 this.animate(); | |
| 482 }, | |
| 483 | |
| 484 onAnimationComplete: function() { | |
| 485 this._animating = false; | |
| 486 this.$.background.style.backgroundColor = null; | |
| 487 this.fire('transitionend'); | |
| 488 }, | |
| 489 | |
| 490 addRipple: function() { | |
| 491 var ripple = new Ripple(this); | |
| 492 | |
| 493 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); | |
| 494 this.$.background.style.backgroundColor = ripple.color; | |
| 495 this.ripples.push(ripple); | |
| 496 | |
| 497 this._setAnimating(true); | |
| 498 | |
| 499 return ripple; | |
| 500 }, | |
| 501 | |
| 502 removeRipple: function(ripple) { | |
| 503 var rippleIndex = this.ripples.indexOf(ripple); | |
| 504 | |
| 505 if (rippleIndex < 0) { | |
| 506 return; | |
| 507 } | |
| 508 | |
| 509 this.ripples.splice(rippleIndex, 1); | |
| 510 | |
| 511 ripple.remove(); | |
| 512 | |
| 513 if (!this.ripples.length) { | |
| 514 this._setAnimating(false); | |
| 515 } | |
| 516 }, | |
| 517 | |
| 518 animate: function() { | |
| 519 var index; | |
| 520 var ripple; | |
| 521 | |
| 522 this._animating = true; | |
| 523 | |
| 524 for (index = 0; index < this.ripples.length; ++index) { | |
| 525 ripple = this.ripples[index]; | |
| 526 | |
| 527 ripple.draw(); | |
| 528 | |
| 529 this.$.background.style.opacity = ripple.outerOpacity; | |
| 530 | |
| 531 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { | |
| 532 this.removeRipple(ripple); | |
| 533 } | |
| 534 } | |
| 535 | |
| 536 if (!this.shouldKeepAnimating && this.ripples.length === 0) { | |
| 537 this.onAnimationComplete(); | |
| 538 } else { | |
| 539 window.requestAnimationFrame(this._boundAnimate); | |
| 540 } | |
| 541 }, | |
| 542 | |
| 543 _onEnterKeydown: function() { | |
| 544 this.downAction(); | |
| 545 this.async(this.upAction, 1); | |
| 546 }, | |
| 547 | |
| 548 _onSpaceKeydown: function() { | |
| 549 this.downAction(); | |
| 550 }, | |
| 551 | |
| 552 _onSpaceKeyup: function() { | |
| 553 this.upAction(); | |
| 554 }, | |
| 555 | |
| 556 _holdDownChanged: function(holdDown) { | |
| 557 if (holdDown) { | |
| 558 this.downAction(); | |
| 559 } else { | |
| 560 this.upAction(); | |
| 561 } | |
| 562 } | |
| 563 }); | |
| 564 })(); | |
| OLD | NEW |