import { PaperWave } from './paper-wave'; import { PLATFORM } from 'aurelia-pal'; /** * Provides all the logic to produce ripple visual effect. * Other elements can use it to simulate rippling effect emanating from the point of contact. */ export class PaperRipple { /** * Gets or sets the initial opacity of the each wave. */ public initialOpacity: number; /** * Gets or sets how fast (opacity per second) the wave fades out. */ public opacityDecayVelocity: number; /** * The object wrapper containing the `$background` DOM element belongs to the current instance. */ public $background: HTMLElement; /** * The object wrapper containing the waves container belongs to the current instance. */ public $waves: HTMLElement; /** * The object wrapper containing all the DOM elements belongs to the current instance. */ public $: HTMLElement; private waves: PaperWave[] = []; /** * Initializes a new instance of the `PaperRipple` class with the specified `config` object. */ constructor(cfg: any = {}) { this.initialOpacity = cfg.initialOpacity || 0.25; this.opacityDecayVelocity = cfg.opacityDecayVelocity || 0.8; this.initTarget(cfg && cfg.nodeType ? cfg : cfg.target && cfg.target.nodeType ? cfg.target : null); this.recenters = cfg.recenters || this.recenters; this.center = cfg.center || this.center; this.round = cfg.round || this.round; } /** * Determines whether all the waves should be re-centered towards the center of the container. * @returns If `true`, waves will exhibit a gravitational pull towards the center of the container as they fade away. */ get recenters(): boolean { return this.$.hasAttribute('recenters'); } /** * Sets the value that indicates whether all the waves should be re-centered towards the center of the container. * @returns Nothing. */ set recenters(newValue: boolean) { if (newValue) { this.$.setAttribute('recenters', ''); } else { this.$.removeAttribute('recenters'); } } /** * Determines whether all the waves should start a movement from the center of the container. * @returns If `true`, waves will center inside its container */ get center(): boolean { return this.$.hasAttribute('center'); } /** * Sets the value that indicates whether all the waves should start a movement from the center of the container. * @returns Nothing. */ set center(newValue: boolean) { if (newValue) { this.$.setAttribute('center', ''); } else { this.$.removeAttribute('center'); } } /** * Determines whether ripple effect should apply within a circle. * @returns If `true`, ripple effect will apply within a circle. */ get round(): boolean { return this.$.classList.contains('paper-ripple--round'); } /** * Sets the value that indicates whether ripple effect should apply within a circle. * @returns Nothing. */ set round(newValue: boolean) { this.$.classList.toggle('paper-ripple--round', newValue); } /** * Determines whether the ripple should keep animating or not. * @returns `true`, if so, otherwise `false`. */ get shouldKeepAnimating() { return this.waves.some(wave => !wave.isAnimationComplete); } private initTarget(target: HTMLElement | null = null) { const doc: Document = PLATFORM.global.document; this.$ = target || doc.createElement('div'); this.$.classList.add('paper-ripple'); if (!this.$background) { this.$background = target && target.querySelector('.paper-ripple__background') as HTMLElement || doc.createElement('div'); this.$background.classList.add('paper-ripple__background'); this.$.appendChild(this.$background); } if (!this.$waves) { this.$waves = target && target.querySelector('.paper-ripple__waves') as HTMLElement || doc.createElement('div'); this.$waves.classList.add('paper-ripple__waves'); this.$.appendChild(this.$waves); } return this; } /** * Adds new wave to the list of visual ripples. * @returns Current instance for method chaining. */ public addWave() { const wave = new PaperWave(this); this.$waves.appendChild(wave.$); this.$background.style.backgroundColor = wave.color!; this.waves.push(wave); return wave; } /** * Produces a ripple-down effect. * * @param ev Object containing coordinates of the point of contact. * @returns Current instance for method chaining. */ public downAction(ev: MouseEvent) { const wave = this.addWave(); wave.downAction(ev); this.animate(); return this; } /** * Produces a ripple-up effect. * @returns {PaperRipple} Current instance for method chaining. */ public upAction() { this.waves.forEach(wave => { wave.upAction(); }); this.animate(); return this; } /** * Removes given wave from the list of visual ripples. * @param wave - The wave to remove. * @returns Current instance for method chaining. */ public removeWave(wave: PaperWave) { const waveIndex = this.waves.indexOf(wave); if (waveIndex < 0) { return this; } this.waves.splice(waveIndex, 1); wave.remove(); return this; } /** * Animates all the waves in the list of visual ripples. * @returns Current instance for method chaining. */ public animate() { // tslint:disable:prefer-const for (let i = 0, l = this.waves.length; i < l; i++) { const wave = this.waves[i]; if (wave) { wave.draw(); this.$background.style.opacity = wave.outerOpacity.toString(); if (wave.isWaveFullyOpaque && !wave.isMaxRadiusReached) { this.removeWave(wave); } } } // tslint:enable:prefer-const if (!this.shouldKeepAnimating && this.waves.length === 0) { this.$background.style.backgroundColor = null!; } else { PLATFORM.requestAnimationFrame(this.animate.bind(this)); } return this; } }