@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
text/typescript
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 {
colorMultiplier!: 1;
disabledColor!: RGBAColor;
fadeDuration!: number;
highlightedColor!: RGBAColor;
normalColor!: RGBAColor;
pressedColor!: 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();
}
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);
}
}
}
colors?: ButtonColors;
transition?: Transition;
animationTriggers?: AnimationTriggers;
animator?: Animator;
// @serializable(Image)
// image? : Image;
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;
}
}