threepipe
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

LoadingScreenPlugin.ts 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import {createDiv, createStyles, onChange, serialize, timeout} from 'ts-browser-helpers'
  2. import styles from './LoadingScreenPlugin.css?inline'
  3. import spinner1 from './loaders/spinner1.css?inline'
  4. import {uiButton, uiDropdown, uiFolderContainer, uiInput, uiSlider, uiToggle} from 'uiconfig.js'
  5. import {AAssetManagerProcessStatePlugin} from '../base/AAssetManagerProcessStatePlugin'
  6. import {ThreeViewer} from '../../viewer'
  7. /**
  8. * Loading Screen Plugin
  9. *
  10. * Shows a configurable loading screen overlay over the canvas.
  11. *
  12. * @category Plugins
  13. */
  14. @uiFolderContainer('Loading Screen')
  15. export class LoadingScreenPlugin extends AAssetManagerProcessStatePlugin {
  16. public static readonly PluginType = 'LoadingScreenPlugin'
  17. styles = styles
  18. spinners = [{
  19. styles: spinner1,
  20. html: '<span class="loader"></span>',
  21. }]
  22. refresh() {
  23. if (!this._viewer) return
  24. this._updateMainDiv(this._isPreviewing ? this._previewState : this._viewer.assetManager.processState, false)
  25. }
  26. @uiDropdown('Loader', ['Spinner 1'].map((v, i) => ({value: i, label: v})))
  27. @serialize() loader = 0
  28. @uiInput('Loading text header')
  29. @onChange(LoadingScreenPlugin.prototype.refresh)
  30. @serialize() loadingTextHeader = 'Loading Files'
  31. @uiInput('Error text header')
  32. @serialize() errorTextHeader = 'Error Loading Files'
  33. @uiToggle('Show file names')
  34. @onChange(LoadingScreenPlugin.prototype.refresh)
  35. @serialize() showFileNames = true
  36. @uiToggle('Show process states')
  37. @onChange(LoadingScreenPlugin.prototype.refresh)
  38. @serialize() showProcessStates = true
  39. @uiToggle('Show progress')
  40. @onChange(LoadingScreenPlugin.prototype.refresh)
  41. @serialize() showProgress = true
  42. @uiToggle('Hide on only errors')
  43. @serialize() hideOnOnlyErrors = true
  44. @uiToggle('Hide on files load')
  45. @serialize() hideOnFilesLoad = true
  46. @uiToggle('Hide on scene object load')
  47. @serialize() hideOnSceneObjectLoad = false
  48. /**
  49. * Minimize when scene has objects
  50. * Note: also checks for scene.environment and doesnt minimize when environment is null or undefined
  51. * @default true
  52. */
  53. @uiToggle('Minimize on scene object load')
  54. @serialize() minimizeOnSceneObjectLoad = true
  55. @uiToggle('Show when files start loading')
  56. @serialize() showOnFilesLoading = true
  57. @uiToggle('Show when scene empty')
  58. @serialize() showOnSceneEmpty = true
  59. @uiInput('Hide delay (ms)')
  60. @serialize() hideDelay = 500
  61. @uiSlider('Background Opacity', [0, 1])
  62. @onChange(LoadingScreenPlugin.prototype.refresh)
  63. @serialize() backgroundOpacity = 0.5
  64. @uiSlider('Background Blur', [0, 100])
  65. @onChange(LoadingScreenPlugin.prototype.refresh)
  66. @serialize() backgroundBlur = 24
  67. @uiInput('Background Color')
  68. @onChange(LoadingScreenPlugin.prototype.refresh)
  69. @serialize() background = '#ffffff'
  70. @uiInput('Text Color')
  71. @onChange(LoadingScreenPlugin.prototype.refresh)
  72. @serialize() textColor = '#222222'
  73. static LS_DEFAULT_LOGO = 'https://threepipe.org/logo.svg'
  74. @uiInput('Logo Image')
  75. @onChange(LoadingScreenPlugin.prototype.refresh)
  76. @serialize() logoImage = LoadingScreenPlugin.LS_DEFAULT_LOGO
  77. private _isPreviewing = false
  78. private _previewState = new Map([['file.glb', {state: 'downloading', progress: 50}], ['environment.hdr', {state: 'adding'}]])
  79. @uiButton('Toggle preview')
  80. togglePreview() {
  81. this.maximize()
  82. this._isPreviewing = !this._isPreviewing
  83. this.refresh()
  84. if (this._isPreviewing)
  85. this.show()
  86. else
  87. this.hideWithDelay()
  88. }
  89. loadingElement = createDiv({classList: ['loadingScreenLoadingElement'], addToBody: false})
  90. filesElement = createDiv({classList: ['loadingScreenFilesElement'], addToBody: false})
  91. logoElement = createDiv({classList: ['loadingScreenLogoElement'], addToBody: false})
  92. constructor(container?: HTMLElement) {
  93. super('LoadingScreen', container)
  94. // const popupClose = createDiv({
  95. // id: 'assetManagerLoadingScreenClose',
  96. // addToBody: false,
  97. // innerHTML: '&#10005',
  98. // })
  99. // popupClose.addEventListener('click', () => {
  100. // this._mainDiv.style.display = 'none'
  101. // })
  102. // this._mainDiv.appendChild(popupClose)
  103. this._mainDiv.prepend(this.loadingElement)
  104. this._mainDiv.prepend(this.logoElement)
  105. this._mainDiv.appendChild(this.filesElement)
  106. }
  107. private _isHidden = false
  108. get visible() {
  109. return !this._isHidden
  110. }
  111. async hide() {
  112. this._isHidden = true
  113. this._mainDiv.style.opacity = '0'
  114. await timeout(502)
  115. if (this._isHidden) {
  116. this._mainDiv.style.display = 'none'
  117. this._showMainDiv()
  118. }
  119. }
  120. async hideWithDelay() {
  121. this._isHidden = true
  122. await timeout(this.hideDelay)
  123. if (!this._isHidden) return
  124. return this.hide()
  125. }
  126. show() {
  127. if (!this._isHidden) return
  128. this._isHidden = false
  129. this._showMainDiv()
  130. this._mainDiv.style.display = 'flex'
  131. }
  132. protected _showMainDiv() {
  133. // this._mainDiv.style.opacity = this.opacity.toString()
  134. this._mainDiv.style.opacity = '1'
  135. }
  136. @uiButton('Minimize')
  137. minimize() {
  138. this._mainDiv.classList.add('minimizedLoadingScreen')
  139. if (!this.showFileNames) this.loadingElement.style.display = 'block'
  140. }
  141. @uiButton('Maximize')
  142. maximize() {
  143. this._mainDiv.classList.remove('minimizedLoadingScreen')
  144. this.loadingElement.style.display = ''
  145. }
  146. private _temp = document.createElement('template')
  147. private _setHTML(elem: HTMLElement, html:string) {
  148. this._temp.innerHTML = html
  149. // Compare the parsed content instead of raw strings, as browsers might change html after setting.
  150. if (this._temp.innerHTML.trim() !== elem.innerHTML.trim()) elem.innerHTML = html
  151. }
  152. protected _updateMainDiv(processState: Map<string, {state: string, progress?: number|undefined}>, updateVisibility = true) {
  153. if (!this._viewer) return
  154. if (!this._contentDiv) return
  155. if (!this.enabled) {
  156. this._mainDiv.style.display = 'none'
  157. return
  158. }
  159. if (this.showFileNames) {
  160. let text = ''
  161. processState.forEach((v, k) => {
  162. text += (this.showProcessStates ? `<span class="loadingScreenProcessState">${v.state}</span>: ` : '') +
  163. (k || '').split('/').pop() +
  164. (this.showProgress && v.progress ? ' - ' + (v.progress.toFixed(0) + '%') : '') +
  165. '<br>'
  166. })
  167. this._setHTML(this.filesElement, text)
  168. } else {
  169. this._setHTML(this.filesElement, '')
  170. }
  171. const errors = [...processState.values()].filter(v => v.state === 'error')
  172. if (errors.length > 0 && errors.length === processState.size && !this.hideOnOnlyErrors) {
  173. this._setHTML(this._contentDiv, this.errorTextHeader)
  174. } else {
  175. this._setHTML(this._contentDiv, this.loadingTextHeader)
  176. }
  177. this._setHTML(this.loadingElement, this.spinners[this.loader].html)
  178. this._mainDiv.style.setProperty('--b-opacity', this.backgroundOpacity.toString())
  179. this._mainDiv.style.setProperty('--b-background', this.background)
  180. ;(this._mainDiv.style as any).backdropFilter = `blur(${this.backgroundBlur}px)`
  181. this._mainDiv.style.color = this.textColor
  182. this._setHTML(this.logoElement, this.logoImage ? `<img class="loadingScreenLogoImage" src="${this.logoImage}"/>` : '')
  183. if (updateVisibility) {
  184. this._updateVisibility(processState, errors.length)
  185. }
  186. }
  187. protected _updateVisibility(processState: Map<string, {state: string, progress?: number|undefined}>, errors: number) {
  188. if (!this._viewer) return false
  189. if (this.hideOnFilesLoad && (processState.size === 0 ||
  190. errors === processState.size && this.hideOnOnlyErrors) && !this._isHidden) {
  191. this.hideDelay ? this.hideWithDelay() : this.hide()
  192. return true
  193. } else if (processState.size > 0 && this.showOnFilesLoading && this._isHidden) {
  194. const sceneObjects = this._viewer.scene.modelRoot.children
  195. if (sceneObjects.length > 0 && this.minimizeOnSceneObjectLoad && this._viewer.scene.environment) this.minimize()
  196. else this.maximize()
  197. this.show()
  198. return true
  199. }
  200. return false
  201. }
  202. // disables showOnSceneEmpty
  203. isEditor = false
  204. private _sceneUpdate = (e: any) => {
  205. if (!this._viewer) return
  206. if (!e.hierarchyChanged) return
  207. const sceneObjects = this._viewer.scene.modelRoot.children
  208. if (sceneObjects.length === 0 && this.showOnSceneEmpty && !this.isEditor) {
  209. this.show()
  210. }
  211. if (sceneObjects.length > 0) {
  212. // case - objects loaded, clear current scene, load loaded objects
  213. // load - process state 0, hide with delay. clear scene shows loading screen, loading current object doesnt change process state...
  214. const processState = this._viewer.assetManager.processState
  215. const errors = [...processState.values()].filter(v => v.state === 'error')
  216. if (!this._updateVisibility(processState, errors.length)) {
  217. if (this.hideOnSceneObjectLoad)
  218. this.hideWithDelay()
  219. else if (this.minimizeOnSceneObjectLoad && this._viewer.scene.environment)
  220. timeout(this.hideDelay + 300).then(() => this.minimize())
  221. }
  222. } else if (this.minimizeOnSceneObjectLoad)
  223. this.maximize()
  224. }
  225. stylesheet?: HTMLStyleElement
  226. stylesheetLoader?: HTMLStyleElement[]
  227. onAdded(viewer: ThreeViewer) {
  228. this.stylesheet = createStyles(this.styles, viewer.container)
  229. this.stylesheetLoader = this.spinners.map(s => createStyles(s.styles, viewer.container))
  230. viewer.scene.addEventListener('sceneUpdate', this._sceneUpdate)
  231. super.onAdded(viewer)
  232. }
  233. onRemove(viewer: ThreeViewer) {
  234. viewer.scene.removeEventListener('sceneUpdate', this._sceneUpdate)
  235. this.stylesheet?.remove()
  236. this.stylesheet = undefined
  237. this.stylesheetLoader?.forEach(s => s.remove())
  238. this.stylesheetLoader = undefined
  239. return super.onRemove(viewer)
  240. }
  241. }