@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
238 lines • 11 kB
JavaScript
import { isDevEnvironment, showBalloonWarning } from "../debug/debug.js";
import { generateQRCode } 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 {
static _instance;
/** 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();
}
_fullscreenButton;
/**
* 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) {
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;
}
_muteButton;
/** 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) {
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;
}
_qrButton;
/**
* 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() {
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;
}
hideElementDuringXRSession(element) {
onXRSessionStart(_ => {
element["previous-display"] = element.style.display;
element.style.display = "none";
});
onXRSessionEnd(_ => {
if (element["previous-display"] != undefined)
element.style.display = element["previous-display"];
});
}
}
//# sourceMappingURL=buttons.js.map