UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

308 lines (271 loc) • 10.7 kB
import { showBalloonMessage } from "../../engine/debug/index.js"; import { Gizmos } from "../../engine/engine_gizmos.js"; import { PointerType } from "../../engine/engine_input.js"; import { serializable } from "../../engine/engine_serialization_decorator.js"; import { getParam } from "../../engine/engine_utils.js"; import { RGBAColor } from "../../engine/js-extensions/index.js"; import { Animator } from "../Animator.js"; import { Behaviour, GameObject } from "../Component.js"; import { EventList } from "../EventList.js"; import { Image } from "./Image.js"; import type { IPointerClickHandler, IPointerEnterHandler, IPointerEventHandler, IPointerExitHandler, PointerEventData } from "./PointerEvents.js"; import { GraphicRaycaster, ObjectRaycaster, Raycaster } from "./Raycaster.js"; const debug = getParam("debugbutton"); /// <summary> ///Transition mode for a Selectable. /// </summary> export enum Transition { /// <summary> /// No Transition. /// </summary> None, /// <summary> /// Use an color tint transition. /// </summary> ColorTint, /// <summary> /// Use a sprite swap transition. /// </summary> SpriteSwap, /// <summary> /// Use an animation transition. /// </summary> Animation } class ButtonColors { @serializable() colorMultiplier!: 1; @serializable(RGBAColor) disabledColor!: RGBAColor; @serializable() fadeDuration!: number; @serializable(RGBAColor) highlightedColor!: RGBAColor; @serializable(RGBAColor) normalColor!: RGBAColor; @serializable(RGBAColor) pressedColor!: RGBAColor; @serializable(RGBAColor) selectedColor!: RGBAColor; } class AnimationTriggers { disabledTrigger!: string; highlightedTrigger!: string; normalTrigger!: string; pressedTrigger!: string; selectedTrigger!: string; } /** * @category User Interface * @group Components */ export class Button extends Behaviour implements IPointerEventHandler { /** * Invokes the onClick event */ click() { this.onClick?.invoke(); } @serializable(EventList) onClick: EventList<void> = new EventList(); private _isHovered: number = 0; onPointerEnter(evt: PointerEventData) { const canSetCursor = evt.event.pointerType === "mouse" && evt.button === 0; if (canSetCursor) this._isHovered += 1; if (debug) console.warn("Button Enter", canSetCursor, this._isHovered, this.animationTriggers?.highlightedTrigger, this.animator); if (!this.interactable) return; if (this.transition == Transition.Animation && this.animationTriggers && this.animator) { this.animator.setTrigger(this.animationTriggers.highlightedTrigger); } else if (this.transition === Transition.ColorTint && this.colors) { this._image?.setState("hovered"); } if (canSetCursor) this.context.input.setCursor("pointer"); } onPointerExit() { this._isHovered -= 1; if (this._isHovered < 0) this._isHovered = 0; if (debug) console.log("Button Exit", this._isHovered, this.animationTriggers?.highlightedTrigger, this.animator); if (!this.interactable) return; if (this._isHovered > 0) return; this._isHovered = 0; if (this.transition == Transition.Animation && this.animationTriggers && this.animator) { this.animator.setTrigger(this.animationTriggers.normalTrigger); } else if (this.transition === Transition.ColorTint && this.colors) { this._image?.setState("normal"); } this.context.input.unsetCursor("pointer"); } onPointerDown(_) { if (debug) console.log("Button Down", this.animationTriggers?.highlightedTrigger, this.animator); if (!this.interactable) return; if (this.transition == Transition.Animation && this.animationTriggers && this.animator) { this.animator.setTrigger(this.animationTriggers.pressedTrigger); } else if (this.transition === Transition.ColorTint && this.colors) { this._image?.setState("pressed"); } } onPointerUp(_) { if (debug) console.warn("Button Up", this.animationTriggers?.highlightedTrigger, this.animator, this._isHovered); if (!this.interactable) return; if (this.transition == Transition.Animation && this.animationTriggers && this.animator) { this.animator.setTrigger(this._isHovered ? this.animationTriggers.highlightedTrigger : this.animationTriggers.normalTrigger); } else if (this.transition === Transition.ColorTint && this.colors) { this._image?.setState(this._isHovered ? "hovered" : "normal"); } } onPointerClick(args: PointerEventData) { if (!this.interactable) return; if (args.button !== 0 && args.event.pointerType === PointerType.Mouse) return; // Button clicks should only run with left mouse button while using mouse if (debug) { console.warn("Button Click", this.onClick); showBalloonMessage("CLICKED button " + this.name + " at " + this.context.time.frameCount); } // TODO: we can not *always* use the event right now because the hotspot sample is relying on onPointerClick on a parent object // and it's not using the button if (this.onClick && this.onClick.listenerCount > 0) { this.onClick.invoke(); args.use(); // debug clicks for WebXR if (debug) { const pos = this.gameObject.worldPosition; pos.add(this.gameObject.worldUp.multiplyScalar(1 + Math.random() * .5)) Gizmos.DrawLabel(pos, "CLICK:" + Date.now(), .1, 1 + Math.random() * .5); } } } @serializable(ButtonColors) colors?: ButtonColors; @serializable() transition?: Transition; @serializable(AnimationTriggers) animationTriggers?: AnimationTriggers; @serializable(Animator) animator?: Animator; // @serializable(Image) // image? : Image; @serializable() set interactable(value) { this._interactable = value; if (this._image) { this._image.setInteractable(value); if (value) this._image.setState("normal"); else this._image.setState("disabled"); } } get interactable(): boolean { return this._interactable; } private _interactable: boolean = true; private set_interactable(value: boolean) { this.interactable = value; } awake(): void { super.awake(); if (debug) console.log(this); this._isInit = false; this.init(); } start() { this._image?.setInteractable(this.interactable); if (!this.gameObject.getComponentInParent(Raycaster)) { this.gameObject.addComponent(GraphicRaycaster); } } onEnable() { super.onEnable(); } onDestroy(): void { if (this._isHovered) this.context.input.unsetCursor("pointer"); } private _requestedAnimatorTrigger?: string; private *setAnimatorTriggerAtEndOfFrame(requestedTriggerId: string) { this._requestedAnimatorTrigger = requestedTriggerId; yield; yield; if (this._requestedAnimatorTrigger == requestedTriggerId) { this.animator?.setTrigger(requestedTriggerId); } } private _isInit: boolean = false; private _image?: Image; private init() { if (this._isInit) return; this._isInit = true; this._image = GameObject.getComponent(this.gameObject, Image) as Image; if (this._image) { this.stateSetup(this._image); if (this.interactable) this._image.setState("normal"); else this._image.setState("disabled"); } } private stateSetup(image: Image) { image.setInteractable(this.interactable); // @marwie : If this piece of code could be moved to the SimpleStateBehavior instanciation location, // Its setup could be eased : // @see https://github.com/felixmariotto/three-mesh-ui/blob/7.1.x/examples/ex__keyboard.js#L407 const normal = this.getFinalColor(image.color, this.colors?.normalColor); const normalState = { state: "normal", attributes: { backgroundColor: normal, backgroundOpacity: normal.alpha, }, }; image.setupState(normalState); const hover = this.getFinalColor(image.color, this.colors?.highlightedColor); const hoverState = { state: "hovered", attributes: { backgroundColor: hover, backgroundOpacity: hover.alpha, }, }; image.setupState(hoverState); const pressed = this.getFinalColor(image.color, this.colors?.pressedColor); const pressedState = { state: "pressed", attributes: { backgroundColor: pressed, backgroundOpacity: pressed.alpha, } }; image.setupState(pressedState); const selected = this.getFinalColor(image.color, this.colors?.selectedColor); const selectedState = { state: "selected", attributes: { backgroundColor: selected, backgroundOpacity: selected.alpha, } }; image.setupState(selectedState); const disabled = this.getFinalColor(image.color, this.colors?.disabledColor); const disabledState = { state: "disabled", attributes: { backgroundColor: disabled, // @marwie, this disabled alpha property doesn't seem to have the opacity requested in unity backgroundOpacity: disabled.alpha } }; image.setupState(disabledState); } private getFinalColor(col: RGBAColor, col2?: RGBAColor): RGBAColor { if (col2) { return col.clone().multiply(col2).convertLinearToSRGB() as RGBAColor; } return col.clone().convertLinearToSRGB() as RGBAColor; } }