MA

makepad-shaders

Specialized support for Makepad shaders. Helps with GLSL, SDF, and GPU rendering logic.

Install

mkdir -p .claude/skills/makepad-shaders && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/4886" && unzip -o skill.zip -d .claude/skills/makepad-shaders && rm skill.zip

Installs to .claude/skills/makepad-shaders

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

CRITICAL: Use for Makepad 2.0 shader system. Triggers on: makepad shader, Sdf2d, pixel shader, draw_bg, draw_text, draw_quad, makepad gpu, shader function, pixel fn, vertex fn, instance, uniform, shader variable, sdf, premultiply, Pal.premul, GaussShadow, makepad graphics, custom draw, DrawQuad, DrawVector, 着色器, 像素, 渲染, 自定义绘制, 距离场
332 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)
Advanced

Key capabilities

  • Define pixel and vertex shader functions inline
  • Implement SDF2D primitives and combinators
  • Manage instance and uniform shader variables
  • Apply premultiplied alpha color operations
  • Create custom draw types with repr(C) structs

How it works

Shaders are defined within draw objects using pixel and vertex functions that override default rendering behavior. The system uses SDF2D for geometry and requires Pal.premul for color output.

Inputs & outputs

You give it
Shader function definitions and widget properties
You get back
GPU-rendered visual elements

When to use makepad-shaders

  • Writing custom GPU shaders for Makepad
  • Debugging shader syntax and draw calls
  • Implementing SDF rendering logic
  • Optimizing draw_bg and quad elements

About this skill

Makepad 2.0 Shader Skill

Version: makepad-widgets (dev branch) | Last Updated: 2026-03-03

Overview

Makepad uses a custom GPU shader system integrated into the widget property tree. Shaders are defined inline using pixel: fn() { ... } and vertex: fn() { ... } blocks within draw_bg, draw_text, or custom draw objects.

Documentation

Refer to the local files for detailed documentation:

  • ./references/shader-reference.md - Shader syntax, variables, built-ins, custom functions
  • ./references/sdf2d-reference.md - SDF2D primitives, combinators, drawing operations

Shader Basics

Pixel Shader Structure

draw_bg +: {
    // Declare variables
    instance hover: 0.0          // Animatable per-instance
    uniform accent: #4488ff      // Shared across all instances

    pixel: fn() {
        let sdf = Sdf2d.viewport(self.pos * self.rect_size)
        // ... SDF operations ...
        return sdf.result
    }
}

Variable Types

TypeDeclarationAnimatableScope
instanceinstance hover: 0.0Yes (via Animator)Per-widget instance
uniformuniform color: #fffNoShared across instances
texture_2dtexture_2d tex: noneNoTexture sampler
varyingvarying uv: vec2NoVertex → fragment

Built-in Variables

VariableTypeDescription
self.posvec2Normalized position (0.0 to 1.0)
self.rect_sizevec2Widget size in pixels
self.dpi_factorfloatScreen DPI factor
self.draw_pass.timefloatTime in seconds

CRITICAL: Premultiply Alpha

Every pixel shader MUST return premultiplied alpha color!

// WRONG - non-premultiplied
pixel: fn() {
    return vec4(1.0, 0.0, 0.0, 0.5)
}

// CORRECT - use Pal.premul()
pixel: fn() {
    return Pal.premul(vec4(1.0, 0.0, 0.0, 0.5))
}

// ALSO CORRECT - sdf.result is already premultiplied
pixel: fn() {
    let sdf = Sdf2d.viewport(self.pos * self.rect_size)
    sdf.circle(cx, cy, r)
    sdf.fill(#f00)
    return sdf.result
}

SDF2D Quick Reference

Setup

let sdf = Sdf2d.viewport(self.pos * self.rect_size)

Primitives

sdf.circle(cx, cy, radius)
sdf.rect(x, y, w, h)
sdf.box(x, y, w, h, border_radius)
sdf.hexagon(cx, cy, radius)
sdf.arc(cx, cy, radius, start_angle, end_angle, thickness)
sdf.move_to(x, y)
sdf.line_to(x, y)
sdf.close_path()

Drawing

sdf.fill(color)         // Filled shape
sdf.stroke(color, width) // Outlined shape
sdf.glow(color, amount)  // Glow effect
sdf.clear(color)         // Clear with color

Combinators

sdf.union()       // Add shapes together
sdf.intersect()   // Keep overlap only
sdf.subtract()    // Remove second from first
sdf.gloop(radius) // Smooth union
sdf.blend(amount) // Linear blend

Transforms

sdf.translate(x, y)
sdf.rotate(angle, cx, cy)
sdf.scale(factor, cx, cy)

Color Operations

// Mix two colors
mix(#f00, #00f, 0.5)           // 50% blend

// Premultiply alpha
Pal.premul(vec4(r, g, b, a))

// HSV conversions
Pal.hsv2rgb(vec4(h, s, v, 1.0))
Pal.rgb2hsv(color)

// Random
Math.random_2d(vec2(x, y))

Common Shader Patterns

Gradient Background

draw_bg +: {
    pixel: fn() {
        let grad = mix(#1a1a2e, #16213e, self.pos.y)
        return Pal.premul(vec4(grad.xyz, 1.0))
    }
}

Hover Color Change

draw_bg +: {
    instance hover: 0.0
    color: #333
    pixel: fn() {
        return Pal.premul(mix(self.color, self.color * 1.3, self.hover))
    }
}

Box Shadow

draw_bg +: {
    pixel: fn() {
        let sdf = Sdf2d.viewport(self.pos * self.rect_size)
        // Shadow
        sdf.box(2.0, 2.0, self.rect_size.x - 4.0, self.rect_size.y - 4.0, 8.0)
        sdf.fill(GaussShadow.box_shadow(sdf, 4.0, #0005))
        // Card
        sdf.box(0.0, 0.0, self.rect_size.x - 2.0, self.rect_size.y - 2.0, 8.0)
        sdf.fill(#2a2a3d)
        return sdf.result
    }
}

Rounded Button with States

draw_bg +: {
    instance hover: 0.0
    instance down: 0.0
    uniform color_bg: #4488ff
    uniform color_hover: #5599ff
    uniform color_down: #3377ee

    pixel: fn() {
        let sdf = Sdf2d.viewport(self.pos * self.rect_size)
        sdf.box(0.0, 0.0, self.rect_size.x, self.rect_size.y, 6.0)
        let color = mix(self.color_bg, self.color_hover, self.hover)
        let color = mix(color, self.color_down, self.down)
        sdf.fill(color)
        return sdf.result
    }
}

Custom Shader Functions

draw_bg +: {
    fn wave(pos: vec2, time: float) -> float {
        return sin(pos.x * 10.0 + time * 3.0) * 0.1
    }

    pixel: fn() {
        let w = self.wave(self.pos, self.draw_pass.time)
        let color = mix(#1a1a2e, #4488ff, self.pos.y + w)
        return Pal.premul(vec4(color.xyz, 1.0))
    }
}

Splash Shader Capability Boundary

Splash CAN:

  • Override pixel: fn(), vertex: fn(), get_color: fn() on existing draw types via +:
  • Define helper shader functions within +: blocks
  • Set instance/uniform variables on existing draw types
  • Use all SDF2D, color, math built-ins in shader functions

Splash CANNOT:

  • Create new DrawQuad/DrawText/DrawSvg types (must define in Rust)
  • Add new instance fields to existing shaders (GPU layout is compile-time)

Rule: Rust defines the draw type struct + registers it; Splash overrides how it draws.

See ./references/shader-reference.md "Splash Shader Capabilities & Boundaries" for the full pattern.


Custom Fullscreen Shader Pattern (learned 2026-03-26)

For standalone shader-driven widgets (e.g. particle fields, visualizers), follow the examples/shader pattern:

1. Custom Draw Type (Rust)

#[derive(Script, ScriptHook)]
#[repr(C)]  // CRITICAL: must be repr(C) for GPU layout
pub struct DrawMyShader {
    #[deref] draw_super: DrawQuad,  // inherits from DrawQuad
    #[live] my_param: f32,           // maps to shader variable
}

2. Register + Define Shader (script_mod!)

set_type_default() do #(DrawMyShader::script_shader(vm)){
    ..mod.draw.DrawQuad        // inherit DrawQuad defaults
    my_param: 0.5              // default value

    // Custom functions: property-style syntax, NOT fn name(self, ...)
    my_helper: fn(a: float, b: float) -> vec2 {
        return vec2(a * 2.0, b * 0.5)
    }

    pixel: fn() {
        let result = self.my_helper(self.pos.x, self.pos.y)
        return Pal.premul(vec4(result.x, result.y, 0.0, 1.0))
    }
}

3. Widget with Turtle Layout

fn draw_walk(&mut self, cx: &mut Cx2d, _: &mut Scope, walk: Walk) -> DrawStep {
    cx.begin_turtle(walk, self.layout);
    let rect = cx.turtle().rect();
    self.draw_bg.draw_abs(cx, rect);      // single fullscreen quad
    cx.end_turtle_with_area(&mut self.area);
    DrawStep::done()
}

4. Updating Shader Variables from Rust

// Direct field access (when draw type has #[live] fields):
self.draw_bg.my_param = 0.75;
self.area.redraw(cx);

// Via NextFrame for animation:
if let Event::NextFrame(ne) = event {
    if ne.set.contains(&self.next_frame) {
        self.draw_bg.my_param += 0.01;
        self.area.redraw(cx);
        self.next_frame = cx.new_next_frame();
    }
}

Instanced Particle Rendering (learned 2026-03-26)

For drawing thousands of independent particles (dots, stars, etc.):

Draw Shader

#[derive(Script, ScriptHook)]
#[repr(C)]
pub struct DrawDot {
    #[deref] draw_super: DrawQuad,
    #[live] dot_color: Vec3,  // per-instance color
}
// Shader: each instance is a small circle
pixel: fn() {
    let d = length(self.pos - vec2(0.5, 0.5))
    let alpha = 1.0 - smoothstep(0.35, 0.5, d)
    return Pal.premul(vec4(self.dot_color * alpha, alpha))
}

Rendering Loop

self.draw_dot.begin_many_instances(cx);  // start batch

for i in 0..particles.len() {
    let (x, y) = particles[i];
    self.draw_dot.dot_color = vec3(r, g, b);  // set per-instance data
    self.draw_dot.draw_abs(cx, Rect {
        pos: dvec2(x - radius, y - radius),
        size: dvec2(radius * 2.0, radius * 2.0),
    });
}

self.draw_dot.end_many_instances(cx);  // submit batch as one draw call

Physics Pattern (spring-back displacement)

// Per particle: store persistent displacement
displacements: Vec<(f64, f64)>,

// Each frame:
for i in 0..dots.len() {
    let (mut dx, mut dy) = displacements[i];

    // 1. Decay (spring back, 0.94 = ~2-3 sec return)
    dx *= 0.94;
    dy *= 0.94;

    // 2. Apply forces (cursor push, ripples, etc.)
    let dist = distance(dot_pos, mouse_pos);
    let t = (1.0 - dist / radius).max(0.0);
    let push = t * t * t * strength;  // cubic falloff
    dx += direction.x * push;
    dy += direction.y * push;

    displacements[i] = (dx, dy);
    // Draw at original_pos + displacement
}

Performance Notes

  • 10,000 particles at 60fps: OK on macOS Metal (one draw call via instancing)
  • CPU physics loop: 10K × 17 distance checks = ~170K ops/frame, negligible
  • Key: begin_many_instances / end_many_instances batches into single GPU draw call

Shader Syntax Pitfalls (learned 2026-03-26)

PitfallErrorFix
let x = 1.0; x = 2.0cannot assign to let bindingUse different names: let x2 = ...
fn push(self, ...) -> vec2method not found on selfUse property syntax: push: fn(...) -> vec2 { }
return vec4(r, g, b, a) without premulIncorrect alpha blendingreturn Pal.premul(vec4(r, g, b, a))
Custom shader in Splash evalSilent blank renderMust use compiled script_mod! path
Missing #[repr(C)] on draw structGPU layout mismatchAlways add #[repr(C)]
fn calc(self, x: float) syntaxcannot push to frozen vecUse calc: fn(x: float) -> float { }

Best Practices

  1. **Always premultip

Content truncated.

When not to use it

  • Creating new DrawQuad or DrawText types without Rust registration

Prerequisites

Makepad 2.0 widgets dev branchRust environment for custom draw types

Limitations

  • Cannot add new instance fields to existing shaders at runtime
  • Requires continuous redraw calls to animate time-based shaders

How it compares

Unlike standard GPU programming, this system integrates shaders directly into the widget property tree using a custom domain-specific language.

Compared to similar skills

makepad-shaders side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
makepad-shaders (this skill)14moReviewAdvanced
tauri761moReviewAdvanced
rust-errors51moNo flagsAdvanced
hula-skill37moReviewIntermediate

Try saying

Example prompts that trigger this skill in your AI assistant.

Search skills

Search the agent skills registry