| name | composable-svelte-graphics |
| description | 3D graphics and WebGPU/WebGL rendering with Composable Svelte. Use when building 3D scenes, working with cameras, lights, meshes, materials, or implementing WebGPU/WebGL graphics. Covers Scene, Camera, Light, Mesh components, geometry types (box, sphere, cylinder, torus, plane), material properties, and state-driven 3D rendering. |
Composable Svelte Graphics
State-driven 3D graphics for Composable Svelte using WebGPU/WebGL with Babylon.js.
PACKAGE OVERVIEW
Package: @composable-svelte/graphics
Purpose: Declarative 3D graphics with automatic WebGPU/WebGL renderer selection.
Technology Stack:
- Babylon.js: Industry-standard 3D engine
- WebGPU: Modern high-performance graphics API (auto-detected)
- WebGL: Fallback for broader browser support
Renderer Selection:
- Try WebGPU (if browser supports it)
- Fallback to WebGL (universal support)
- Transparent to the user
Core Components:
Scene- Root container, manages renderer lifecycleCamera- Viewpoint and projectionLight- Illumination (ambient, directional, point, spot)Mesh- 3D objects with geometry and materials
State Management:
graphicsReducer- Pure reducer for all graphics statecreateInitialGraphicsState()- Initial state factory- Store-driven updates sync automatically to Babylon.js
QUICK START
import { createStore } from '@composable-svelte/core';
import {
Scene,
Camera,
Light,
Mesh,
graphicsReducer,
createInitialGraphicsState
} from '@composable-svelte/graphics';
// Create graphics store
const store = createStore({
initialState: createInitialGraphicsState({
backgroundColor: '#1a1a2e'
}),
reducer: graphicsReducer,
dependencies: {}
});
// Track rotation for animation
let rotation = $state(0);
function rotateShapes() {
rotation += Math.PI / 4;
}
// Render 3D scene
<Scene {store} height="500px">
<Camera {store} position={[0, 4, 12]} lookAt={[0, 0, 0]} fov={45} />
<Light {store} type="ambient" intensity={0.4} />
<Light {store} type="directional" position={[5, 10, 7.5]} intensity={1.2} />
<Mesh
{store}
id="rotating-box"
geometry={{ type: 'box', size: 1.5 }}
material={{ color: '#ff6b6b', metallic: 0.7, roughness: 0.3 }}
position={[0, 1.5, 0]}
rotation={[0, rotation, 0]}
/>
</Scene>
<button onclick={rotateShapes}>Rotate 45°</button>
SCENE COMPONENT
Purpose: Root container for 3D rendering. Manages Babylon.js engine lifecycle and syncs store state to the renderer.
Props:
store: Store<GraphicsState, GraphicsAction>- Graphics store (required)width: string | number- Canvas width (default: '100%')height: string | number- Canvas height (default: '600px')children: Snippet- Child components (Camera, Light, Mesh)
Behavior:
- Creates canvas element
- Initializes Babylon.js engine
- Attempts WebGPU, falls back to WebGL
- Dispatches
rendererInitializedaction with capabilities - Syncs store updates to Babylon.js scene
- Cleans up engine on unmount
Usage:
<Scene {store} height="500px">
<!-- Children render here -->
</Scene>
State Synchronization: Scene uses manual subscription (like MapPrimitive) to avoid infinite loops:
- Tracks previous state values
- Only updates Babylon.js when state actually changes
- Uses JSON.stringify for deep comparison
Renderer Info: Access renderer info from store:
$store.renderer.activeRenderer // 'webgpu' | 'webgl'
$store.renderer.isInitialized // boolean
$store.renderer.capabilities // { maxTextureSize, ... }
$store.renderer.error // string | null
CAMERA COMPONENT
Purpose: Defines the viewpoint and projection for the scene.
Props:
store: Store<GraphicsState, GraphicsAction>- Graphics store (required)type: 'perspective' | 'orthographic'- Camera type (default: 'perspective')position: [number, number, number]- Camera position (required)lookAt: [number, number, number]- Target point to look at (required)fov: number- Field of view in degrees (default: 45, perspective only)near: number- Near clipping plane (optional)far: number- Far clipping plane (optional)
Behavior:
- Dispatches
updateCameraaction on mount - Re-dispatches when props change
- Does not render visual output (state-only component)
Usage:
<!-- Perspective camera (default) -->
<Camera
{store}
position={[0, 4, 12]}
lookAt={[0, 0, 0]}
fov={45}
/>
<!-- Orthographic camera -->
<Camera
{store}
type="orthographic"
position={[0, 10, 0]}
lookAt={[0, 0, 0]}
/>
Common Camera Positions:
- Front view:
position={[0, 0, 10]}, lookAt={[0, 0, 0]} - Top-down:
position={[0, 10, 0]}, lookAt={[0, 0, 0]} - Isometric:
position={[5, 5, 5]}, lookAt={[0, 0, 0]} - Close-up:
position={[0, 2, 5]}, lookAt={[0, 1, 0]}
LIGHT COMPONENT
Purpose: Add illumination to the scene. Supports multiple light types.
Light Types:
Ambient Light
Uniform light from all directions (no position/direction).
Props:
type: 'ambient'intensity: number- Light intensity (0-1 typical, can exceed)color: string- Light color (hex or CSS color, default: '#ffffff')
Usage:
<Light {store} type="ambient" intensity={0.4} color="#ffffff" />
Directional Light
Parallel rays from a specific direction (like sunlight).
Props:
type: 'directional'position: [number, number, number]- Light position (defines direction)intensity: number- Light intensitycolor: string- Light color (optional)
Usage:
<Light {store} type="directional" position={[5, 10, 7.5]} intensity={1.2} />
Point Light
Emits light in all directions from a point (like a light bulb).
Props:
type: 'point'position: [number, number, number]- Light positionintensity: number- Light intensityradius: number- Light radius/range (optional)color: string- Light color (optional)
Usage:
<Light {store} type="point" position={[0, 3, 0]} intensity={1.5} radius={10} />
Spot Light
Cone-shaped light (like a flashlight).
Props:
type: 'spot'position: [number, number, number]- Light positiondirection: [number, number, number]- Light direction vectorangle: number- Cone angle in radians (default: Math.PI / 4)intensity: number- Light intensitycolor: string- Light color (optional)
Usage:
<Light
{store}
type="spot"
position={[0, 5, 0]}
direction={[0, -1, 0]}
angle={Math.PI / 6}
intensity={2.0}
/>
Common Lighting Setups:
<!-- Three-point lighting (photography standard) -->
<Light {store} type="ambient" intensity={0.3} />
<Light {store} type="directional" position={[5, 5, 5]} intensity={1.0} /> <!-- Key -->
<Light {store} type="directional" position={[-3, 3, -3]} intensity={0.5} /> <!-- Fill -->
<Light {store} type="directional" position={[0, 2, -5]} intensity={0.3} /> <!-- Back -->
<!-- Outdoor scene (sun + ambient) -->
<Light {store} type="ambient" intensity={0.4} color="#87ceeb" />
<Light {store} type="directional" position={[10, 20, 10]} intensity={1.5} color="#fff8dc" />
<!-- Indoor scene (ambient + point lights) -->
<Light {store} type="ambient" intensity={0.2} />
<Light {store} type="point" position={[0, 3, 0]} intensity={1.0} radius={5} />
<Light {store} type="point" position={[5, 2, 5]} intensity={0.8} radius={4} />
MESH COMPONENT
Purpose: Render 3D objects with geometry and materials.
Props:
store: Store<GraphicsState, GraphicsAction>- Graphics store (required)id: string- Unique identifier (required)geometry: GeometryConfig- Geometry configuration (required)material: MaterialConfig- Material configuration (required)position: [number, number, number]- Position (required)rotation: [number, number, number]- Rotation in radians (default: [0, 0, 0])scale: [number, number, number]- Scale (default: [1, 1, 1])visible: boolean- Visibility (default: true)
Lifecycle:
onMount: DispatchesaddMeshaction- Props change: Dispatches
updateMeshaction onDestroy: DispatchesremoveMeshaction
Usage:
<Mesh
{store}
id="my-cube"
geometry={{ type: 'box', size: 1.5 }}
material={{ color: '#ff6b6b', metallic: 0.7, roughness: 0.3 }}
position={[0, 1, 0]}
rotation={[0, Math.PI / 4, 0]}
scale={[1, 1, 1]}
/>
GEOMETRY TYPES
Box
Rectangular prism.
Config:
{ type: 'box'; size: number }
Example:
<Mesh
{store}
id="cube"
geometry={{ type: 'box', size: 1.5 }}
material={{ color: '#ff6b6b' }}
position={[0, 1, 0]}
/>
Sphere
Spherical geometry.
Config:
{
type: 'sphere';
radius: number;
segments?: number; // Default: 32 (higher = smoother)
}
Example:
<Mesh
{store}
id="ball"
geometry={{ type: 'sphere', radius: 0.8, segments: 32 }}
material={{ color: '#4ecdc4', metallic: 0.8, roughness: 0.2 }}
position={[0, 1, 0]}
/>
Segments: Higher values create smoother spheres but increase draw calls.
- Low poly (16 segments): Retro/stylized look
- Medium (32 segments): Default, good balance
- High poly (64 segments): Smooth, more expensive
Cylinder
Cylindrical geometry.
Config:
{
type: 'cylinder';
height: number;
diameter: number;
}
Example:
<Mesh
{store}
id="pillar"
geometry={{ type: 'cylinder', height: 2, diameter: 1 }}
material={{ color: '#95e1d3' }}
position={[0, 1, 0]}
/>
Torus
Donut-shaped geometry.
Config:
{
type: 'torus';
diameter: number; // Outer diameter
thickness: number; // Tube thickness
segments?: number; // Default: 32
}
Example:
<Mesh
{store}
id="ring"
geometry={{ type: 'torus', diameter: 1.5, thickness: 0.3, segments: 32 }}
material={{ color: '#f38181', metallic: 0.9, roughness: 0.1 }}
position={[0, 1, 0]}
/>
Plane
Flat rectangular surface.
Config:
{
type: 'plane';
width: number;
height: number;
}
Example:
<!-- Ground plane (rotated to horizontal) -->
<Mesh
{store}
id="ground"
geometry={{ type: 'plane', width: 12, height: 12 }}
material={{ color: '#aa96da', metallic: 0.3, roughness: 0.7 }}
position={[0, 0, 0]}
rotation={[Math.PI / 2, 0, 0]}
/>
Note: Planes are initially vertical (facing Z-axis). Rotate by [Math.PI / 2, 0, 0] to make horizontal (ground).
MATERIAL PROPERTIES
MaterialConfig Interface:
interface MaterialConfig {
color: string; // Hex or CSS color
metallic?: number; // 0-1 (default: 0.5)
roughness?: number; // 0-1 (default: 0.5)
emissive?: string; // Emissive color (optional)
alpha?: number; // 0-1 transparency (optional)
wireframe?: boolean; // Wireframe mode (default: false)
}
PBR Workflow: Materials use Physically Based Rendering (PBR) with metallic/roughness workflow.
Metallic (0-1)
Controls how metal-like the surface appears.
0.0: Non-metallic (plastic, rubber, wood, stone)0.5: Semi-metallic (painted metal, worn surfaces)1.0: Fully metallic (polished metal, chrome)
Examples:
// Plastic
{ color: '#ff0000', metallic: 0.0, roughness: 0.5 }
// Painted metal
{ color: '#4ecdc4', metallic: 0.5, roughness: 0.4 }
// Polished chrome
{ color: '#ffffff', metallic: 1.0, roughness: 0.1 }
Roughness (0-1)
Controls surface smoothness/reflectivity.
0.0: Mirror-smooth (glossy, high reflections)0.5: Semi-rough (satin finish)1.0: Very rough (matte, diffuse)
Examples:
// Glass/mirror
{ color: '#ffffff', metallic: 0.0, roughness: 0.0 }
// Satin finish
{ color: '#ff6b6b', metallic: 0.3, roughness: 0.5 }
// Matte rubber
{ color: '#333333', metallic: 0.0, roughness: 1.0 }
Common Material Presets
// Polished gold
const gold = { color: '#ffd700', metallic: 1.0, roughness: 0.2 };
// Brushed aluminum
const aluminum = { color: '#c0c0c0', metallic: 1.0, roughness: 0.4 };
// Copper
const copper = { color: '#b87333', metallic: 1.0, roughness: 0.3 };
// Wood
const wood = { color: '#8b4513', metallic: 0.0, roughness: 0.8 };
// Plastic
const plastic = { color: '#ff6b6b', metallic: 0.0, roughness: 0.4 };
// Stone
const stone = { color: '#808080', metallic: 0.0, roughness: 0.9 };
// Rubber
const rubber = { color: '#1a1a1a', metallic: 0.0, roughness: 1.0 };
COMPLETE EXAMPLE
Full scene with all geometry types:
<script lang="ts">
import { createStore } from '@composable-svelte/core';
import {
Scene,
Camera,
Light,
Mesh,
graphicsReducer,
createInitialGraphicsState
} from '@composable-svelte/graphics';
// Create graphics store
const store = createStore({
initialState: createInitialGraphicsState({
backgroundColor: '#1a1a2e'
}),
reducer: graphicsReducer,
dependencies: {}
});
// Track rotation for animation
let rotation = $state(0);
function rotateShapes() {
rotation += Math.PI / 4;
}
</script>
<!-- Renderer info -->
<div>
{#if $store.renderer.isInitialized}
<span>Renderer: {$store.renderer.activeRenderer?.toUpperCase()}</span>
<span>Max Texture: {$store.renderer.capabilities.maxTextureSize}px</span>
{:else if $store.renderer.error}
<span>Error: {$store.renderer.error}</span>
{:else}
<span>Initializing...</span>
{/if}
</div>
<!-- 3D Scene -->
<Scene {store} height="500px">
<Camera {store} position={[0, 4, 12]} lookAt={[0, 0, 0]} fov={45} />
<Light {store} type="ambient" intensity={0.4} color="#ffffff" />
<Light {store} type="directional" position={[5, 10, 7.5]} intensity={1.2} color="#ffffff" />
<!-- Row 1: Box, Sphere, Cylinder -->
<Mesh
{store}
id="box"
geometry={{ type: 'box', size: 1.5 }}
material={{ color: '#ff6b6b', metallic: 0.7, roughness: 0.3 }}
position={[-4, 1.5, 0]}
rotation={[0, rotation, 0]}
/>
<Mesh
{store}
id="sphere"
geometry={{ type: 'sphere', radius: 0.8, segments: 32 }}
material={{ color: '#4ecdc4', metallic: 0.8, roughness: 0.2 }}
position={[-1.5, 1.5, 0]}
rotation={[0, rotation, 0]}
/>
<Mesh
{store}
id="cylinder"
geometry={{ type: 'cylinder', height: 2, diameter: 1 }}
material={{ color: '#95e1d3', metallic: 0.6, roughness: 0.4 }}
position={[1, 1.5, 0]}
rotation={[0, rotation, 0]}
/>
<!-- Row 2: Torus, Plane -->
<Mesh
{store}
id="torus"
geometry={{ type: 'torus', diameter: 1.5, thickness: 0.3, segments: 32 }}
material={{ color: '#f38181', metallic: 0.9, roughness: 0.1 }}
position={[3.5, 1.5, 0]}
rotation={[0, rotation, 0]}
/>
<!-- Ground plane -->
<Mesh
{store}
id="plane"
geometry={{ type: 'plane', width: 12, height: 12 }}
material={{ color: '#aa96da', metallic: 0.3, roughness: 0.7 }}
position={[0, -0.5, 0]}
rotation={[Math.PI / 2, 0, 0]}
/>
</Scene>
<button onclick={rotateShapes}>Rotate All Shapes 45°</button>
STATE MANAGEMENT
GraphicsState Interface
interface GraphicsState {
// Renderer
renderer: {
activeRenderer: 'webgpu' | 'webgl' | null;
isInitialized: boolean;
capabilities: {
supportsWebGPU: boolean;
supportsWebGL: boolean;
maxTextureSize: number;
maxVertexAttributes: number;
};
error: string | null;
};
// Scene
scene: SceneNode;
backgroundColor: string;
// Camera
camera: CameraConfig;
// Lights
lights: LightConfig[];
// Meshes
meshes: MeshConfig[];
// Animations (future)
animations: AnimationState[];
// Loading
isLoading: boolean;
loadingProgress: number; // 0-1
}
GraphicsAction Types
type GraphicsAction =
// Renderer
| { type: 'rendererInitialized'; renderer: 'webgpu' | 'webgl'; capabilities: RendererCapabilities }
| { type: 'rendererError'; error: string }
// Camera
| { type: 'updateCamera'; camera: Partial<CameraConfig> }
| { type: 'setCameraPosition'; position: Vector3 }
| { type: 'setCameraLookAt'; lookAt: Vector3 }
// Mesh
| { type: 'addMesh'; mesh: MeshConfig }
| { type: 'removeMesh'; id: string }
| { type: 'updateMesh'; id: string; updates: Partial<MeshConfig> }
| { type: 'setMeshPosition'; id: string; position: Vector3 }
| { type: 'setMeshRotation'; id: string; rotation: Vector3 }
| { type: 'setMeshScale'; id: string; scale: Vector3 }
| { type: 'toggleMeshVisibility'; id: string }
// Light
| { type: 'addLight'; light: LightConfig }
| { type: 'removeLight'; index: number }
| { type: 'updateLight'; index: number; light: Partial<LightConfig> }
// Scene
| { type: 'setBackgroundColor'; color: string }
| { type: 'clearScene' };
Reducer Pattern
Graphics reducer is pure and testable:
import { graphicsReducer } from '@composable-svelte/graphics';
import { TestStore } from '@composable-svelte/core';
const store = new TestStore({
initialState: createInitialGraphicsState(),
reducer: graphicsReducer,
dependencies: {}
});
// Add mesh
await store.send({
type: 'addMesh',
mesh: {
id: 'test-cube',
geometry: { type: 'box', size: 1 },
material: { color: '#ff0000' },
position: [0, 0, 0]
}
}, (state) => {
expect(state.meshes.length).toBe(1);
expect(state.meshes[0].id).toBe('test-cube');
});
// Update position
await store.send({
type: 'setMeshPosition',
id: 'test-cube',
position: [1, 2, 3]
}, (state) => {
expect(state.meshes[0].position).toEqual([1, 2, 3]);
});
PERFORMANCE CONSIDERATIONS
Geometry Complexity
Segments: Higher segment counts create smoother geometry but increase draw calls.
// Low poly (fast, retro look)
geometry={{ type: 'sphere', radius: 1, segments: 16 }}
// Default (good balance)
geometry={{ type: 'sphere', radius: 1, segments: 32 }}
// High poly (slow, smooth)
geometry={{ type: 'sphere', radius: 1, segments: 64 }}
Draw Calls
Each mesh = 1 draw call. Minimize meshes for better performance.
Good:
// 3 meshes = 3 draw calls
<Mesh id="obj1" ... />
<Mesh id="obj2" ... />
<Mesh id="obj3" ... />
Bad:
// 1000 meshes = 1000 draw calls (very slow!)
{#each items as item}
<Mesh id={item.id} ... />
{/each}
Solution: Use instancing for many similar objects (future feature).
Update Frequency
Scene sync uses deep comparison (JSON.stringify). Avoid updating mesh props every frame.
Good:
// Update rotation only when button clicked
let rotation = $state(0);
function rotate() { rotation += Math.PI / 4; }
<Mesh rotation={[0, rotation, 0]} ... />
Bad:
// Updates every frame (60 FPS) - expensive!
let time = $state(0);
setInterval(() => { time += 0.01; }, 16);
<Mesh rotation={[0, time, 0]} ... />
Solution: Use animation system (future feature) or throttle updates.
COMMON PATTERNS
Rotation Animation
let rotation = $state(0);
function rotateObject() {
rotation += Math.PI / 4; // 45 degrees
}
<Mesh rotation={[0, rotation, 0]} ... />
<button onclick={rotateObject}>Rotate 45°</button>
Camera Controls
let cameraDistance = $state(12);
function zoomIn() {
cameraDistance = Math.max(5, cameraDistance - 2);
}
function zoomOut() {
cameraDistance = Math.min(20, cameraDistance + 2);
}
<Camera position={[0, 4, cameraDistance]} lookAt={[0, 0, 0]} />
<button onclick={zoomIn}>Zoom In</button>
<button onclick={zoomOut}>Zoom Out</button>
Dynamic Lighting
let lightIntensity = $state(1.0);
function adjustBrightness(delta: number) {
lightIntensity = Math.max(0, Math.min(2, lightIntensity + delta));
}
<Light type="directional" position={[5, 10, 7.5]} intensity={lightIntensity} />
<button onclick={() => adjustBrightness(0.2)}>Brighter</button>
<button onclick={() => adjustBrightness(-0.2)}>Dimmer</button>
Toggle Visibility
let showObject = $state(true);
// Option 1: Conditional rendering
{#if showObject}
<Mesh id="object" ... />
{/if}
// Option 2: Visible prop
<Mesh id="object" visible={showObject} ... />
<button onclick={() => showObject = !showObject}>
{showObject ? 'Hide' : 'Show'}
</button>
FUTURE FEATURES
These features are planned but not yet implemented:
Custom Shaders
// Future API
<Mesh
id="custom"
geometry={{ type: 'sphere', radius: 1 }}
material={{
type: 'custom',
vertexShader: '...',
fragmentShader: '...',
uniforms: { time: 0.0 }
}}
position={[0, 0, 0]}
/>
Textures
// Future API
<Mesh
id="textured"
geometry={{ type: 'box', size: 1 }}
material={{
color: '#ffffff',
albedoTexture: '/textures/wood.jpg',
normalMap: '/textures/wood_normal.jpg'
}}
position={[0, 0, 0]}
/>
Animations
// Future API
<Mesh
id="animated"
geometry={{ type: 'box', size: 1 }}
material={{ color: '#ff6b6b' }}
position={[0, 0, 0]}
animation={{
property: 'rotation',
from: [0, 0, 0],
to: [0, Math.PI * 2, 0],
duration: 2000,
loop: true,
easing: 'linear'
}}
/>
Post-Processing
// Future API
<Scene {store} postProcessing={{
bloom: { enabled: true, intensity: 0.5 },
ssao: { enabled: true, radius: 2 },
fxaa: true
}}>
...
</Scene>
CROSS-REFERENCES
Related Skills:
- composable-svelte-core: Store, reducer, Effect system
- composable-svelte-components: UI components that complement 3D scenes
- composable-svelte-testing: TestStore for testing graphics reducers
When to Use Each Package:
- graphics: 3D scenes, WebGPU/WebGL rendering
- charts: 2D data visualization (see composable-svelte-charts)
- maps: Geospatial data (see composable-svelte-maps)
- code: Code editors, syntax highlighting (see composable-svelte-code)
TROUBLESHOOTING
Scene not rendering:
- Check browser WebGPU/WebGL support
- Verify store is created with
graphicsReducer - Check console for renderer errors in
$store.renderer.error
Objects not visible:
- Ensure Camera is pointing at objects (
lookAtprop) - Add at least one Light (scene is dark by default)
- Check mesh
visibleprop - Verify position values (objects might be off-screen)
Poor performance:
- Reduce segment counts on spheres/toruses
- Minimize number of meshes (each mesh = 1 draw call)
- Avoid updating mesh props every frame
- Use simpler geometry (box vs sphere)
TypeScript errors:
- Ensure
@composable-svelte/graphicsis installed - Check geometry config matches type (e.g.,
boxrequiressize, notradius) - Verify Vector3 arrays are exactly 3 numbers
[x, y, z]