This guide walks you through installing MotionGPU and building your first fullscreen shader, step by step. By the end, you will understand the core
defineMaterialFragCanvasuseFramePrerequisites
- Svelte 5 project (SvelteKit or standalone)
- A browser with WebGPU support (Chrome 113+, Edge 113+, Safari 18+, Firefox Nightly behind flag)
Install
npm install @motion-core/motion-gpu Peer dependency:
svelte ^5Step 1: Draw a static gradient
The simplest possible setup — a fullscreen UV gradient with no animation:
<!-- App.svelte -->
<script lang="ts">
import { FragCanvas, defineMaterial } from '@motion-core/motion-gpu';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
return vec4f(uv.x, uv.y, 0.2, 1.0);
}
`
});
</script>
<div style="width: 100vw; height: 100vh;">
<FragCanvas {material} />
</div><!-- App.svelte -->
<script lang="ts">
import { FragCanvas, defineMaterial } from '@motion-core/motion-gpu';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
return vec4f(uv.x, uv.y, 0.2, 1.0);
}
`
});
</script>
<div style="width: 100vw; height: 100vh;">
<FragCanvas {material} />
</div>What is happening here:
- validates your fragment string and freezes the result into an immutable
defineMaterial.FragMaterial - The fragment must declare — this is the only hard contract.
fn frag(uv: vec2f) -> vec4franges fromuvat bottom-left to(0, 0)at top-right.(1, 1) - initializes WebGPU, compiles the shader, and renders every frame.
FragCanvas - The container determines the canvas size.
<div>fills its parent.FragCanvas
Step 2: Add a time-based animation
To animate your shader, you need a
uniformuseFrameuseFrameFragCanvas<!-- App.svelte -->
<script lang="ts">
import { FragCanvas, defineMaterial } from '@motion-core/motion-gpu';
import Runtime from './Runtime.svelte';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
let wave = 0.5 + 0.5 * sin(motiongpuUniforms.uTime + uv.x * 8.0);
return vec4f(vec3f(wave), 1.0);
}
`,
uniforms: {
uTime: { type: 'f32', value: 0 }
}
});
</script>
<FragCanvas {material}>
<Runtime />
</FragCanvas><!-- App.svelte -->
<script lang="ts">
import { FragCanvas, defineMaterial } from '@motion-core/motion-gpu';
import Runtime from './Runtime.svelte';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
let wave = 0.5 + 0.5 * sin(motiongpuUniforms.uTime + uv.x * 8.0);
return vec4f(vec3f(wave), 1.0);
}
`,
uniforms: {
uTime: { type: 'f32', value: 0 }
}
});
</script>
<FragCanvas {material}>
<Runtime />
</FragCanvas><!-- Runtime.svelte -->
<script lang="ts">
import { useFrame } from '@motion-core/motion-gpu';
useFrame((state) => {
state.setUniform('uTime', state.time);
});
</script><!-- Runtime.svelte -->
<script lang="ts">
import { useFrame } from '@motion-core/motion-gpu';
useFrame((state) => {
state.setUniform('uTime', state.time);
});
</script>What is happening here:
- declares a single
uniforms: { uTime: { type: 'f32', value: 0 } }uniform with initial valuef32.0 - In the fragment shader, is available as
uTime— the library generates the binding automatically.motiongpuUniforms.uTime - registers a task that runs every frame before rendering.
useFrameis thestate.timetimestamp in seconds.requestAnimationFrame - updates the uniform value in the runtime override map. The renderer writes only dirty ranges to the GPU buffer.
state.setUniform('uTime', state.time)
Step 3: Add mouse interaction
You can declare multiple uniforms. Here we add a
vec2f<!-- App.svelte -->
<script lang="ts">
import { FragCanvas, defineMaterial } from '@motion-core/motion-gpu';
import Runtime from './Runtime.svelte';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
let dist = distance(uv, motiongpuUniforms.uMouse);
let glow = 0.02 / (dist * dist + 0.001);
let color = vec3f(glow * 0.3, glow * 0.6, glow);
return vec4f(color, 1.0);
}
`,
uniforms: {
uTime: 0,
uMouse: [0.5, 0.5]
}
});
</script>
<FragCanvas {material}>
<Runtime />
</FragCanvas><!-- App.svelte -->
<script lang="ts">
import { FragCanvas, defineMaterial } from '@motion-core/motion-gpu';
import Runtime from './Runtime.svelte';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
let dist = distance(uv, motiongpuUniforms.uMouse);
let glow = 0.02 / (dist * dist + 0.001);
let color = vec3f(glow * 0.3, glow * 0.6, glow);
return vec4f(color, 1.0);
}
`,
uniforms: {
uTime: 0,
uMouse: [0.5, 0.5]
}
});
</script>
<FragCanvas {material}>
<Runtime />
</FragCanvas><!-- Runtime.svelte -->
<script lang="ts">
import { useFrame, useMotionGPU } from '@motion-core/motion-gpu';
const gpu = useMotionGPU();
let mouseX = 0.5;
let mouseY = 0.5;
$effect(() => {
const canvas = gpu.canvas;
if (!canvas) return;
const handler = (e: PointerEvent) => {
const rect = canvas.getBoundingClientRect();
mouseX = (e.clientX - rect.left) / rect.width;
mouseY = 1.0 - (e.clientY - rect.top) / rect.height;
};
canvas.addEventListener('pointermove', handler);
return () => canvas.removeEventListener('pointermove', handler);
});
useFrame((state) => {
state.setUniform('uTime', state.time);
state.setUniform('uMouse', [mouseX, mouseY]);
});
</script><!-- Runtime.svelte -->
<script lang="ts">
import { useFrame, useMotionGPU } from '@motion-core/motion-gpu';
const gpu = useMotionGPU();
let mouseX = 0.5;
let mouseY = 0.5;
$effect(() => {
const canvas = gpu.canvas;
if (!canvas) return;
const handler = (e: PointerEvent) => {
const rect = canvas.getBoundingClientRect();
mouseX = (e.clientX - rect.left) / rect.width;
mouseY = 1.0 - (e.clientY - rect.top) / rect.height;
};
canvas.addEventListener('pointermove', handler);
return () => canvas.removeEventListener('pointermove', handler);
});
useFrame((state) => {
state.setUniform('uTime', state.time);
state.setUniform('uMouse', [mouseX, mouseY]);
});
</script>What is new here:
- uses the shorthand form — a two-element array is automatically inferred as
uMouse: [0.5, 0.5].vec2f - also uses shorthand — a plain number is inferred as
uTime: 0.f32 - gives access to the canvas element and other runtime state.
useMotionGPU() - The Y coordinate is flipped () because UV space has
1.0 - ...at bottom-left, while DOM coordinates have(0, 0)at top-left.(0, 0)
Step 4: Switch to on-demand rendering
By default,
FragCanvasrenderMode: 'always'on-demand<!-- Runtime.svelte -->
<script lang="ts">
import { useMotionGPU } from '@motion-core/motion-gpu';
const gpu = useMotionGPU();
$effect(() => {
gpu.renderMode.set('on-demand');
});
</script><!-- Runtime.svelte -->
<script lang="ts">
import { useMotionGPU } from '@motion-core/motion-gpu';
const gpu = useMotionGPU();
$effect(() => {
gpu.renderMode.set('on-demand');
});
</script>In
on-demandgpu.invalidate()useFrameon-demandFor fully manual control, use
'manual'gpu.advance()Step 5: Add a post-process pass
MotionGPU supports post-processing through pass classes. Here is a simple vignette:
<script lang="ts">
import { FragCanvas, defineMaterial, ShaderPass } from '@motion-core/motion-gpu';
import Runtime from './Runtime.svelte';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
let wave = 0.5 + 0.5 * sin(motiongpuUniforms.uTime + uv.x * 8.0);
return vec4f(vec3f(wave), 1.0);
}
`,
uniforms: { uTime: 0 }
});
const vignette = new ShaderPass({
fragment: `
fn shade(inputColor: vec4f, uv: vec2f) -> vec4f {
let dist = distance(uv, vec2f(0.5, 0.5));
let v = smoothstep(0.9, 0.35, dist);
return vec4f(inputColor.rgb * v, inputColor.a);
}
`
});
</script>
<FragCanvas {material} passes={[vignette]}>
<Runtime />
</FragCanvas><script lang="ts">
import { FragCanvas, defineMaterial, ShaderPass } from '@motion-core/motion-gpu';
import Runtime from './Runtime.svelte';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
let wave = 0.5 + 0.5 * sin(motiongpuUniforms.uTime + uv.x * 8.0);
return vec4f(vec3f(wave), 1.0);
}
`,
uniforms: { uTime: 0 }
});
const vignette = new ShaderPass({
fragment: `
fn shade(inputColor: vec4f, uv: vec2f) -> vec4f {
let dist = distance(uv, vec2f(0.5, 0.5));
let v = smoothstep(0.9, 0.35, dist);
return vec4f(inputColor.rgb * v, inputColor.a);
}
`
});
</script>
<FragCanvas {material} passes={[vignette]}>
<Runtime />
</FragCanvas>Note the different contract: pass fragments use
fn shade(inputColor: vec4f, uv: vec2f) -> vec4ffn frag(...)inputColorRecommended first-production checklist
- Keep the shader contract strict — for materials,
fn frag(uv: vec2f) -> vec4ffor passes.fn shade(inputColor: vec4f, uv: vec2f) -> vec4f - Define uniform names and types upfront in . Mismatches throw.
defineMaterial - Pick render mode intentionally — burns battery;
alwaysis better for interactive UIs.on-demand - Set if your callbacks can become unstable when the browser throttles background-inset tabs (default is
maxDelta).0.1s - Wire for telemetry and decide error UI strategy explicitly: default overlay, custom
onError, or no UI (errorRenderer).showErrorOverlay={false}
Advanced entrypoint
For power-user APIs (namespaced shared user state and scheduler tooling):
import {
useMotionGPUUserContext,
setMotionGPUUserContext,
applySchedulerPreset,
captureSchedulerDebugSnapshot
} from '@motion-core/motion-gpu/advanced';import {
useMotionGPUUserContext,
setMotionGPUUserContext,
applySchedulerPreset,
captureSchedulerDebugSnapshot
} from '@motion-core/motion-gpu/advanced';See User Context (Advanced) and Render Modes and Scheduling for details.
Next steps
- Writing WGSL Shaders — WGSL authoring patterns specific to MotionGPU.
- Defining Materials — deep dive into the material pipeline.
- Frame Scheduler — everything about , invalidation, and timing.
useFrame