Source: framework/components/button/component.js

Object.assign(pc, function () {
    var VisualState = {
        DEFAULT: 'DEFAULT',
        HOVER: 'HOVER',
        PRESSED: 'PRESSED',
        INACTIVE: 'INACTIVE'
    };

    var STATES_TO_TINT_NAMES = {};
    STATES_TO_TINT_NAMES[VisualState.DEFAULT] = '_defaultTint';
    STATES_TO_TINT_NAMES[VisualState.HOVER] = 'hoverTint';
    STATES_TO_TINT_NAMES[VisualState.PRESSED] = 'pressedTint';
    STATES_TO_TINT_NAMES[VisualState.INACTIVE] = 'inactiveTint';

    var STATES_TO_SPRITE_ASSET_NAMES = {};
    STATES_TO_SPRITE_ASSET_NAMES[VisualState.DEFAULT] = '_defaultSpriteAsset';
    STATES_TO_SPRITE_ASSET_NAMES[VisualState.HOVER] = 'hoverSpriteAsset';
    STATES_TO_SPRITE_ASSET_NAMES[VisualState.PRESSED] = 'pressedSpriteAsset';
    STATES_TO_SPRITE_ASSET_NAMES[VisualState.INACTIVE] = 'inactiveSpriteAsset';

    var STATES_TO_SPRITE_FRAME_NAMES = {};
    STATES_TO_SPRITE_FRAME_NAMES[VisualState.DEFAULT] = '_defaultSpriteFrame';
    STATES_TO_SPRITE_FRAME_NAMES[VisualState.HOVER] = 'hoverSpriteFrame';
    STATES_TO_SPRITE_FRAME_NAMES[VisualState.PRESSED] = 'pressedSpriteFrame';
    STATES_TO_SPRITE_FRAME_NAMES[VisualState.INACTIVE] = 'inactiveSpriteFrame';

    /**
     * @component
     * @constructor
     * @name pc.ButtonComponent
     * @extends pc.Component
     * @classdesc A ButtonComponent enables a group of entities to behave like a button, with different visual states for hover and press interactions.
     * @description Create a new ButtonComponent.
     * @param {pc.ButtonComponentSystem} system The ComponentSystem that created this Component
     * @param {pc.Entity} entity The Entity that this Component is attached to.
     * @property {Boolean} active If set to false, the button will be visible but will not respond to hover or touch interactions.
     * @property {pc.Entity} imageEntity A reference to the entity to be used as the button background. The entity must have an ImageElement component.
     * @property {pc.Vec4} hitPadding Padding to be used in hit-test calculations. Can be used to expand the bounding box so that the button is easier to tap.
     * @property {pc.BUTTON_TRANSITION_MODE} transitionMode Controls how the button responds when the user hovers over it/presses it.
     * @property {pc.Color} hoverTint Color to be used on the button image when the user hovers over it.
     * @property {pc.Color} pressedTint Color to be used on the button image when the user presses it.
     * @property {pc.Color} inactiveTint Color to be used on the button image when the button is not interactive.
     * @property {Number} fadeDuration Duration to be used when fading between tints, in milliseconds.
     * @property {pc.Asset} hoverSpriteAsset Sprite to be used as the button image when the user hovers over it.
     * @property {Number} hoverSpriteFrame Frame to be used from the hover sprite.
     * @property {pc.Asset} pressedSpriteAsset Sprite to be used as the button image when the user presses it.
     * @property {Number} pressedSpriteFrame Frame to be used from the pressed sprite.
     * @property {pc.Asset} inactiveSpriteAsset Sprite to be used as the button image when the button is not interactive.
     * @property {Number} inactiveSpriteFrame Frame to be used from the inactive sprite.
     */
    var ButtonComponent = function ButtonComponent(system, entity) {
        pc.Component.call(this, system, entity);

        this._visualState = VisualState.DEFAULT;
        this._isHovering = false;
        this._isPressed = false;

        this._defaultTint = new pc.Color(1, 1, 1, 1);
        this._defaultSpriteAsset = null;
        this._defaultSpriteFrame = 0;

        this._imageReference = new pc.EntityReference(this, 'imageEntity', {
            'element#gain': this._onImageElementGain,
            'element#lose': this._onImageElementLose,
            'element#set:color': this._onSetColor,
            'element#set:opacity': this._onSetOpacity,
            'element#set:spriteAsset': this._onSetSpriteAsset,
            'element#set:spriteFrame': this._onSetSpriteFrame
        });

        this._toggleLifecycleListeners('on', system);
    };
    ButtonComponent.prototype = Object.create(pc.Component.prototype);
    ButtonComponent.prototype.constructor = ButtonComponent;

    Object.assign(ButtonComponent.prototype, {
        _toggleLifecycleListeners: function (onOrOff, system) {
            this[onOrOff]('set_active', this._onSetActive, this);
            this[onOrOff]('set_transitionMode', this._onSetTransitionMode, this);
            this[onOrOff]('set_hoverTint', this._onSetTransitionValue, this);
            this[onOrOff]('set_pressedTint', this._onSetTransitionValue, this);
            this[onOrOff]('set_inactiveTint', this._onSetTransitionValue, this);
            this[onOrOff]('set_hoverSpriteAsset', this._onSetTransitionValue, this);
            this[onOrOff]('set_hoverSpriteFrame', this._onSetTransitionValue, this);
            this[onOrOff]('set_pressedSpriteAsset', this._onSetTransitionValue, this);
            this[onOrOff]('set_pressedSpriteFrame', this._onSetTransitionValue, this);
            this[onOrOff]('set_inactiveSpriteAsset', this._onSetTransitionValue, this);
            this[onOrOff]('set_inactiveSpriteFrame', this._onSetTransitionValue, this);

            system.app.systems.element[onOrOff]('add', this._onElementComponentAdd, this);
            system.app.systems.element[onOrOff]('beforeremove', this._onElementComponentRemove, this);
        },

        _onSetActive: function (name, oldValue, newValue) {
            if (oldValue !== newValue) {
                this._updateVisualState();
            }
        },

        _onSetTransitionMode: function (name, oldValue, newValue) {
            if (oldValue !== newValue) {
                this._cancelTween();
                this._resetToDefaultVisualState(oldValue);
                this._forceReapplyVisualState();
            }
        },

        _onSetTransitionValue: function (name, oldValue, newValue) {
            if (oldValue !== newValue) {
                this._forceReapplyVisualState();
            }
        },

        _onElementComponentRemove: function (entity) {
            if (this.entity === entity) {
                this._toggleHitElementListeners('off');
            }
        },

        _onElementComponentAdd: function (entity) {
            if (this.entity === entity) {
                this._toggleHitElementListeners('on');
            }
        },

        _onImageElementLose: function () {
            this._cancelTween();
            this._resetToDefaultVisualState(this.transitionMode);
        },

        _onImageElementGain: function () {
            this._storeDefaultVisualState();
            this._forceReapplyVisualState();
        },

        _toggleHitElementListeners: function (onOrOff) {
            if (this.entity.element) {
                var isAdding = (onOrOff === 'on');

                // Prevent duplicate listeners
                if (isAdding && this._hasHitElementListeners) {
                    return;
                }

                this.entity.element[onOrOff]('mouseenter', this._onMouseEnter, this);
                this.entity.element[onOrOff]('mouseleave', this._onMouseLeave, this);
                this.entity.element[onOrOff]('mousedown', this._onMouseDown, this);
                this.entity.element[onOrOff]('mouseup', this._onMouseUp, this);
                this.entity.element[onOrOff]('touchstart', this._onTouchStart, this);
                this.entity.element[onOrOff]('touchend', this._onTouchEnd, this);
                this.entity.element[onOrOff]('touchleave', this._onTouchLeave, this);
                this.entity.element[onOrOff]('touchcancel', this._onTouchCancel, this);
                this.entity.element[onOrOff]('click', this._onClick, this);

                this._hasHitElementListeners = isAdding;
            }
        },

        _storeDefaultVisualState: function () {
            if (this._imageReference.hasComponent('element')) {
                this._storeDefaultColor(this._imageReference.entity.element.color);
                this._storeDefaultOpacity(this._imageReference.entity.element.opacity);
                this._storeDefaultSpriteAsset(this._imageReference.entity.element.spriteAsset);
                this._storeDefaultSpriteFrame(this._imageReference.entity.element.spriteFrame);
            }
        },

        _storeDefaultColor: function (color) {
            this._defaultTint.r = color.r;
            this._defaultTint.g = color.g;
            this._defaultTint.b = color.b;
        },

        _storeDefaultOpacity: function (opacity) {
            this._defaultTint.a = opacity;
        },

        _storeDefaultSpriteAsset: function (spriteAsset) {
            this._defaultSpriteAsset = spriteAsset;
        },

        _storeDefaultSpriteFrame: function (spriteFrame) {
            this._defaultSpriteFrame = spriteFrame;
        },

        _onSetColor: function (color) {
            if (!this._isApplyingTint) {
                this._storeDefaultColor(color);
                this._forceReapplyVisualState();
            }
        },

        _onSetOpacity: function (opacity) {
            if (!this._isApplyingTint) {
                this._storeDefaultOpacity(opacity);
                this._forceReapplyVisualState();
            }
        },

        _onSetSpriteAsset: function (spriteAsset) {
            if (!this._isApplyingSprite) {
                this._storeDefaultSpriteAsset(spriteAsset);
                this._forceReapplyVisualState();
            }
        },

        _onSetSpriteFrame: function (spriteFrame) {
            if (!this._isApplyingSprite) {
                this._storeDefaultSpriteFrame(spriteFrame);
                this._forceReapplyVisualState();
            }
        },

        _onMouseEnter: function (event) {
            this._isHovering = true;

            this._updateVisualState();
            this._fireIfActive('mouseenter', event);
        },

        _onMouseLeave: function (event) {
            this._isHovering = false;
            this._isPressed = false;

            this._updateVisualState();
            this._fireIfActive('mouseleave', event);
        },

        _onMouseDown: function (event) {
            this._isPressed = true;

            this._updateVisualState();
            this._fireIfActive('mousedown', event);
        },

        _onMouseUp: function (event) {
            this._isPressed = false;

            this._updateVisualState();
            this._fireIfActive('mouseup', event);
        },

        _onTouchStart: function (event) {
            this._isPressed = true;

            this._updateVisualState();
            this._fireIfActive('touchstart', event);
        },

        _onTouchEnd: function (event) {
            // The default behaviour of the browser is to simulate a series of
            // `mouseenter/down/up` events immediately after the `touchend` event,
            // in order to ensure that websites that don't explicitly listen for
            // touch events will still work on mobile (see https://www.html5rocks.com/en/mobile/touchandmouse/
            // for reference). This leads to an issue whereby buttons will enter
            // the `hover` state on mobile browsers after the `touchend` event is
            // received, instead of going back to the `default` state. Calling
            // preventDefault() here fixes the issue.
            event.event.preventDefault();

            this._isPressed = false;

            this._updateVisualState();
            this._fireIfActive('touchend', event);
        },

        _onTouchLeave: function (event) {
            this._isPressed = false;

            this._updateVisualState();
            this._fireIfActive('touchleave', event);
        },

        _onTouchCancel: function (event) {
            this._isPressed = false;

            this._updateVisualState();
            this._fireIfActive('touchcancel', event);
        },

        _onClick: function (event) {
            this._fireIfActive('click', event);
        },

        _fireIfActive: function (name, event) {
            if (this.data.active) {
                this.fire(name, event);
            }
        },

        _updateVisualState: function (force) {
            var oldVisualState = this._visualState;
            var newVisualState = this._determineVisualState();

            if ((oldVisualState !== newVisualState || force) && this.enabled) {
                this._visualState = newVisualState;

                if (oldVisualState === VisualState.HOVER) {
                    this._fireIfActive('hoverend');
                }

                if (oldVisualState === VisualState.PRESSED) {
                    this._fireIfActive('pressedend');
                }

                if (newVisualState === VisualState.HOVER) {
                    this._fireIfActive('hoverstart');
                }

                if (newVisualState === VisualState.PRESSED) {
                    this._fireIfActive('pressedstart');
                }

                switch (this.transitionMode) {
                    case pc.BUTTON_TRANSITION_MODE_TINT:
                        var tintName = STATES_TO_TINT_NAMES[this._visualState];
                        var tintColor = this[tintName];
                        this._applyTint(tintColor);
                        break;

                    case pc.BUTTON_TRANSITION_MODE_SPRITE_CHANGE:
                        var spriteAssetName = STATES_TO_SPRITE_ASSET_NAMES[this._visualState];
                        var spriteFrameName = STATES_TO_SPRITE_FRAME_NAMES[this._visualState];
                        var spriteAsset = this[spriteAssetName];
                        var spriteFrame = this[spriteFrameName];
                        this._applySprite(spriteAsset, spriteFrame);
                        break;
                }
            }
        },

        // Called when a property changes that mean the visual state must be reapplied,
        // even if the state enum has not changed. Examples of this are when the tint
        // value for one of the states is changed via the editor.
        _forceReapplyVisualState: function () {
            this._updateVisualState(true);
        },

        // Called before the image entity changes, in order to restore the previous
        // image back to its original tint. Note that this happens immediately, i.e.
        // without any animation.
        _resetToDefaultVisualState: function (transitionMode) {
            if (this._imageReference.hasComponent('element')) {
                switch (transitionMode) {
                    case pc.BUTTON_TRANSITION_MODE_TINT:
                        this._cancelTween();
                        this._applyTintImmediately(this._defaultTint);
                        break;

                    case pc.BUTTON_TRANSITION_MODE_SPRITE_CHANGE:
                        this._applySprite(this._defaultSpriteAsset, this._defaultSpriteFrame);
                        break;
                }
            }
        },

        _determineVisualState: function () {
            if (!this.active) {
                return VisualState.INACTIVE;
            } else if (this._isPressed) {
                return VisualState.PRESSED;
            } else if (this._isHovering) {
                return VisualState.HOVER;
            }

            return VisualState.DEFAULT;
        },

        _applySprite: function (spriteAsset, spriteFrame) {
            spriteFrame = spriteFrame || 0;

            if (this._imageReference.hasComponent('element')) {
                this._isApplyingSprite = true;
                this._imageReference.entity.element.spriteAsset = spriteAsset;
                this._imageReference.entity.element.spriteFrame = spriteFrame;
                this._isApplyingSprite = false;
            }
        },

        _applyTint: function (tintColor) {
            this._cancelTween();

            if (this.fadeDuration === 0) {
                this._applyTintImmediately(tintColor);
            } else {
                this._applyTintWithTween(tintColor);
            }
        },

        _applyTintImmediately: function (tintColor) {
            if (this._imageReference.hasComponent('element') && tintColor) {
                this._isApplyingTint = true;
                this._imageReference.entity.element.color = toColor3(tintColor);
                this._imageReference.entity.element.opacity = tintColor.a;
                this._isApplyingTint = false;
            }
        },

        _applyTintWithTween: function (tintColor) {
            if (this._imageReference.hasComponent('element') && tintColor) {
                var color = this._imageReference.entity.element.color;
                var opacity = this._imageReference.entity.element.opacity;

                this._tweenInfo = {
                    startTime: pc.now(),
                    from: new pc.Color(color.r, color.g, color.b, opacity),
                    to: tintColor.clone(),
                    lerpColor: new pc.Color()
                };
            }
        },

        _updateTintTween: function () {
            var elapsedTime = pc.now() - this._tweenInfo.startTime;
            var elapsedProportion = this.fadeDuration === 0 ? 1 : (elapsedTime / this.fadeDuration);
            elapsedProportion = pc.math.clamp(elapsedProportion, 0, 1);

            if (Math.abs(elapsedProportion - 1) > 1e-5) {
                var lerpColor = this._tweenInfo.lerpColor;
                lerpColor.lerp(this._tweenInfo.from, this._tweenInfo.to, elapsedProportion);
                this._applyTintImmediately(new pc.Color(lerpColor.r, lerpColor.g, lerpColor.b, lerpColor.a));
            } else {
                this._applyTintImmediately(this._tweenInfo.to);
                this._cancelTween();
            }
        },

        _cancelTween: function () {
            delete this._tweenInfo;
        },

        onUpdate: function () {
            if (this._tweenInfo) {
                this._updateTintTween();
            }
        },

        onEnable: function () {
            this._imageReference.onParentComponentEnable();
            this._toggleHitElementListeners('on');
            this._forceReapplyVisualState();
        },

        onDisable: function () {
            this._toggleHitElementListeners('off');
            this._resetToDefaultVisualState(this.transitionMode);
        },

        onRemove: function () {
            this._toggleLifecycleListeners('off', this.system);
            this.onDisable();
        }
    });

    function toColor3(color4) {
        return new pc.Color(color4.r, color4.g, color4.b);
    }

    return {
        ButtonComponent: ButtonComponent
    };
}());

/**
 * @event
 * @name pc.ButtonComponent#mousedown
 * @description Fired when the mouse is pressed while the cursor is on the component.
 * @param {pc.ElementMouseEvent} event The event
 */

/**
 * @event
 * @name pc.ButtonComponent#mouseup
 * @description Fired when the mouse is released while the cursor is on the component.
 * @param {pc.ElementMouseEvent} event The event
 */

/**
 * @event
 * @name pc.ButtonComponent#mouseenter
 * @description Fired when the mouse cursor enters the component.
 * @param {pc.ElementMouseEvent} event The event
 */

/**
 * @event
 * @name pc.ButtonComponent#mouseleave
 * @description Fired when the mouse cursor leaves the component.
 * @param {pc.ElementMouseEvent} event The event
 */

/**
 * @event
 * @name pc.ButtonComponent#click
 * @description Fired when the mouse is pressed and released on the component or when a touch starts and ends on the component.
 * @param {pc.ElementMouseEvent|pc.ElementTouchEvent} event The event
 */

/**
 * @event
 * @name pc.ButtonComponent#touchstart
 * @description Fired when a touch starts on the component.
 * @param {pc.ElementTouchEvent} event The event
 */

/**
 * @event
 * @name pc.ButtonComponent#touchend
 * @description Fired when a touch ends on the component.
 * @param {pc.ElementTouchEvent} event The event
 */

/**
 * @event
 * @name pc.ButtonComponent#touchcancel
 * @description Fired when a touch is cancelled on the component.
 * @param {pc.ElementTouchEvent} event The event
 */

/**
 * @event
 * @name pc.ButtonComponent#touchleave
 * @description Fired when a touch leaves the component.
 * @param {pc.ElementTouchEvent} event The event
 */

/**
 * @event
 * @name pc.ButtonComponent#hoverstart
 * @description Fired when the button changes state to be hovered
 */

/**
 * @event
 * @name pc.ButtonComponent#hoverend
 * @description Fired when the button changes state to be not hovered
 */

/**
 * @event
 * @name pc.ButtonComponent#pressedstart
 * @description Fired when the button changes state to be pressed
 */

/**
 * @event
 * @name pc.ButtonComponent#pressedend
 * @description Fired when the button changes state to be not pressed
 */