pub const PATH_SHADER: &str = r#"
// ============================================================================
// Path Rendering Shader
// ============================================================================
//
// Renders tessellated vector paths as colored triangles.
// Supports solid colors and gradients via per-vertex UV coordinates.
// Supports multi-stop gradients via 1D texture lookup.
// Supports clipping via rect/circle/ellipse shapes.
// Clip type constants
const CLIP_NONE: u32 = 0u;
const CLIP_RECT: u32 = 1u;
const CLIP_CIRCLE: u32 = 2u;
const CLIP_ELLIPSE: u32 = 3u;
const CLIP_POLYGON: u32 = 4u;
struct Uniforms {
// viewport_size (vec2) + padding (vec2) = 16 bytes, offset 0
viewport_size: vec2<f32>,
opacity: f32,
_pad0: f32,
// 3x3 transform stored as 3 vec4s (xyz used, w is padding) = 48 bytes, offset 16
transform_row0: vec4<f32>,
transform_row1: vec4<f32>,
transform_row2: vec4<f32>,
// Clip parameters = 32 bytes, offset 64
clip_bounds: vec4<f32>, // (x, y, width, height) or (cx, cy, rx, ry)
clip_radius: vec4<f32>, // corner radii or (rx, ry, 0, 0)
// clip_type + flags = 16 bytes, offset 96
clip_type: u32,
use_gradient_texture: u32, // 0=use vertex colors, 1=sample gradient texture
use_image_texture: u32, // 0=no image, 1=sample image texture
use_glass_effect: u32, // 0=no glass, 1=glass effect on path
// Image UV bounds = 16 bytes, offset 112
image_uv_bounds: vec4<f32>, // (u_min, v_min, u_max, v_max)
// Glass parameters = 16 bytes, offset 128
glass_params: vec4<f32>, // (blur_radius, saturation, tint_strength, opacity)
// Glass tint color = 16 bytes, offset 144
glass_tint: vec4<f32>, // RGBA tint color
}
// Total: 160 bytes
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var gradient_texture: texture_1d<f32>;
@group(0) @binding(2) var gradient_sampler: sampler;
@group(0) @binding(3) var image_texture: texture_2d<f32>;
@group(0) @binding(4) var image_sampler: sampler;
@group(0) @binding(5) var backdrop_texture: texture_2d<f32>;
@group(0) @binding(6) var backdrop_sampler: sampler;
struct VertexInput {
@location(0) position: vec2<f32>,
@location(1) color: vec4<f32>, // start color for gradients, solid color otherwise
@location(2) end_color: vec4<f32>, // end color for gradients
@location(3) uv: vec2<f32>,
@location(4) gradient_params: vec4<f32>, // linear: (x1,y1,x2,y2); radial: (cx,cy,r,0)
@location(5) gradient_type: u32,
@location(6) edge_distance: f32, // distance to nearest edge (for AA)
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) end_color: vec4<f32>,
@location(2) uv: vec2<f32>,
@location(3) @interpolate(flat) gradient_params: vec4<f32>,
@location(4) @interpolate(flat) gradient_type: u32,
@location(5) edge_distance: f32,
@location(6) screen_pos: vec2<f32>, // screen position for clip calculations
}
// ============================================================================
// SDF Functions for Clipping
// ============================================================================
// Rounded rectangle SDF
fn sd_rounded_rect(p: vec2<f32>, origin: vec2<f32>, size: vec2<f32>, radius: vec4<f32>) -> f32 {
let half_size = size * 0.5;
let center = origin + half_size;
let rel = p - center;
let q = abs(rel) - half_size;
// Select corner radius based on quadrant
// radius: (top-left, top-right, bottom-right, bottom-left)
var r: f32;
if rel.y < 0.0 {
if rel.x > 0.0 {
r = radius.y; // top-right
} else {
r = radius.x; // top-left
}
} else {
if rel.x > 0.0 {
r = radius.z; // bottom-right
} else {
r = radius.w; // bottom-left
}
}
r = min(r, min(half_size.x, half_size.y));
let q_adjusted = q + vec2<f32>(r);
return length(max(q_adjusted, vec2<f32>(0.0))) + min(max(q_adjusted.x, q_adjusted.y), 0.0) - r;
}
// Circle SDF
fn sd_circle(p: vec2<f32>, center: vec2<f32>, radius: f32) -> f32 {
return length(p - center) - radius;
}
// Ellipse SDF (approximation)
fn sd_ellipse(p: vec2<f32>, center: vec2<f32>, radii: vec2<f32>) -> f32 {
let p_centered = p - center;
let p_norm = p_centered / radii;
let dist = length(p_norm);
return (dist - 1.0) * min(radii.x, radii.y);
}
// Calculate clip alpha (1.0 = inside clip, 0.0 = outside)
// For non-rect clips: clip_bounds = rect scissor, clip_radius = shape data
fn calculate_clip_alpha(p: vec2<f32>, clip_bounds: vec4<f32>, clip_radius: vec4<f32>, clip_type: u32) -> f32 {
if clip_type == CLIP_NONE {
return 1.0;
}
let aa_width = 0.75;
switch clip_type {
case CLIP_RECT: {
let clip_origin = clip_bounds.xy;
let clip_size = clip_bounds.zw;
let clip_d = sd_rounded_rect(p, clip_origin, clip_size, clip_radius);
return 1.0 - smoothstep(-aa_width, aa_width, clip_d);
}
case CLIP_CIRCLE: {
let scissor_d = sd_rounded_rect(p, clip_bounds.xy, clip_bounds.zw, vec4<f32>(0.0));
let scissor_alpha = 1.0 - smoothstep(-aa_width, aa_width, scissor_d);
let center = clip_radius.xy;
let radius = clip_radius.z;
let clip_d = sd_circle(p, center, radius);
let shape_alpha = 1.0 - smoothstep(-aa_width, aa_width, clip_d);
return scissor_alpha * shape_alpha;
}
case CLIP_ELLIPSE: {
let scissor_d = sd_rounded_rect(p, clip_bounds.xy, clip_bounds.zw, vec4<f32>(0.0));
let scissor_alpha = 1.0 - smoothstep(-aa_width, aa_width, scissor_d);
let center = clip_radius.xy;
let radii = clip_radius.zw;
let clip_d = sd_ellipse(p, center, radii);
let shape_alpha = 1.0 - smoothstep(-aa_width, aa_width, clip_d);
return scissor_alpha * shape_alpha;
}
default: {
return 1.0;
}
}
}
// ============================================================================
// Vertex Shader
// ============================================================================
@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
var out: VertexOutput;
// Reconstruct transform matrix and apply
let p = vec3<f32>(in.position, 1.0);
let transformed = vec3<f32>(
dot(uniforms.transform_row0.xyz, p),
dot(uniforms.transform_row1.xyz, p),
dot(uniforms.transform_row2.xyz, p)
);
// Store screen position for clip calculations
out.screen_pos = transformed.xy;
// Convert to clip space (-1 to 1)
let clip_pos = vec2<f32>(
(transformed.x / uniforms.viewport_size.x) * 2.0 - 1.0,
1.0 - (transformed.y / uniforms.viewport_size.y) * 2.0
);
out.position = vec4<f32>(clip_pos, 0.0, 1.0);
out.color = in.color;
out.end_color = in.end_color;
out.uv = in.uv;
out.gradient_params = in.gradient_params;
out.gradient_type = in.gradient_type;
out.edge_distance = in.edge_distance;
return out;
}
// ============================================================================
// Fragment Shader
// ============================================================================
// Simple box blur for glass effect (samples backdrop in a small radius)
fn sample_blur(uv: vec2<f32>, blur_radius: f32, viewport_size: vec2<f32>) -> vec4<f32> {
let pixel_size = 1.0 / viewport_size;
var total = vec4<f32>(0.0);
var samples = 0.0;
// Simple 5x5 box blur
let sample_radius = i32(clamp(blur_radius * 0.1, 1.0, 3.0));
for (var x = -sample_radius; x <= sample_radius; x++) {
for (var y = -sample_radius; y <= sample_radius; y++) {
let offset = vec2<f32>(f32(x), f32(y)) * pixel_size * blur_radius * 0.5;
let sample_uv = clamp(uv + offset, vec2<f32>(0.0), vec2<f32>(1.0));
total += textureSample(backdrop_texture, backdrop_sampler, sample_uv);
samples += 1.0;
}
}
return total / samples;
}
// Adjust saturation of a color
fn adjust_saturation(color: vec3<f32>, saturation: f32) -> vec3<f32> {
let gray = dot(color, vec3<f32>(0.299, 0.587, 0.114));
return mix(vec3<f32>(gray), color, saturation);
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
// Calculate clip alpha first
let clip_alpha = calculate_clip_alpha(
in.screen_pos,
uniforms.clip_bounds,
uniforms.clip_radius,
uniforms.clip_type
);
// Early out if fully clipped
if clip_alpha < 0.001 {
discard;
}
var color: vec4<f32>;
// Check for glass effect first
if (uniforms.use_glass_effect == 1u) {
// Glass effect: sample and blur backdrop, apply tint
let screen_uv = in.screen_pos / uniforms.viewport_size;
let blur_radius = uniforms.glass_params.x;
let saturation = uniforms.glass_params.y;
let tint_strength = uniforms.glass_params.z;
let glass_opacity = uniforms.glass_params.w;
// Sample blurred backdrop
var backdrop = sample_blur(screen_uv, blur_radius, uniforms.viewport_size);
// Adjust saturation
backdrop = vec4<f32>(adjust_saturation(backdrop.rgb, saturation), backdrop.a);
// Apply tint
let tinted = mix(backdrop.rgb, uniforms.glass_tint.rgb, tint_strength * uniforms.glass_tint.a);
// Final color with glass opacity
color = vec4<f32>(tinted, glass_opacity);
} else if (uniforms.use_image_texture == 1u) {
// Image brush: sample from image texture using UV coordinates
// Map the path UV (0-1 in bounding box) to image UV bounds
let uv_min = uniforms.image_uv_bounds.xy;
let uv_max = uniforms.image_uv_bounds.zw;
let image_uv = uv_min + in.uv * (uv_max - uv_min);
color = textureSample(image_texture, image_sampler, image_uv);
// Apply tint from vertex color (multiply)
color = vec4<f32>(color.rgb * in.color.rgb, color.a * in.color.a);
} else if (in.gradient_type == 0u) {
// Solid color
color = in.color;
} else if (in.gradient_type == 1u) {
// Linear gradient - use gradient_params for direction
// params: (x1, y1, x2, y2) in ObjectBoundingBox space (0-1)
let g_start = in.gradient_params.xy;
let g_end = in.gradient_params.zw;
let g_dir = g_end - g_start;
let g_len_sq = dot(g_dir, g_dir);
// Project UV onto gradient line
var t: f32;
if (g_len_sq > 0.0001) {
let p = in.uv - g_start;
t = clamp(dot(p, g_dir) / g_len_sq, 0.0, 1.0);
} else {
t = 0.0;
}
// Sample from gradient texture or mix vertex colors
if (uniforms.use_gradient_texture == 1u) {
// Multi-stop gradient: sample from 1D texture
color = textureSample(gradient_texture, gradient_sampler, t);
} else {
// 2-stop fast path: mix vertex colors
color = mix(in.color, in.end_color, t);
}
} else {
// Radial gradient - params: (cx, cy, r, 0) in ObjectBoundingBox space
let center = in.gradient_params.xy;
let radius = in.gradient_params.z;
let dist = length(in.uv - center);
let t = clamp(dist / max(radius, 0.001), 0.0, 1.0);
// Sample from gradient texture or mix vertex colors
if (uniforms.use_gradient_texture == 1u) {
// Multi-stop gradient: sample from 1D texture
color = textureSample(gradient_texture, gradient_sampler, t);
} else {
// 2-stop fast path: mix vertex colors
color = mix(in.color, in.end_color, t);
}
}
// Apply opacity and clip alpha
// Note: edge-distance AA disabled - tessellated geometry has vertices ON path edges
// (edge_distance = 0), which causes entire shape to fade. Need different AA approach.
color.a *= uniforms.opacity * clip_alpha;
return color;
}
"#;Expand description
Shader for tessellated path rendering (triangles with per-vertex colors)