playcanvas
Version:
PlayCanvas WebGL game engine
615 lines (612 loc) • 19.9 kB
JavaScript
import { now } from '../../../core/time.js';
import { math } from '../../../core/math/math.js';
import { Color } from '../../../core/math/color.js';
import { GraphNode } from '../../../scene/graph-node.js';
import { Component } from '../component.js';
import { BUTTON_TRANSITION_MODE_SPRITE_CHANGE, BUTTON_TRANSITION_MODE_TINT } from './constants.js';
import { ELEMENTTYPE_GROUP } from '../element/constants.js';
const VisualState = {
DEFAULT: 'DEFAULT',
HOVER: 'HOVER',
PRESSED: 'PRESSED',
INACTIVE: 'INACTIVE'
};
const 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';
const 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';
const 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';
class ButtonComponent extends Component {
static{
this.EVENT_MOUSEDOWN = 'mousedown';
}
static{
this.EVENT_MOUSEUP = 'mouseup';
}
static{
this.EVENT_MOUSEENTER = 'mouseenter';
}
static{
this.EVENT_MOUSELEAVE = 'mouseleave';
}
static{
this.EVENT_CLICK = 'click';
}
static{
this.EVENT_TOUCHSTART = 'touchstart';
}
static{
this.EVENT_TOUCHEND = 'touchend';
}
static{
this.EVENT_TOUCHCANCEL = 'touchcancel';
}
static{
this.EVENT_TOUCHLEAVE = 'touchleave';
}
static{
this.EVENT_SELECTSTART = 'selectstart';
}
static{
this.EVENT_SELECTEND = 'selectend';
}
static{
this.EVENT_SELECTENTER = 'selectenter';
}
static{
this.EVENT_SELECTLEAVE = 'selectleave';
}
static{
this.EVENT_HOVERSTART = 'hoverstart';
}
static{
this.EVENT_HOVEREND = 'hoverend';
}
static{
this.EVENT_PRESSEDSTART = 'pressedstart';
}
static{
this.EVENT_PRESSEDEND = 'pressedend';
}
constructor(system, entity){
super(system, entity), this._visualState = VisualState.DEFAULT, this._isHovering = false, this._hoveringCounter = 0, this._isPressed = false, this._defaultTint = new Color(1, 1, 1, 1), this._defaultSpriteAsset = null, this._defaultSpriteFrame = 0, this._imageEntity = null, this._evtElementAdd = null, this._evtImageEntityElementAdd = null, this._evtImageEntityElementRemove = null, this._evtImageEntityElementColor = null, this._evtImageEntityElementOpacity = null, this._evtImageEntityElementSpriteAsset = null, this._evtImageEntityElementSpriteFrame = null;
this._visualState = VisualState.DEFAULT;
this._isHovering = false;
this._hoveringCounter = 0;
this._isPressed = false;
this._defaultTint = new Color(1, 1, 1, 1);
this._defaultSpriteAsset = null;
this._defaultSpriteFrame = 0;
this._toggleLifecycleListeners('on', system);
}
get data() {
const record = this.system.store[this.entity.getGuid()];
return record ? record.data : null;
}
set enabled(arg) {
this._setValue('enabled', arg);
}
get enabled() {
return this.data.enabled;
}
set active(arg) {
this._setValue('active', arg);
}
get active() {
return this.data.active;
}
set imageEntity(arg) {
if (this._imageEntity !== arg) {
const isString = typeof arg === 'string';
if (this._imageEntity && isString && this._imageEntity.getGuid() === arg) {
return;
}
if (this._imageEntity) {
this._imageEntityUnsubscribe();
}
if (arg instanceof GraphNode) {
this._imageEntity = arg;
} else if (isString) {
this._imageEntity = this.system.app.getEntityFromIndex(arg) || null;
} else {
this._imageEntity = null;
}
if (this._imageEntity) {
this._imageEntitySubscribe();
}
if (this._imageEntity) {
this.data.imageEntity = this._imageEntity.getGuid();
} else if (isString && arg) {
this.data.imageEntity = arg;
}
}
}
get imageEntity() {
return this._imageEntity;
}
set hitPadding(arg) {
this._setValue('hitPadding', arg);
}
get hitPadding() {
return this.data.hitPadding;
}
set transitionMode(arg) {
this._setValue('transitionMode', arg);
}
get transitionMode() {
return this.data.transitionMode;
}
set hoverTint(arg) {
this._setValue('hoverTint', arg);
}
get hoverTint() {
return this.data.hoverTint;
}
set pressedTint(arg) {
this._setValue('pressedTint', arg);
}
get pressedTint() {
return this.data.pressedTint;
}
set inactiveTint(arg) {
this._setValue('inactiveTint', arg);
}
get inactiveTint() {
return this.data.inactiveTint;
}
set fadeDuration(arg) {
this._setValue('fadeDuration', arg);
}
get fadeDuration() {
return this.data.fadeDuration;
}
set hoverSpriteAsset(arg) {
this._setValue('hoverSpriteAsset', arg);
}
get hoverSpriteAsset() {
return this.data.hoverSpriteAsset;
}
set hoverSpriteFrame(arg) {
this._setValue('hoverSpriteFrame', arg);
}
get hoverSpriteFrame() {
return this.data.hoverSpriteFrame;
}
set pressedSpriteAsset(arg) {
this._setValue('pressedSpriteAsset', arg);
}
get pressedSpriteAsset() {
return this.data.pressedSpriteAsset;
}
set pressedSpriteFrame(arg) {
this._setValue('pressedSpriteFrame', arg);
}
get pressedSpriteFrame() {
return this.data.pressedSpriteFrame;
}
set inactiveSpriteAsset(arg) {
this._setValue('inactiveSpriteAsset', arg);
}
get inactiveSpriteAsset() {
return this.data.inactiveSpriteAsset;
}
set inactiveSpriteFrame(arg) {
this._setValue('inactiveSpriteFrame', arg);
}
get inactiveSpriteFrame() {
return this.data.inactiveSpriteFrame;
}
_setValue(name, value) {
const data = this.data;
const oldValue = data[name];
data[name] = value;
this.fire('set', name, oldValue, value);
}
_toggleLifecycleListeners(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);
if (onOrOff === 'on') {
this._evtElementAdd = this.entity.on('element:add', this._onElementComponentAdd, this);
} else {
this._evtElementAdd?.off();
this._evtElementAdd = null;
}
}
_onSetActive(name, oldValue, newValue) {
if (oldValue !== newValue) {
this._updateVisualState();
}
}
_onSetTransitionMode(name, oldValue, newValue) {
if (oldValue !== newValue) {
this._cancelTween();
this._resetToDefaultVisualState(oldValue);
this._forceReapplyVisualState();
}
}
_onSetTransitionValue(name, oldValue, newValue) {
if (oldValue !== newValue) {
this._forceReapplyVisualState();
}
}
_imageEntitySubscribe() {
this._evtImageEntityElementAdd = this._imageEntity.on('element:add', this._onImageElementGain, this);
if (this._imageEntity.element) {
this._onImageElementGain();
}
}
_imageEntityUnsubscribe() {
this._evtImageEntityElementAdd?.off();
this._evtImageEntityElementAdd = null;
if (this._imageEntity?.element) {
this._onImageElementLose();
}
}
_imageEntityElementSubscribe() {
const element = this._imageEntity.element;
this._evtImageEntityElementRemove = element.once('beforeremove', this._onImageElementLose, this);
this._evtImageEntityElementColor = element.on('set:color', this._onSetColor, this);
this._evtImageEntityElementOpacity = element.on('set:opacity', this._onSetOpacity, this);
this._evtImageEntityElementSpriteAsset = element.on('set:spriteAsset', this._onSetSpriteAsset, this);
this._evtImageEntityElementSpriteFrame = element.on('set:spriteFrame', this._onSetSpriteFrame, this);
}
_imageEntityElementUnsubscribe() {
this._evtImageEntityElementRemove?.off();
this._evtImageEntityElementRemove = null;
this._evtImageEntityElementColor?.off();
this._evtImageEntityElementColor = null;
this._evtImageEntityElementOpacity?.off();
this._evtImageEntityElementOpacity = null;
this._evtImageEntityElementSpriteAsset?.off();
this._evtImageEntityElementSpriteAsset = null;
this._evtImageEntityElementSpriteFrame?.off();
this._evtImageEntityElementSpriteFrame = null;
}
_onElementComponentRemove() {
this._toggleHitElementListeners('off');
}
_onElementComponentAdd() {
this._toggleHitElementListeners('on');
}
_onImageElementLose() {
this._imageEntityElementUnsubscribe();
this._cancelTween();
this._resetToDefaultVisualState(this.transitionMode);
}
_onImageElementGain() {
this._imageEntityElementSubscribe();
this._storeDefaultVisualState();
this._forceReapplyVisualState();
}
_toggleHitElementListeners(onOrOff) {
if (this.entity.element) {
const isAdding = onOrOff === 'on';
if (isAdding && this._hasHitElementListeners) {
return;
}
this.entity.element[onOrOff]('beforeremove', this._onElementComponentRemove, this);
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]('selectstart', this._onSelectStart, this);
this.entity.element[onOrOff]('selectend', this._onSelectEnd, this);
this.entity.element[onOrOff]('selectenter', this._onSelectEnter, this);
this.entity.element[onOrOff]('selectleave', this._onSelectLeave, this);
this.entity.element[onOrOff]('click', this._onClick, this);
this._hasHitElementListeners = isAdding;
}
}
_storeDefaultVisualState() {
const element = this._imageEntity?.element;
if (!element || element.type === ELEMENTTYPE_GROUP) {
return;
}
this._storeDefaultColor(element.color);
this._storeDefaultOpacity(element.opacity);
this._storeDefaultSpriteAsset(element.spriteAsset);
this._storeDefaultSpriteFrame(element.spriteFrame);
}
_storeDefaultColor(color) {
this._defaultTint.r = color.r;
this._defaultTint.g = color.g;
this._defaultTint.b = color.b;
}
_storeDefaultOpacity(opacity) {
this._defaultTint.a = opacity;
}
_storeDefaultSpriteAsset(spriteAsset) {
this._defaultSpriteAsset = spriteAsset;
}
_storeDefaultSpriteFrame(spriteFrame) {
this._defaultSpriteFrame = spriteFrame;
}
_onSetColor(color) {
if (!this._isApplyingTint) {
this._storeDefaultColor(color);
this._forceReapplyVisualState();
}
}
_onSetOpacity(opacity) {
if (!this._isApplyingTint) {
this._storeDefaultOpacity(opacity);
this._forceReapplyVisualState();
}
}
_onSetSpriteAsset(spriteAsset) {
if (!this._isApplyingSprite) {
this._storeDefaultSpriteAsset(spriteAsset);
this._forceReapplyVisualState();
}
}
_onSetSpriteFrame(spriteFrame) {
if (!this._isApplyingSprite) {
this._storeDefaultSpriteFrame(spriteFrame);
this._forceReapplyVisualState();
}
}
_onMouseEnter(event) {
this._isHovering = true;
this._updateVisualState();
this._fireIfActive('mouseenter', event);
}
_onMouseLeave(event) {
this._isHovering = false;
this._isPressed = false;
this._updateVisualState();
this._fireIfActive('mouseleave', event);
}
_onMouseDown(event) {
this._isPressed = true;
this._updateVisualState();
this._fireIfActive('mousedown', event);
}
_onMouseUp(event) {
this._isPressed = false;
this._updateVisualState();
this._fireIfActive('mouseup', event);
}
_onTouchStart(event) {
this._isPressed = true;
this._updateVisualState();
this._fireIfActive('touchstart', event);
}
_onTouchEnd(event) {
event.event.preventDefault();
this._isPressed = false;
this._updateVisualState();
this._fireIfActive('touchend', event);
}
_onTouchLeave(event) {
this._isPressed = false;
this._updateVisualState();
this._fireIfActive('touchleave', event);
}
_onTouchCancel(event) {
this._isPressed = false;
this._updateVisualState();
this._fireIfActive('touchcancel', event);
}
_onSelectStart(event) {
this._isPressed = true;
this._updateVisualState();
this._fireIfActive('selectstart', event);
}
_onSelectEnd(event) {
this._isPressed = false;
this._updateVisualState();
this._fireIfActive('selectend', event);
}
_onSelectEnter(event) {
this._hoveringCounter++;
if (this._hoveringCounter === 1) {
this._isHovering = true;
this._updateVisualState();
}
this._fireIfActive('selectenter', event);
}
_onSelectLeave(event) {
this._hoveringCounter--;
if (this._hoveringCounter === 0) {
this._isHovering = false;
this._isPressed = false;
this._updateVisualState();
}
this._fireIfActive('selectleave', event);
}
_onClick(event) {
this._fireIfActive('click', event);
}
_fireIfActive(name, event) {
if (this.data.active) {
this.fire(name, event);
}
}
_updateVisualState(force) {
const oldVisualState = this._visualState;
const 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 BUTTON_TRANSITION_MODE_TINT:
{
const tintName = STATES_TO_TINT_NAMES[this._visualState];
const tintColor = this[tintName];
this._applyTint(tintColor);
break;
}
case BUTTON_TRANSITION_MODE_SPRITE_CHANGE:
{
const spriteAssetName = STATES_TO_SPRITE_ASSET_NAMES[this._visualState];
const spriteFrameName = STATES_TO_SPRITE_FRAME_NAMES[this._visualState];
const spriteAsset = this[spriteAssetName];
const spriteFrame = this[spriteFrameName];
this._applySprite(spriteAsset, spriteFrame);
break;
}
}
}
}
_forceReapplyVisualState() {
this._updateVisualState(true);
}
_resetToDefaultVisualState(transitionMode) {
if (!this._imageEntity?.element) {
return;
}
switch(transitionMode){
case BUTTON_TRANSITION_MODE_TINT:
this._cancelTween();
this._applyTintImmediately(this._defaultTint);
break;
case BUTTON_TRANSITION_MODE_SPRITE_CHANGE:
this._applySprite(this._defaultSpriteAsset, this._defaultSpriteFrame);
break;
}
}
_determineVisualState() {
if (!this.active) {
return VisualState.INACTIVE;
} else if (this._isPressed) {
return VisualState.PRESSED;
} else if (this._isHovering) {
return VisualState.HOVER;
}
return VisualState.DEFAULT;
}
_applySprite(spriteAsset, spriteFrame) {
const element = this._imageEntity?.element;
if (!element) {
return;
}
spriteFrame = spriteFrame || 0;
this._isApplyingSprite = true;
if (element.spriteAsset !== spriteAsset) {
element.spriteAsset = spriteAsset;
}
if (element.spriteFrame !== spriteFrame) {
element.spriteFrame = spriteFrame;
}
this._isApplyingSprite = false;
}
_applyTint(tintColor) {
this._cancelTween();
if (this.fadeDuration === 0) {
this._applyTintImmediately(tintColor);
} else {
this._applyTintWithTween(tintColor);
}
}
_applyTintImmediately(tintColor) {
const element = this._imageEntity?.element;
if (!tintColor || !element || element.type === ELEMENTTYPE_GROUP) {
return;
}
const color3 = toColor3(tintColor);
this._isApplyingTint = true;
if (!color3.equals(element.color)) {
element.color = color3;
}
if (element.opacity !== tintColor.a) {
element.opacity = tintColor.a;
}
this._isApplyingTint = false;
}
_applyTintWithTween(tintColor) {
const element = this._imageEntity?.element;
if (!tintColor || !element || element.type === ELEMENTTYPE_GROUP) {
return;
}
const color3 = toColor3(tintColor);
const color = element.color;
const opacity = element.opacity;
if (color3.equals(color) && tintColor.a === opacity) return;
this._tweenInfo = {
startTime: now(),
from: new Color(color.r, color.g, color.b, opacity),
to: tintColor.clone(),
lerpColor: new Color()
};
}
_updateTintTween() {
const elapsedTime = now() - this._tweenInfo.startTime;
let elapsedProportion = this.fadeDuration === 0 ? 1 : elapsedTime / this.fadeDuration;
elapsedProportion = math.clamp(elapsedProportion, 0, 1);
if (Math.abs(elapsedProportion - 1) > 1e-5) {
const lerpColor = this._tweenInfo.lerpColor;
lerpColor.lerp(this._tweenInfo.from, this._tweenInfo.to, elapsedProportion);
this._applyTintImmediately(new Color(lerpColor.r, lerpColor.g, lerpColor.b, lerpColor.a));
} else {
this._applyTintImmediately(this._tweenInfo.to);
this._cancelTween();
}
}
_cancelTween() {
delete this._tweenInfo;
}
onUpdate() {
if (this._tweenInfo) {
this._updateTintTween();
}
}
onEnable() {
this._isHovering = false;
this._hoveringCounter = 0;
this._isPressed = false;
this._toggleHitElementListeners('on');
this._forceReapplyVisualState();
}
onDisable() {
this._toggleHitElementListeners('off');
this._resetToDefaultVisualState(this.transitionMode);
}
onRemove() {
this._imageEntityUnsubscribe();
this._toggleLifecycleListeners('off', this.system);
this.onDisable();
}
resolveDuplicatedEntityReferenceProperties(oldButton, duplicatedIdsMap) {
if (oldButton.imageEntity) {
this.imageEntity = duplicatedIdsMap[oldButton.imageEntity.getGuid()];
}
}
}
function toColor3(color4) {
return new Color(color4.r, color4.g, color4.b);
}
export { ButtonComponent };