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.

374 lines (336 loc) • 16.3 kB
import { needleLogoOnlySVG } from "../assets/index.js" import { isDevEnvironment, showBalloonWarning } from "../debug/index.js"; import { hasCommercialLicense, hasProLicense, runtimeLicenseCheckPromise } from "../engine_license.js"; import { Mathf } from "../engine_math.js"; import { LoadingProgressArgs } from "../engine_setup.js"; import { getParam } from "../engine_utils.js"; const debug = getParam("debugloading"); const debugRendering = getParam("debugloadingrendering"); const debugLicense = getParam("debuglicense"); declare type LoadingStyleOption = "dark" | "light" | "auto"; /** @internal */ export class LoadingElementOptions { className?: string; additionalClasses?: string[]; } /** @internal */ export interface ILoadingViewHandler { onLoadingBegin(message?: string) onLoadingUpdate(progress: LoadingProgressArgs | number, message?: string); onLoadingFinished(message?: string); setMessage(string: string); } let currentFileProgress = 0; let currentFileName: string; /** @internal */ export function calculateProgress01(progress: LoadingProgressArgs) { if (debug) console.log(progress.progress.loaded.toFixed(0) + "/" + progress.progress.total.toFixed(0), progress); const count = progress.count; const total: number | undefined = progress.progress.total; // if the progress event total amount is unknown / not reported // we slowly move the progress bar forward if (total === 0 || total === undefined) { // reset the temp progress when the file has changed if (currentFileName !== progress.name) currentFileProgress = 0; currentFileName = progress.name; // slowly move the progress bar forward currentFileProgress += (1 - currentFileProgress) * .001; if (debug) showBalloonWarning("Loading " + progress.name + " did not report total size"); } else { currentFileProgress = progress.progress.loaded / total; } const prog = progress.index / count + currentFileProgress / count; return Mathf.clamp01(prog); } /** @internal */ export class EngineLoadingView implements ILoadingViewHandler { static LoadingContainerClassName = "loading"; // the raw progress loadingProgress: number = 0; /** Usually the NeedleEngineHTMLElement */ private _element: HTMLElement; private _progress: number = 0; private _allowCustomLoadingElement: boolean = true; private _loadingElement?: HTMLElement; private _loadingTextContainer: HTMLElement | null = null; private _loadingBar: HTMLElement | null = null; private _messageContainer: HTMLElement | null = null; private _loadingElementOptions?: LoadingElementOptions; /** * Creates a new loading view * @param owner the element that will contain the loading view (should be the NeedleEngineHTMLElement) */ constructor(owner: HTMLElement, opts?: LoadingElementOptions) { this._element = owner; this._loadingElementOptions = opts; } async onLoadingBegin(message?: string) { const _element = this._element.shadowRoot || this._element; if (debug) console.warn("Begin Loading") if (!this._loadingElement) { for (let i = 0; i < _element.children.length; i++) { const el = _element.children[i] as HTMLElement; if (el.classList.contains(EngineLoadingView.LoadingContainerClassName)) { if (!this._allowCustomLoadingElement) { if (debug) console.warn("Remove custom loading container") _element.removeChild(el); continue; } this._loadingElement = this.createLoadingElement(el); } } if (!this._loadingElement) this._loadingElement = this.createLoadingElement(); } this._progress = 0; this.loadingProgress = 0; this._loadingElement.style.display = "flex"; _element.appendChild(this._loadingElement); this.smoothProgressLoop(); this.setMessage(message ?? ""); } onLoadingUpdate(progress: LoadingProgressArgs | ProgressEvent | number, message?: string) { if (!this._loadingElement?.parentNode) { return; } // console.log(callback.name, callback.progress.loaded / callback.progress.total, callback.index + "/" + callback.count); let total01 = 0; if (typeof progress === "number") { total01 = progress; } else { if ("index" in progress) total01 = calculateProgress01(progress); if (!message && "name" in progress) this.setMessage("loading " + progress.name); } this.loadingProgress = total01; if (message) this.setMessage(message); this.updateDisplay(); } onLoadingFinished() { if (debug) console.warn("Finished Loading"); if (!debugRendering) { this.loadingProgress = 1; this.onDoneLoading(); } } setMessage(message: string) { if (this._messageContainer) { this._messageContainer.innerText = message; } } private _progressLoop: any; private smoothProgressLoop() { if (this._progressLoop) return; let dt = 1 / 12; if (debugRendering) { dt = 1 / 500; if (typeof debugRendering === "number") dt *= debugRendering; } this._progressLoop = setInterval(() => { // increase loading speed when almost done if (this.loadingProgress >= .95 && !debugRendering) dt = .9; this._progress = Mathf.lerp(this._progress, this.loadingProgress, dt * this.loadingProgress); this.updateDisplay(); }, dt); } private onDoneLoading() { if (this._loadingElement) { if (debug) console.log("Hiding loading element"); this._loadingElement.style.display = "none"; this._loadingElement.remove(); } if (this._progressLoop) clearInterval(this._progressLoop); this._progressLoop = null; } private updateDisplay() { const t = this._progress; const percent = (t * 100).toFixed(0) + "%"; if (this._loadingBar) { this._loadingBar.style.width = t * 100 + "%"; } if (this._loadingTextContainer) this._loadingTextContainer.textContent = percent; } private createLoadingElement(existing?: HTMLElement) { if (debug && !existing) console.log("Creating loading element"); this._loadingElement = existing || document.createElement("div"); let loadingStyle: LoadingStyleOption = this._element.getAttribute("loading-style") as LoadingStyleOption; // if nothing is defined OR loadingStyle is set to auto if (!loadingStyle || loadingStyle === "auto") { if (window.matchMedia('(prefers-color-scheme: dark)').matches) loadingStyle = "dark"; else loadingStyle = "light"; } const hasLicense = hasProLicense(); if (!existing) { this._loadingElement.style.position = "absolute"; this._loadingElement.style.width = "100%"; this._loadingElement.style.height = "100%"; this._loadingElement.style.left = "0"; this._loadingElement.style.top = "0"; const loadingBackgroundColor = this._element.getAttribute("loading-background"); if (loadingBackgroundColor) { this._loadingElement.style.background = loadingBackgroundColor; } else this._loadingElement.style.backgroundColor = "transparent"; this._loadingElement.style.display = "flex"; this._loadingElement.style.alignItems = "center"; this._loadingElement.style.justifyContent = "center"; this._loadingElement.style.zIndex = Number.MAX_SAFE_INTEGER.toString(); this._loadingElement.style.flexDirection = "column"; this._loadingElement.style.pointerEvents = "none"; this._loadingElement.style.color = "white"; this._loadingElement.style.fontFamily = 'system-ui, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'; this._loadingElement.style.fontSize = "1rem"; if (loadingStyle === "light") this._loadingElement.style.color = "rgba(0,0,0,.6)"; else this._loadingElement.style.color = "rgba(255,255,255,.3)"; } const className = this._loadingElementOptions?.className ?? EngineLoadingView.LoadingContainerClassName; this._loadingElement.classList.add(className); if (this._loadingElementOptions?.additionalClasses) { for (const c of this._loadingElementOptions.additionalClasses) { this._loadingElement.classList.add(c); } } const content = document.createElement("div"); this._loadingElement.appendChild(content); const logo = document.createElement("img"); const logoSize = 120; logo.style.width = `${logoSize}px`; logo.style.height = `${logoSize}px`; logo.style.paddingTop = "20px"; logo.style.paddingBottom = "10px"; logo.style.margin = "0px"; logo.style.userSelect = "none"; logo.style.objectFit = "contain"; logo.style.transition = "transform 1.5s ease-out, opacity .3s ease-in-out"; logo.style.transform = "translateY(30px)"; logo.style.opacity = "0.05"; setTimeout(() => { logo.style.opacity = "1"; logo.style.transform = "translateY(0px)"; }, 1); logo.src = needleLogoOnlySVG; let isUsingCustomLogo = false; if (hasLicense && this._element) { const customLogo = this._element.getAttribute("loading-logo-src"); if (customLogo) { isUsingCustomLogo = true; logo.src = customLogo; } } content.appendChild(logo); const details = document.createElement("div"); details.style.cssText = ` display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; opacity: 0; transition: opacity 1s ease-in-out 4s; `; setTimeout(() => { details.style.opacity = "1"; }, 1); this._loadingElement.appendChild(details); const loadingBarContainer = document.createElement("div"); const maxWidth = 100; loadingBarContainer.style.display = "flex"; loadingBarContainer.style.width = maxWidth + "%"; loadingBarContainer.style.height = "3px"; loadingBarContainer.style.position = "absolute"; loadingBarContainer.style.left = "0"; loadingBarContainer.style.bottom = "0px"; loadingBarContainer.style.opacity = "0"; loadingBarContainer.style.transition = "opacity 1s ease-in-out 2s"; setTimeout(() => { loadingBarContainer.style.opacity = "1"; }, 1); if (loadingStyle === "light") loadingBarContainer.style.backgroundColor = "rgba(0,0,0,.2)" else loadingBarContainer.style.backgroundColor = "rgba(255,255,255,.2)" // loadingBarContainer.style.alignItems = "center"; this._loadingElement.appendChild(loadingBarContainer); this._loadingBar = document.createElement("div"); loadingBarContainer.appendChild(this._loadingBar); const getGradientPos = function (t: number): string { return Mathf.lerp(0, maxWidth, t) + "%"; } this._loadingBar.style.background = "#66A22F"; // `linear-gradient(90deg, #204f49 ${getGradientPos(0)}, #0BA398 ${getGradientPos(.3)}, #66A22F ${getGradientPos(.6)}, #D7DB0A ${getGradientPos(1)})`; this._loadingBar.style.backgroundAttachment = "fixed"; this._loadingBar.style.width = "0%"; this._loadingBar.style.height = "100%"; if (hasLicense && this._element) { const primaryColor = this._element.getAttribute("primary-color"); const secondaryColor = this._element.getAttribute("secondary-color"); if (primaryColor && secondaryColor) { this._loadingBar.style.background = `linear-gradient(90deg, ${primaryColor} ${getGradientPos(0)}, ${secondaryColor} ${getGradientPos(1)})`; } else if (primaryColor) { this._loadingBar.style.background = primaryColor; } else if (secondaryColor) { this._loadingBar.style.background = secondaryColor; } } // this._loadingTextContainer = document.createElement("div"); // this._loadingTextContainer.style.display = "flex"; // this._loadingTextContainer.style.justifyContent = "center"; // this._loadingTextContainer.style.marginTop = ".2rem"; // details.appendChild(this._loadingTextContainer); // const messageContainer = document.createElement("div"); // this._messageContainer = messageContainer; // messageContainer.style.display = "flex"; // messageContainer.style.fontSize = ".8rem"; // messageContainer.style.paddingTop = ".1rem"; // // messageContainer.style.border = "1px solid rgba(255,255,255,.1)"; // messageContainer.style.justifyContent = "center"; // details.appendChild(messageContainer); // if (hasLicense && this._element) { // const loadingTextColor = this._element.getAttribute("loading-text-color"); // if (loadingTextColor) { // messageContainer.style.color = loadingTextColor; // } // } this.handleRuntimeLicense(this._loadingElement); return this._loadingElement; } private async handleRuntimeLicense(loadingElement: HTMLElement) { // First check if we have a commercial license let commercialLicense = hasCommercialLicense(); // if it's the case then we don't need to perform a runtime check if (commercialLicense) return; // If we don't have a commercial license, then we need to display our message if (debugLicense) console.log("Loading UI has commercial license?", commercialLicense); const nonCommercialContainer = document.createElement("div"); nonCommercialContainer.style.paddingTop = ".6em"; nonCommercialContainer.style.fontSize = ".8em"; nonCommercialContainer.style.textTransform = "uppercase"; nonCommercialContainer.innerText = "NEEDLE ENGINE NON COMMERCIAL VERSION\nCLICK HERE TO GET A LICENSE"; nonCommercialContainer.style.cursor = "pointer"; nonCommercialContainer.style.userSelect = "none"; nonCommercialContainer.style.textAlign = "center"; nonCommercialContainer.style.pointerEvents = "all"; nonCommercialContainer.addEventListener("click", () => window.open("https://needle.tools/pricing", "_self")); nonCommercialContainer.style.opacity = "0"; loadingElement.appendChild(nonCommercialContainer); // Use the runtime license check if (!isDevEnvironment() && runtimeLicenseCheckPromise) { if (debugLicense) console.log("Waiting for runtime license check"); await runtimeLicenseCheckPromise; commercialLicense = hasCommercialLicense(); } if (commercialLicense) return; nonCommercialContainer.style.transition = "opacity .5s ease-in-out"; nonCommercialContainer.style.opacity = "1"; } }