virtyaluk/paper-ripple

View on GitHub
src/js/PaperWave.js

Summary

Maintainability
A
3 hrs
Test Coverage
/**
 * This file\code is part of Paper UI project.
 *
 * Paper UI - is a modern front-end framework based on Material Design by Google
 * https://github.com/virtyaluk/paper-ui
 *
 * Copyright (c) 2015 Bohdan Shtepan
 * http://modern-dev.com/
 *
 * Licensed under the MIT license.
 */

/*eslint no-unused-vars: 0*/

let _window = window || this,
    _doc = _window.document,
    _now = (function() {
        return _window.performance && _window.performance.now ? _window.performance.now.bind(_window.performance) : Date.now;
    }());

/**
 * Provides all the logic to produce a one-time rippling effect.
 *
 * @class PaperWave
 */
class PaperWave {
    /**
     * Initializes a new instance of the `PaperWave` class with the specified `PaperRipple` instance.
     *
     * @constructs PaperWave
     * @returns {PaperWave} The new instance of a class.
     *
     * @example
     * var paperWave = new PaperWave(paperRipple);
     *
     * paperWave.downAction(null);
     * paperWave.upAction();
     *
     * if (paperWave.isAnimationCompleted) {
     *     paperWave.remove();
     * }
     */
    constructor({ $, recenters = false, center = false, initialOpacity = 0.25, opacityDecayVelocity = 0.8 }) {
        /**
         * Gets or sets the color of the wave.
         *
         * @type {String}
         */
        this.color = _window.getComputedStyle($).color;

        /**
         * Gets or sets the container metrics of the wave.
         *
         * @type {ElementRect}
         */
        this.containerRect = new ElementRect($);

        /**
         * Determines whether the wave should be re-centered towards the center of its container.
         *
         * @type {Boolean}
         */
        this.recenters = recenters;

        /**
         * Determines whether the wave should start a movement from the center of its container.
         *
         * @type {Boolean}
         */
        this.center = center;

        /**
         * Gets initial opacity of the wave.
         *
         * @type {Number}
         */
        this.initialOpacity = initialOpacity;

        /**
         * Gets opacity decay velocity of the wave.
         *
         * @type {Number}
         */
        this.opacityDecayVelocity = opacityDecayVelocity;

        /**
         * Represents the object wrapped around the `wave` DOM element that belongs to the current instance.
         *
         * @type {Object}
         */
        this.$wave = _doc.createElement('div');

        this.$wave.classList.add('paper-ripple__wave');
        this.$wave.style.backgroundColor = this.color;

        /**
         * Represents the object wrapped around the main DOM element that belongs to the current instance.
         *
         * @type {Object}
         */
        this.$ = _doc.createElement('div');

        this.$.classList.add('paper-ripple__wave-container');
        this.$.appendChild(this.$wave);

        this.resetDefaults();

        return this;
    }

    /**
     * Represents the max possible value of the wave's radius.
     *
     * @const {Number}
     * @default
     */
    static MAX_RADIUS = 300;


    /**
     * Gets the time in milliseconds elapsed from the moment where interaction with the wave was started.
     *
     * @returns {Number} The time in milliseconds.
     */
    get touchDownElapsed() {
        var elapsed;

        if (!this.touchDownStarted) {
            return 0;
        }

        elapsed = _now() - this.touchDownStarted;

        if (this.touchUpStarted) {
            elapsed -= this.touchUpElapsed;
        }

        return elapsed;
    }

    /**
     * Gets the time in milliseconds elapsed from the moment where interaction with the wave was ended.
     *
     * @returns {Number} The time in milliseconds.
     */
    get touchUpElapsed() {
        return this.touchUpStarted ? _now() - this.touchUpStarted : 0;
    }

    /**
     * Gets the time in seconds elapsed since the moment where interaction with the wave was started.
     *
     * @returns {Number} The time in seconds.
     */
    get touchDownElapsedSeconds() {
        return this.touchDownElapsed / 1000;
    }

    /**
     * Gets the time in seconds elapsed since the moment where interaction with the wave was ended.
     *
     * @returns {number} The time in seconds.
     */
    get touchUpElapsedSeconds() {
        return this.touchUpElapsed / 1000;
    }

    /**
     * Gets the total interaction time.
     *
     * @returns {Number} The time in seconds
     */
    get mouseInteractionSeconds() {
        return this.touchDownElapsedSeconds + this.touchUpElapsedSeconds;
    }

    /**
     * Gets the wave's radius at the current time.
     *
     * @returns {Number} The value of the wave's radius.
     */
    get radius() {
        let radius = Math.min(
                Math.sqrt(Math.pow(this.containerRect.width, 2) + Math.pow(this.containerRect.height, 2)),
                PaperWave.MAX_RADIUS
                ) * 1.1 + 5,
            elapsed = 1.1 - 0.2 * (radius / PaperWave.MAX_RADIUS),
            currentTime = this.mouseInteractionSeconds / elapsed,
            actualRadius = radius * (1 - Math.pow(80, -currentTime));

        return Math.abs(actualRadius);
    }

    /**
     * Gets the wave's opacity at the current time.
     *
     * @returns {Number} The value of the wave's opacity.
     */
    get opacity() {
        if (!this.touchUpStarted) {
            return this.initialOpacity;
        }

        return Math.max(0, this.initialOpacity - this.touchUpElapsedSeconds * this.opacityDecayVelocity);
    }

    /**
     * Gets the wave's outer opacity at the current time.
     *
     * @returns {Number} The value of the wave's outer opacity.
     */
    get outerOpacity() {
        return Math.max(0, Math.min(this.touchUpElapsedSeconds * 0.3, this.opacity));
    }

    /**
     * Determines whether the wave is fully opaque or not.
     *
     * @returns {Boolean} `true`, if so, otherwise `false`.
     */
    get isWaveFullyOpaque() {
        return this.opacity < 0.01 && this.radius >= Math.min(this.maxRadius, PaperWave.MAX_RADIUS);
    }

    /**
     * Determines whether the wave reached its max radius or not.
     *
     * @returns {Boolean} `true`, if so, otherwise `false`.
     */
    get isMaxRadiusReached() {
        return this.opacity >= this.initialOpacity && this.radius >= Math.min(this.maxRadius, PaperWave.MAX_RADIUS);
    }

    /**
     * Determines whether the animation of rippling effect completed or not.
     *
     * @returns {Boolean} `true`, if so, otherwise `false`.
     */
    get isAnimationComplete() {
        return this.touchUpStarted ? this.isWaveFullyOpaque : this.isMaxRadiusReached;
    }

    /**
     * Gets the wave's translation fraction value.
     *
     * @returns {Number} The value of the wave's translation fraction.
     */
    get translationFraction() {
        return Math.min(1, this.radius / this.containerRect.size * 2 / Math.sqrt(2));
    }

    /**
     * Gets the wave's current position.
     *
     * @returns {{x: Number, y: Number}} Object containing coordinates of the wave's current position.
     */
    get currentPosition() {
        let translateFraction = this.translationFraction,
            x = this.startPosition.x,
            y = this.startPosition.y;

        if (this.endPosition.x) {
            x = this.startPosition.x + translateFraction * (this.endPosition.x - this.startPosition.x);
        }

        if (this.endPosition.y) {
            y = this.startPosition.y + translateFraction * (this.endPosition.y - this.startPosition.y);
        }

        return { x, y };
    }

    /**
     * Determines whether the pointing device is still in interaction with the current wave.
     *
     * @returns {Boolean} `true`, if so, otherwise `false`.
     */
    get isTouchDown() {
        return this.touchDownStarted && !this.touchUpStarted;
    }

    /**
     * Resets all the wave's values.
     *
     * @returns {PaperWave} Current instance for method chaining.
     */
    resetDefaults() {
        /**
         * Gets or sets max radius of the wave.
         *
         * @type {Number}
         */
        this.maxRadius = 0;

        /**
         * Gets or sets the time of starting interaction with the wave.
         *
         * @type {Number}
         */
        this.touchDownStarted = 0;

        /**
         * Gets or sets the time of ending interaction with the wave.
         *
         * @type {Number}
         */
        this.touchUpStarted = 0;

        /**
         * Gets or sets the start position of the wave.
         *
         * @type {{x: Number, y: Number}}
         */
        this.startPosition = { x: 0, y: 0 };

        /**
         * Gets or sets the end position of the wave.
         *
         * @type {{x: Number, y: Number}}
         */
        this.endPosition = { x: 0, y: 0 };

        return this;
    }

    /**
     * Performs updating of the wave's values.
     *
     * @returns {PaperWave} Current instance for method chaining.
     */
    draw() {
        let cssString,
            scaleFactor = this.radius / (this.containerRect.size / 2),
            containerCenter = this.containerRect.center,
            currentPos = this.currentPosition,
            deltaPos = {
                x: currentPos.x - containerCenter.x,
                y: currentPos.y - containerCenter.y
            };

        this.$wave.style.opacity = this.opacity;

        cssString = 'translate(' + deltaPos.x + 'px, ' + deltaPos.y + 'px)';
        this.$.style.webkitTransform = cssString;
        this.$.style.mozTransform = cssString;
        this.$.style.msTransform = cssString;
        this.$.style.oTransform = cssString;
        this.$.style.transform = 'translate3d(' + deltaPos.x + 'px, ' + deltaPos.y + 'px, 0)';

        cssString = 'scale(' + scaleFactor + ',' + scaleFactor + ')';
        this.$wave.style.webkitTransform = cssString;
        this.$wave.style.mozTransform = cssString;
        this.$wave.style.msTransform = cssString;
        this.$wave.style.oTransform = cssString;
        this.$wave.style.transform = 'scale3d(' + scaleFactor + ',' + scaleFactor + ', 1)';

        return this;
    }

    /**
     * Performs ripple-down effect on the current wave.
     *
     * @param {(Event|Object)} [event=null] - An object containing coordinates of interaction point to set start position of ripple effect.
     * @returns {PaperWave} Current instance for method chaining.
     */
    downAction(event = null) {
        let containerCenter = this.containerRect.center;

        this.resetDefaults();

        this.touchDownStarted = _now();
        this.startPosition = this.center || !event ? containerCenter : {
            x: (event.clientX || event.x) - this.containerRect.boundingRect.left,
            y: (event.clientY || event.y) - this.containerRect.boundingRect.top
        };
        this.endPosition = this.recenters ? containerCenter : this.endPosition;
        this.maxRadius = this.containerRect.distanceToFarthestCorner(this.startPosition);

        this.$.style.top = (this.containerRect.height - this.containerRect.size) / 2 + 'px';
        this.$.style.left = (this.containerRect.width - this.containerRect.size) / 2 + 'px';
        this.$.style.width = this.containerRect.size + 'px';
        this.$.style.height = this.containerRect.size + 'px';

        return this;
    }

    /**
     * Performs ripple-up effect on the current wave.
     *
     * @returns {PaperWave} Current instance for method chaining.
     */
    upAction() {
        if (!this.isTouchDown) {
            return this;
        }

        /**
         * @private
         */
        this.touchUpStarted = _now();

        return this;
    }

    /**
     * Removes the wave from a DOM.
     *
     * @returns {PaperWave} Current instance for method chaining.
     */
    remove() {
        this.$.parentNode.removeChild(this.$);

        return this;
    }
}