@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.
405 lines (363 loc) • 16.8 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 _loadingBarFinishedColor: string | 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");
// animate alpha to 0
const element = this._loadingElement;
element.animate([
{ opacity: 1 },
{ opacity: 0 }
], {
duration: 200,
easing: 'ease-in-out',
}).addEventListener('finish', () => {
element.style.display = "none";
element.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(t >= 1 && this._loadingBarFinishedColor) {
this._loadingBar.style.background = this._loadingBarFinishedColor;
}
}
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";
this._loadingElement.style.overflow = "hidden";
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 = "0";
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");
content.style.cssText = `
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
pointer-events: none;
`
this._loadingElement.appendChild(content);
const poster = this._element.getAttribute("poster");
if (poster !== null && poster !== "0") {
const backgroundImage = document.createElement("div");
const backgroundBlur = poster?.length ? "0px" : "50px";
backgroundImage.style.cssText = `
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: -1;
overflow: hidden;
margin: -${backgroundBlur};
background: url('${poster?.length ? poster : "/include/poster.webp"}') center center no-repeat;
background-size: cover;
filter: blur(${backgroundBlur});
`;
this._loadingElement.appendChild(backgroundImage);
}
const logo = document.createElement("img");
const logoWidth = "80%";
const logoHeight = "15%";
const logoDelay = ".2s";
logo.style.userSelect = "none";
logo.style.objectFit = "contain";
logo.style.transform = "translateY(30px)";
logo.style.opacity = "0.0000001";
logo.style.transition = `transform 1s ease-out ${logoDelay}, opacity .3s ease-in-out ${logoDelay}`;
logo.src = needleLogoOnlySVG;
let isUsingCustomLogo = false;
if (hasLicense && this._element) {
const customLogo = this._element.getAttribute("logo-src");
if (customLogo) {
isUsingCustomLogo = true;
logo.src = customLogo;
setTimeout(() => {
logo.style.opacity = "1";
logo.style.transform = "translateY(0px)";
}, 1);
}
}
logo.style.width = `${logoWidth}`;
logo.style.height = `min(1000px, max(${logoHeight}, 50px))`;
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 = "5px";
loadingBarContainer.style.position = "absolute";
loadingBarContainer.style.left = "0";
loadingBarContainer.style.top = "0px";
loadingBarContainer.style.opacity = "0";
loadingBarContainer.style.transition = "opacity 1s ease-in-out";
loadingBarContainer.style.backgroundColor = "rgba(240,240,240,.5)"
setTimeout(() => { loadingBarContainer.style.opacity = "1"; }, 1);
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) + "%";
}
// `linear-gradient(90deg, #204f49 ${getGradientPos(0)}, #0BA398 ${getGradientPos(.3)}, #66A22F ${getGradientPos(.6)}, #D7DB0A ${getGradientPos(1)})`;
this._loadingBar.style.backgroundAttachment = "fixed";
this._loadingBar.style.background = "#c4c4c4ab";
this._loadingBarFinishedColor = "#ddddddab";
this._loadingBar.style.width = "0%";
this._loadingBar.style.height = "100%";
// 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";
// }
}