OLD | NEW |
| (Empty) |
1 <!-- | |
2 Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
3 This code may only be used under the BSD style license found at http://polymer.g
ithub.io/LICENSE.txt | |
4 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
5 The complete set of contributors may be found at http://polymer.github.io/CONTRI
BUTORS.txt | |
6 Code distributed by Google as part of the polymer project is also | |
7 subject to an additional IP rights grant found at http://polymer.github.io/PATEN
TS.txt | |
8 --> | |
9 | |
10 <link rel="import" href="../polymer/polymer.html"> | |
11 <link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html
"> | |
12 | |
13 <!-- | |
14 `paper-ripple` provides a visual effect that other paper elements can | |
15 use to simulate a rippling effect emanating from the point of contact. The | |
16 effect can be visualized as a concentric circle with motion. | |
17 | |
18 Example: | |
19 | |
20 <paper-ripple></paper-ripple> | |
21 | |
22 `paper-ripple` listens to "mousedown" and "mouseup" events so it would display r
ipple | |
23 effect when touches on it. You can also defeat the default behavior and | |
24 manually route the down and up actions to the ripple element. Note that it is | |
25 important if you call downAction() you will have to make sure to call | |
26 upAction() so that `paper-ripple` would end the animation loop. | |
27 | |
28 Example: | |
29 | |
30 <paper-ripple id="ripple" style="pointer-events: none;"></paper-ripple> | |
31 ... | |
32 downAction: function(e) { | |
33 this.$.ripple.downAction({x: e.x, y: e.y}); | |
34 }, | |
35 upAction: function(e) { | |
36 this.$.ripple.upAction(); | |
37 } | |
38 | |
39 Styling ripple effect: | |
40 | |
41 Use CSS color property to style the ripple: | |
42 | |
43 paper-ripple { | |
44 color: #4285f4; | |
45 } | |
46 | |
47 Note that CSS color property is inherited so it is not required to set it on | |
48 the `paper-ripple` element directly. | |
49 | |
50 By default, the ripple is centered on the point of contact. Apply the `recenter
s` | |
51 attribute to have the ripple grow toward the center of its container. | |
52 | |
53 <paper-ripple recenters></paper-ripple> | |
54 | |
55 You can also center the ripple inside its container from the start. | |
56 | |
57 <paper-ripple center></paper-ripple> | |
58 | |
59 Apply `circle` class to make the rippling effect within a circle. | |
60 | |
61 <paper-ripple class="circle"></paper-ripple> | |
62 | |
63 @group Paper Elements | |
64 @element paper-ripple | |
65 @hero hero.svg | |
66 @demo demo/index.html | |
67 --> | |
68 | |
69 <dom-module id="paper-ripple"> | |
70 | |
71 <!-- | |
72 Fired when the animation finishes. This is useful if you want to wait until th
e ripple | |
73 animation finishes to perform some action. | |
74 | |
75 @event transitionend | |
76 @param {Object} detail | |
77 @param {Object} detail.node The animated node | |
78 --> | |
79 | |
80 <style> | |
81 :host { | |
82 display: block; | |
83 position: absolute; | |
84 border-radius: inherit; | |
85 overflow: hidden; | |
86 top: 0; | |
87 left: 0; | |
88 right: 0; | |
89 bottom: 0; | |
90 } | |
91 | |
92 :host([animating]) { | |
93 /* This resolves a rendering issue in Chrome (as of 40) where the | |
94 ripple is not properly clipped by its parent (which may have | |
95 rounded corners). See: http://jsbin.com/temexa/4 | |
96 | |
97 Note: We only apply this style conditionally. Otherwise, the browser | |
98 will create a new compositing layer for every ripple element on the | |
99 page, and that would be bad. */ | |
100 -webkit-transform: translate(0, 0); | |
101 transform: translate3d(0, 0, 0); | |
102 } | |
103 | |
104 :host([noink]) { | |
105 pointer-events: none; | |
106 } | |
107 | |
108 #background, | |
109 #waves, | |
110 .wave-container, | |
111 .wave { | |
112 pointer-events: none; | |
113 position: absolute; | |
114 top: 0; | |
115 left: 0; | |
116 width: 100%; | |
117 height: 100%; | |
118 } | |
119 | |
120 #background, | |
121 .wave { | |
122 opacity: 0; | |
123 } | |
124 | |
125 #waves, | |
126 .wave { | |
127 overflow: hidden; | |
128 } | |
129 | |
130 .wave-container, | |
131 .wave { | |
132 border-radius: 50%; | |
133 } | |
134 | |
135 :host(.circle) #background, | |
136 :host(.circle) #waves { | |
137 border-radius: 50%; | |
138 } | |
139 | |
140 :host(.circle) .wave-container { | |
141 overflow: hidden; | |
142 } | |
143 | |
144 </style> | |
145 <template> | |
146 <div id="background"></div> | |
147 <div id="waves"></div> | |
148 </template> | |
149 </dom-module> | |
150 <script> | |
151 (function() { | |
152 var Utility = { | |
153 cssColorWithAlpha: function(cssColor, alpha) { | |
154 var parts = cssColor.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); | |
155 | |
156 if (typeof alpha == 'undefined') { | |
157 alpha = 1; | |
158 } | |
159 | |
160 if (!parts) { | |
161 return 'rgba(255, 255, 255, ' + alpha + ')'; | |
162 } | |
163 | |
164 return 'rgba(' + parts[1] + ', ' + parts[2] + ', ' + parts[3] + ', ' + a
lpha + ')'; | |
165 }, | |
166 | |
167 distance: function(x1, y1, x2, y2) { | |
168 var xDelta = (x1 - x2); | |
169 var yDelta = (y1 - y2); | |
170 | |
171 return Math.sqrt(xDelta * xDelta + yDelta * yDelta); | |
172 }, | |
173 | |
174 now: (function() { | |
175 if (window.performance && window.performance.now) { | |
176 return window.performance.now.bind(window.performance); | |
177 } | |
178 | |
179 return Date.now; | |
180 })() | |
181 }; | |
182 | |
183 /** | |
184 * @param {HTMLElement} element | |
185 * @constructor | |
186 */ | |
187 function ElementMetrics(element) { | |
188 this.element = element; | |
189 this.width = this.boundingRect.width; | |
190 this.height = this.boundingRect.height; | |
191 | |
192 this.size = Math.max(this.width, this.height); | |
193 } | |
194 | |
195 ElementMetrics.prototype = { | |
196 get boundingRect () { | |
197 return this.element.getBoundingClientRect(); | |
198 }, | |
199 | |
200 furthestCornerDistanceFrom: function(x, y) { | |
201 var topLeft = Utility.distance(x, y, 0, 0); | |
202 var topRight = Utility.distance(x, y, this.width, 0); | |
203 var bottomLeft = Utility.distance(x, y, 0, this.height); | |
204 var bottomRight = Utility.distance(x, y, this.width, this.height); | |
205 | |
206 return Math.max(topLeft, topRight, bottomLeft, bottomRight); | |
207 } | |
208 }; | |
209 | |
210 /** | |
211 * @param {HTMLElement} element | |
212 * @constructor | |
213 */ | |
214 function Ripple(element) { | |
215 this.element = element; | |
216 this.color = window.getComputedStyle(element).color; | |
217 | |
218 this.wave = document.createElement('div'); | |
219 this.waveContainer = document.createElement('div'); | |
220 this.wave.style.backgroundColor = this.color; | |
221 this.wave.classList.add('wave'); | |
222 this.waveContainer.classList.add('wave-container'); | |
223 Polymer.dom(this.waveContainer).appendChild(this.wave); | |
224 | |
225 this.resetInteractionState(); | |
226 } | |
227 | |
228 Ripple.MAX_RADIUS = 300; | |
229 | |
230 Ripple.prototype = { | |
231 get recenters() { | |
232 return this.element.recenters; | |
233 }, | |
234 | |
235 get center() { | |
236 return this.element.center; | |
237 }, | |
238 | |
239 get mouseDownElapsed() { | |
240 var elapsed; | |
241 | |
242 if (!this.mouseDownStart) { | |
243 return 0; | |
244 } | |
245 | |
246 elapsed = Utility.now() - this.mouseDownStart; | |
247 | |
248 if (this.mouseUpStart) { | |
249 elapsed -= this.mouseUpElapsed; | |
250 } | |
251 | |
252 return elapsed; | |
253 }, | |
254 | |
255 get mouseUpElapsed() { | |
256 return this.mouseUpStart ? | |
257 Utility.now () - this.mouseUpStart : 0; | |
258 }, | |
259 | |
260 get mouseDownElapsedSeconds() { | |
261 return this.mouseDownElapsed / 1000; | |
262 }, | |
263 | |
264 get mouseUpElapsedSeconds() { | |
265 return this.mouseUpElapsed / 1000; | |
266 }, | |
267 | |
268 get mouseInteractionSeconds() { | |
269 return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds; | |
270 }, | |
271 | |
272 get initialOpacity() { | |
273 return this.element.initialOpacity; | |
274 }, | |
275 | |
276 get opacityDecayVelocity() { | |
277 return this.element.opacityDecayVelocity; | |
278 }, | |
279 | |
280 get radius() { | |
281 var width2 = this.containerMetrics.width * this.containerMetrics.width; | |
282 var height2 = this.containerMetrics.height * this.containerMetrics.heigh
t; | |
283 var waveRadius = Math.min( | |
284 Math.sqrt(width2 + height2), | |
285 Ripple.MAX_RADIUS | |
286 ) * 1.1 + 5; | |
287 | |
288 var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS); | |
289 var timeNow = this.mouseInteractionSeconds / duration; | |
290 var size = waveRadius * (1 - Math.pow(80, -timeNow)); | |
291 | |
292 return Math.abs(size); | |
293 }, | |
294 | |
295 get opacity() { | |
296 if (!this.mouseUpStart) { | |
297 return this.initialOpacity; | |
298 } | |
299 | |
300 return Math.max( | |
301 0, | |
302 this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVe
locity | |
303 ); | |
304 }, | |
305 | |
306 get outerOpacity() { | |
307 // Linear increase in background opacity, capped at the opacity | |
308 // of the wavefront (waveOpacity). | |
309 var outerOpacity = this.mouseUpElapsedSeconds * 0.3; | |
310 var waveOpacity = this.opacity; | |
311 | |
312 return Math.max( | |
313 0, | |
314 Math.min(outerOpacity, waveOpacity) | |
315 ); | |
316 }, | |
317 | |
318 get isOpacityFullyDecayed() { | |
319 return this.opacity < 0.01 && | |
320 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
321 }, | |
322 | |
323 get isRestingAtMaxRadius() { | |
324 return this.opacity >= this.initialOpacity && | |
325 this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS); | |
326 }, | |
327 | |
328 get isAnimationComplete() { | |
329 return this.mouseUpStart ? | |
330 this.isOpacityFullyDecayed : this.isRestingAtMaxRadius; | |
331 }, | |
332 | |
333 get translationFraction() { | |
334 return Math.min( | |
335 1, | |
336 this.radius / this.containerMetrics.size * 2 / Math.sqrt(2) | |
337 ); | |
338 }, | |
339 | |
340 get xNow() { | |
341 if (this.xEnd) { | |
342 return this.xStart + this.translationFraction * (this.xEnd - this.xSta
rt); | |
343 } | |
344 | |
345 return this.xStart; | |
346 }, | |
347 | |
348 get yNow() { | |
349 if (this.yEnd) { | |
350 return this.yStart + this.translationFraction * (this.yEnd - this.ySta
rt); | |
351 } | |
352 | |
353 return this.yStart; | |
354 }, | |
355 | |
356 get isMouseDown() { | |
357 return this.mouseDownStart && !this.mouseUpStart; | |
358 }, | |
359 | |
360 resetInteractionState: function() { | |
361 this.maxRadius = 0; | |
362 this.mouseDownStart = 0; | |
363 this.mouseUpStart = 0; | |
364 | |
365 this.xStart = 0; | |
366 this.yStart = 0; | |
367 this.xEnd = 0; | |
368 this.yEnd = 0; | |
369 this.slideDistance = 0; | |
370 | |
371 this.containerMetrics = new ElementMetrics(this.element); | |
372 }, | |
373 | |
374 draw: function() { | |
375 var scale; | |
376 var translateString; | |
377 var dx; | |
378 var dy; | |
379 | |
380 this.wave.style.opacity = this.opacity; | |
381 | |
382 scale = this.radius / (this.containerMetrics.size / 2); | |
383 dx = this.xNow - (this.containerMetrics.width / 2); | |
384 dy = this.yNow - (this.containerMetrics.height / 2); | |
385 | |
386 | |
387 // 2d transform for safari because of border-radius and overflow:hidden
clipping bug. | |
388 // https://bugs.webkit.org/show_bug.cgi?id=98538 | |
389 this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' +
dy + 'px)'; | |
390 this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy +
'px, 0)'; | |
391 this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')'; | |
392 this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)'; | |
393 }, | |
394 | |
395 /** @param {Event=} event */ | |
396 downAction: function(event) { | |
397 var xCenter = this.containerMetrics.width / 2; | |
398 var yCenter = this.containerMetrics.height / 2; | |
399 | |
400 this.resetInteractionState(); | |
401 this.mouseDownStart = Utility.now(); | |
402 | |
403 if (this.center) { | |
404 this.xStart = xCenter; | |
405 this.yStart = yCenter; | |
406 this.slideDistance = Utility.distance( | |
407 this.xStart, this.yStart, this.xEnd, this.yEnd | |
408 ); | |
409 } else { | |
410 this.xStart = event ? | |
411 event.detail.x - this.containerMetrics.boundingRect.left : | |
412 this.containerMetrics.width / 2; | |
413 this.yStart = event ? | |
414 event.detail.y - this.containerMetrics.boundingRect.top : | |
415 this.containerMetrics.height / 2; | |
416 } | |
417 | |
418 if (this.recenters) { | |
419 this.xEnd = xCenter; | |
420 this.yEnd = yCenter; | |
421 this.slideDistance = Utility.distance( | |
422 this.xStart, this.yStart, this.xEnd, this.yEnd | |
423 ); | |
424 } | |
425 | |
426 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom( | |
427 this.xStart, | |
428 this.yStart | |
429 ); | |
430 | |
431 this.waveContainer.style.top = | |
432 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px'
; | |
433 this.waveContainer.style.left = | |
434 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px'; | |
435 | |
436 this.waveContainer.style.width = this.containerMetrics.size + 'px'; | |
437 this.waveContainer.style.height = this.containerMetrics.size + 'px'; | |
438 }, | |
439 | |
440 /** @param {Event=} event */ | |
441 upAction: function(event) { | |
442 if (!this.isMouseDown) { | |
443 return; | |
444 } | |
445 | |
446 this.mouseUpStart = Utility.now(); | |
447 }, | |
448 | |
449 remove: function() { | |
450 Polymer.dom(this.waveContainer.parentNode).removeChild( | |
451 this.waveContainer | |
452 ); | |
453 } | |
454 }; | |
455 | |
456 Polymer({ | |
457 is: 'paper-ripple', | |
458 | |
459 behaviors: [ | |
460 Polymer.IronA11yKeysBehavior | |
461 ], | |
462 | |
463 properties: { | |
464 /** | |
465 * The initial opacity set on the wave. | |
466 * | |
467 * @attribute initialOpacity | |
468 * @type number | |
469 * @default 0.25 | |
470 */ | |
471 initialOpacity: { | |
472 type: Number, | |
473 value: 0.25 | |
474 }, | |
475 | |
476 /** | |
477 * How fast (opacity per second) the wave fades out. | |
478 * | |
479 * @attribute opacityDecayVelocity | |
480 * @type number | |
481 * @default 0.8 | |
482 */ | |
483 opacityDecayVelocity: { | |
484 type: Number, | |
485 value: 0.8 | |
486 }, | |
487 | |
488 /** | |
489 * If true, ripples will exhibit a gravitational pull towards | |
490 * the center of their container as they fade away. | |
491 * | |
492 * @attribute recenters | |
493 * @type boolean | |
494 * @default false | |
495 */ | |
496 recenters: { | |
497 type: Boolean, | |
498 value: false | |
499 }, | |
500 | |
501 /** | |
502 * If true, ripples will center inside its container | |
503 * | |
504 * @attribute recenters | |
505 * @type boolean | |
506 * @default false | |
507 */ | |
508 center: { | |
509 type: Boolean, | |
510 value: false | |
511 }, | |
512 | |
513 /** | |
514 * A list of the visual ripples. | |
515 * | |
516 * @attribute ripples | |
517 * @type Array | |
518 * @default [] | |
519 */ | |
520 ripples: { | |
521 type: Array, | |
522 value: function() { | |
523 return []; | |
524 } | |
525 }, | |
526 | |
527 /** | |
528 * True when there are visible ripples animating within the | |
529 * element. | |
530 */ | |
531 animating: { | |
532 type: Boolean, | |
533 readOnly: true, | |
534 reflectToAttribute: true, | |
535 value: false | |
536 }, | |
537 | |
538 /** | |
539 * If true, the ripple will remain in the "down" state until `holdDown` | |
540 * is set to false again. | |
541 */ | |
542 holdDown: { | |
543 type: Boolean, | |
544 value: false, | |
545 observer: '_holdDownChanged' | |
546 }, | |
547 | |
548 _animating: { | |
549 type: Boolean | |
550 }, | |
551 | |
552 _boundAnimate: { | |
553 type: Function, | |
554 value: function() { | |
555 return this.animate.bind(this); | |
556 } | |
557 } | |
558 }, | |
559 | |
560 get target () { | |
561 var ownerRoot = Polymer.dom(this).getOwnerRoot(); | |
562 var target; | |
563 | |
564 if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE | |
565 target = ownerRoot.host; | |
566 } else { | |
567 target = this.parentNode; | |
568 } | |
569 | |
570 return target; | |
571 }, | |
572 | |
573 keyBindings: { | |
574 'enter:keydown': '_onEnterKeydown', | |
575 'space:keydown': '_onSpaceKeydown', | |
576 'space:keyup': '_onSpaceKeyup' | |
577 }, | |
578 | |
579 attached: function() { | |
580 this.listen(this.target, 'up', 'upAction'); | |
581 this.listen(this.target, 'down', 'downAction'); | |
582 | |
583 if (!this.target.hasAttribute('noink')) { | |
584 this.keyEventTarget = this.target; | |
585 } | |
586 }, | |
587 | |
588 get shouldKeepAnimating () { | |
589 for (var index = 0; index < this.ripples.length; ++index) { | |
590 if (!this.ripples[index].isAnimationComplete) { | |
591 return true; | |
592 } | |
593 } | |
594 | |
595 return false; | |
596 }, | |
597 | |
598 simulatedRipple: function() { | |
599 this.downAction(null); | |
600 | |
601 // Please see polymer/polymer#1305 | |
602 this.async(function() { | |
603 this.upAction(); | |
604 }, 1); | |
605 }, | |
606 | |
607 /** @param {Event=} event */ | |
608 downAction: function(event) { | |
609 if (this.holdDown && this.ripples.length > 0) { | |
610 return; | |
611 } | |
612 | |
613 var ripple = this.addRipple(); | |
614 | |
615 ripple.downAction(event); | |
616 | |
617 if (!this._animating) { | |
618 this.animate(); | |
619 } | |
620 }, | |
621 | |
622 /** @param {Event=} event */ | |
623 upAction: function(event) { | |
624 if (this.holdDown) { | |
625 return; | |
626 } | |
627 | |
628 this.ripples.forEach(function(ripple) { | |
629 ripple.upAction(event); | |
630 }); | |
631 | |
632 this.animate(); | |
633 }, | |
634 | |
635 onAnimationComplete: function() { | |
636 this._animating = false; | |
637 this.$.background.style.backgroundColor = null; | |
638 this.fire('transitionend'); | |
639 }, | |
640 | |
641 addRipple: function() { | |
642 var ripple = new Ripple(this); | |
643 | |
644 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); | |
645 this.$.background.style.backgroundColor = ripple.color; | |
646 this.ripples.push(ripple); | |
647 | |
648 this._setAnimating(true); | |
649 | |
650 return ripple; | |
651 }, | |
652 | |
653 removeRipple: function(ripple) { | |
654 var rippleIndex = this.ripples.indexOf(ripple); | |
655 | |
656 if (rippleIndex < 0) { | |
657 return; | |
658 } | |
659 | |
660 this.ripples.splice(rippleIndex, 1); | |
661 | |
662 ripple.remove(); | |
663 | |
664 if (!this.ripples.length) { | |
665 this._setAnimating(false); | |
666 } | |
667 }, | |
668 | |
669 animate: function() { | |
670 var index; | |
671 var ripple; | |
672 | |
673 this._animating = true; | |
674 | |
675 for (index = 0; index < this.ripples.length; ++index) { | |
676 ripple = this.ripples[index]; | |
677 | |
678 ripple.draw(); | |
679 | |
680 this.$.background.style.opacity = ripple.outerOpacity; | |
681 | |
682 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { | |
683 this.removeRipple(ripple); | |
684 } | |
685 } | |
686 | |
687 if (!this.shouldKeepAnimating && this.ripples.length === 0) { | |
688 this.onAnimationComplete(); | |
689 } else { | |
690 window.requestAnimationFrame(this._boundAnimate); | |
691 } | |
692 }, | |
693 | |
694 _onEnterKeydown: function() { | |
695 this.downAction(); | |
696 this.async(this.upAction, 1); | |
697 }, | |
698 | |
699 _onSpaceKeydown: function() { | |
700 this.downAction(); | |
701 }, | |
702 | |
703 _onSpaceKeyup: function() { | |
704 this.upAction(); | |
705 }, | |
706 | |
707 _holdDownChanged: function(holdDown) { | |
708 if (holdDown) { | |
709 this.downAction(); | |
710 } else { | |
711 this.upAction(); | |
712 } | |
713 } | |
714 }); | |
715 })(); | |
716 </script> | |
OLD | NEW |