UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

591 lines (590 loc) 18.5 kB
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 EVENT_MOUSEDOWN = "mousedown"; static EVENT_MOUSEUP = "mouseup"; static EVENT_MOUSEENTER = "mouseenter"; static EVENT_MOUSELEAVE = "mouseleave"; static EVENT_CLICK = "click"; static EVENT_TOUCHSTART = "touchstart"; static EVENT_TOUCHEND = "touchend"; static EVENT_TOUCHCANCEL = "touchcancel"; static EVENT_TOUCHLEAVE = "touchleave"; static EVENT_SELECTSTART = "selectstart"; static EVENT_SELECTEND = "selectend"; static EVENT_SELECTENTER = "selectenter"; static EVENT_SELECTLEAVE = "selectleave"; static EVENT_HOVERSTART = "hoverstart"; static EVENT_HOVEREND = "hoverend"; static EVENT_PRESSEDSTART = "pressedstart"; static EVENT_PRESSEDEND = "pressedend"; _visualState = VisualState.DEFAULT; _isHovering = false; _hoveringCounter = 0; _isPressed = false; _defaultTint = new Color(1, 1, 1, 1); _defaultSpriteAsset = null; _defaultSpriteFrame = 0; _imageEntity = null; _evtElementAdd = null; _evtImageEntityElementAdd = null; _evtImageEntityElementRemove = null; _evtImageEntityElementColor = null; _evtImageEntityElementOpacity = null; _evtImageEntityElementSpriteAsset = null; _evtImageEntityElementSpriteFrame = null; constructor(system, entity) { super(system, entity); this._toggleLifecycleListeners("on", system); } 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.guid === 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.guid; } 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; } } } } // 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() { 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(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.guid]; } } } function toColor3(color4) { return new Color(color4.r, color4.g, color4.b); } export { ButtonComponent };