Effects

Gradients, dashes, shadows, blur, blend modes and clipping. Every effect is a signal, so it animates like any other property — .to(), .ease() and .bind() all work.

Paint

Rect::new()
    .fill_gradient(Gradient::linear((-100.0, 0.0), (100.0, 0.0)).stops([color!("#e13238"), color!("#f5c242")]))
    .stroke(Color::WHITE).line_width(4.0)
    .dash(vec![12.0, 8.0])          // animate dash_offset for marching ants
    .line_cap(LineCap::Butt)

Gradients are in the node's own coordinates (origin at its center). Gradient::radial(center, radius) works the same way. Two gradients with the same number of stops interpolate.

Layers

A node is drawn into its own layer when its effect must apply to the node and its children as a whole: group opacity (overlaps do not get darker), blend modes, blur and shadows. Plain shapes skip the layer and stay fast.

Group::new().opacity(0.5)                         // group opacity
Circle::new().clip(true).child(...)              // children clipped to the circle
Circle::new().blend(BlendMode::Multiply)         // Multiply, Screen, Overlay, …
Txt::new().blur(12.0)                            // Gaussian blur (σ, node units)
Rect::new().shadow(Color::BLACK.with_alpha(0.6), 16.0, (0.0, 12.0))
Circle::new().glow(color!("#68abdf"), 20.0)      // shadow without offset

Order and pivot

Rect::new().z_index(-1.0)            // drawn below its siblings
Rect::new().anchor((-1.0, -1.0))     // (x, y) is the top-left corner and the rotation pivot

Loading live preview…

Source of effects — edit in the playground
//! Paint and compositing: gradients, dashes, group opacity, clipping, blend
//! modes, blur, shadows and glow — all animatable.

use oxframe::prelude::*;

#[oxframe::scene]
pub async fn effects(view: View) {
    // A gradient card with marching-ants border and a soft shadow.
    let card = view.add(
        Rect::new()
            .width(520.0)
            .height(300.0)
            .radius(28.0)
            .position((-400.0, -120.0))
            .fill_gradient(
                Gradient::linear((-260.0, -150.0), (260.0, 150.0))
                    .stops([color!("#e13238"), color!("#f5c242")]),
            )
            .stroke(Color::WHITE)
            .line_width(5.0)
            .dash(vec![18.0, 12.0])
            .shadow(Color::BLACK.with_alpha(0.6), 18.0, (0.0, 16.0)),
    );
    spawn(async move {
        card.dash_offset()
            .to(-300.0, 6.0)
            .ease(presets::LINEAR)
            .await;
    });

    // Clipped content: a stripe sweeping inside a circle.
    let lens = view.add(
        Circle::new()
            .size(300.0)
            .position((400.0, -120.0))
            .fill(color!("#1b1d22"))
            .clip(true),
    );
    let stripe = lens.add(
        Rect::new()
            .width(80.0)
            .height(400.0)
            .x(-220.0)
            .rotation(20.0)
            .fill(color!("#68abdf")),
    );

    // Glow that breathes, blur that focuses.
    let orb = view.add(
        Circle::new()
            .size(120.0)
            .position((-300.0, 260.0))
            .fill(color!("#68abdf"))
            .glow(color!("#68abdf"), 4.0),
    );
    let title = view.add(
        Txt::new()
            .text("in focus")
            .font_size(80.0)
            .weight(800.0)
            .position((200.0, 260.0))
            .blur(14.0),
    );

    all((
        stripe.x().to(220.0, 2.0),
        orb.shadow_blur().to(30.0, 2.0),
        title.blur().to(0.0, 2.0),
    ))
    .await;

    // Group opacity: the overlap of the two circles never gets darker.
    let pair = view.add(
        Group::new()
            .y(260.0)
            .x(-40.0)
            .opacity(1.0)
            .child(Circle::new().size(140.0).x(-50.0).fill(color!("#2ecc71")))
            .child(Circle::new().size(140.0).x(50.0).fill(color!("#2ecc71"))),
    );
    pair.opacity().to(0.4, 1.0).await;

    // Multiply blend over the card.
    let mul = view.add(
        Circle::new()
            .size(200.0)
            .position((-400.0, -120.0))
            .fill(color!("#68abdf"))
            .blend(BlendMode::Multiply)
            .scale(0.0),
    );
    mul.scale()
        .to(Vec2::new(1.0, 1.0), 0.8)
        .ease(presets::OUT_BACK)
        .await;
    wait_for(1.0).await;
}

pub fn project() -> Project {
    Project::new("effects").scene(effects())
}

Blur and shadows are rasterized by the same code on both renderers (tiny-skia), so CPU and GPU output match; only the area a layer can touch is processed.