High-performance WebGL2/WebGPU bindings for Svelte 5, powered by luma.gl.
$state, $effect, $props)npm install svelte-luma
WebGPU support (@luma.gl/webgpu) is included as an optional dependency and will be used automatically when available.
<script lang="ts">
import { Canvas, Mesh, OrbitCamera, createCubeGeometry, createPhongMaterial } from 'svelte-luma';
const cube = createCubeGeometry(1);
const { vs, fs } = createPhongMaterial({ color: [0.8, 0.2, 0.2] });
let rotation = $state<[number, number, number]>([0, 0, 0]);
</script>
<Canvas
width={800}
height={600}
clearColor={[0.1, 0.1, 0.1, 1]}
onframe={(ctx) => rotation = [0, ctx.time * 0.001, 0]}
>
<OrbitCamera distance={5}>
<Mesh geometry={cube} vs={vs} fs={fs} {rotation} />
</OrbitCamera>
</Canvas>
Root component that initializes the GPU device and render loop.
<Canvas
width={800}
height={600}
pixelRatio={window.devicePixelRatio}
backend="best"
clearColor={[0, 0, 0, 1]}
autoClear={true}
antialias={true}
ondevicecreated={(device) => {}}
onframe={(ctx) => {}}
onerror={(err) => {}}
>
{children}
</Canvas>
Renderable component with transforms and material support.
<Mesh
geometry={createCubeGeometry()}
vs={vertexShader}
fs={fragmentShader}
position={[0, 0, 0]}
rotation={[0, Math.PI / 4, 0]}
scale={[1, 1, 1]}
uniforms={{ uColor: [1, 0, 0, 1] }}
visible={true}
renderOrder={100}
/>
Transform hierarchy container. Children inherit the group's transform.
<Group position={[0, 2, 0]} rotation={[0, time, 0]}>
<Mesh geometry={cube} vs={vs} fs={fs} />
<Group position={[2, 0, 0]}>
<Mesh geometry={sphere} vs={vs} fs={fs} />
</Group>
</Group>
Efficient GPU instancing for rendering many objects.
<script>
const instances = Array.from({ length: 1000 }, (_, i) => ({
position: [Math.random() * 10 - 5, Math.random() * 10 - 5, Math.random() * 10 - 5],
rotation: [0, 0, 0],
scale: [0.1, 0.1, 0.1],
color: [Math.random(), Math.random(), Math.random(), 1]
}));
</script>
<InstancedMesh
geometry={createSphereGeometry(1)}
vs={INSTANCED_MATERIAL_VS}
fs={INSTANCED_MATERIAL_FS}
count={1000}
{instances}
/>
Interactive camera with drag and zoom controls.
<OrbitCamera
distance={5}
rotationX={0}
rotationY={0}
target={[0, 0, 0]}
fov={60}
near={0.1}
far={1000}
enableDrag={true}
enableZoom={true}
zoomSpeed={0.1}
rotationSpeed={0.01}
minDistance={1}
maxDistance={100}
damping={0}
>
<Mesh ... />
</OrbitCamera>
<PointLight position={[5, 5, 5]} color={[1, 1, 1]} intensity={1} />
<DirectionalLight direction={[0, -1, 0]} color={[1, 1, 1]} intensity={0.5} />
<AmbientLight color={[0.2, 0.2, 0.2]} intensity={1} />
<SpotLight position={[0, 5, 0]} direction={[0, -1, 0]} angle={Math.PI / 6} />
Access frame timing in children.
<RenderLoop onframe={(ctx) => console.log(ctx.time)}>
{#snippet children(ctx)}
<Mesh uniforms={{ uTime: ctx.time }} ... />
{/snippet}
</RenderLoop>
GPU resource components.
<Buffer data={new Float32Array([...])} onbuffercreated={(buf) => {}} />
<Texture src="/image.png" ontextureloaded={(tex) => {}} />
import {
useDevice,
useReady,
useFrame,
useFrameCallback,
useRender,
useViewport,
useMouse,
useScene,
useParentTransform,
useLights,
useBuffer,
useTexture,
useAnimatedValue,
useSpring
} from 'svelte-luma';
// Get the GPU device
const device = useDevice();
console.log(device.current); // Device | null
// Check if ready
const ready = useReady();
if (ready.value) { ... }
// Frame timing
const frame = useFrame();
console.log(frame.time, frame.deltaTime, frame.frameCount);
// Register frame callback with priority
useFrameCallback((time, deltaTime) => {
// Called every frame
}, { priority: 100 });
// Custom render callback
const { unregister } = useRender(() => {
// Render something
}, priority);
// Viewport info
const viewport = useViewport();
console.log(viewport.width, viewport.height, viewport.aspect);
// Mouse state
const mouse = useMouse();
console.log(mouse.x, mouse.y, mouse.normalizedX, mouse.normalizedY);
// Scene matrices (when inside OrbitCamera/Scene)
const scene = useScene();
console.log(scene.projectionMatrix, scene.viewMatrix, scene.cameraPosition);
// Animation helpers
const animated = useAnimatedValue((time) => Math.sin(time * 0.001), 0);
const spring = useSpring(() => targetValue, { stiffness: 0.1, damping: 0.8 });
Pre-built material factories:
import {
createBasicMaterial,
createNormalMaterial,
createPhongMaterial,
createLambertMaterial,
createInstancedMaterial
} from 'svelte-luma';
// Basic flat color
const basic = createBasicMaterial({ color: [1, 0, 0] });
// Normal visualization
const normal = createNormalMaterial();
// Phong lighting
const phong = createPhongMaterial({
color: [0.8, 0.2, 0.2],
ambient: [0.1, 0.1, 0.1],
specular: [1, 1, 1],
shininess: 32
});
// Lambert diffuse
const lambert = createLambertMaterial({ color: [0.5, 0.5, 0.8] });
// For instanced rendering
const instanced = createInstancedMaterial();
// Use in Mesh
<Mesh geometry={cube} vs={phong.vs} fs={phong.fs} />
import {
createTriangleGeometry,
createQuadGeometry,
createPlaneGeometry,
createCubeGeometry,
createSphereGeometry,
createCircleGeometry,
createCylinderGeometry,
createTorusGeometry
} from 'svelte-luma';
const cube = createCubeGeometry(1);
const sphere = createSphereGeometry(1, 32, 16);
const torus = createTorusGeometry(1, 0.4, 32, 64);
const plane = createPlaneGeometry(10, 10, 10, 10);
const cylinder = createCylinderGeometry(0.5, 0.5, 2, 32);
import {
// Vector operations
vec3, vec3Add, vec3Sub, vec3Scale, vec3Dot, vec3Cross,
vec3Length, vec3Normalize, vec3Lerp,
// Scalar utilities
degToRad, radToDeg, clamp, lerp, smoothstep,
// Matrix creation (uses internal pool - zero allocations)
mat4Identity, mat4Perspective, mat4Orthographic, mat4LookAt,
mat4Translation, mat4Scaling, mat4RotationX, mat4RotationY, mat4RotationZ,
mat4FromEuler, mat4FromQuaternion, mat4Compose,
// Matrix operations
mat4Multiply, mat4Invert, mat4Transpose,
mat4TransformPoint, mat4TransformDirection,
// Create persistent copies (allocates new array)
mat4Clone, mat4Copy
} from 'svelte-luma';
const projection = mat4Clone(mat4Perspective(degToRad(60), aspect, 0.1, 1000));
const view = mat4Clone(mat4LookAt([0, 5, 10], [0, 0, 0], [0, 1, 0]));
Control render order with priority constants:
import { RenderLayers } from 'svelte-luma';
// Built-in priorities
RenderLayers.BACKGROUND // 0
RenderLayers.OPAQUE // 100
RenderLayers.TRANSPARENT // 200
RenderLayers.OVERLAY // 300
RenderLayers.UI // 400
// Use in Mesh
<Mesh renderOrder={RenderLayers.TRANSPARENT} ... />
Access the unified state directly:
import { getLumaState, LumaState } from 'svelte-luma';
// Inside a component (must be child of Canvas)
const state = getLumaState();
// Read reactive state
state.device // GPU device
state.ready // boolean
state.frame // { time, deltaTime, frameCount }
state.viewport // { width, height, pixelRatio, aspect }
state.mouse // { x, y, normalizedX, normalizedY, buttons, isOver, isDragging }
state.scene // { projectionMatrix, viewMatrix, cameraPosition }
state.parentTransform // Parent group's world matrix
// Light management
state.addLight(id, lightData);
state.updateLight(id, partialData);
state.removeLight(id);
state.getLightsArray();
// Render callbacks
const id = state.addRenderCallback(callback, priority);
state.removeRenderCallback(id);
Pre-built GLSL shaders:
import {
BASIC_VS_GLSL, BASIC_FS_GLSL,
TEXTURED_VS_GLSL, TEXTURED_FS_GLSL,
NORMAL_VS_GLSL, NORMAL_FS_GLSL,
PHONG_VS_GLSL, PHONG_FS_GLSL,
GRADIENT_VS_GLSL, GRADIENT_FS_GLSL,
INSTANCED_VS_GLSL, INSTANCED_FS_GLSL,
SIMPLE_TRIANGLE_VS_GLSL, SIMPLE_TRIANGLE_FS_GLSL,
SIMPLE_TRIANGLE_WGSL
} from 'svelte-luma';
import type {
Backend,
CanvasProps,
ModelComponentProps,
GeometryData,
AnimationContext,
BufferProps,
TextureProps,
FrameState,
ViewportState,
MouseState,
SceneState,
LightState,
RenderPriority,
RenderCallback,
Vec3, Vec4, Mat4
} from 'svelte-luma';
| Backend | Support |
|---|---|
| WebGPU | Chrome 113+, Edge 113+, Firefox (flag) |
| WebGL2 | All modern browsers |
The library auto-selects the best backend.
npm install
npm run dev # Start dev server
npm run package # Build library
npm run check # Type check
MIT
Built on luma.gl by vis.gl.