@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
text/typescript
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";
}
}