feat(web): lighten tabletop scene and tune contact shadows

Replace the dark surface plane with a grey background and a spotlight,
size the contact shadow to the table, and scale its blur and fade with
the shadow plane. Disable depth test on the stacking curve overlay.
This commit is contained in:
2026-08-10 00:57:32 +08:00
parent 50c6df48b5
commit 5a3a9c1fcc
6 changed files with 32 additions and 39 deletions
@@ -8,6 +8,7 @@ import {
resolveMountTree,
serializedToPackage,
useTabletopStore,
MM_TO_WORLD,
} from '@tts/tabletop';
import Scene from '../viewers/Scene';
@@ -35,11 +36,26 @@ export default function TabletopScene({
[packageData, surfaces],
);
// Size the contact shadow to cover 2x the largest enabled surface, so the
// shadow plane always extends past the table. Surface sizes are in mm;
// `MM_TO_WORLD` converts to world units.
const shadowScale = useMemo(() => {
let maxMm = 0;
for (const [id, enabled] of Object.entries(surfaces)) {
if (!enabled) continue;
const surface = packageData.surfaces.get(id);
if (!surface?.size) continue;
maxMm = Math.max(maxMm, ...surface.size);
}
return maxMm > 0 ? maxMm * 2 * MM_TO_WORLD : 22;
}, [packageData, surfaces]);
return (
<Scene
autoRotate={false}
enablePan
fullscreen
shadowScale={shadowScale}
overlay={
<button
onClick={() => setShowSurface((v) => !v)}
+13 -10
View File
@@ -2,7 +2,7 @@ import { Suspense, useEffect, useRef, useState, type ReactNode } from 'react';
import { Icon } from '@iconify/react';
import { Canvas } from '@react-three/fiber';
import { Bounds, ContactShadows, OrbitControls, useProgress } from '@react-three/drei';
import { Bloom, EffectComposer, Vignette } from '@react-three/postprocessing';
import { EffectComposer, Vignette } from '@react-three/postprocessing';
/**
* Shared 3D scene wrapper for object viewers. Provides a consistent camera,
@@ -24,6 +24,7 @@ export default function Scene({
enablePan = false,
fullscreen = false,
overlay,
shadowScale = 22,
}: {
children: ReactNode;
autoRotate?: boolean;
@@ -31,6 +32,8 @@ export default function Scene({
fullscreen?: boolean;
/** HTML rendered inside the scene container (e.g. controls), shown in fullscreen. */
overlay?: ReactNode;
/** Contact shadow plane size in world units; defaults to a generous 22. */
shadowScale?: number;
}) {
const containerRef = useRef<HTMLDivElement>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
@@ -49,7 +52,7 @@ export default function Scene({
return (
<div
ref={containerRef}
className="relative h-80 w-full overflow-hidden rounded-lg bg-linear-to-b from-zinc-900 to-zinc-950"
className="relative h-80 w-full overflow-hidden rounded-lg bg-linear-to-b from-zinc-300 to-zinc-400"
>
<LoadingOverlay />
{fullscreen && (
@@ -66,7 +69,7 @@ export default function Scene({
<Canvas
camera={{ position: [2.2, 1.8, 2.6], fov: 40 }}
dpr={[1, 2]}
gl={{ antialias: true }}
gl={{ antialias: true, alpha: true }}
>
<ambientLight intensity={0.5} />
<directionalLight position={[4, 6, 3]} intensity={1.4} />
@@ -78,12 +81,13 @@ export default function Scene({
</Suspense>
<ContactShadows
position={[0, -0.5, 0]}
opacity={0.55}
scale={8}
blur={2.4}
far={3}
resolution={256}
position={[0, -0.01, 0]}
opacity={0.2}
scale={shadowScale}
blur={shadowScale * 0.005}
far={shadowScale * 0.01}
resolution={1024}
color="#000000"
/>
<OrbitControls
enablePan={enablePan}
@@ -94,7 +98,6 @@ export default function Scene({
/>
<EffectComposer>
<Bloom intensity={0.25} luminanceThreshold={0.85} mipmapBlur />
<Vignette eskil={false} offset={0.25} darkness={0.6} />
</EffectComposer>
</Canvas>