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 |