UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

241 lines (201 loc) • 8.2 kB
import RadialMenuView from "../../../../../view/elements/radial/RadialMenu.js"; import { MouseEvents } from "../../../../input/devices/events/MouseEvents.js"; import { TouchEvents } from "../../../../input/devices/events/TouchEvents.js"; import InputController from "../../../../input/ecs/components/InputController.js"; import { SerializationMetadata } from "../../../components/SerializationMetadata.js"; import Entity from "../../../Entity.js"; import { EntityFlags } from "../../../EntityFlags.js"; import GUIElement from "../../GUIElement.js"; import { animateAppearance } from "./AnimateAppearance.js"; import { animateDisappearance } from "./AnimateDisappearance.js"; import { RadialMenuSettings } from "./RadialMenuSettings.js"; /** * * @param {EntityComponentDataset} ecd * @param {Vector2} position Center of the menu * @param {RadialMenuElementDefinition[]} items Menu options that are to be presented * @param {number} [innerRadius] see {@link RadialMenuElementDefinition} for more details * @param {number} [outerRadius] see {@link RadialMenuElementDefinition} for more details * @param {number} [backdropInnerRadius] * @param {number} [backdropOuterRadius] * @param {number} [focusWidth] * @param {string} [backgroundColor] * @param {number} [padding] see {@link RadialMenuElementDefinition} for more details * @param {function(RadialMenuView)} [hook] * @param {function} [closeCallback] * @param {boolean} [autoLayout=true] * @param {string[]} [classList] * @param {Vector2} [pointerPosition] Used to set initial selection, same as position is not set * @param {number} [selectionDistance] minimum distance in pixels at which selection registers * @returns {Entity} */ export function makeMenu({ ecd, position, items, innerRadius = RadialMenuSettings.innerRadius, outerRadius = RadialMenuSettings.outerRadius, backdropInnerRadius = RadialMenuSettings.backdropInnerRadius, backdropOuterRadius = RadialMenuSettings.backdropOuterRadius, focusWidth = RadialMenuSettings.focusWidth, backgroundColor, padding = RadialMenuSettings.padding, hook, closeCallback, autoLayout = true, classList = [], pointerPosition = position, selectionDistance = 20 }) { if (position === undefined) { throw new Error("Required parameter 'position' is undefined"); } const origin = position.clone(); const mainView = new RadialMenuView(items, { innerRadius, outerRadius, backdropInnerRadius, backdropOuterRadius, focusWidth, backgroundColor, padding, classList }); if (autoLayout !== false) { mainView.autoLayout(); } mainView.render(); mainView.addClass("radial-context-menu"); const mainStyle = mainView.el.style; mainStyle.cursor = "none"; // position the menu relative to cursor mainView.position.set( origin.x - mainView.width / 2, origin.y - mainView.height / 2 ); if (typeof hook === "function") { // invoke menu creation hook try { hook(mainView); } catch (e) { // quietly eat the error console.error(`Failed to execute hook`, e); } } //play appearance animation const appearanceAnimationFinished = animateAppearance(mainView, ecd); const builder = new Entity(); //prevent menu serialization builder.add(SerializationMetadata.Transient); function cleanup() { if (typeof closeCallback === "function") { closeCallback(); } clearHandlers(); } function die() { builder.sendEvent("radial-menu-died"); //first remove input controller to prevent further interactions builder.removeComponent(InputController); //return cursor mainStyle.cursor = "auto"; //schedule disappearance animation const disappeared = appearanceAnimationFinished .then(() => animateDisappearance(mainView, ecd)); //execute cleanup cleanup(); disappeared.then(function () { //clear all selection mainView.resetElementSelection(); //destroy entity builder.destroy(); }); } /** * * @param {number} x * @param {number} y */ function setMarkerPosition(x, y) { mainView.linePosition.set(x, y); const distance = Math.sqrt(x * x + y * y); if (distance > selectionDistance) { //figure out angle let a = Math.atan2(y, x); while (a < 0) { // convert negative angle values to positive a += Math.PI * 2; } a = Math.PI * 2 - a; mainView.selectByAngle(a); } else { mainView.resetElementSelection(); } } /** * * @param {Vector2} target */ function moveSelector(target) { const delta = target.clone().sub(origin); const distanceFromCenter = delta.length(); const outerMarginMultiplier = 1.3; const outerMarginConstant = 120; const maximumAllowedDistance = mainView.focusOuterRadius * outerMarginMultiplier + outerMarginConstant; if (distanceFromCenter > maximumAllowedDistance) { //too far, kill self die(); } else { setMarkerPosition(delta.x, delta.y); } } moveSelector(pointerPosition); let selectionMade = false; /** * * @param {Vector2} p * @param {Event|MouseEvent} event */ function performSelection(p, event) { if (selectionMade) { //already done return; } selectionMade = true; try { mainView.runSelected(); } catch (e) { console.error("Failed to execute selected menu option: ", e); } die(); } builder.add(GUIElement.fromView(mainView)); builder.add(new InputController([ { path: "pointer/on/move", listener: function (p, event) { //calculate delta from the origin moveSelector(p); }, exclusive: true }, { path: "pointer/on/up", listener: performSelection, exclusive: true, priority: 10 } ])); function clearHandlers() { window.removeEventListener(MouseEvents.Up, performSelection); window.removeEventListener(TouchEvents.End, performSelection); } //workaround for pointer leaving the screen area before selection is made window.addEventListener(MouseEvents.Up, performSelection); window.addEventListener(TouchEvents.End, performSelection); // to simplify usage of this class, we turn on component registration on the builder builder.setFlag(EntityFlags.RegisterComponents); // actually build the menu builder.build(ecd); return builder; }