Getting Started

Examples

Concept-first MotionGPU examples for the core workflow: materials, uniforms, frame updates, textures, and passes.


These examples focus on the core MotionGPU mental model from Getting Started:

  1. Define an immutable material with
    defineMaterial(...)
    .
  2. Render it with
    <FragCanvas />
    .
  3. 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:

defineMaterial
requires exactly
fn 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:

  • defineMaterial
    validates and freezes the material.
  • The
    frag
    signature is strict and is the main shader contract.
  • Canvas size comes from its parent element.

2. Uniforms +
useFrame

Concept: 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.
  • useFrame
    runs before each render and is the main animation hook.

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:

  • useMotionGPU()
    gives access to runtime canvas/context controls.
  • Y flip is required when mapping pointer input to UV space.

4. Render modes (
always
,
on-demand
,
manual
)

Concept: 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:

  • always
    : continuous animation.
  • on-demand
    : render only when invalidated.
  • manual
    : render exactly one frame per
    advance()
    call.

5. Texture sampling with
useTexture

Concept: 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
    null
    is safe (fallback texture is used).

6. Post-process with
ShaderPass

Concept: base shader (

frag
) and post-process shader (
shade
) have different contracts.

<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
    .
  • inputColor
    is output from base shader or previous pass.

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:

  • needsSwap: true
    means ping-pong (
    source
    /
    target
    ) flow.
  • Final pass typically writes to
    canvas
    with
    needsSwap: false
    .

Suggested learning order

  1. Example 1 + 2: material contract and runtime uniforms.
  2. Example 3 + 4: interactive state and render cadence.
  3. Example 5: texture pipeline.
  4. Example 6 + 7: post-processing and pass graph flow.