bevy-react 0.5.0

Drive bevy_ui from a React app over an embedded V8 runtime.
Documentation
// Crossfade morph (`builtin/morph.rs::CrossfadeParams`): a noise-staggered
// two-input blend. A smooth fbm value-noise field gives each pixel a stagger
// offset so blob-shaped regions cross-dissolve earlier than others; at
// `spread == 0` it degenerates to the plain `mix(from, to, progress)`.
//
// Packing: params[0] = (spread, scale_physical_px, softness, seed)
// `scale` is a Length slot — it arrives rewritten to PHYSICAL px, so the
// resolution-based noise coords below are DPI-correct.
//
// Each pixel fades linearly over a window `w = max(softness, eps)` starting
// at its noise stagger `n * spread`; the sweep spans `spread + w` in total so
// progress 0 is all "from" and progress 1 all "to". At spread == 0 the
// formula reduces to clamp(progress * w / w) == progress for ANY window; an
// explicit fast path makes that identity bit-exact. `local` is deliberately
// NOT smoothstepped — the engine easing owns the curve.
//
// Per the prelude's premultiplied contract both inputs are premultiplied and
// the lerp operates on them DIRECTLY — premultiplied color is the
// linear-interpolation-safe form, so coverage edges cross-dissolve without
// fringes. Endpoint guards keep progress 0 exactly "from" and progress 1
// exactly "to" (the identity contract: the settle frame must be pixel-equal
// to no pass). Both samples are taken up front in uniform control flow, so
// the plain (implicit-grad) samplers are fine.
//
// (The entry point is `fragment`, not `filter` — `filter` is a WGSL reserved
// word.)

#import bevy_react::filter::{
    FullscreenVertexOutput,
    morph_progress,
    morph_sample_from,
    morph_sample_to,
    uniforms,
}

// Cheap 2D->1D hash (Dave Hoskins' hash12; digit-free name for naga_oil).
fn hash_two(p: vec2<f32>) -> f32 {
    var h = fract(vec3<f32>(p.x, p.y, p.x) * 0.1031);
    h += dot(h, h.yzx + 33.33);
    return fract((h.x + h.y) * h.z);
}

// Smooth value noise over an integer lattice, in [0, 1).
fn value_noise(p: vec2<f32>) -> f32 {
    let i = floor(p);
    let f = fract(p);
    let u = f * f * (3.0 - 2.0 * f);
    let a = hash_two(i);
    let b = hash_two(i + vec2<f32>(1.0, 0.0));
    let c = hash_two(i + vec2<f32>(0.0, 1.0));
    let d = hash_two(i + vec2<f32>(1.0, 1.0));
    return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

// Three-octave fbm normalized to [0, 1) (raw amplitude sum is 0.875).
fn fbm(p: vec2<f32>) -> f32 {
    let s = 0.5 * value_noise(p)
        + 0.25 * value_noise(p * 2.03 + vec2<f32>(17.3, 23.7))
        + 0.125 * value_noise(p * 4.01 + vec2<f32>(41.7, 9.1));
    return s / 0.875;
}

@fragment
fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
    let from_color = morph_sample_from(in.uv);
    let to_color = morph_sample_to(in.uv);
    let progress = morph_progress();

    if progress <= 0.0 {
        return from_color;
    }
    if progress >= 1.0 {
        return to_color;
    }

    let spread = clamp(uniforms.params[0].x, 0.0, 1.0);
    let scale = max(uniforms.params[0].y, 1.0); // physical px; guard divide
    let softness = clamp(uniforms.params[0].z, 0.0, 1.0);
    let seed = uniforms.params[0].w;

    // Bit-exact uniform crossfade at spread 0 (skips the noise work too).
    if spread == 0.0 {
        return mix(from_color, to_color, progress);
    }

    // Noise coords in physical px; seed re-rolls by SLIDING the domain
    // (lattice coords must stay floored integers — fractional hash input
    // stripes).
    let q = in.uv * uniforms.resolution / scale + seed * vec2<f32>(103.7, 59.3);
    // Mild clamped contrast stretch: raw fbm mass sits near 0.5.
    let n = clamp((fbm(q) - 0.5) * 1.6 + 0.5, 0.0, 1.0);

    let w = max(softness, 1e-3);
    let local = clamp((progress * (spread + w) - n * spread) / w, 0.0, 1.0);

    return mix(from_color, to_color, local);
}