@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.
223 lines (197 loc) • 7.61 kB
text/typescript
import { isDevEnvironment } from "../debug/index.js";
import { ButtonsFactory } from "./buttons.js";
import { iconFontUrl, loadFont } from "./fonts.js";
import { WebXRButtonFactory } from "./WebXRButtons.js";
declare global {
interface HTMLElementTagNameMap {
"needle-button": NeedleButtonElement;
}
}
const htmlTagName = "needle-button";
const isDev = isDevEnvironment();
/**
* [<needle-button>](https://engine.needle.tools/docs/api/NeedleButtonElement) is a web component for easily adding AR, VR, Quicklook, or QR code buttons to your website without writing JavaScript code.
*
* The button automatically handles session management and displays appropriate UI based on device capabilities.
* It comes with default styling (glassmorphism design) but can be fully customized with CSS.
*
* **Supported button types:**
* - `ar` - WebXR AR session button
* - `vr` - WebXR VR session button
* - `quicklook` - Apple AR Quick Look button (iOS only)
* - `qrcode` - QR code sharing button
*
* @example Basic AR/VR buttons
* ```html
* <needle-engine src="scene.glb"></needle-engine>
* <needle-button ar></needle-button>
* <needle-button vr></needle-button>
* <needle-button quicklook></needle-button>
* ```
*
* @example Custom button labels
* ```html
* <needle-button ar>Start AR Experience</needle-button>
* <needle-button vr>Enter VR Mode</needle-button>
* <needle-button quicklook>View in AR</needle-button>
* ```
*
* @example Custom styling
* ```html
* <style>
* needle-button {
* background-color: #ff6b6b;
* color: white;
* border-radius: 8px;
* padding: 1rem 2rem;
* }
* needle-button:hover {
* background-color: #ff5252;
* }
* </style>
* <needle-button ar>Start AR</needle-button>
* ```
*
* @example Unstyled button (for complete custom styling)
* ```html
* <needle-button ar unstyled>
* <span class="my-icon">🥽</span>
* Launch AR
* </needle-button>
* ```
*
* @see {@link NeedleEngineWebComponent} for the main <needle-engine> element
* @see {@link NeedleMenu} for the built-in menu component that can display similar buttons
*/
export class NeedleButtonElement extends HTMLElement {
static observedAttributes = ["ar", "vr", "quicklook", "qrcode"];
constructor() {
super();
this.removeEventListener("click", this.#onclick);
this.addEventListener("click", this.#onclick);
}
attributeChangedCallback(_name: string, _oldValue: string, _newValue: string) {
this.#update()
}
#root!: ShadowRoot;
#slot!: HTMLSlotElement;
/** These are the default styles that can be overridden by the user from the outside by styling <needle-button> */
#styles!: HTMLStyleElement;
/** This is the button that was generated using one of the factories */
#button: HTMLButtonElement | undefined;
/** If AR or VR is requested we create and use the webxr button factory to create a button with default behaviour */
#webxrfactory: WebXRButtonFactory | undefined;
#buttonfactory: ButtonsFactory | undefined;
#observer: MutationObserver | undefined;
#update() {
this.#button?.remove();
if (this.getAttribute("ar") != null) {
this.#webxrfactory ??= new WebXRButtonFactory()
this.#button = this.#webxrfactory.createARButton();
this.setAttribute("aria-label", "Enter augmented reality mode");
}
else if (this.getAttribute("vr") != null) {
this.#webxrfactory ??= new WebXRButtonFactory()
this.#button = this.#webxrfactory.createVRButton();
this.setAttribute("aria-label", "Enter virtual reality mode");
}
else if (this.getAttribute("quicklook") != null) {
this.#webxrfactory ??= new WebXRButtonFactory()
this.#button = this.#webxrfactory.createQuicklookButton();
this.setAttribute("aria-label", "View in AR with Apple Quick Look");
}
else if (this.getAttribute("qrcode") != null) {
this.#buttonfactory ??= new ButtonsFactory();
this.#button = this.#buttonfactory.createQRCode({ anchorElement: this });
this.setAttribute("aria-label", "Share application with QR code");
}
else {
if (isDev) {
console.warn("No button type specified for <needle-button>. Use either ar, vr or quicklook attribute.")
}
else {
console.debug("No button type specified for <needle-button>. Use either ar, vr or quicklook attribute.")
}
this.setAttribute("aria-label", "Needle Button with no specified type");
return;
}
this.#root ??= this.attachShadow({ mode: "open" });
this.#slot ??= document.createElement("slot");
this.#styles ??= document.createElement("style");
this.#styles.innerHTML = `
button {
all: unset;
}
`;
const hasUnstyledAttribute = this.getAttribute("unstyled") != undefined;
if (!hasUnstyledAttribute) {
this.#styles.innerHTML += `
:host {
display: inline-flex;
align-items: center;
justify-content: center;
width: fit-content;
padding: 0.4rem .5rem;
border-radius: 100vw;
background: rgba(245, 245, 245, .8);
backdrop-filter: blur(10px);
cursor: pointer;
color: black;
outline: rgba(0,0,0,.05) 1px solid;
transition: all .2s;
}
:host(:hover) {
background: rgba(255, 255, 255, 1);
transition: background .2s;
}
slot {
display: flex;
align-items: center;
justify-content: center;
gap: .5rem;
}
`
}
/**
* We now structure the results as follows:
* <button>
* <slot>
* <original_button_content>
* </slot>
* </button>
*/
this.#slot.innerHTML = this.#button.innerHTML;
this.#slot.style.cssText = `display: flex; align-items: center; justify-content: center;`
this.#button.innerHTML = this.#slot.outerHTML;
this.#root.innerHTML = this.#button.outerHTML;
this.#root.prepend(this.#styles);
loadFont(iconFontUrl, { element: this.#root });
this.#observer?.disconnect();
this.#observer ??= new MutationObserver(() => this.#updateVisibility());
this.#observer.observe(this.#button, { attributes: true });
if (isDev) {
console.log("Needle Button updated", this);
}
}
#updateVisibility() {
if (this.#button) {
if (this.#button.style.display === "none") {
this.style.display = "none";
}
else if (this.style.display === "none") {
this.style.display = "";
}
}
}
#onclick = (_ev: MouseEvent) => {
if (isDev) {
console.log("Needle Button clicked", { defaultPrevented: _ev.defaultPrevented, hasButton: !!this.#button });
}
if (_ev.defaultPrevented) return;
if (this.#button) {
this.#button.click()
}
}
}
if (typeof window !== "undefined" && !window.customElements.get(htmlTagName))
window.customElements.define(htmlTagName, NeedleButtonElement);