From 0d5a69e543197ea854e6ebe45426a2e738a79374 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 6 Apr 2026 00:24:28 +0800 Subject: [PATCH] feat: add splat loading overlay transition --- components/canvas/world3d/World3DViewer.tsx | 156 ++++++++++++++++++-- 1 file changed, 147 insertions(+), 9 deletions(-) diff --git a/components/canvas/world3d/World3DViewer.tsx b/components/canvas/world3d/World3DViewer.tsx index 5c0327dc5..316a73836 100644 --- a/components/canvas/world3d/World3DViewer.tsx +++ b/components/canvas/world3d/World3DViewer.tsx @@ -167,6 +167,128 @@ function WorldEmptyState({ ); } +const SPLAT_LOADING_PARTICLES = [ + { x: 8, y: 18, size: 5, dx: 18, dy: -22, delay: 0.1, duration: 4.6 }, + { x: 18, y: 64, size: 7, dx: 26, dy: -18, delay: 0.4, duration: 5.2 }, + { x: 26, y: 34, size: 4, dx: -12, dy: -28, delay: 0.7, duration: 4.8 }, + { x: 34, y: 78, size: 6, dx: 14, dy: -24, delay: 1.1, duration: 5.6 }, + { x: 44, y: 16, size: 8, dx: -18, dy: 18, delay: 0.3, duration: 5.4 }, + { x: 52, y: 56, size: 5, dx: 20, dy: -26, delay: 0.9, duration: 4.9 }, + { x: 61, y: 28, size: 6, dx: -22, dy: -16, delay: 1.4, duration: 5.8 }, + { x: 68, y: 72, size: 4, dx: 16, dy: -20, delay: 0.2, duration: 4.7 }, + { x: 76, y: 42, size: 8, dx: -14, dy: 22, delay: 1.2, duration: 5.1 }, + { x: 84, y: 62, size: 5, dx: 22, dy: -14, delay: 0.6, duration: 5.7 }, + { x: 91, y: 24, size: 6, dx: -16, dy: 18, delay: 1.5, duration: 4.5 }, + { x: 12, y: 44, size: 4, dx: 12, dy: 14, delay: 1.8, duration: 5.3 }, +] as const; + +function WorldSplatLoadingOverlay({ + previewImageUrl, + phase, +}: { + previewImageUrl?: string | null; + phase: "loading" | "fading"; +}) { + const isFading = phase === "fading"; + + return ( +
+ {previewImageUrl ? ( + 世界预览 + ) : null} +
+
+ +
+ {SPLAT_LOADING_PARTICLES.map((particle, index) => ( + + ))} +
+ +
+ +
+
+
+ +
+
+

+ {isFading ? "正在显露 3D 世界细节" : "正在构建 3D 世界体积"} +

+

+ {isFading + ? "Gaussian Splat 已加载完成,正在把真实细节从过渡层中释放出来。" + : "SPZ 会先以粗糙体积快速占位,再逐步补齐细节。先用粒子过渡能让加载观感更顺。"} +

+
+
+ + {isFading ? "即将完成" : "初始化 Splat 渲染器"} +
+
+
+ + +
+ ); +} + function createMovementState(): MovementState { return { ...EMPTY_MOVEMENT_STATE }; } @@ -587,10 +709,33 @@ function WorldSplatViewer({ semanticsMetadata?: Record | null; }) { const [loadState, setLoadState] = useState<"loading" | "ready" | "error">("loading"); + const [overlayPhase, setOverlayPhase] = useState<"loading" | "fading" | "hidden">( + "loading" + ); + useEffect(() => { setLoadState("loading"); + setOverlayPhase("loading"); }, [splatUrl]); + useEffect(() => { + if (loadState === "ready") { + setOverlayPhase("fading"); + const timer = window.setTimeout(() => { + setOverlayPhase("hidden"); + }, 720); + return () => window.clearTimeout(timer); + } + + if (loadState === "loading") { + setOverlayPhase("loading"); + } + + if (loadState === "error") { + setOverlayPhase("hidden"); + } + }, [loadState]); + if (!splatUrl) { return ( - {loadState === "loading" ? ( -
- -
+ {overlayPhase !== "hidden" ? ( + ) : null}
);