|
1 | | -import { ILoadingScreen } from '@babylonjs/core'; |
2 | | -import { createRoot } from 'react-dom/client'; |
| 1 | +import { AbstractEngine, EngineStore, ILoadingScreen as BabylonILoadingScreen, Nullable, Observer } from '@babylonjs/core'; |
| 2 | +import { renderToStaticMarkup } from 'react-dom/server'; |
3 | 3 |
|
4 | | -class CustomLoadingScreen implements ILoadingScreen { |
5 | | - //optional, but needed due to interface definitions |
6 | | - public loadingUIBackgroundColor: string; |
7 | | - public loadingUIText: string; |
| 4 | +type TransitionStyle = { |
| 5 | + start: Partial<CSSStyleDeclaration>; |
| 6 | + end: Partial<CSSStyleDeclaration>; |
| 7 | +}; |
| 8 | + |
| 9 | +export type LoadingScreenOptions = { |
| 10 | + component: React.FC; |
| 11 | + animationStyle?: TransitionStyle; |
| 12 | +}; |
| 13 | + |
| 14 | +class CustomLoadingScreen implements Pick<BabylonILoadingScreen, 'displayLoadingUI' | 'hideLoadingUI'> { |
8 | 15 | private _loadingDiv: HTMLDivElement; |
9 | | - private _renderingCanvas: HTMLCanvasElement; |
10 | 16 |
|
| 17 | + private _engine: Nullable<AbstractEngine>; |
| 18 | + private _resizeObserver: Nullable<Observer<AbstractEngine>>; |
| 19 | + private _isLoading: boolean; |
11 | 20 | /** |
12 | | - * Creates a new loading screen rendering the React element in input |
13 | | - * @param renderingCanvas defines the canvas used to render the scene |
| 21 | + * Maps a loading `HTMLDivElement` to a tuple containing the associated `HTMLCanvasElement` |
| 22 | + * and its `DOMRect` (or `null` if not yet available). |
14 | 23 | */ |
15 | | - constructor(renderingCanvas: HTMLCanvasElement, Loader: React.FC) { |
16 | | - this._resizeLoadingUI = this._resizeLoadingUI.bind(this); |
17 | | - this._renderingCanvas = renderingCanvas; |
18 | | - /* if (this._loadingDiv) { |
19 | | - // Do not add a loading screen if there is already one |
20 | | - return; |
21 | | - } */ |
22 | | - this._loadingDiv = document.createElement('div'); |
23 | | - this._loadingDiv.id = 'loader'; |
24 | | - this._loadingDiv.style.display = 'none'; |
25 | | - document.body.appendChild(this._loadingDiv); |
26 | | - const loadingDiv = createRoot(this._loadingDiv); |
27 | | - loadingDiv.render(<Loader />); |
28 | | - } |
| 24 | + private _loadingDivToRenderingCanvasMap: Map<HTMLDivElement, [HTMLCanvasElement, DOMRect | null]> = new Map(); |
29 | 25 |
|
30 | | - // Resize |
31 | | - _resizeLoadingUI() { |
32 | | - const canvasRect = this._renderingCanvas.getBoundingClientRect(); |
33 | | - const canvasPositioning = window.getComputedStyle(this._renderingCanvas).position; |
34 | | - if (!this._loadingDiv) { |
35 | | - return; |
| 26 | + /** |
| 27 | + * Creates a new loading screen rendering the React element in input |
| 28 | + * @param _renderingCanvas defines the canvas used to render the scene |
| 29 | + * @param loader defines the custom React component to show |
| 30 | + * @param _animationStyle the custom CSS styles applied at the beginning and end of the animation |
| 31 | + */ |
| 32 | + constructor( |
| 33 | + private _renderingCanvas: HTMLCanvasElement, |
| 34 | + Loader: React.FC, |
| 35 | + private _animationStyle?: TransitionStyle, |
| 36 | + ) { |
| 37 | + const loadingDiv = document.createElement('div'); |
| 38 | + loadingDiv.id = 'loader'; |
| 39 | + loadingDiv.innerHTML = renderToStaticMarkup(<Loader />); |
| 40 | + if (this._animationStyle) { |
| 41 | + Object.assign(loadingDiv.style, this._animationStyle.start); |
| 42 | + } else { |
| 43 | + loadingDiv.style.transition = 'opacity 1s'; |
36 | 44 | } |
37 | | - this._loadingDiv.style.position = canvasPositioning === 'fixed' ? 'fixed' : 'absolute'; |
38 | | - const scrollLeft = document.documentElement.scrollLeft; |
39 | | - const scrollTop = document.documentElement.scrollTop; |
40 | | - this._loadingDiv.style.left = canvasRect.left + scrollLeft + 'px'; |
41 | | - this._loadingDiv.style.top = canvasRect.top + scrollTop + 'px'; |
42 | | - this._loadingDiv.style.width = canvasRect.width + 'px'; |
43 | | - this._loadingDiv.style.height = canvasRect.height + 'px'; |
| 45 | + loadingDiv.style.display = 'none'; |
| 46 | + this._loadingDiv = loadingDiv; |
44 | 47 | } |
45 | | - |
46 | 48 | /** |
47 | 49 | * Function called to display the loading screen |
48 | 50 | */ |
49 | | - displayLoadingUI() { |
| 51 | + public displayLoadingUI(): void { |
| 52 | + if (this._isLoading) { |
| 53 | + // Do not add a loading screen if it is already loading |
| 54 | + return; |
| 55 | + } |
| 56 | + |
| 57 | + this._isLoading = true; |
| 58 | + // get current engine by rendering canvas |
| 59 | + this._engine = EngineStore.Instances.find(engine => engine.getRenderingCanvas() === this._renderingCanvas) as AbstractEngine; |
| 60 | + |
| 61 | + const canvases: Array<HTMLCanvasElement> = []; |
| 62 | + const views = this._engine.views; |
| 63 | + if (views?.length) { |
| 64 | + for (const view of views) { |
| 65 | + if (view.enabled) { |
| 66 | + canvases.push(view.target); |
| 67 | + } |
| 68 | + } |
| 69 | + } else { |
| 70 | + canvases.push(this._renderingCanvas); |
| 71 | + } |
| 72 | + canvases.forEach((canvas, index) => { |
| 73 | + const clonedLoadingDiv = this._loadingDiv!.cloneNode(true) as HTMLDivElement; |
| 74 | + clonedLoadingDiv.id += `-${index}`; |
| 75 | + clonedLoadingDiv.style.display = 'block'; |
| 76 | + this._loadingDivToRenderingCanvasMap.set(clonedLoadingDiv, [canvas, null]); |
| 77 | + }); |
| 78 | + |
50 | 79 | this._resizeLoadingUI(); |
51 | | - window.addEventListener('resize', this._resizeLoadingUI); |
52 | | - this._loadingDiv.style.display = 'block'; |
| 80 | + |
| 81 | + this._resizeObserver = this._engine.onResizeObservable.add(() => { |
| 82 | + this._resizeLoadingUI(); |
| 83 | + }); |
| 84 | + |
| 85 | + this._loadingDivToRenderingCanvasMap.forEach((_, loadingDiv) => { |
| 86 | + document.body.appendChild(loadingDiv); |
| 87 | + }); |
53 | 88 | } |
54 | 89 |
|
55 | 90 | /** |
56 | 91 | * Function called to hide the loading screen |
57 | 92 | */ |
58 | | - hideLoadingUI() { |
59 | | - window.removeEventListener('resize', this._resizeLoadingUI); |
60 | | - this._loadingDiv.style.display = 'none'; |
| 93 | + public hideLoadingUI(): void { |
| 94 | + if (!this._isLoading) { |
| 95 | + return; |
| 96 | + } |
| 97 | + |
| 98 | + let completedTransitions = 0; |
| 99 | + |
| 100 | + const onTransitionEnd = (event: TransitionEvent) => { |
| 101 | + const loadingDiv = event.target as HTMLDivElement; |
| 102 | + // ensure that ending transition event is generated by one of the current loadingDivs |
| 103 | + const isTransitionEndOnLoadingDiv = this._loadingDivToRenderingCanvasMap.has(loadingDiv); |
| 104 | + |
| 105 | + if (isTransitionEndOnLoadingDiv) { |
| 106 | + completedTransitions++; |
| 107 | + loadingDiv.remove(); |
| 108 | + |
| 109 | + const allTransitionsCompleted = completedTransitions === this._loadingDivToRenderingCanvasMap.size; |
| 110 | + if (allTransitionsCompleted) { |
| 111 | + window.removeEventListener('transitionend', onTransitionEnd); |
| 112 | + this._engine!.onResizeObservable.remove(this._resizeObserver); |
| 113 | + this._loadingDivToRenderingCanvasMap.clear(); |
| 114 | + this._engine = null; |
| 115 | + this._isLoading = false; |
| 116 | + } |
| 117 | + } |
| 118 | + }; |
| 119 | + |
| 120 | + this._loadingDivToRenderingCanvasMap.forEach((_, loadingDiv) => { |
| 121 | + if (this._animationStyle) { |
| 122 | + Object.assign(loadingDiv.style, this._animationStyle.end); |
| 123 | + } else { |
| 124 | + loadingDiv.style.opacity = '0'; |
| 125 | + } |
| 126 | + }); |
| 127 | + |
| 128 | + window.addEventListener('transitionend', onTransitionEnd); |
61 | 129 | } |
| 130 | + |
| 131 | + /** |
| 132 | + * Checks if the layout of the canvas has changed by comparing the current layout |
| 133 | + * rectangle with the previous one. |
| 134 | + * |
| 135 | + * This function compares of the two `DOMRect` objects to determine if any of the layout dimensions have changed. |
| 136 | + * If the layout has changed or if there is no previous layout (i.e., `previousCanvasRect` is `null`), |
| 137 | + * it returns `true`. Otherwise, it returns `false`. |
| 138 | + * |
| 139 | + * @param previousCanvasRect defines the previously recorded `DOMRect` of the canvas, or `null` if no previous state exists. |
| 140 | + * @param currentCanvasRect defines the current `DOMRect` of the canvas to compare against the previous layout. |
| 141 | + * @returns `true` if the layout has changed, otherwise `false`. |
| 142 | + */ |
| 143 | + private _isCanvasLayoutChanged(previousCanvasRect: DOMRect | null, currentCanvasRect: DOMRect) { |
| 144 | + return ( |
| 145 | + !previousCanvasRect || |
| 146 | + previousCanvasRect.left !== currentCanvasRect.left || |
| 147 | + previousCanvasRect.top !== currentCanvasRect.top || |
| 148 | + previousCanvasRect.right !== currentCanvasRect.right || |
| 149 | + previousCanvasRect.bottom !== currentCanvasRect.bottom || |
| 150 | + previousCanvasRect.width !== currentCanvasRect.width || |
| 151 | + previousCanvasRect.height !== currentCanvasRect.height || |
| 152 | + previousCanvasRect.x !== currentCanvasRect.x || |
| 153 | + previousCanvasRect.y !== currentCanvasRect.y |
| 154 | + ); |
| 155 | + } |
| 156 | + |
| 157 | + // Resize |
| 158 | + private _resizeLoadingUI = () => { |
| 159 | + if (!this._isLoading) { |
| 160 | + return; |
| 161 | + } |
| 162 | + |
| 163 | + this._loadingDivToRenderingCanvasMap.forEach(([canvas, previousCanvasRect], loadingDiv) => { |
| 164 | + const currentCanvasRect = canvas.getBoundingClientRect(); |
| 165 | + if (this._isCanvasLayoutChanged(previousCanvasRect, currentCanvasRect)) { |
| 166 | + const canvasPositioning = window.getComputedStyle(canvas).position; |
| 167 | + |
| 168 | + loadingDiv.style.position = canvasPositioning === 'fixed' ? 'fixed' : 'absolute'; |
| 169 | + loadingDiv.style.left = currentCanvasRect.left + window.scrollX + 'px'; |
| 170 | + loadingDiv.style.top = currentCanvasRect.top + window.scrollY + 'px'; |
| 171 | + loadingDiv.style.width = currentCanvasRect.width + 'px'; |
| 172 | + loadingDiv.style.height = currentCanvasRect.height + 'px'; |
| 173 | + |
| 174 | + this._loadingDivToRenderingCanvasMap.set(loadingDiv, [canvas, currentCanvasRect]); |
| 175 | + } |
| 176 | + }); |
| 177 | + }; |
62 | 178 | } |
63 | 179 |
|
64 | 180 | export default CustomLoadingScreen; |
0 commit comments