Claude-skill-registry composable-svelte-graphics
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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/composable-svelte-graphics" ~/.claude/skills/majiayu000-claude-skill-registry-composable-svelte-graphics && rm -rf "$T"
skills/data/composable-svelte-graphics/SKILL.mdComposable 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:
- Root container, manages renderer lifecycleScene
- Viewpoint and projectionCamera
- Illumination (ambient, directional, point, spot)Light
- 3D objects with geometry and materialsMesh
State Management:
- Pure reducer for all graphics stategraphicsReducer
- Initial state factorycreateInitialGraphicsState()- 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:
- Graphics store (required)store: Store<GraphicsState, GraphicsAction>
- Canvas width (default: '100%')width: string | number
- Canvas height (default: '600px')height: string | number
- Child components (Camera, Light, Mesh)children: Snippet
Behavior:
- Creates canvas element
- Initializes Babylon.js engine
- Attempts WebGPU, falls back to WebGL
- Dispatches
action with capabilitiesrendererInitialized - 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:
- Graphics store (required)store: Store<GraphicsState, GraphicsAction>
- Camera type (default: 'perspective')type: 'perspective' | 'orthographic'
- Camera position (required)position: [number, number, number]
- Target point to look at (required)lookAt: [number, number, number]
- Field of view in degrees (default: 45, perspective only)fov: number
- Near clipping plane (optional)near: number
- Far clipping plane (optional)far: number
Behavior:
- Dispatches
action on mountupdateCamera - 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'
- Light intensity (0-1 typical, can exceed)intensity: number
- Light color (hex or CSS color, default: '#ffffff')color: string
Usage:
<Light {store} type="ambient" intensity={0.4} color="#ffffff" />
Directional Light
Parallel rays from a specific direction (like sunlight).
Props:
type: 'directional'
- Light position (defines direction)position: [number, number, number]
- Light intensityintensity: number
- Light color (optional)color: string
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'
- Light positionposition: [number, number, number]
- Light intensityintensity: number
- Light radius/range (optional)radius: number
- Light color (optional)color: string
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'
- Light positionposition: [number, number, number]
- Light direction vectordirection: [number, number, number]
- Cone angle in radians (default: Math.PI / 4)angle: number
- Light intensityintensity: number
- Light color (optional)color: string
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:
- Graphics store (required)store: Store<GraphicsState, GraphicsAction>
- Unique identifier (required)id: string
- Geometry configuration (required)geometry: GeometryConfig
- Material configuration (required)material: MaterialConfig
- Position (required)position: [number, number, number]
- Rotation in radians (default: [0, 0, 0])rotation: [number, number, number]
- Scale (default: [1, 1, 1])scale: [number, number, number]
- Visibility (default: true)visible: boolean
Lifecycle:
: DispatchesonMount
actionaddMesh- Props change: Dispatches
actionupdateMesh
: DispatchesonDestroy
actionremoveMesh
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.
: Non-metallic (plastic, rubber, wood, stone)0.0
: Semi-metallic (painted metal, worn surfaces)0.5
: Fully metallic (polished metal, chrome)1.0
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.
: Mirror-smooth (glossy, high reflections)0.0
: Semi-rough (satin finish)0.5
: Very rough (matte, diffuse)1.0
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 (
prop)lookAt - Add at least one Light (scene is dark by default)
- Check mesh
propvisible - 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
is installed@composable-svelte/graphics - Check geometry config matches type (e.g.,
requiresbox
, notsize
)radius - Verify Vector3 arrays are exactly 3 numbers
[x, y, z]