diff --git a/scripts/build-area.js b/scripts/build-area.js index 2834a1f..cbab702 100755 --- a/scripts/build-area.js +++ b/scripts/build-area.js @@ -739,6 +739,16 @@ function cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, a
Loading diagnostics...
Loading ${escapeHtml(glbName)}...
+
+
+ +
+ Loading scene + ${escapeHtml(areaId)} +
+ +
+
diff --git a/scripts/lib/cesium-preview.css b/scripts/lib/cesium-preview.css index e963a59..5d82b2b 100644 --- a/scripts/lib/cesium-preview.css +++ b/scripts/lib/cesium-preview.css @@ -9,6 +9,15 @@ body, background: #d9e0e2; } +#cesiumContainer { + opacity: 0; + transition: opacity 180ms ease-out; +} + +body.scene-ready #cesiumContainer { + opacity: 1; +} + #controls, #diagnostics, #status { @@ -22,6 +31,110 @@ body, box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22); } +#loadingOverlay { + position: absolute; + inset: 0; + z-index: 3; + display: grid; + place-items: center; + background: #d9e0e2; + color: #1b2528; + font-size: 13px; + letter-spacing: 0; + transition: opacity 220ms ease-out, visibility 220ms ease-out; +} + +.loading-card { + display: grid; + grid-template-columns: 32px minmax(180px, 260px); + gap: 12px; + align-items: center; + padding: 14px 16px; + border: 1px solid rgba(70, 86, 90, 0.16); + border-radius: 6px; + background: rgba(244, 248, 248, 0.88); + box-shadow: 0 12px 34px rgba(54, 68, 72, 0.20); +} + +.loading-spinner { + width: 28px; + height: 28px; + border: 3px solid rgba(42, 67, 72, 0.18); + border-top-color: #147d8a; + border-radius: 50%; + animation: loading-spin 840ms linear infinite; +} + +.loading-copy { + display: grid; + gap: 3px; +} + +.loading-copy strong { + font-size: 13px; + font-weight: 650; +} + +#loadingOverlay span { + color: #526064; +} + +.loading-bar { + grid-column: 1 / -1; + position: relative; + height: 3px; + overflow: hidden; + border-radius: 999px; + background: rgba(42, 67, 72, 0.12); +} + +.loading-bar span { + position: absolute; + inset: 0 auto 0 0; + width: 42%; + border-radius: inherit; + background: #147d8a; + animation: loading-bar 1.2s ease-in-out infinite; +} + +@keyframes loading-spin { + to { + transform: rotate(360deg); + } +} + +@keyframes loading-bar { + 0% { + transform: translateX(-110%); + } + 55% { + transform: translateX(70%); + } + 100% { + transform: translateX(250%); + } +} + +body.scene-ready #loadingOverlay { + visibility: hidden; + opacity: 0; +} + +body.scene-error #loadingOverlay { + color: #5b1414; +} + +body.scene-error .loading-spinner { + border-color: rgba(120, 30, 30, 0.18); + border-top-color: #9f2f2f; + animation-play-state: paused; +} + +body.scene-error .loading-bar span { + background: #9f2f2f; + animation-play-state: paused; +} + #controls { top: 12px; left: 12px; diff --git a/scripts/lib/cesium-preview.js b/scripts/lib/cesium-preview.js index 7b2e380..49822a6 100644 --- a/scripts/lib/cesium-preview.js +++ b/scripts/lib/cesium-preview.js @@ -4,6 +4,7 @@ const config = window.OSM_ASSET_PREVIEW_CONFIG || {}; const statusEl = document.getElementById("status"); const diagnosticsEl = document.getElementById("diagnostics"); + const loadingOverlay = document.getElementById("loadingOverlay"); const toggleCruise = document.getElementById("toggleCruise"); const toggleFollow = document.getElementById("toggleFollow"); const toggleScene = document.getElementById("toggleScene"); @@ -22,14 +23,17 @@ // from a control the user just touched. Keep the summary so the transient // note can be replaced instead of destroying it. let baseStatus = ""; + const PREVIEW_BACKGROUND = "#d9e0e2"; Cesium.Ion.defaultAccessToken = ""; async function main() { + setLoadingMessage("Loading scene", config.areaId || ""); const metadata = await fetchJson(config.metadataName); const routeData = await fetchOptionalJson(config.routeName); const placement = scenePlacement(metadata); const viewer = createViewer(); + setLoadingMessage("Loading model", config.glbName || ""); const assets = await loadSceneAssets(viewer, metadata, placement); const cruise = addVehicleCruises(viewer, routeData, config.vehicleModelName); const cameras = createCameraPresets(viewer, metadata, placement, cruise); @@ -40,6 +44,9 @@ cameras.overview(); baseStatus = summaryText(metadata, assets, cruise); setStatus(baseStatus); + setLoadingMessage("Preparing view", "finalizing materials"); + await waitForStableFrames(viewer); + document.body.classList.add("scene-ready"); // Handle for the browser console and for headless checks: everything else // in here is closed over by the IIFE and unreachable from outside. window.osmPreview = { viewer, metadata, placement, assets, cruise, cameras }; @@ -78,16 +85,49 @@ selectionIndicator: false, baseLayer: false }); - viewer.scene.globe.depthTestAgainstTerrain = true; - viewer.scene.backgroundColor = Cesium.Color.fromCssColorString("#d9e0e2"); + viewer.scene.globe.show = false; + viewer.scene.globe.depthTestAgainstTerrain = false; + viewer.scene.backgroundColor = Cesium.Color.fromCssColorString(PREVIEW_BACKGROUND); viewer.scene.skyAtmosphere.show = false; viewer.scene.skyBox.show = false; viewer.scene.sun.show = false; viewer.scene.moon.show = false; - viewer.scene.globe.baseColor = Cesium.Color.fromCssColorString("#d4dcde"); + viewer.scene.globe.baseColor = Cesium.Color.fromCssColorString(PREVIEW_BACKGROUND); return viewer; } + function setLoadingMessage(title, detail) { + if (!loadingOverlay) return; + const titleEl = loadingOverlay.querySelector("strong"); + const detailEl = loadingOverlay.querySelector("span"); + if (titleEl) titleEl.textContent = title; + if (detailEl) detailEl.textContent = detail || ""; + } + + function waitForStableFrames(viewer) { + return new Promise((resolve) => { + const started = performance.now(); + let frames = 0; + let done = false; + let remove = function () {}; + function finish() { + if (done) return; + done = true; + remove(); + resolve(); + } + remove = viewer.scene.postRender.addEventListener(() => { + frames += 1; + const elapsed = performance.now() - started; + if ((frames >= 14 && elapsed >= 650) || elapsed >= 2800) { + finish(); + } + }); + window.setTimeout(finish, 3200); + viewer.scene.requestRender(); + }); + } + function scenePlacement(metadata) { const anchor = metadata.anchor || {}; const longitude = Number(anchor.longitude || 0); @@ -626,5 +666,7 @@ main().catch((error) => { console.error(error); setStatus("Failed to load Cesium preview: " + error.message); + document.body.classList.add("scene-error"); + setLoadingMessage("Failed to load scene", error.message); }); }());