These examples focus on the core MotionGPU mental model from Getting Started:
- Define an immutable material with .
defineMaterial(...) - Render it with .
<FragCanvas /> - Drive runtime state from a child component using hooks (,
useFrame,useMotionGPU).useTexture
Each example is intentionally small and centered on one concept.
1. Minimal material + fragment contract
Concept:
defineMaterialfn frag(uv: vec2f) -> vec4f<!-- 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>Why this matters:
- validates and freezes the material.
defineMaterial - The signature is strict and is the main shader contract.
frag - Canvas size comes from its parent element.
2. Uniforms + useFrame
useFrameConcept: shader values change over time through runtime uniform updates.
<!-- 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 * 10.0);
return vec4f(vec3f(wave), 1.0);
}
`,
uniforms: {
uTime: 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 * 10.0);
return vec4f(vec3f(wave), 1.0);
}
`,
uniforms: {
uTime: 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>Why this matters:
- Material defines uniform names/types; runtime updates values.
- runs before each render and is the main animation hook.
useFrame
3. Interaction + UV vs DOM coordinates
Concept: DOM pointer coordinates are top-left origin, UV is bottom-left origin.
<!-- 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 onMove = (event: PointerEvent) => {
const rect = canvas.getBoundingClientRect();
mouseX = (event.clientX - rect.left) / rect.width;
mouseY = 1.0 - (event.clientY - rect.top) / rect.height;
};
canvas.addEventListener('pointermove', onMove);
return () => canvas.removeEventListener('pointermove', onMove);
});
useFrame((state) => {
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 onMove = (event: PointerEvent) => {
const rect = canvas.getBoundingClientRect();
mouseX = (event.clientX - rect.left) / rect.width;
mouseY = 1.0 - (event.clientY - rect.top) / rect.height;
};
canvas.addEventListener('pointermove', onMove);
return () => canvas.removeEventListener('pointermove', onMove);
});
useFrame((state) => {
state.setUniform('uMouse', [mouseX, mouseY]);
});
</script>Why this matters:
- gives access to runtime canvas/context controls.
useMotionGPU() - Y flip is required when mapping pointer input to UV space.
4. Render modes (always, on-demand, manual)
alwayson-demandmanualConcept: rendering cadence is separate from your shader logic.
<!-- Runtime.svelte -->
<script lang="ts">
import { useMotionGPU, useFrame } from '@motion-core/motion-gpu';
const gpu = useMotionGPU();
$effect(() => {
gpu.renderMode.set('on-demand');
});
useFrame((state) => {
state.setUniform('uTime', state.time);
}, { autoInvalidate: false });
function refreshOnce() {
gpu.invalidate();
}
function renderOneFrame() {
gpu.renderMode.set('manual');
gpu.advance();
}
</script>
<button onclick={refreshOnce}>Invalidate once</button>
<button onclick={renderOneFrame}>Advance one frame</button><!-- Runtime.svelte -->
<script lang="ts">
import { useMotionGPU, useFrame } from '@motion-core/motion-gpu';
const gpu = useMotionGPU();
$effect(() => {
gpu.renderMode.set('on-demand');
});
useFrame((state) => {
state.setUniform('uTime', state.time);
}, { autoInvalidate: false });
function refreshOnce() {
gpu.invalidate();
}
function renderOneFrame() {
gpu.renderMode.set('manual');
gpu.advance();
}
</script>
<button onclick={refreshOnce}>Invalidate once</button>
<button onclick={renderOneFrame}>Advance one frame</button>Why this matters:
- : continuous animation.
always - : render only when invalidated.
on-demand - : render exactly one frame per
manualcall.advance()
5. Texture sampling with useTexture
useTextureConcept: textures are declared in the material and populated at runtime.
<!-- App.svelte (material excerpt) -->
<script lang="ts">
import { FragCanvas, defineMaterial } from '@motion-core/motion-gpu';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
let tex = textureSample(uMain, uMainSampler, uv);
return tex;
}
`,
textures: {
uMain: { filter: 'linear', update: 'once' }
}
});
</script><!-- App.svelte (material excerpt) -->
<script lang="ts">
import { FragCanvas, defineMaterial } from '@motion-core/motion-gpu';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
let tex = textureSample(uMain, uMainSampler, uv);
return tex;
}
`,
textures: {
uMain: { filter: 'linear', update: 'once' }
}
});
</script><!-- Runtime.svelte -->
<script lang="ts">
import { useFrame, useTexture } from '@motion-core/motion-gpu';
const loaded = useTexture(['/assets/albedo.png'], {
colorSpace: 'srgb',
generateMipmaps: true
});
useFrame((state) => {
const tex = loaded.textures.current?.[0];
state.setTexture('uMain', tex ? { source: tex.source } : null);
});
</script><!-- Runtime.svelte -->
<script lang="ts">
import { useFrame, useTexture } from '@motion-core/motion-gpu';
const loaded = useTexture(['/assets/albedo.png'], {
colorSpace: 'srgb',
generateMipmaps: true
});
useFrame((state) => {
const tex = loaded.textures.current?.[0];
state.setTexture('uMain', tex ? { source: tex.source } : null);
});
</script>Why this matters:
- Material defines texture bindings; runtime sets actual sources.
- Passing is safe (fallback texture is used).
null
6. Post-process with ShaderPass
ShaderPassConcept: base shader (
fragshade<script lang="ts">
import { FragCanvas, ShaderPass, defineMaterial } from '@motion-core/motion-gpu';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
return vec4f(uv.x, uv.y, 1.0 - uv.x, 1.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]} /><script lang="ts">
import { FragCanvas, ShaderPass, defineMaterial } from '@motion-core/motion-gpu';
const material = defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
return vec4f(uv.x, uv.y, 1.0 - uv.x, 1.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]} />Why this matters:
- Material shader: .
fn frag(uv: vec2f) -> vec4f - Pass shader: .
fn shade(inputColor: vec4f, uv: vec2f) -> vec4f - is output from base shader or previous pass.
inputColor
7. Two-pass chain (source -> target -> canvas)
Concept: pass graph flow and final output.
import { ShaderPass } from '@motion-core/motion-gpu';
const bloomPrefilter = new ShaderPass({
needsSwap: true,
fragment: `
fn shade(inputColor: vec4f, uv: vec2f) -> vec4f {
let luma = dot(inputColor.rgb, vec3f(0.2126, 0.7152, 0.0722));
let threshold = step(0.8, luma);
return vec4f(inputColor.rgb * threshold, inputColor.a);
}
`
});
const gammaToCanvas = new ShaderPass({
needsSwap: false,
input: 'source',
output: 'canvas',
fragment: `
fn shade(inputColor: vec4f, uv: vec2f) -> vec4f {
return vec4f(pow(inputColor.rgb, vec3f(1.0 / 2.2)), inputColor.a);
}
`
});import { ShaderPass } from '@motion-core/motion-gpu';
const bloomPrefilter = new ShaderPass({
needsSwap: true,
fragment: `
fn shade(inputColor: vec4f, uv: vec2f) -> vec4f {
let luma = dot(inputColor.rgb, vec3f(0.2126, 0.7152, 0.0722));
let threshold = step(0.8, luma);
return vec4f(inputColor.rgb * threshold, inputColor.a);
}
`
});
const gammaToCanvas = new ShaderPass({
needsSwap: false,
input: 'source',
output: 'canvas',
fragment: `
fn shade(inputColor: vec4f, uv: vec2f) -> vec4f {
return vec4f(pow(inputColor.rgb, vec3f(1.0 / 2.2)), inputColor.a);
}
`
});<FragCanvas {material} passes={[bloomPrefilter, gammaToCanvas]} /><FragCanvas {material} passes={[bloomPrefilter, gammaToCanvas]} />Why this matters:
- means ping-pong (
needsSwap: true/source) flow.target - Final pass typically writes to with
canvas.needsSwap: false
Suggested learning order
- Example 1 + 2: material contract and runtime uniforms.
- Example 3 + 4: interactive state and render cadence.
- Example 5: texture pipeline.
- Example 6 + 7: post-processing and pass graph flow.