xiaohongshu
xiaohongshu
bridging-2d-3d-rendering
Bridging 2D canvas and 3D spatial rendering in StickerNest. Use when working with coordinate conversion, parallel DOM/WebGL architecture, widget rendering across modes, Html vs pure 3D decisions, or mode-aware components. Covers spatialCoordinates utilities, SpatialCanvas, SpatialWidgetContainer,...
Full skill instructions
StickerNest uses a parallel rendering architecture where 2D DOM canvas and 3D WebGL scene coexist. Understanding this bridge is critical for building features that work across desktop, VR, and AR.
┌─────────────────────────────────────────────────────────────┐
│ CanvasPage.tsx │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ CanvasRenderer │ │ SpatialCanvas │ │
│ │ (DOM/2D) │ │ (WebGL/3D) │ │
│ │ │ │ │ │
│ │ - HTML elements │ │ - Three.js scene │ │
│ │ - CSS positioning │ │ - 3D meshes & materials │ │
│ │ - React components │ │ - WebXR sessions │ │
│ │ │ │ │ │
│ │ visible: desktop │ │ visible: vr/ar │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Shared State (Zustand stores) ││
│ │ - useCanvasStore (widgets, stickers, positions) ││
│ │ - useSpatialModeStore (desktop/vr/ar mode) ││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
Key principle: Both renderers read from the same state. Position a widget in 2D, and it appears in the correct 3D location automatically.
import { useActiveSpatialMode, useIsDesktopMode } from '@/state/useSpatialModeStore';
type SpatialMode = 'desktop' | 'vr' | 'ar';
// In components:
const spatialMode = useActiveSpatialMode();
const isDesktopMode = spatialMode === 'desktop';
| Mode | Renderer | Use Case |
|---|---|---|
desktop | CanvasRenderer (DOM) | Traditional 2D editing |
vr | SpatialCanvas (WebGL) | Immersive VR headset |
ar | SpatialCanvas (WebGL) | AR passthrough |
The bridge between 2D and 3D is coordinate conversion. Use these utilities from src/utils/spatialCoordinates.ts:
import {
PIXELS_PER_METER, // 100 - conversion factor
DEFAULT_WIDGET_Z, // -2 meters (in front of user)
DEFAULT_EYE_HEIGHT, // 1.6 meters (standing user)
} from '@/utils/spatialCoordinates';
import { toSpatialPosition, toSpatialSize, toSpatialRotation } from '@/utils/spatialCoordinates';
// Position: pixels → meters
const pos3D = toSpatialPosition({ x: 500, y: 300 });
// Returns: [5, 1.1, -2] (x in meters, y adjusted for eye height, z = default depth)
// Size: pixels → meters
const size3D = toSpatialSize({ width: 200, height: 150 });
// Returns: { width: 2, height: 1.5 }
// Rotation: degrees → radians (around Z axis)
const rot3D = toSpatialRotation(45);
// Returns: [0, 0, -0.785] (Euler angles)
import { toDOMPosition, toDOMSize } from '@/utils/spatialCoordinates';
// Position: meters → pixels
const pos2D = toDOMPosition([5, 1.1, -2]);
// Returns: { x: 500, y: 300 }
// Size: meters → pixels
const size2D = toDOMSize({ width: 2, height: 1.5 });
// Returns: { width: 200, height: 150 }
import { toSpatialTransform } from '@/utils/spatialCoordinates';
const transform = toSpatialTransform({
x: 500,
y: 300,
width: 200,
height: 150,
rotation: 45,
scale: 1,
z: -3, // optional custom depth
});
// Returns: { position: [x,y,z], rotation: [rx,ry,rz], scale: [s,s,s] }
Critical: DOM Y grows downward, 3D Y grows upward. The conversion handles this:
DOM: (0,0) ──────► X 3D: Y ▲
│ │
│ │
▼ Y └──────► X
The <Html> component from @react-three/drei renders DOM content in 3D space. BUT it creates DOM overlays that break immersive WebXR:
// This breaks immersive VR! DOM overlays appear as flat screen
<Html transform position={[0, 0, 0]}>
<div>Widget content</div>
</Html>
Always check if in an XR session before rendering Html:
import { useXR } from '@react-three/xr';
function SpatialWidget({ widget }) {
// Detect active XR session
const session = useXR((state) => state.session);
const isPresenting = !!session;
return (
<group position={position3D}>
{/* 3D panel mesh - always renders */}
<mesh>
<planeGeometry args={[width, height]} />
<meshStandardMaterial color="#1e1b4b" />
</mesh>
{/* Html content - ONLY when NOT in XR session */}
{!isPresenting && (
<Html transform center>
<WidgetContent />
</Html>
)}
{/* 3D placeholder - ONLY when IN XR session */}
{isPresenting && (
<Text position={[0, 0, 0.01]} fontSize={0.05}>
{widget.name}
</Text>
)}
</group>
);
}
Is this for XR (VR/AR)?
├─ YES: Use pure Three.js
│ - <mesh> with materials
│ - <Text> from drei for labels
│ - Textures for images
│ - NO <Html> components
│
└─ NO (desktop/preview): Can use Html
- <Html> for complex React UI
- iframes for widget sandboxing
- Full CSS styling
import { useActiveSpatialMode } from '@/state/useSpatialModeStore';
import { useXR } from '@react-three/xr';
function ModeAwareWidget({ widget }) {
const spatialMode = useActiveSpatialMode();
const session = useXR((state) => state.session);
const isXRActive = !!session;
// Desktop mode: use CanvasRenderer (not this component)
// This component only runs in SpatialCanvas (vr/ar modes)
if (isXRActive) {
// TRUE XR: Pure WebGL only
return <PureWebGLWidget widget={widget} />;
}
// Preview mode (vr/ar without XR session): Can use Html
return <HtmlBasedWidget widget={widget} />;
}
// src/pages/CanvasPage.tsx structure
function CanvasPage() {
const spatialMode = useActiveSpatialMode();
const isDesktopMode = spatialMode === 'desktop';
return (
<>
{/* DOM Renderer - visible only in desktop mode */}
{isDesktopMode && (
<CanvasRenderer
widgets={widgets}
// ... DOM-based rendering
/>
)}
{/* WebGL/XR Renderer - visible in VR/AR modes */}
<SpatialCanvas active={!isDesktopMode} />
</>
);
}
function WidgetPanel({ widget, isPresenting }) {
const size3D = toSpatialSize({ width: widget.width, height: widget.height });
return (
<group>
{/* Base panel - always visible */}
<mesh>
<planeGeometry args={[size3D.width, size3D.height]} />
<meshStandardMaterial color="#1e1b4b" transparent opacity={0.95} />
</mesh>
{/* Content layer */}
{isPresenting ? (
// XR mode: 3D placeholder
<group position={[0, 0, 0.01]}>
<Text fontSize={0.05} color="white" anchorX="center">
{widget.name || 'Widget'}
</Text>
<Text position={[0, -0.08, 0]} fontSize={0.03} color="#888">
{widget.widgetDefId}
</Text>
</group>
) : (
// Preview mode: Full HTML content
<Html transform center distanceFactor={1.5}>
<WidgetIframe widget={widget} />
</Html>
)}
</group>
);
}
When users move widgets in VR, update the 2D canvas state:
function handleWidgetMove(widgetId: string, newPos3D: [number, number, number]) {
// Convert 3D position back to 2D
const pos2D = toDOMPosition(newPos3D);
// Update the shared canvas store
useCanvasStore.getState().updateWidget(widgetId, {
x: pos2D.x,
y: pos2D.y,
});
}
HTML content can look pixelated in VR. Use resolution scaling:
const VR_RESOLUTION_SCALE = 2.5;
function getWidgetResolutionScale(width: number, height: number): number {
const maxDimension = Math.max(width, height);
if (maxDimension <= 600) return VR_RESOLUTION_SCALE;
// Scale down for large widgets to save memory
return Math.max(1.5, VR_RESOLUTION_SCALE * (600 / maxDimension));
}
// Render at higher resolution, scale down visually
const scaledWidth = widget.width * resolutionScale;
const scaledHeight = widget.height * resolutionScale;
const inverseScale = 1 / resolutionScale;
<Html scale={inverseScale} style={{ width: scaledWidth, height: scaledHeight }}>
{/* Content renders at higher resolution */}
</Html>
| File | Purpose |
|---|---|
src/utils/spatialCoordinates.ts | All coordinate conversion utilities |
src/state/useSpatialModeStore.ts | Spatial mode state (desktop/vr/ar) |
src/pages/CanvasPage.tsx | Parallel renderer orchestration |
src/components/spatial/SpatialCanvas.tsx | WebGL/Three.js canvas |
src/components/spatial/SpatialScene.tsx | 3D scene composition |
src/components/spatial/SpatialWidgetContainer.tsx | Widget rendering in 3D |
src/components/canvas/CanvasRenderer.tsx | DOM-based 2D rendering |
Cause: <Html> components rendering in XR session
Fix: Check isPresenting and skip Html when true
Cause: Missing Y-axis inversion or eye height offset
Fix: Use toSpatialPosition() / toDOMPosition() consistently
Cause: Default resolution too low for VR displays
Fix: Apply VR_RESOLUTION_SCALE multiplier to Html content
Cause: Pointer events not reaching 3D meshes
Fix: Ensure pointerEvents: 'auto' on Html or use onClick on meshes
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience