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.

293 lines (257 loc) • 12.3 kB
import { isDevEnvironment, showBalloonWarning } from "../debug/debug.js"; import { IContext } from "../engine_types.js"; import { generateQRCode, isMobileDevice } from "../engine_utils.js"; import { onXRSessionEnd, onXRSessionStart } from "../xr/events.js"; import { getIconElement } from "./icons.js"; /** * Use the ButtonsFactory to create buttons with icons and functionality * Get access to the default buttons by using `ButtonsFactory.instance` * The factory will create the buttons if they don't exist yet, and return the existing ones if they do (this allows you to reparent or modify created buttons) */ export class ButtonsFactory { private static _instance?: ButtonsFactory; /** * Get access to the default HTML button factory. * Use this to get or create default Needle Engine buttons that can be added to your HTML UI * If you want to create a new factory and create new button instances instead of shared buttons, use `ButtonsFactory.create()` instead */ static get instance() { return this.getOrCreate(); } /** * Get access to the default HTML button factory. * Use this to get or create default Needle Engine buttons that can be added to your HTML UI * If you want to create a new factory and create new button instances instead of shared buttons, use `ButtonsFactory.create()` instead */ static getOrCreate() { if (!this._instance) { this._instance = new ButtonsFactory(); } return this._instance; } /** create a new buttons factory */ static create() { return new ButtonsFactory(); } private _fullscreenButton?: HTMLButtonElement; /** * Get the fullscreen button (or undefined if it doesn't exist yet). Call {@link ButtonsFactory.createFullscreenButton} to get or create it */ get fullscreenButton() { return this._fullscreenButton; } /** Create a fullscreen button (or return the existing one if it already exists) */ createFullscreenButton(ctx: IContext): HTMLButtonElement | null { if (this._fullscreenButton) { return this._fullscreenButton; } // check for fullscreen support if (!document.fullscreenEnabled) { if (isDevEnvironment()) console.warn("NeedleMenu: Fullscreen button could not be created, device doesn't support the Fullscreen API"); return null; } const button = document.createElement("button"); this._fullscreenButton = button; button.classList.add("fullscreen-button"); button.title = "Click to enter fullscreen mode"; const enterFullscreenIcon = getIconElement("fullscreen"); const exitFullscreenIcon = getIconElement("fullscreen_exit"); button.appendChild(enterFullscreenIcon); button.onclick = () => { if (document.fullscreenElement) { document.exitFullscreen(); } else { if ("webkitRequestFullscreen" in ctx.domElement && typeof ctx.domElement["webkitRequestFullscreen"] === "function") ctx.domElement["webkitRequestFullscreen"](); else if ("requestFullscreen" in ctx.domElement) ctx.domElement.requestFullscreen(); } }; document.addEventListener("fullscreenchange", () => { if (document.fullscreenElement) { enterFullscreenIcon.remove(); button.appendChild(exitFullscreenIcon); button.title = "Click to enter fullscreen mode"; } else { exitFullscreenIcon.remove(); button.appendChild(enterFullscreenIcon); button.title = "Click to exit fullscreen mode"; } }); // xr session started? globalThis.addEventListener("needle-xrsession-start", () => { button.style.display = "none"; }); globalThis.addEventListener("needle-xrsession-end", () => { button.style.display = ""; }); return button; } private _muteButton?: HTMLButtonElement; /** Get the mute button (or undefined if it doesn't exist yet). Call {@link ButtonsFactory.createMuteButton} to get or create it */ get muteButton() { return this._muteButton; } /** Create a mute button (or return the existing one if it already exists) */ createMuteButton(ctx: IContext) { if (this._muteButton) { return this._muteButton; } const button = document.createElement("button"); this._muteButton = button; button.classList.add("mute-button"); button.title = "Click to mute/unmute"; const muteIcon = getIconElement("volume_off"); const unmuteIcon = getIconElement("volume_up"); // save state in session storage (this needs consent) // if (sessionStorage.getItem("muted") === "true") { // ctx.application.muted = true; // } // else { // ctx.application.muted = false; // } if (ctx.application.muted) { button.appendChild(muteIcon); } else { button.appendChild(unmuteIcon); } button.onclick = () => { if (ctx.application.muted) { muteIcon.remove(); button.appendChild(unmuteIcon); ctx.application.muted = false; // sessionStorage.setItem("muted", "false"); } else { unmuteIcon.remove(); button.appendChild(muteIcon); ctx.application.muted = true; // sessionStorage.setItem("muted", "true"); } }; return button; } private _qrButton?: HTMLButtonElement; /** * Get the QR code button (or undefined if it doesn't exist yet). Call {@link ButtonsFactory.createQRCode} to get or create it */ get qrButton() { return this._qrButton; } private _customQRButtonUrl: string | undefined; /** Get or set the QR code button URL - this URL will open when scanning the QR code */ set qrButtonUrl(url: string) { // make sure it's a URL try { new URL(url); this._customQRButtonUrl = url; } catch (e) { console.warn(`[Needle] QR code button URL is not a valid URL '${url}'`); } } get qrButtonUrl() { return this._customQRButtonUrl || window.location.href; } /** Create a QR code button (or return the existing one if it already exists) * The QR code button will show a QR code that can be scanned to open the current page on a phone * The QR code will be generated with the current URL when the button is clicked * @returns the QR code button element */ createQRCode(): HTMLButtonElement { if (this._qrButton) return this._qrButton; const instance = this; const qrCodeButton = document.createElement("button"); this._qrButton = qrCodeButton; qrCodeButton.innerText = "QR Code"; qrCodeButton.prepend(getIconElement("qr_code")); qrCodeButton.title = "Scan this QR code with your phone to open this page"; this.hideElementDuringXRSession(qrCodeButton); const qrCodeContainer = document.createElement("div"); qrCodeContainer.style.cssText = ` position: fixed; display: inline-block; padding: 0.5rem; background-color: white; border-radius: 0.4rem; cursor: pointer; z-index: 1000; box-shadow: 0 0 12px rgba(0, 0, 0, 0.2); `; const qrCodeElement = document.createElement("div"); qrCodeElement.classList.add("qr-code-container"); qrCodeContainer.appendChild(qrCodeElement); qrCodeButton.addEventListener("click", () => { if (qrCodeContainer.parentNode) return hideQRCode(); if (isDevEnvironment() && window.location.href.includes("://localhost")) { showBalloonWarning("To access your website from another device in the same local network you have to use the IP address instead of localhost. The IP address is logged in your development server console when you start the server."); } showQRCode(); }); /** shows the QRCode near the button */ async function showQRCode() { // generate the qr code when the button is clicked // this ensures that we get the QRcode with the latest URL await generateAndInsertQRCode(); // TODO: make sure it doesnt overflow the screen // we need to add the qrCodeContainer to the body to get the correct size document.body.appendChild(qrCodeContainer); const containerRect = qrCodeElement.getBoundingClientRect(); const buttonRect = qrCodeButton.getBoundingClientRect(); qrCodeContainer.style.left = (buttonRect.left + buttonRect.width * .5 - containerRect.width * .5) + "px"; const isButtonInTopHalf = buttonRect.top < containerRect.height; const padding = "1.3rem"; if (isButtonInTopHalf) qrCodeContainer.style.top = `calc(${buttonRect.bottom}px + ${qrCodeContainer.style.padding} + 0.0rem)`; else qrCodeContainer.style.top = `calc(${buttonRect.top - containerRect.height}px - ${qrCodeContainer.style.padding} - ${padding})`; qrCodeContainer.style.opacity = "0"; qrCodeContainer.style.pointerEvents = "all"; qrCodeContainer.style.transition = "opacity 0.2s ease-in-out"; // context click to hide the QR code again, if we dont timeout the event will be triggered immediately setTimeout(() => { qrCodeContainer.style.opacity = "1"; window.addEventListener("click", hideQRCode, { once: true }) }); window.addEventListener("resize", hideQRCode); window.addEventListener("scroll", hideQRCode); // if we're in fullscreen: if (document.fullscreenElement) { document.fullscreenElement.appendChild(qrCodeContainer); } else document.body.appendChild(qrCodeContainer); } /** hides to QRCode overlay and unsubscribes from events */ function hideQRCode() { qrCodeContainer.style.pointerEvents = "none"; qrCodeContainer.style.transition = "opacity 0.2s"; qrCodeContainer.style.opacity = "0"; setTimeout(() => qrCodeContainer.parentNode?.removeChild(qrCodeContainer), 500); window.removeEventListener("click", hideQRCode); window.removeEventListener("resize", hideQRCode); window.removeEventListener("scroll", hideQRCode); }; /** generates a QR code and inserts it into the qrCodeElement */ async function generateAndInsertQRCode() { const size = 200; const code = await generateQRCode({ text: instance.qrButtonUrl, width: size, height: size, }); qrCodeElement.innerHTML = ""; qrCodeElement.appendChild(code); } // lazily create the qr button qrCodeButton.addEventListener("pointerenter", () => { generateAndInsertQRCode(); }, { once: true }); return qrCodeButton; } private hideElementDuringXRSession(element: HTMLElement) { onXRSessionStart(_ => { element["previous-display"] = element.style.display; element.style.display = "none"; }); onXRSessionEnd(_ => { if (element["previous-display"] != undefined) element.style.display = element["previous-display"]; }); } }