@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.
182 lines (159 loc) • 6.02 kB
text/typescript
import { isDevEnvironment } from "../debug/index.js";
import { iconFontUrl, loadFont } from "./fonts.js";
import { WebXRButtonFactory } from "./WebXRButtons.js";
const htmlTagName = "needle-button";
const isDev = isDevEnvironment();
/**
* A <needle-button> can be used to simply add VR, AR or Quicklook buttons to your website without having to write any code.
* @example
* ```html
* <needle-button ar></needle-button>
* <needle-button vr></needle-button>
* <needle-button quicklook></needle-button>
* ```
*
* @example custom label
* ```html
* <needle-button ar>Start AR</needle-button>
* <needle-button vr>Start VR</needle-button>
* <needle-button quicklook>View in AR</needle-button>
* ```
*
* @example custom styling
* ```html
* <!-- You can either style the element directly or use a CSS stylesheet -->
* <style>
* needle-button {
* background-color: red;
* color: white;
* }
* </style>
* <needle-button ar>Start AR</needle-button>
* ```
*/
export class NeedleButtonElement extends HTMLElement {
static observedAttributes = ["ar", "vr", "quicklook"];
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;
#observer: MutationObserver | undefined;
#update() {
this.#button?.remove();
if (this.getAttribute("ar") != null) {
this.#webxrfactory ??= new WebXRButtonFactory()
this.#button = this.#webxrfactory.createARButton();
}
else if (this.getAttribute("vr") != null) {
this.#webxrfactory ??= new WebXRButtonFactory()
this.#button = this.#webxrfactory.createVRButton();
}
else if (this.getAttribute("quicklook") != null) {
this.#webxrfactory ??= new WebXRButtonFactory()
this.#button = this.#webxrfactory.createQuicklookButton();
}
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.")
}
return;
}
this.#root ??= this.attachShadow({ mode: "open" });
this.#slot ??= document.createElement("slot");
this.#styles ??= document.createElement("style");
this.#styles.innerHTML = `
button {
all: initial;
cursor: inherit;
color: inherit;
font-family: inherit;
gap: inherit;
white-space: nowrap;
}
`;
const hasUnstyledAttribute = this.getAttribute("unstyled") != undefined;
if (!hasUnstyledAttribute) {
this.#styles.innerHTML += `
:host {
display: inline-block;
background: rgba(255, 255, 255, .8);
backdrop-filter: blur(10px);
width: fit-content;
transition: background .2s;
cursor: pointer;
padding: 0.4rem .5rem;
border-radius: 0.8rem;
color: black;
background: rgba(245, 245, 245, .8);
outline: rgba(0,0,0,.05) 1px solid;
}
: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")
}
}
#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")
}
if (_ev.defaultPrevented) return;
if (this.#button) {
this.#button.click()
}
}
}
if (typeof window !== "undefined" && !window.customElements.get(htmlTagName))
window.customElements.define(htmlTagName, NeedleButtonElement);