UNPKG

threepipe

Version:

A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.

281 lines (246 loc) 10.8 kB
import {createDiv, createStyles, escapeHtml, onChange, serialize, timeout} from 'ts-browser-helpers' import styles from './LoadingScreenPlugin.css?inline' import spinner1 from './loaders/spinner1.css?inline' import {uiButton, uiDropdown, uiFolderContainer, uiInput, uiSlider, uiToggle} from 'uiconfig.js' import {AAssetManagerProcessStatePlugin} from '../base/AAssetManagerProcessStatePlugin' import {ThreeViewer} from '../../viewer' /** * Loading Screen Plugin * * Shows a configurable loading screen overlay over the canvas. * * @category Plugins */ @uiFolderContainer('Loading Screen') export class LoadingScreenPlugin extends AAssetManagerProcessStatePlugin { public static readonly PluginType = 'LoadingScreenPlugin' styles = styles spinners = [{ styles: spinner1, html: '<span class="loader"></span>', }] refresh() { if (!this._viewer) return this._updateMainDiv(this._isPreviewing ? this._previewState : this._viewer.assetManager.processState, false) } @uiDropdown('Loader', ['Spinner 1'].map((v, i) => ({value: i, label: v}))) @serialize() loader = 0 @uiInput('Loading text header') @onChange(LoadingScreenPlugin.prototype.refresh) @serialize() loadingTextHeader = 'Loading Files' @uiInput('Error text header') @serialize() errorTextHeader = 'Error Loading Files' @uiToggle('Show file names') @onChange(LoadingScreenPlugin.prototype.refresh) @serialize() showFileNames = true @uiToggle('Show process states') @onChange(LoadingScreenPlugin.prototype.refresh) @serialize() showProcessStates = true @uiToggle('Show progress') @onChange(LoadingScreenPlugin.prototype.refresh) @serialize() showProgress = true @uiToggle('Hide on only errors') @serialize() hideOnOnlyErrors = true @uiToggle('Hide on files load') @serialize() hideOnFilesLoad = true @uiToggle('Hide on scene object load') @serialize() hideOnSceneObjectLoad = false /** * Minimize when scene has objects * Note: also checks for scene.environment and doesnt minimize when environment is null or undefined * @default true */ @uiToggle('Minimize on scene object load') @serialize() minimizeOnSceneObjectLoad = true @uiToggle('Show when files start loading') @serialize() showOnFilesLoading = true @uiToggle('Show when scene empty') @serialize() showOnSceneEmpty = true @uiInput('Hide delay (ms)') @serialize() hideDelay = 500 @uiSlider('Background Opacity', [0, 1]) @onChange(LoadingScreenPlugin.prototype.refresh) @serialize() backgroundOpacity = 0.5 @uiSlider('Background Blur', [0, 100]) @onChange(LoadingScreenPlugin.prototype.refresh) @serialize() backgroundBlur = 24 @uiInput('Background Color') @onChange(LoadingScreenPlugin.prototype.refresh) @serialize() background = '#ffffff' @uiInput('Text Color') @onChange(LoadingScreenPlugin.prototype.refresh) @serialize() textColor = '#222222' /** * Default logo image shown during loading * @default 'https://threepipe.org/logo.svg' */ static LS_DEFAULT_LOGO = 'https://threepipe.org/logo.svg' @uiInput('Logo Image') @onChange(LoadingScreenPlugin.prototype.refresh) @serialize() logoImage = LoadingScreenPlugin.LS_DEFAULT_LOGO private _isPreviewing = false private _previewState = new Map([['file.glb', {state: 'downloading', progress: 50}], ['environment.hdr', {state: 'adding'}]]) @uiButton('Toggle preview') togglePreview() { this.maximize() this._isPreviewing = !this._isPreviewing this.refresh() if (this._isPreviewing) this.show() else this.hideWithDelay() } loadingElement = createDiv({classList: ['loadingScreenLoadingElement'], addToBody: false}) filesElement = createDiv({classList: ['loadingScreenFilesElement'], addToBody: false}) logoElement = createDiv({classList: ['loadingScreenLogoElement'], addToBody: false}) constructor(container?: HTMLElement) { super('LoadingScreen', container) // const popupClose = createDiv({ // id: 'assetManagerLoadingScreenClose', // addToBody: false, // innerHTML: '&#10005', // }) // popupClose.addEventListener('click', () => { // this._mainDiv.style.display = 'none' // }) // this._mainDiv.appendChild(popupClose) this._mainDiv.prepend(this.loadingElement) this._mainDiv.prepend(this.logoElement) this._mainDiv.appendChild(this.filesElement) } private _isHidden = false get visible() { return !this._isHidden } async hide() { this._isHidden = true this._mainDiv.style.opacity = '0' await timeout(502) if (this._isHidden) { this._mainDiv.style.display = 'none' this._showMainDiv() } } async hideWithDelay() { this._isHidden = true await timeout(this.hideDelay) if (!this._isHidden) return return this.hide() } show() { if (!this._isHidden) return this._isHidden = false this._showMainDiv() this._mainDiv.style.display = 'flex' } protected _showMainDiv() { // this._mainDiv.style.opacity = this.opacity.toString() this._mainDiv.style.opacity = '1' } @uiButton('Minimize') minimize() { this._mainDiv.classList.add('minimizedLoadingScreen') if (!this.showFileNames) this.loadingElement.style.display = 'block' } @uiButton('Maximize') maximize() { this._mainDiv.classList.remove('minimizedLoadingScreen') this.loadingElement.style.display = '' } private _temp = document.createElement('template') private _setHTML(elem: HTMLElement, html:string) { this._temp.innerHTML = html // Compare the parsed content instead of raw strings, as browsers might change html after setting. if (this._temp.innerHTML.trim() !== elem.innerHTML.trim()) elem.innerHTML = html } protected _updateMainDiv(processState: Map<string, {state: string, progress?: number|undefined}>, updateVisibility = true) { if (!this._viewer) return if (!this._contentDiv) return if (!this.enabled) { this._mainDiv.style.display = 'none' return } if (this.showFileNames) { let text = '' processState.forEach((v, k) => { text += (this.showProcessStates ? `<span class="loadingScreenProcessState">${escapeHtml(v.state)}</span>: ` : '') + escapeHtml((k || '').split('/').pop() || '') + (this.showProgress && v.progress ? ' - ' + (v.progress.toFixed(0) + '%') : '') + '<br>' }) this._setHTML(this.filesElement, text) } else { this._setHTML(this.filesElement, '') } const errors = [...processState.values()].filter(v => v.state === 'error') if (errors.length > 0 && errors.length === processState.size && !this.hideOnOnlyErrors) { this._setHTML(this._contentDiv, escapeHtml(this.errorTextHeader)) } else { this._setHTML(this._contentDiv, escapeHtml(this.loadingTextHeader)) } this._setHTML(this.loadingElement, this.spinners[this.loader].html) this._mainDiv.style.setProperty('--b-opacity', this.backgroundOpacity.toString()) this._mainDiv.style.setProperty('--b-background', this.background) this._mainDiv.style.setProperty('--b-blur', this.backgroundBlur + 'px') // ;(this._mainDiv.style as any).backdropFilter = `blur(${this.backgroundBlur}px)` this._mainDiv.style.color = this.textColor this._setHTML(this.logoElement, this.logoImage ? `<img class="loadingScreenLogoImage" src=${JSON.stringify(this.logoImage)}/>` : '') if (updateVisibility) { this._updateVisibility(processState, errors.length) } } protected _updateVisibility(processState: Map<string, {state: string, progress?: number|undefined}>, errors: number) { if (!this._viewer) return false if (this.hideOnFilesLoad && (processState.size === 0 || errors === processState.size && this.hideOnOnlyErrors) && !this._isHidden) { this.hideDelay ? this.hideWithDelay() : this.hide() return true } else if (processState.size > 0 && this.showOnFilesLoading && this._isHidden) { const sceneObjects = this._viewer.scene.modelRoot.children if (sceneObjects.length > 0 && this.minimizeOnSceneObjectLoad && this._viewer.scene.environment) this.minimize() else this.maximize() this.show() return true } return false } // disables showOnSceneEmpty isEditor = false private _sceneUpdate = (e: any) => { if (!this._viewer) return if (!e.hierarchyChanged) return const sceneObjects = this._viewer.scene.modelRoot.children if (sceneObjects.length === 0 && this.showOnSceneEmpty && !this.isEditor) { this.show() } if (sceneObjects.length > 0) { // case - objects loaded, clear current scene, load loaded objects // load - process state 0, hide with delay. clear scene shows loading screen, loading current object doesnt change process state... const processState = this._viewer.assetManager.processState const errors = [...processState.values()].filter(v => v.state === 'error') if (!this._updateVisibility(processState, errors.length)) { if (this.hideOnSceneObjectLoad) this.hideWithDelay() else if (this.minimizeOnSceneObjectLoad && this._viewer.scene.environment) timeout(this.hideDelay + 300).then(() => this.minimize()) } } else if (this.minimizeOnSceneObjectLoad) this.maximize() } stylesheet?: HTMLStyleElement stylesheetLoader?: HTMLStyleElement[] onAdded(viewer: ThreeViewer) { this.stylesheet = createStyles(this.styles, viewer.container) this.stylesheetLoader = this.spinners.map(s => createStyles(s.styles, viewer.container)) viewer.scene.addEventListener('sceneUpdate', this._sceneUpdate) super.onAdded(viewer) } onRemove(viewer: ThreeViewer) { viewer.scene.removeEventListener('sceneUpdate', this._sceneUpdate) this.stylesheet?.remove() this.stylesheet = undefined this.stylesheetLoader?.forEach(s => s.remove()) this.stylesheetLoader = undefined return super.onRemove(viewer) } }