diff --git a/example/assets/images/loading-bg.jpg b/example/assets/images/loading-bg.jpg new file mode 100644 index 00000000..54fc5814 Binary files /dev/null and b/example/assets/images/loading-bg.jpg differ diff --git a/example/multi-user-3d-web-experience/client/src/index.ts b/example/multi-user-3d-web-experience/client/src/index.ts index 6711141c..9bf80df7 100644 --- a/example/multi-user-3d-web-experience/client/src/index.ts +++ b/example/multi-user-3d-web-experience/client/src/index.ts @@ -1,6 +1,7 @@ import { Networked3dWebExperienceClient } from "@mml-io/3d-web-experience-client"; import hdrJpgUrl from "../../../assets/hdr/puresky_2k.jpg"; +import loadingBackground from "../../../assets/images/loading-bg.jpg"; import airAnimationFileUrl from "../../../assets/models/anim_air.glb"; import doubleJumpAnimationFileUrl from "../../../assets/models/anim_double_jump.glb"; import idleAnimationFileUrl from "../../../assets/models/anim_idle.glb"; @@ -33,6 +34,14 @@ const app = new Networked3dWebExperienceClient(holder, { avatarConfiguration: { availableAvatars: [], }, + loadingScreen: { + background: "#424242", + color: "#ffffff", + backgroundImageUrl: loadingBackground, + backgroundBlurAmount: 12, + title: "3D Web Experience", + subtitle: "Powered by Metaverse Markup Language", + }, }); app.update(); diff --git a/packages/3d-web-client-core/src/index.ts b/packages/3d-web-client-core/src/index.ts index 631b53f6..71954aad 100644 --- a/packages/3d-web-client-core/src/index.ts +++ b/packages/3d-web-client-core/src/index.ts @@ -14,6 +14,6 @@ export { TimeManager } from "./time/TimeManager"; export { CollisionsManager } from "./collisions/CollisionsManager"; export { Sun } from "./sun/Sun"; export { GroundPlane } from "./ground-plane/GroundPlane"; -export { LoadingScreen } from "./loading-screen/LoadingScreen"; +export { LoadingScreenConfig, LoadingScreen } from "./loading-screen/LoadingScreen"; export { ErrorScreen } from "./error-screen/ErrorScreen"; export { EnvironmentConfiguration } from "./rendering/composer"; diff --git a/packages/3d-web-client-core/src/loading-screen/LoadingScreen.ts b/packages/3d-web-client-core/src/loading-screen/LoadingScreen.ts index e0d9ac5a..804cac39 100644 --- a/packages/3d-web-client-core/src/loading-screen/LoadingScreen.ts +++ b/packages/3d-web-client-core/src/loading-screen/LoadingScreen.ts @@ -1,9 +1,29 @@ import { LoadingProgressManager } from "mml-web"; +export type LoadingScreenConfig = { + background?: string; + backgroundImageUrl?: string; + backgroundBlurAmount?: number; + overlayLayers?: Array<{ + overlayImageUrl: string; + overlayAnchor: "top-left" | "top-right" | "bottom-left" | "bottom-right"; + overlayOffset?: { x: number; y: number }; + }>; + title?: string; + subtitle?: string; + color?: string; +}; + export class LoadingScreen { public readonly element: HTMLDivElement; - private loadingBannerText: HTMLDivElement; + private readonly backgroundBlur: HTMLDivElement; + + private overlayLayers: HTMLDivElement[] = []; + + private loadingBanner: HTMLDivElement; + private loadingBannerTitle: HTMLDivElement; + private loadingBannerSubtitle: HTMLDivElement; private progressBarBackground: HTMLDivElement; private progressBarHolder: HTMLDivElement; @@ -21,57 +41,148 @@ export class LoadingScreen { private loadingCallback: () => void; private disposed: boolean = false; - constructor(private loadingProgressManager: LoadingProgressManager) { + constructor( + private loadingProgressManager: LoadingProgressManager, + private config?: LoadingScreenConfig, + ) { + const defaultBackground = "linear-gradient(45deg, #28284B 0%, #303056 100%)"; this.element = document.createElement("div"); + this.element.id = "loading-screen"; + this.element.style.position = "absolute"; this.element.style.top = "0"; this.element.style.left = "0"; this.element.style.width = "100%"; this.element.style.height = "100%"; - this.element.style.background = "linear-gradient(45deg, #28284B 0%, #303056 100%)"; - this.element.style.color = "white"; - this.element.addEventListener("click", (event) => { - event.stopPropagation(); - }); - this.element.addEventListener("mousedown", (event) => { - event.stopPropagation(); - }); - this.element.addEventListener("mousemove", (event) => { - event.stopPropagation(); - }); - this.element.addEventListener("mouseup", (event) => { - event.stopPropagation(); - }); + this.element.style.backgroundColor = this.config?.background || defaultBackground; + this.element.style.background = this.config?.background || defaultBackground; + this.element.style.zIndex = "10001"; + + this.backgroundBlur = document.createElement("div"); + this.backgroundBlur.id = "loading-screen-blur"; + this.backgroundBlur.style.position = "absolute"; + this.backgroundBlur.style.top = "0"; + this.backgroundBlur.style.left = "0"; + this.backgroundBlur.style.width = "100%"; + this.backgroundBlur.style.height = "100%"; + this.backgroundBlur.style.display = "flex"; + if (this.config?.backgroundBlurAmount) { + this.backgroundBlur.style.backdropFilter = `blur(${this.config.backgroundBlurAmount}px)`; + } + this.element.append(this.backgroundBlur); + + if (this.config?.backgroundImageUrl) { + this.element.style.backgroundImage = `url(${this.config.backgroundImageUrl})`; + this.element.style.backgroundPosition = "center"; + this.element.style.backgroundSize = "cover"; + } + + if (this.config?.overlayLayers) { + const logLoadError = (imageUrl: string) => { + console.error(`Failed to load overlay image: ${imageUrl}`); + }; + + for (const layer of this.config.overlayLayers) { + const overlayLayer = document.createElement("div"); + overlayLayer.style.position = "absolute"; + overlayLayer.style.background = `url(${layer.overlayImageUrl}) no-repeat`; + overlayLayer.style.backgroundSize = "contain"; + + const anchor = layer.overlayAnchor; + const offsetX = layer.overlayOffset?.x || 0; + const offsetY = layer.overlayOffset?.y || 0; + + if (anchor.includes("top")) { + overlayLayer.style.top = `${offsetY}px`; + } else if (anchor.includes("bottom")) { + overlayLayer.style.bottom = `${offsetY}px`; + } + + if (anchor.includes("left")) { + overlayLayer.style.left = `${offsetX}px`; + } else if (anchor.includes("right")) { + overlayLayer.style.right = `${offsetX}px`; + } + + const image = new Image(); + image.src = layer.overlayImageUrl; + image.onload = () => { + const naturalWidth = image.naturalWidth; + const naturalHeight = image.naturalHeight; + + overlayLayer.style.width = `${naturalWidth}px`; + overlayLayer.style.height = `${naturalHeight}px`; + }; + + image.onerror = () => logLoadError(layer.overlayImageUrl); + + this.overlayLayers.push(overlayLayer); + this.backgroundBlur.append(overlayLayer); + } + } + + this.element.style.color = this.config?.color || "white"; + + this.loadingBanner = document.createElement("div"); + this.loadingBanner.style.position = "absolute"; + this.loadingBanner.style.display = "flex"; + this.loadingBanner.style.flexDirection = "column"; + this.loadingBanner.style.left = "0"; + this.loadingBanner.style.bottom = "0"; + this.loadingBanner.style.padding = "0"; + this.loadingBanner.style.width = "100%"; + this.loadingBanner.style.justifyContent = "flex-end"; + this.backgroundBlur.append(this.loadingBanner); - this.loadingBannerText = document.createElement("div"); - this.loadingBannerText.textContent = "Loading..."; - this.loadingBannerText.style.position = "absolute"; - this.loadingBannerText.style.display = "flex"; - this.loadingBannerText.style.top = "0"; - this.loadingBannerText.style.left = "0"; - this.loadingBannerText.style.width = "100%"; - this.loadingBannerText.style.height = "100%"; - this.loadingBannerText.style.color = "white"; - this.loadingBannerText.style.fontSize = "80px"; - this.loadingBannerText.style.fontWeight = "bold"; - this.loadingBannerText.style.fontFamily = "sans-serif"; - this.loadingBannerText.style.alignItems = "center"; - this.loadingBannerText.style.justifyContent = "center"; - this.element.append(this.loadingBannerText); + if (this.config?.title) { + this.loadingBannerTitle = document.createElement("div"); + this.loadingBannerTitle.textContent = this.config.title; + this.loadingBannerTitle.style.color = this.config?.color || "white"; + this.loadingBannerTitle.style.paddingLeft = "40px"; + this.loadingBannerTitle.style.paddingRight = "40px"; + this.loadingBannerTitle.style.fontSize = "42px"; + this.loadingBannerTitle.style.fontWeight = "bold"; + this.loadingBannerTitle.style.fontFamily = "sans-serif"; + if (this.config?.background) { + this.loadingBannerTitle.style.textShadow = `0px 0px 80px ${this.config.background}`; + } + this.loadingBanner.append(this.loadingBannerTitle); + } + + if (this.config?.subtitle) { + this.loadingBannerSubtitle = document.createElement("div"); + this.loadingBannerSubtitle.style.color = this.config?.color || "white"; + this.loadingBannerSubtitle.style.paddingLeft = "40px"; + this.loadingBannerSubtitle.style.paddingRight = "40px"; + this.loadingBannerSubtitle.style.fontSize = "16px"; + this.loadingBannerSubtitle.style.fontWeight = "400"; + this.loadingBannerSubtitle.style.fontFamily = "sans-serif"; + this.loadingBannerSubtitle.style.marginTop = "12px"; + if (this.config?.background) { + this.loadingBannerSubtitle.style.textShadow = `0px 0px 40px ${this.config.background}`; + } + + this.loadingBannerSubtitle.textContent = this.config.subtitle; + this.loadingBanner.append(this.loadingBannerSubtitle); + } this.progressDebugViewHolder = document.createElement("div"); - this.progressDebugViewHolder.style.display = "flex"; + this.progressDebugViewHolder.style.display = "none"; this.progressDebugViewHolder.style.position = "absolute"; - this.progressDebugViewHolder.style.maxHeight = "calc(100% - 74px)"; - this.progressDebugViewHolder.style.left = "0"; - this.progressDebugViewHolder.style.bottom = "74px"; - this.progressDebugViewHolder.style.width = "100%"; + this.progressDebugViewHolder.style.width = "calc(100% - 80px)"; + this.progressDebugViewHolder.style.maxHeight = "calc(100% - 120px)"; + this.progressDebugViewHolder.style.left = "40px"; + this.progressDebugViewHolder.style.bottom = "60px"; + this.progressDebugViewHolder.style.alignItems = "center"; this.progressDebugViewHolder.style.justifyContent = "center"; + this.progressDebugViewHolder.style.zIndex = "10003"; this.element.append(this.progressDebugViewHolder); this.progressDebugView = document.createElement("div"); - this.progressDebugView.style.backgroundColor = "rgba(128, 128, 128, 0.25)"; + this.progressDebugView.style.backgroundColor = "rgba(128, 128, 128, 0.5)"; this.progressDebugView.style.border = "1px solid black"; + this.progressDebugView.style.borderRadius = "7px"; + this.progressDebugView.style.width = "100%"; this.progressDebugView.style.maxWidth = "100%"; this.progressDebugView.style.overflow = "auto"; this.progressDebugViewHolder.append(this.progressDebugView); @@ -81,6 +192,8 @@ export class LoadingScreen { this.debugCheckbox.checked = false; this.debugCheckbox.addEventListener("change", () => { this.progressDebugElement.style.display = this.debugCheckbox.checked ? "block" : "none"; + this.loadingBannerTitle.style.display = this.debugCheckbox.checked ? "none" : "flex"; + this.loadingBannerSubtitle.style.display = this.debugCheckbox.checked ? "none" : "flex"; if (this.hasCompleted) { this.dispose(); } @@ -102,24 +215,37 @@ export class LoadingScreen { this.progressBarHolder = document.createElement("div"); this.progressBarHolder.style.display = "flex"; - this.progressBarHolder.style.alignItems = "center"; - this.progressBarHolder.style.justifyContent = "center"; - this.progressBarHolder.style.position = "absolute"; - this.progressBarHolder.style.bottom = "20px"; - this.progressBarHolder.style.left = "0"; + this.progressBarHolder.style.alignItems = "start"; + this.progressBarHolder.style.justifyContent = "flex-start"; this.progressBarHolder.style.width = "100%"; - this.element.append(this.progressBarHolder); + this.progressBarHolder.style.marginLeft = "40px"; + this.progressBarHolder.style.marginBottom = "40px"; + this.progressBarHolder.style.cursor = "pointer"; + this.progressBarHolder.style.marginTop = "24px"; + this.loadingBanner.append(this.progressBarHolder); this.progressBarBackground = document.createElement("div"); this.progressBarBackground.style.position = "relative"; - this.progressBarBackground.style.width = "500px"; - this.progressBarBackground.style.maxWidth = "80%"; - this.progressBarBackground.style.backgroundColor = "gray"; - this.progressBarBackground.style.height = "50px"; - this.progressBarBackground.style.lineHeight = "50px"; - this.progressBarBackground.style.borderRadius = "25px"; - this.progressBarBackground.style.border = "2px solid white"; + this.progressBarBackground.style.width = "80%"; + this.progressBarBackground.style.maxWidth = "400px"; + this.progressBarBackground.style.minWidth = "240px"; + this.progressBarBackground.style.backgroundColor = "rgba(32,32,32, 0.25)"; + this.progressBarBackground.style.backdropFilter = "blur(4px)"; + this.progressBarBackground.style.height = "16px"; + this.progressBarBackground.style.lineHeight = "16px"; + this.progressBarBackground.style.borderRadius = "16px"; this.progressBarBackground.style.overflow = "hidden"; + this.progressBarBackground.addEventListener("click", () => { + const display = this.progressDebugViewHolder.style.display; + if (display === "none") { + this.progressDebugViewHolder.style.display = "flex"; + } else { + this.progressDebugViewHolder.style.display = "none"; + this.debugCheckbox.checked = false; + this.progressDebugElement.style.display = this.debugCheckbox.checked ? "block" : "none"; + this.loadingBannerTitle.style.display = this.debugCheckbox.checked ? "none" : "flex"; + } + }); this.progressBarHolder.append(this.progressBarBackground); this.progressBar = document.createElement("div"); @@ -128,7 +254,8 @@ export class LoadingScreen { this.progressBar.style.left = "0"; this.progressBar.style.width = "0"; this.progressBar.style.height = "100%"; - this.progressBar.style.backgroundColor = "#0050a4"; + this.progressBar.style.pointerEvents = "none"; + this.progressBar.style.backgroundColor = this.config?.color || "#0050a4"; this.progressBarBackground.append(this.progressBar); this.loadingStatusText = document.createElement("div"); @@ -137,11 +264,14 @@ export class LoadingScreen { this.loadingStatusText.style.left = "0"; this.loadingStatusText.style.width = "100%"; this.loadingStatusText.style.height = "100%"; - this.loadingStatusText.style.color = "white"; + this.loadingStatusText.style.color = "rgba(200,200,200,0.9)"; + this.loadingStatusText.style.fontSize = "10px"; this.loadingStatusText.style.textAlign = "center"; this.loadingStatusText.style.verticalAlign = "middle"; + this.loadingStatusText.style.mixBlendMode = "difference"; this.loadingStatusText.style.fontFamily = "sans-serif"; this.loadingStatusText.style.fontWeight = "bold"; + this.loadingStatusText.style.userSelect = "none"; this.loadingStatusText.textContent = "Loading..."; this.progressBarBackground.append(this.loadingStatusText); @@ -157,7 +287,7 @@ export class LoadingScreen { this.loadingStatusText.textContent = "Completed"; this.progressBar.style.width = "100%"; } else { - this.loadingStatusText.textContent = `Loading... ${(loadingRatio * 100).toFixed(2)}%`; + this.loadingStatusText.textContent = `${(loadingRatio * 100).toFixed(2)}%`; this.progressBar.style.width = `${loadingRatio * 100}%`; } this.progressDebugElement.textContent = LoadingProgressManager.LoadingProgressSummaryToString( diff --git a/packages/3d-web-experience-client/src/Networked3dWebExperienceClient.ts b/packages/3d-web-experience-client/src/Networked3dWebExperienceClient.ts index fed1713d..bb79b61e 100644 --- a/packages/3d-web-experience-client/src/Networked3dWebExperienceClient.ts +++ b/packages/3d-web-experience-client/src/Networked3dWebExperienceClient.ts @@ -19,6 +19,7 @@ import { GroundPlane, KeyInputManager, LoadingScreen, + LoadingScreenConfig, MMLCompositionScene, TimeManager, TweakPane, @@ -81,6 +82,7 @@ export type Networked3dWebExperienceClientConfig = { voiceChatAddress?: string; updateURLLocation?: boolean; onServerBroadcast?: (broadcast: { broadcastType: string; payload: any }) => void; + loadingScreen?: LoadingScreenConfig; } & UpdatableConfig; export type UpdatableConfig = { @@ -276,7 +278,7 @@ export class Networked3dWebExperienceClient { this.setupMMLScene(); - this.loadingScreen = new LoadingScreen(this.loadingProgressManager); + this.loadingScreen = new LoadingScreen(this.loadingProgressManager, this.config.loadingScreen); this.element.append(this.loadingScreen.element); this.loadingProgressManager.addProgressCallback(() => {