
Makepad 2.0 Shaders
- 44 installs
- 745 repo stars
- Updated April 7, 2026
- zhanghandong/makepad-skills
Helps with ai & agent building tasks during AI-assisted development.
About
makepad-2.0-shaders is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- makepad-2.0-shaders
- AI & Agent Building
- AI-coding skill
Makepad 2.0 Shaders by the numbers
- 44 all-time installs (skills.sh)
- +1 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #7,851 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/zhanghandong/makepad-skills --skill makepad-2.0-shadersAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 44 |
|---|---|
| repo stars | ★ 745 |
| Last updated | April 7, 2026 |
| Repository | zhanghandong/makepad-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
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
| Type | Declaration | Animatable | Scope |
|---|---|---|---|
instance | instance hover: 0.0 | Yes (via Animator) | Per-widget instance |
uniform | uniform color: #fff | No | Shared across instances |
texture_2d | texture_2d tex: none | No | Texture sampler |
varying | varying uv: vec2 | No | Vertex → fragment |
Built-in Variables
| Variable | Type | Description |
|---|---|---|
self.pos | vec2 | Normalized position (0.0 to 1.0) |
self.rect_size | vec2 | Widget size in pixels |
self.dpi_factor | float | Screen DPI factor |
self.draw_pass.time | float | Time 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 colorCombinators
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 blendTransforms
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 callPhysics 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_instancesbatches into single GPU draw call
---
Shader Syntax Pitfalls (learned 2026-03-26)
| Pitfall | Error | Fix |
|---|---|---|
let x = 1.0; x = 2.0 | cannot assign to let binding | Use different names: let x2 = ... |
fn push(self, ...) -> vec2 | method not found on self | Use property syntax: push: fn(...) -> vec2 { } |
return vec4(r, g, b, a) without premul | Incorrect alpha blending | return Pal.premul(vec4(r, g, b, a)) |
| Custom shader in Splash eval | Silent blank render | Must use compiled script_mod! path |
Missing #[repr(C)] on draw struct | GPU layout mismatch | Always add #[repr(C)] |
fn calc(self, x: float) syntax | cannot push to frozen vec | Use calc: fn(x: float) -> float { } |
---
Best Practices
1. Always premultiply - Use Pal.premul() or return sdf.result 2. Use `instance` for animation - Only instance variables work with Animator 3. Use `uniform` for shared values - Colors, sizes shared across instances 4. Use `+:` merge operator - Extend default shaders: draw_bg +: { ... } 5. Keep shaders simple - Complex shaders impact rendering performance 6. Use SDF for shapes - Much cleaner than manual math 7. Test with `new_batch: true` - Required when mixing shaders with text 8. Property-style functions - name: fn(args) -> type { }, call via self.name(args) 9. Immutable let - Shader let cannot be reassigned; use unique names per step 10. `#[repr(C)]` on draw structs - Required for GPU memory layout alignment
---
SDF Capsule (Pill) Shape Pattern (learned 2026-03-31)
sdf.box() with large border_radius breaks when radius approaches half the dimension — the formula size.xy - vec2(2*r, 2*r) goes negative, producing diamond/spiky shapes. Use this standard capsule SDF instead:
draw_bg +: {
pixel: fn() {
let w = self.rect_size.x
let h = self.rect_size.y
let r = h * 0.5
let px = self.pos.x * w
let py = self.pos.y * h
// Standard capsule: clamp x to center segment, then circle distance
let cx = clamp(px, r, max(r, w - r))
let cy = h * 0.5
let d = length(vec2(px - cx, py - cy)) - r
let alpha = 1.0 - smoothstep(-1.0, 1.0, d)
return Pal.premul(vec4(0.1, 0.1, 0.18, alpha * 0.82))
}
}Key points:
clamp(px, r, w-r)constrains x to the center line segment between the two end circlesmax(r, w-r)prevents clamp range inversion when widget is very narrow- Smoothstep
(-1.0, 1.0, d)provides 2px anti-aliasing - Works correctly at any width — dynamically adapts as
width: Fitcontent changes - No three-part union (circles + rect) needed — single formula, no seam artifacts
Pulsing Dot in Background Shader (learned 2026-03-31)
Embed animation directly in the background shader to avoid z-order issues with child widgets (LoadingSpinner/other widgets can cause bleed-through at capsule edges):
draw_bg +: {
pixel: fn() {
let w = self.rect_size.x
let h = self.rect_size.y
let r = h * 0.5
let px = self.pos.x * w
let py = self.pos.y * h
// Capsule background
let cx_bg = clamp(px, r, max(r, w - r))
let cy = h * 0.5
let d_bg = length(vec2(px - cx_bg, py - cy)) - r
let bg_alpha = 1.0 - smoothstep(-1.0, 1.0, d_bg)
let bg = vec4(0.1, 0.1, 0.18, bg_alpha * 0.82)
// Pulsing dot (driven by draw_pass.time)
let t = self.draw_pass.time
let pulse = 0.5 + 0.5 * sin(t * 4.0)
let dot_r = 4.0 + pulse * 3.0
let dot_cx = r + 2.0
let d_dot = length(vec2(px - dot_cx, py - cy)) - dot_r
let dot_alpha = (1.0 - smoothstep(-1.0, 1.0, d_dot)) * bg_alpha
let dot_color = mix(vec3(0.3, 0.6, 1.0), vec3(0.2, 0.9, 0.5), pulse)
// Composite
let final_rgb = mix(bg.xyz, dot_color, dot_alpha * 0.8)
let final_a = bg.w + dot_alpha * 0.6 * (1.0 - bg.w)
return Pal.premul(vec4(final_rgb, final_a))
}
}IMPORTANT: Must call self.ui.widget(cx, ids!(my_window)).redraw(cx) from handle_next_frame to keep draw_pass.time advancing. Without continuous redraw, time-based animation freezes.
Makepad 2.0 Sdf2d Reference
SDF2D Primitives
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
sdf.circle(cx, cy, radius)
sdf.rect(x, y, w, h)
sdf.box(x, y, w, h, border_radius)
sdf.box_all(x, y, w, h, r_lt, r_rt, r_rb, r_lb) // per-corner
sdf.box_x(x, y, w, h, r_left, r_right)
sdf.box_y(x, y, w, h, r_top, r_bottom)
sdf.hexagon(cx, cy, radius)
sdf.hline(y, half_height)
sdf.arc_round_caps(cx, cy, radius, start_angle, end_angle, thickness)
sdf.arc_flat_caps(cx, cy, radius, start_angle, end_angle, thickness)Note: Args are space-separated in shader code, no commas.
SDF Path Operations
sdf.move_to(x, y)
sdf.line_to(x, y)
sdf.close_path()SDF Combinators
Operate on current + previous shape:
sdf.union() // merge (min distances)
sdf.intersect() // overlap only (max distances)
sdf.subtract() // cut current from previous
sdf.gloop(k) // smooth/gooey union
sdf.blend(k) // linear blend (0=previous, 1=current)Ring example: circle -> smaller circle -> subtract -> fill Toggle animation: ring -> circle -> blend(self.active)
SDF Drawing
sdf.fill(color) // fill + reset
sdf.fill_keep(color) // fill + keep (for stroke after)
sdf.stroke(color, width) // stroke + reset
sdf.stroke_keep(c, w) // stroke + keep
sdf.glow(color, width) // additive glow + reset
sdf.glow_keep(c, w) // additive glow + keep
sdf.clear(color) // clear buffer
sdf.fill_premul(color) // fill with premultiplied colorSDF Transforms
sdf.translate(x, y)
sdf.rotate(angle, cx, cy)
sdf.scale(factor, cx, cy)SDF Anti-aliasing
sdf.aa = sdf.aa * 3.0 // sharper edges for small iconsComplete Examples
1. Rounded button with hover
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, 4.0)
sdf.fill(self.color.mix(self.color_hover, self.hover))
return sdf.result
}2. Fill + border stroke
pixel: fn() {
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
sdf.box(1. 1. self.rect_size.x - 2. self.rect_size.y - 2. 4.0)
sdf.fill_keep(self.color)
sdf.stroke(self.border_color, 1.0)
return sdf.result
}3. Loading spinner arc
pixel: fn() {
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
let t = self.draw_pass.time * self.rotation_speed
sdf.arc_round_caps(cx, cy, r, t, t + gap, self.stroke_width)
sdf.fill(self.color)
return sdf.result
}Makepad 2.0 Shader Reference
Shader Function Syntax (Makepad 2.0)
draw_bg +: {
pixel: fn() {
// shader code
return sdf.result
}
}Note: Use pixel: fn() NOT fn pixel(self) -> vec4 (old syntax).
Variable Types
instance(value)- Per-widget, animatable by Animator, varies per draw calluniform(value)- Shared across all instances, theme constantstexture_2d(float)- Texture samplervarying(vec2(0))- Vertex->pixel interpolated
Built-in Variables
self.pos- vec2: normalized position [0,1]self.rect_size- vec2: pixel sizeself.rect_pos- vec2: pixel positionself.dpi_factor- float: display DPIself.draw_pass.time- float: elapsed secondsself.draw_pass.dpi_dilate- float: DPI dilationself.draw_depth- float: z depthself.geom_pos- vec2: raw geometry position
CRITICAL: Premultiply Alpha!
When hand-coding pixel() that returns color (not via sdf.result):
pixel: fn(){
return Pal.premul(self.color.mix(self.color_hover, self.hover))
}sdf.fill()/sdf.stroke() already premultiply, so return sdf.result is safe.
Color Operations
mix(color1, color2, factor)- linear interpolationcolor1.mix(color2, factor)- method chaining (preferred)Pal.premul(color)- premultiply alpha (REQUIRED for pixel() return)Pal.hsv2rgb(vec4(h s v 1.0))- HSV to RGBPal.rgb2hsv(color)- RGB to HSVPal.iq(t a b c d)/Pal.iq0(t)..Pal.iq7(t)- cosine palettes
Custom Shader Functions
draw_bg +: {
get_color: fn() {
return self.color.mix(self.color_hover, self.hover)
}
pixel: fn() {
return Pal.premul(self.get_color())
}
}With parameters:
get_color_at: fn(scale: vec2, pan: vec2) {
return self.my_texture.sample(self.pos * scale + pan)
}Vertex Shader
vertex: fn() {
let dpi = self.dpi_factor
let ceil_size = ceil(self.rect_size * dpi) / dpi
return self.clip_and_transform_vertex(self.rect_pos, self.rect_size)
}Texture Sampling
self.my_tex.sample(self.pos) // standard 2D
sample2d(self.my_tex, uv) // free-function form
sample2d_rt(self.image, uv) // render-target (handles Y-flip)Mutable Variables
let mut color = self.color
if self.hover > 0.5 { color = self.color_hover }Control Flow
- Conditionals:
if/else,matchon enum instance variables - For loops:
for i in 0..4 { ... }
Math Utilities
Math.random_2d(vec2)- pseudo-randomMath.rotate_2d(v, angle)- 2D rotation- Constants:
PI,E,TORAD,GOLDEN - Standard GLSL: sin, cos, pow, sqrt, abs, floor, ceil, fract, clamp, smoothstep, etc.
- Vector: length, distance, dot, cross, normalize
- Fragment: dFdx, dFdy
Gradient Pattern
color_2: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) // sentinel: no gradient
pixel: fn() {
let mut fill = self.color
if self.color_2.x > -0.5 {
let dither = Math.random_2d(self.pos.xy) * 0.04
fill = mix(self.color, self.color_2, self.pos.y + dither)
}
return Pal.premul(fill)
}GaussShadow
GaussShadow.box_shadow(lower, upper, point, sigma)
GaussShadow.rounded_box_shadow(lower, upper, point, sigma, corner)Splash Shader Capabilities & Boundaries
What Splash CAN Do (via +: merge operator)
Splash can override shader functions on any pre-defined draw type:
// Override pixel shader
draw_bg +: {
pixel: fn() {
let sdf = Sdf2d.viewport(self.pos * self.rect_size)
sdf.circle(self.rect_size.x * 0.5, self.rect_size.y * 0.5, 20.0)
sdf.fill(#f00)
return sdf.result
}
}
// Override get_color (e.g. on DrawSvg)
draw_svg +: {
svg: crate_resource("self:resources/scene.svg")
get_color: fn() {
let base = self.eval_gradient();
let t = self.svg_time;
return mix(base, vec4(1.0, 0.0, 0.0, 1.0), sin(t) * 0.5 + 0.5);
}
}
// Override vertex shader
draw_bg +: {
vertex: fn() {
let pos = self.clip_and_transform_vertex(self.rect_pos, self.rect_size);
return pos;
}
}
// Define helper functions
draw_bg +: {
wave: fn(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);
return Pal.premul(vec4(w, w, w, 1.0))
}
}
// Set instance/uniform variables
draw_bg +: {
instance hover: 0.0
uniform accent: #4488ff
}What Splash CANNOT Do
| Not Supported | Reason | Workaround |
|---|---|---|
| Create new DrawQuad types | Requires #[repr(C)] + Rust struct | Define in Rust with #[derive(Script)] |
| Add new instance fields to existing shaders | GPU memory layout fixed at compile time | Define custom DrawQuad in Rust |
| Define new draw types (DrawBar, DrawArc, etc.) | Must be registered via script_shader(vm) | Rust-side #[derive(Script, ScriptHook)] |
Rule: Rust defines "what exists" (types, fields), Splash overrides "how it draws" (pixel/vertex/get_color)
Custom Draw Type Pattern (Rust + Splash)
Step 1: Define in Rust
#[derive(Script, ScriptHook)]
#[repr(C)]
pub struct DrawBar {
#[deref] pub draw_super: DrawQuad,
#[live] pub amplitude: f32, // instance field (AFTER deref)
#[live] pub bar_color: Vec4, // instance field
}Step 2: Register in script_mod!
script_mod! {
mod.draw.DrawBar = #(DrawBar::script_shader(vm))
}Step 3: Override pixel shader in Splash
draw_bar +: {
pixel: fn() {
let bar_h = self.amplitude * self.rect_size.y;
let sdf = Sdf2d.viewport(self.pos * self.rect_size);
sdf.rect(0.0, self.rect_size.y - bar_h, self.rect_size.x, bar_h);
sdf.fill(self.bar_color);
return sdf.result
}
}---
Custom Draw Shader Struct
#[derive(Script, ScriptHook)]
#[repr(C)]
struct DrawMyShader {
#[live] pub svg: Option<ScriptHandleRef>, // non-instance BEFORE deref
#[rust] my_state: bool, // non-instance BEFORE deref
#[deref] pub draw_super: DrawQuad, // contains DrawVars
#[live] pub tint: Vec4f, // instance field AFTER deref
}CRITICAL: Non-instance data MUST be BEFORE #[deref], instance fields AFTER.