feat: add splat loading overlay transition
This commit is contained in:
parent
b3b49b4fe2
commit
0d5a69e543
@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 overflow-hidden transition-all duration-700",
|
||||
isFading ? "scale-[1.02] opacity-0 blur-[1px]" : "opacity-100"
|
||||
)}
|
||||
>
|
||||
{previewImageUrl ? (
|
||||
<img
|
||||
alt="世界预览"
|
||||
className="absolute inset-0 h-full w-full object-cover opacity-35 blur-[2px] saturate-[0.85]"
|
||||
src={previewImageUrl}
|
||||
/>
|
||||
) : null}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(14,165,233,0.16),_rgba(1,1,1,0)_34%),linear-gradient(180deg,rgba(2,6,23,0.72)_0%,rgba(2,6,23,0.9)_100%)]" />
|
||||
<div className="absolute inset-0 bg-[linear-gradient(120deg,transparent_0%,rgba(255,255,255,0.05)_44%,transparent_58%)] [background-size:220%_100%] animate-[worldlabs-scan_3.6s_ease-in-out_infinite]" />
|
||||
|
||||
<div className="absolute inset-0">
|
||||
{SPLAT_LOADING_PARTICLES.map((particle, index) => (
|
||||
<span
|
||||
className="absolute rounded-full bg-cyan-300/70 shadow-[0_0_24px_rgba(103,232,249,0.45)]"
|
||||
key={`${particle.x}-${particle.y}-${index}`}
|
||||
style={
|
||||
{
|
||||
left: `${particle.x}%`,
|
||||
top: `${particle.y}%`,
|
||||
width: `${particle.size}px`,
|
||||
height: `${particle.size}px`,
|
||||
"--worldlabs-dx": `${particle.dx}px`,
|
||||
"--worldlabs-dy": `${particle.dy}px`,
|
||||
animation: `worldlabs-particle ${particle.duration}s ease-in-out ${particle.delay}s infinite`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 h-40 bg-[linear-gradient(180deg,rgba(2,6,23,0)_0%,rgba(2,6,23,0.88)_100%)]" />
|
||||
|
||||
<div className="absolute inset-0 flex items-center justify-center p-6">
|
||||
<div className="w-full max-w-lg rounded-3xl border border-white/10 bg-black/35 px-6 py-5 text-center shadow-[0_24px_100px_rgba(0,0,0,0.45)] backdrop-blur-xl">
|
||||
<div className="mb-3 inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-cyan-400/20 bg-cyan-400/10">
|
||||
<Sparkles className="h-6 w-6 text-cyan-200" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-lg font-semibold text-white">
|
||||
{isFading ? "正在显露 3D 世界细节" : "正在构建 3D 世界体积"}
|
||||
</p>
|
||||
<p className="mx-auto max-w-md text-sm leading-6 text-zinc-300">
|
||||
{isFading
|
||||
? "Gaussian Splat 已加载完成,正在把真实细节从过渡层中释放出来。"
|
||||
: "SPZ 会先以粗糙体积快速占位,再逐步补齐细节。先用粒子过渡能让加载观感更顺。"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center justify-center gap-2 text-[11px] text-cyan-100/85">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
<span>{isFading ? "即将完成" : "初始化 Splat 渲染器"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes worldlabs-particle {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 0, 0) scale(0.4);
|
||||
}
|
||||
20% {
|
||||
opacity: 0.9;
|
||||
}
|
||||
55% {
|
||||
opacity: 0.8;
|
||||
transform: translate3d(var(--worldlabs-dx), var(--worldlabs-dy), 0) scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate3d(
|
||||
calc(var(--worldlabs-dx) * 1.45),
|
||||
calc(var(--worldlabs-dy) * 1.45),
|
||||
0
|
||||
)
|
||||
scale(1.35);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes worldlabs-scan {
|
||||
0% {
|
||||
background-position: 115% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -15% 0;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function createMovementState(): MovementState {
|
||||
return { ...EMPTY_MOVEMENT_STATE };
|
||||
}
|
||||
@ -587,10 +709,33 @@ function WorldSplatViewer({
|
||||
semanticsMetadata?: Record<string, unknown> | 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 (
|
||||
<WorldEmptyState
|
||||
@ -632,15 +777,8 @@ function WorldSplatViewer({
|
||||
splatUrl={splatUrl}
|
||||
/>
|
||||
</Canvas>
|
||||
{loadState === "loading" ? (
|
||||
<div className="absolute inset-0">
|
||||
<WorldEmptyState
|
||||
description="正在初始化 Gaussian Splat 资源,这一步会比普通 Mesh 略慢。"
|
||||
icon={Sparkles}
|
||||
previewImageUrl={previewImageUrl}
|
||||
title="正在加载 Splat 世界"
|
||||
/>
|
||||
</div>
|
||||
{overlayPhase !== "hidden" ? (
|
||||
<WorldSplatLoadingOverlay phase={overlayPhase} previewImageUrl={previewImageUrl} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user