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

263 lines (231 loc) • 11.2 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.getOrCreate()` * 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 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; } /** 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 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: 1rem; 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 (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."); } 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; if (isButtonInTopHalf) qrCodeContainer.style.top = `calc(${buttonRect.bottom}px + ${qrCodeContainer.style.padding} * .6)`; else qrCodeContainer.style.top = `calc(${buttonRect.top - containerRect.height}px - ${qrCodeContainer.style.padding} * 2.5)`; 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: window.location.href, 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"]; }); } }