bevy-react 0.5.0

Drive bevy_ui from a React app over an embedded V8 runtime.
Documentation
// outline: paint a colored ring around the content's alpha silhouette,
// UNDER the content (`outline.rs`).
//
// Params map (packed by `OutlineParams::pack`; Length slots arrive rewritten
// to physical px):
//   params[0] = (width_px, softness_px, 0, 0)
//   params[1] = outline color, linear straight-alpha RGBA
//
// Alpha-max dilation over concentric rings: for each ring distance `d` the
// coverage contribution is `tap_alpha * falloff(d)`, where `falloff` is 1
// inside `width` and ramps to 0 over `max(softness, 1)` px beyond it — a
// crisp outline gets a 1px antialiasing skirt, softness > 0 reads as a glow.
// Sixteen fixed directions per ring, with a half-step angular offset on odd
// rings to break ring-to-ring aliasing. Quality bound (the blur MAX_HALF
// convention): rings are capped at 12, so a reach (width + softness) up to
// ~12 physical px keeps ring spacing <= 1px and stays crisp; beyond that the
// rings spread and the 16-direction gap starts rounding sharp corners.
//
// PREMULTIPLY: the dilation resamples ALPHA only (interpolation-safe on its
// own — no unpremultiply anywhere), and the composite is a premultiplied
// source-over: src + outline_pm * (1 - src.a).

#import bevy_react::filter::{FullscreenVertexOutput, source_sampler, source_texture, uniforms}

const DIRECTION_COUNT: i32 = 16;
const MAX_RINGS: i32 = 12;
const TAU: f32 = 6.28318530718;

@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
    let width = uniforms.params[0].x;
    let softness = uniforms.params[0].y;
    let color = uniforms.params[1];
    let src = textureSample(source_texture, source_sampler, in.uv);
    if (width <= 0.0 && softness <= 0.0) || color.a <= 0.0 {
        // Exact identity — the transition-padding contract (`width: 0`).
        return src;
    }

    let skirt = max(softness, 1.0);
    let reach = width + skirt;
    let ring_count = clamp(i32(ceil(reach)), 1, MAX_RINGS);
    let spacing = reach / f32(ring_count);

    var cov = src.a;
    for (var ring = 1; ring <= ring_count; ring = ring + 1) {
        let d = f32(ring) * spacing;
        let falloff = 1.0 - smoothstep(width, width + skirt, d);
        if falloff <= 0.0 {
            continue;
        }
        let phase = select(0.0, 0.5, (ring & 1) == 1) * TAU / f32(DIRECTION_COUNT);
        for (var i = 0; i < DIRECTION_COUNT; i = i + 1) {
            let ang = phase + TAU * f32(i) / f32(DIRECTION_COUNT);
            let offset = vec2<f32>(cos(ang), sin(ang)) * d * uniforms.texel_size;
            // Explicit LOD: `textureSample` needs uniform control flow and
            // the `continue` above breaks it; LOD 0 is exact for 1:1 passes.
            let tap = textureSampleLevel(source_texture, source_sampler, in.uv + offset, 0.0);
            cov = max(cov, tap.a * falloff);
        }
    }

    // Premultiplied source-over: the outline sits under the content.
    let outline_pm = vec4<f32>(color.rgb * color.a, color.a) * cov;
    return src + outline_pm * (1.0 - src.a);
}