Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Flow Shaders

Flow shaders let you write GPU fragment shaders using a high-level DAG (directed acyclic graph) that compiles to WGSL. They can be defined in CSS stylesheets or directly in Rust using the flow! macro.

Quick Start

The fastest way to add a flow shader to an element:

#![allow(unused)]
fn main() {
use blinc_layout::flow;

let ripple = flow!(ripple, fragment, {
    input uv: builtin(uv);
    input time: builtin(time);
    node d = distance(uv, vec2(0.5, 0.5));
    node wave = sin(d * 20.0 - time * 4.0) * 0.5 + 0.5;
    output color = vec4(wave, wave, wave, 1.0);
});

div().flow(ripple).w(400.0).h(400.0)
}

The flow! macro produces a FlowGraph using Rust identifiers and primitives. Pass it directly to any element via .flow().

Anatomy of a Flow Shader

Every flow shader has a name, a target (always fragment for visual effects), and a body of declarations:

@flow <name> {
    target: fragment;

    input <name>: builtin(<variable>);    // Input declarations
    step <name>: <step-type> { ... };     // Semantic steps (high-level)
    node <name> = <expression>;           // Raw computation nodes
    chain <name>: <step> | <step> | ...;  // Piped step chains
    use <flow-name>;                      // Compose other flows
    output color = <expression>;          // Output declarations
}

Declarations can appear in any order, but each node can only reference inputs and earlier nodes (the graph must be acyclic).

Builtin Variables

VariableTypeDescription
uvvec2Normalized element coordinates (0,0 = top-left, 1,1 = bottom-right)
timefloatElapsed time in seconds (monotonic)
resolutionvec2Element size in physical pixels
pointervec2Cursor position relative to element (0-1 range)
sdffloatSigned distance field value at the current fragment
frame_indexfloatCurrent frame number

Expressions

Flow expressions support standard arithmetic, vector constructors, function calls, and swizzle access:

node a = sin(uv.x * 10.0 + time);
node b = vec4(a, a * 0.5, 1.0 - a, 1.0);
node c = mix(b, vec4(1.0, 0.0, 0.0, 1.0), 0.5);
node d = c.rgb;

Operators

OperatorExample
+, -, *, /a * 2.0 + b
Unary --a
Swizzlev.xy, v.rgb, v.x

Functions Reference

Math (scalar)

FunctionSignatureDescription
sin, cos, tanf32 -> f32Trigonometric
abs, floor, ceil, fractf32 -> f32Rounding / absolute
sqrt, exp, log, signf32 -> f32Algebraic
pow(f32, f32) -> f32Power
atan2(f32, f32) -> f32Arc tangent
mod(f32, f32) -> f32Modulus
min, max(f32, f32) -> f32Comparative
clamp(f32, f32, f32) -> f32Clamp to range
mix(f32, f32, f32) -> f32Linear interpolation
smoothstep(f32, f32, f32) -> f32Smooth Hermite
step(f32, f32) -> f32Step function

Vector

FunctionDescription
length(v)Vector magnitude
distance(a, b)Distance between two points
dot(a, b)Dot product
cross(a, b)Cross product (vec3)
normalize(v)Unit vector
reflect(v, n)Reflection

Noise

FunctionSignatureDescription
fbm(p, octaves)(vec2, i32) -> f32Fractal Brownian motion
fbm_ex(p, octaves, persistence)(vec2, i32, f32) -> f32FBM with custom persistence
worley(p)vec2 -> f32Worley/cellular noise
worley_grad(p)vec2 -> vec3Worley with analytic gradient (x=dist, y=gx, z=gy)
checkerboard(p, scale)(vec2, f32) -> f32Checkerboard pattern

SDF Primitives

FunctionDescription
sdf_box(p, half_size)Box SDF
sdf_circle(p, radius)Circle SDF
sdf_ellipse(p, radii)Ellipse SDF
sdf_round_rect(p, half_size, radius)Rounded rectangle SDF

SDF Combinators

FunctionDescription
sdf_union(a, b)Union of two SDFs
sdf_intersect(a, b)Intersection
sdf_subtract(a, b)Subtraction
sdf_smooth_union(a, b, k)Smooth union with radius k
sdf_smooth_intersect(a, b, k)Smooth intersection
sdf_smooth_subtract(a, b, k)Smooth subtraction

Lighting

FunctionDescription
phong(normal, light_dir, view_dir, shininess)Phong shading
blinn_phong(normal, light_dir, view_dir, shininess)Blinn-Phong shading

Scene

FunctionDescription
sample_scene(uv)Sample the background behind this element (for refraction/glass effects)

Semantic Steps

Steps are high-level operations that expand to multiple nodes automatically. They provide a more declarative way to build shader effects.

Pattern Steps

Generate procedural textures. Output type: float (scalar field).

Step TypeKey ParametersDescription
pattern_noisescale, detail, animationFBM noise pattern
pattern_worleyscale, threshold, edge, mask, gradientWorley cellular pattern with analytic gradient
pattern_ripplecenter, density, speedConcentric ripple rings
pattern_wavesdirection, frequency, speedDirectional sine waves
pattern_gridscale, line_widthGrid lines
pattern_gradientdirection, start, endLinear gradient (output: vec4)
pattern_plasmascale, speedPlasma texture (output: vec4)

Effect Steps

Post-processing effects that modify appearance.

Step TypeKey ParametersDescription
effect_refractsource, strengthLens refraction via Worley gradient
effect_frostsource, strength, detailFrosted glass UV jitter
effect_specularsource, intensity, powerSpecular highlight scattering
effect_fogdensity, sourceFog/haze composite
effect_lightsource, direction, intensity, powerDirectional highlights from normals

Transform Steps

Spatial coordinate transformations. Output type: vec2 (UV coordinate) or float.

Step TypeKey ParametersDescription
transform_wetaspect, scroll_speed, offsetAspect-corrected gravity scroll (for rain/drip effects)
transform_warpsource, amountWarp UV by a noise field
transform_rotateangleRotate UV coordinates
transform_scalefactorScale UV coordinates
transform_tilecountTile/repeat UV
transform_mirroraxisMirror UV
transform_polarcenterCartesian to polar coordinates

Color Steps

Map scalar values to colors. Output type: vec4.

Step TypeKey ParametersDescription
color_rampsource, stops, opacityMap scalar to color gradient
color_shiftsource, hueHue shift
color_tintsource, colorColor tinting
color_invertsourceColor inversion

Composition Steps

Combine two sources. Output type: vec4.

Step TypeKey ParametersDescription
compose_blenda, b, modeBlend two layers (screen, multiply, overlay, etc.)
compose_masksource, maskAlpha mask one input by another
compose_layerbase, overlay, opacityStack with opacity

Adjust Steps

Value curve shaping. Output type: float.

Step TypeKey ParametersDescription
adjust_falloffradius, centerDistance-based fade
adjust_remapsource, in_min, in_max, out_min, out_maxRemap value range
adjust_thresholdsource, valueHard threshold
adjust_easesource, curveApply easing curve
adjust_clampsource, min, maxClamp value range

Chains

Chains pipe the output of one step into the next, creating a processing pipeline:

chain effect:
    pattern_ripple(center: vec2(0.5, 0.5), density: 25.0)
    | adjust_falloff(radius: 0.5)
    ;

Each link in the chain implicitly receives the previous link’s output as its source parameter.

Flow Composition with use

Flows can import nodes from other flows using use:

@flow base_noise {
    target: fragment;
    input uv: builtin(uv);
    node n = fbm(uv * 4.0, 6);
    output color = vec4(n, n, n, 1.0);
}

@flow enhanced {
    target: fragment;
    use base_noise;
    node bright = smoothstep(0.3, 0.7, n);
    output color = vec4(bright, bright * 0.5, 0.1, 1.0);
}

The use directive imports all nodes from the referenced flow into the current graph.

Scene Sampling

For glass, refraction, or frosted effects, use sample_scene() to read the rendered background behind the element:

#![allow(unused)]
fn main() {
let glass = flow!(glass, fragment, {
    input uv: builtin(uv);
    input time: builtin(time);
    node offset = fbm(uv * 8.0 + vec2(time * 0.1, 0.0), 3) * 0.02;
    node scene = sample_scene(uv + vec2(offset, offset));
    output color = scene;
});
}

The scene texture is automatically captured before flow rendering begins. Elements using sample_scene() see everything rendered behind them.

Applying Flow Shaders

There are three ways to apply flow shaders to elements:

Define the shader in Rust and pass it directly to the element:

#![allow(unused)]
fn main() {
use blinc_layout::flow;

let shader = flow!(my_effect, fragment, {
    input uv: builtin(uv);
    input time: builtin(time);
    node wave = sin(uv.x * 10.0 + time) * 0.5 + 0.5;
    output color = vec4(wave, 0.2, 0.5, 1.0);
});

div().flow(shader).w(300.0).h(300.0)
}

The FlowGraph carries its own name and is auto-persisted by the GPU pipeline cache.

2. CSS Stylesheet

Define flows in CSS and reference them by name:

#![allow(unused)]
fn main() {
ctx.add_css(r#"
    @flow terrain {
        target: fragment;
        input uv: builtin(uv);
        step noise: pattern-noise { scale: 4.0; detail: 6; };
        output color = vec4(noise, noise, noise, 1.0);
    }

    #my-element {
        flow: terrain;
        border-radius: 16px;
    }
"#);

div().id("my-element").w(300.0).h(300.0)
}

3. Style Macros

Reference CSS-defined flows from css! or style! macros:

#![allow(unused)]
fn main() {
let style = css! {
    flow: "terrain";
    border-radius: 16px;
};

// Or with style! macro:
let style = style! {
    flow: "terrain",
    corner_radius: 16.0,
};
}

4. Name Reference

Reference a previously-defined flow by name string:

#![allow(unused)]
fn main() {
div().flow("terrain").w(300.0).h(300.0)
}

Complete Example

Here’s the wet glass demo that creates a realistic rain-on-glass effect using semantic steps:

#![allow(unused)]
fn main() {
use blinc_layout::flow;

let wetglass = flow!(wetglass, fragment, {
    input uv: builtin(uv);
    input time: builtin(time);
    input resolution: builtin(resolution);

    // Gravity gradient: more moisture at bottom
    node grav = smoothstep(0.0, 1.0, uv.y);

    // Background mist
    step mist: pattern_noise { scale: 3.0; detail: 5; animation: time * 0.02; };
    node moist = mist * (0.35 + grav * 0.65);

    // Multi-scale water drops with aspect correction and gravity scroll
    step uv1: transform_wet { aspect: resolution; scroll_speed: 0.001; };
    step uv2: transform_wet { aspect: resolution; scroll_speed: 0.0015; offset: vec2(0.38, 0.21); };
    step uv3: transform_wet { aspect: resolution; scroll_speed: 0.002; offset: vec2(0.17, 0.63); };

    // Worley drops at different scales
    step drops1: pattern_worley { uv: uv1; scale: 7.0; threshold: 0.22; edge: 0.05; mask: step(0.3, moist); gradient: true; };
    step drops2: pattern_worley { uv: uv2; scale: 12.0; threshold: 0.18; edge: 0.04; mask: step(0.2, moist); gradient: true; };
    step drops3: pattern_worley { uv: uv3; scale: 20.0; threshold: 0.13; edge: 0.03; mask: step(0.12, moist); gradient: true; };

    // Combine drops
    node drops_raw = clamp(drops1 + drops2 * 0.6 + drops3 * 0.3, 0.0, 1.0);
    node drops = smoothstep(0.05, 0.4, drops_raw);

    // Specular highlights
    step highlight: effect_specular {
        sources: drops1 drops2 drops3;
        weights: 1.0 0.6 0.3;
        direction: vec2(0.7071068, 0.7071067);
        intensity: 0.25;
        power: 64.0;
    };

    // Fog and lens distortion
    node fog = (1.0 - drops) * (0.12 + mist * 0.05);
    step lens: effect_refract { source: drops; strength: 0.025; };

    // Sample background scene through distorted UVs
    node scene = sample_scene(uv + lens);

    // Composite
    node out_r = scene.x * (1.0 - fog) + fog + highlight;
    node out_g = scene.y * (1.0 - fog) + fog + highlight;
    node out_b = scene.z * (1.0 - fog) + fog + highlight;
    output color = vec4(out_r, out_g, out_b, 0.97);
});

div().flow(wetglass).w(800.0).h(600.0)
}

Performance Tips

  • Analytic gradients: pattern_worley with gradient: true uses worley_grad() which computes distance + gradient in a single 3x3 grid pass (5x faster than finite-difference).
  • Pipeline caching: Compiled WGSL pipelines are cached by flow name in FlowPipelineCache. Reusing the same flow name across frames is free after first compile.
  • Scene copy: sample_scene() triggers a single texture copy per frame (not per element). Multiple elements sharing a scene-sampling flow share the same copy.
  • Step expansion: Semantic steps expand to optimized node graphs at parse time, not at render time. There’s zero per-frame overhead from using steps vs raw nodes.