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 downAction: function(event) { | |
396 var xCenter = this.containerMetrics.width / 2; | |
397 var yCenter = this.containerMetrics.height / 2; | |
398 | |
399 this.resetInteractionState(); | |
400 this.mouseDownStart = Utility.now(); | |
401 | |
402 if (this.center) { | |
403 this.xStart = xCenter; | |
404 this.yStart = yCenter; | |
405 this.slideDistance = Utility.distance( | |
406 this.xStart, this.yStart, this.xEnd, this.yEnd | |
407 ); | |
408 } else { | |
409 this.xStart = event ? | |
410 event.detail.x - this.containerMetrics.boundingRect.left : | |
411 this.containerMetrics.width / 2; | |
412 this.yStart = event ? | |
413 event.detail.y - this.containerMetrics.boundingRect.top : | |
414 this.containerMetrics.height / 2; | |
415 } | |
416 | |
417 if (this.recenters) { | |
418 this.xEnd = xCenter; | |
419 this.yEnd = yCenter; | |
420 this.slideDistance = Utility.distance( | |
421 this.xStart, this.yStart, this.xEnd, this.yEnd | |
422 ); | |
423 } | |
424 | |
425 this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom( | |
426 this.xStart, | |
427 this.yStart | |
428 ); | |
429 | |
430 this.waveContainer.style.top = | |
431 (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px'
; | |
432 this.waveContainer.style.left = | |
433 (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px'; | |
434 | |
435 this.waveContainer.style.width = this.containerMetrics.size + 'px'; | |
436 this.waveContainer.style.height = this.containerMetrics.size + 'px'; | |
437 }, | |
438 | |
439 upAction: function(event) { | |
440 if (!this.isMouseDown) { | |
441 return; | |
442 } | |
443 | |
444 this.mouseUpStart = Utility.now(); | |
445 }, | |
446 | |
447 remove: function() { | |
448 Polymer.dom(this.waveContainer.parentNode).removeChild( | |
449 this.waveContainer | |
450 ); | |
451 } | |
452 }; | |
453 | |
454 Polymer({ | |
455 is: 'paper-ripple', | |
456 | |
457 behaviors: [ | |
458 Polymer.IronA11yKeysBehavior | |
459 ], | |
460 | |
461 properties: { | |
462 /** | |
463 * The initial opacity set on the wave. | |
464 * | |
465 * @attribute initialOpacity | |
466 * @type number | |
467 * @default 0.25 | |
468 */ | |
469 initialOpacity: { | |
470 type: Number, | |
471 value: 0.25 | |
472 }, | |
473 | |
474 /** | |
475 * How fast (opacity per second) the wave fades out. | |
476 * | |
477 * @attribute opacityDecayVelocity | |
478 * @type number | |
479 * @default 0.8 | |
480 */ | |
481 opacityDecayVelocity: { | |
482 type: Number, | |
483 value: 0.8 | |
484 }, | |
485 | |
486 /** | |
487 * If true, ripples will exhibit a gravitational pull towards | |
488 * the center of their container as they fade away. | |
489 * | |
490 * @attribute recenters | |
491 * @type boolean | |
492 * @default false | |
493 */ | |
494 recenters: { | |
495 type: Boolean, | |
496 value: false | |
497 }, | |
498 | |
499 /** | |
500 * If true, ripples will center inside its container | |
501 * | |
502 * @attribute recenters | |
503 * @type boolean | |
504 * @default false | |
505 */ | |
506 center: { | |
507 type: Boolean, | |
508 value: false | |
509 }, | |
510 | |
511 /** | |
512 * A list of the visual ripples. | |
513 * | |
514 * @attribute ripples | |
515 * @type Array | |
516 * @default [] | |
517 */ | |
518 ripples: { | |
519 type: Array, | |
520 value: function() { | |
521 return []; | |
522 } | |
523 }, | |
524 | |
525 /** | |
526 * True when there are visible ripples animating within the | |
527 * element. | |
528 */ | |
529 animating: { | |
530 type: Boolean, | |
531 readOnly: true, | |
532 reflectToAttribute: true, | |
533 value: false | |
534 }, | |
535 | |
536 /** | |
537 * If true, the ripple will remain in the "down" state until `holdDown` | |
538 * is set to false again. | |
539 */ | |
540 holdDown: { | |
541 type: Boolean, | |
542 value: false, | |
543 observer: '_holdDownChanged' | |
544 }, | |
545 | |
546 _animating: { | |
547 type: Boolean | |
548 }, | |
549 | |
550 _boundAnimate: { | |
551 type: Function, | |
552 value: function() { | |
553 return this.animate.bind(this); | |
554 } | |
555 } | |
556 }, | |
557 | |
558 get target () { | |
559 var ownerRoot = Polymer.dom(this).getOwnerRoot(); | |
560 var target; | |
561 | |
562 if (ownerRoot) { | |
563 target = ownerRoot.host; | |
564 } | |
565 | |
566 if (!target) { | |
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', this.upAction.bind(this)); | |
581 this._listen(this.target, 'down', this.downAction.bind(this)); | |
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 downAction: function(event) { | |
608 if (this.holdDown && this.ripples.length > 0) { | |
609 return; | |
610 } | |
611 | |
612 var ripple = this.addRipple(); | |
613 | |
614 ripple.downAction(event); | |
615 | |
616 if (!this._animating) { | |
617 this.animate(); | |
618 } | |
619 }, | |
620 | |
621 upAction: function(event) { | |
622 if (this.holdDown) { | |
623 return; | |
624 } | |
625 | |
626 this.ripples.forEach(function(ripple) { | |
627 ripple.upAction(event); | |
628 }); | |
629 | |
630 this.animate(); | |
631 }, | |
632 | |
633 onAnimationComplete: function() { | |
634 this._animating = false; | |
635 this.$.background.style.backgroundColor = null; | |
636 this.fire('transitionend'); | |
637 }, | |
638 | |
639 addRipple: function() { | |
640 var ripple = new Ripple(this); | |
641 | |
642 Polymer.dom(this.$.waves).appendChild(ripple.waveContainer); | |
643 this.$.background.style.backgroundColor = ripple.color; | |
644 this.ripples.push(ripple); | |
645 | |
646 this._setAnimating(true); | |
647 | |
648 return ripple; | |
649 }, | |
650 | |
651 removeRipple: function(ripple) { | |
652 var rippleIndex = this.ripples.indexOf(ripple); | |
653 | |
654 if (rippleIndex < 0) { | |
655 return; | |
656 } | |
657 | |
658 this.ripples.splice(rippleIndex, 1); | |
659 | |
660 ripple.remove(); | |
661 | |
662 if (!this.ripples.length) { | |
663 this._setAnimating(false); | |
664 } | |
665 }, | |
666 | |
667 animate: function() { | |
668 var index; | |
669 var ripple; | |
670 | |
671 this._animating = true; | |
672 | |
673 for (index = 0; index < this.ripples.length; ++index) { | |
674 ripple = this.ripples[index]; | |
675 | |
676 ripple.draw(); | |
677 | |
678 this.$.background.style.opacity = ripple.outerOpacity; | |
679 | |
680 if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) { | |
681 this.removeRipple(ripple); | |
682 } | |
683 } | |
684 | |
685 if (!this.shouldKeepAnimating && this.ripples.length === 0) { | |
686 this.onAnimationComplete(); | |
687 } else { | |
688 window.requestAnimationFrame(this._boundAnimate); | |
689 } | |
690 }, | |
691 | |
692 _onEnterKeydown: function() { | |
693 this.downAction(); | |
694 this.async(this.upAction, 1); | |
695 }, | |
696 | |
697 _onSpaceKeydown: function() { | |
698 this.downAction(); | |
699 }, | |
700 | |
701 _onSpaceKeyup: function() { | |
702 this.upAction(); | |
703 }, | |
704 | |
705 _holdDownChanged: function(holdDown) { | |
706 if (holdDown) { | |
707 this.downAction(); | |
708 } else { | |
709 this.upAction(); | |
710 } | |
711 } | |
712 }); | |
713 })(); | |
714 </script> | |
OLD | NEW |