bevy_sprinkles_editor 0.2.0

GPU particle system editor for Bevy
#import bevy_ui::ui_vertex_output::UiVertexOutput
#import bevy_sprinkles_editor::common::rounded_box_sdf

const CURVE_WIDTH: f32 = 2.0;
const GRID_CURVE_WIDTH: f32 = 1.0;

const COLOR_GRID: vec3<f32> = vec3<f32>(77.0, 77.0, 77.0);
const GRID_ALPHA: f32 = 0.5;

fn rgb(color: vec3<f32>, alpha: f32) -> vec4<f32> {
    return vec4<f32>(color / 255.0, alpha);
}

struct CurveUniforms {
    border_radius: f32,
    point_count: u32,
    range_min: f32,
    range_max: f32,
    curve_color: vec4<f32>,
    fill_color: vec4<f32>,
    positions_low: vec4<f32>,
    positions_high: vec4<f32>,
    values_low: vec4<f32>,
    values_high: vec4<f32>,
    modes_low: vec4<u32>,
    modes_high: vec4<u32>,
    tensions_low: vec4<f32>,
    tensions_high: vec4<f32>,
    easings_low: vec4<u32>,
    easings_high: vec4<u32>,
}

@group(1) @binding(0)
var<uniform> uniforms: CurveUniforms;

fn get_position(i: u32) -> f32 {
    if i < 4u { return uniforms.positions_low[i]; }
    return uniforms.positions_high[i - 4u];
}

fn get_value(i: u32) -> f32 {
    if i < 4u { return uniforms.values_low[i]; }
    return uniforms.values_high[i - 4u];
}

fn get_mode(i: u32) -> u32 {
    if i < 4u { return uniforms.modes_low[i]; }
    return uniforms.modes_high[i - 4u];
}

fn get_tension(i: u32) -> f32 {
    if i < 4u { return uniforms.tensions_low[i]; }
    return uniforms.tensions_high[i - 4u];
}

fn get_easing(i: u32) -> u32 {
    if i < 4u { return uniforms.easings_low[i]; }
    return uniforms.easings_high[i - 4u];
}

const PI: f32 = 3.14159265359;

fn apply_power(t: f32, tension: f32) -> f32 {
    if abs(tension) < 0.001 {
        return t;
    }
    let exp = 1.0 / (1.0 - abs(tension) * 0.999);
    if tension > 0.0 {
        return pow(t, exp);
    } else {
        return 1.0 - pow(1.0 - t, exp);
    }
}

fn apply_sine(t: f32, tension: f32) -> f32 {
    let intensity = abs(tension);
    if intensity < 0.001 {
        return t;
    }
    var eased: f32;
    if tension >= 0.0 {
        eased = 1.0 - cos(t * PI * 0.5);
    } else {
        eased = sin(t * PI * 0.5);
    }
    return t + (eased - t) * intensity;
}

fn apply_expo(t: f32, tension: f32) -> f32 {
    let intensity = abs(tension);
    if intensity < 0.001 {
        return t;
    }
    var eased: f32;
    if tension >= 0.0 {
        if t <= 0.0 { eased = 0.0; } else { eased = pow(2.0, 10.0 * (t - 1.0)); }
    } else {
        if t >= 1.0 { eased = 1.0; } else { eased = 1.0 - pow(2.0, -10.0 * t); }
    }
    return t + (eased - t) * intensity;
}

fn apply_circ(t: f32, tension: f32) -> f32 {
    let intensity = abs(tension);
    if intensity < 0.001 {
        return t;
    }
    var eased: f32;
    if tension >= 0.0 {
        eased = 1.0 - sqrt(1.0 - t * t);
    } else {
        eased = sqrt(1.0 - (1.0 - t) * (1.0 - t));
    }
    return t + (eased - t) * intensity;
}

fn apply_easing(t: f32, easing: u32, tension: f32) -> f32 {
    switch easing {
        case 0u: { return apply_power(t, tension); }
        case 1u: { return apply_sine(t, tension); }
        case 2u: { return apply_expo(t, tension); }
        case 3u: { return apply_circ(t, tension); }
        default: { return apply_power(t, tension); }
    }
}

fn apply_curve(t: f32, mode: u32, easing: u32, tension: f32) -> f32 {
    switch mode {
        case 0u: {
            return apply_easing(t, easing, tension);
        }
        case 1u: {
            if t < 0.5 {
                let local_t = t * 2.0;
                return apply_easing(local_t, easing, tension) * 0.5;
            } else {
                let local_t = (t - 0.5) * 2.0;
                return 0.5 + apply_easing(local_t, easing, -tension) * 0.5;
            }
        }
        case 2u: {
            return 0.0;
        }
        case 3u: {
            let steps = u32(2.0 + 64.0 * clamp(tension, 0.0, 1.0));
            let step_f = f32(steps);
            return floor(t * step_f) / max(step_f - 1.0, 1.0);
        }
        case 4u: {
            let steps = u32(2.0 + 64.0 * clamp(tension, 0.0, 1.0));
            let step_f = f32(steps);
            let step_size = 1.0 / step_f;
            let current_step = floor(t / step_size);
            let local_t = (t - current_step * step_size) / step_size;
            let smooth_t = local_t * local_t * (3.0 - 2.0 * local_t);
            let start = current_step / max(step_f - 1.0, 1.0);
            let end = min(current_step + 1.0, step_f - 1.0) / max(step_f - 1.0, 1.0);
            return start + (end - start) * smooth_t;
        }
        default: {
            return t;
        }
    }
}

fn sample_curve(x: f32) -> f32 {
    if uniforms.point_count == 0u {
        return 1.0;
    }
    if uniforms.point_count == 1u {
        return get_value(0u);
    }

    let t = clamp(x, 0.0, 1.0);

    var left_idx = 0u;
    var right_idx = uniforms.point_count - 1u;

    for (var i = 0u; i < uniforms.point_count; i++) {
        if get_position(i) <= t {
            left_idx = i;
        }
    }
    for (var i = 0u; i < uniforms.point_count; i++) {
        if get_position(i) >= t {
            right_idx = i;
            break;
        }
    }

    let left_pos = get_position(left_idx);
    let left_val = get_value(left_idx);
    let right_pos = get_position(right_idx);
    let right_val = get_value(right_idx);
    let right_mode = get_mode(right_idx);
    let right_tension = get_tension(right_idx);
    let right_easing = get_easing(right_idx);

    if left_idx == right_idx {
        return left_val;
    }

    let segment_range = right_pos - left_pos;
    if segment_range <= 0.0 {
        return left_val;
    }

    let local_t = (t - left_pos) / segment_range;

    // adjust tension based on slope direction so positive tension always bends down
    let slope_sign = sign(right_val - left_val);
    let effective_tension = right_tension * slope_sign;
    let curved_t = apply_curve(local_t, right_mode, right_easing, effective_tension);

    return left_val + (right_val - left_val) * curved_t;
}

fn normalize_value(value: f32) -> f32 {
    let range_span = uniforms.range_max - uniforms.range_min;
    if abs(range_span) < 0.001 {
        return 0.5;
    }
    return (value - uniforms.range_min) / range_span;
}

// distance from point p to line segment from a to b
fn dist_to_segment(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>) -> f32 {
    let pa = p - a;
    let ba = b - a;
    let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    return length(pa - ba * h);
}

@fragment
fn fragment(in: UiVertexOutput) -> @location(0) vec4<f32> {
    let pixel_pos = (in.uv - 0.5) * in.size;
    let half_size = in.size * 0.5;
    let d = rounded_box_sdf(pixel_pos, half_size, uniforms.border_radius);

    if d > 0.0 {
        return vec4<f32>(0.0);
    }

    var color = vec4<f32>(0.0);

    // draw grid
    let grid_divisions = 4.0;
    let grid_x = fract(in.uv.x * grid_divisions);
    let grid_y = fract(in.uv.y * grid_divisions);
    let grid_line_x = min(grid_x, 1.0 - grid_x) * in.size.x / grid_divisions;
    let grid_line_y = min(grid_y, 1.0 - grid_y) * in.size.y / grid_divisions;

    let grid_color = rgb(COLOR_GRID, GRID_ALPHA);
    if grid_line_x < GRID_CURVE_WIDTH || grid_line_y < GRID_CURVE_WIDTH {
        color = mix(color, grid_color, grid_color.a);
    }

    // draw zero line
    let center_y = 1.0 - normalize_value(0.0);
    let center_dist = abs(in.uv.y - center_y) * in.size.y;
    if center_dist < GRID_CURVE_WIDTH * 0.5 && uniforms.range_min <= 0.0 && uniforms.range_max >= 0.0 {
        color = mix(color, vec4<f32>(0.5, 0.5, 0.5, 0.8), 0.8);
    }

    // compute distance to curve using polyline segments sampled at pixel intervals
    let px = vec2<f32>(in.uv.x * in.size.x, in.uv.y * in.size.y);
    var min_dist = 1000.0;
    let num_samples = i32(in.size.x);
    var prev_pos = vec2<f32>(0.0, (1.0 - normalize_value(sample_curve(0.0))) * in.size.y);

    for (var i = 1; i <= num_samples; i++) {
        let t = f32(i) / f32(num_samples);
        let curr_y = 1.0 - normalize_value(sample_curve(t));
        let curr_pos = vec2<f32>(t * in.size.x, curr_y * in.size.y);

        let seg_dist = dist_to_segment(px, prev_pos, curr_pos);
        min_dist = min(min_dist, seg_dist);

        prev_pos = curr_pos;
    }

    let curve_color = uniforms.curve_color;
    let fill_color = uniforms.fill_color;

    // fill below curve
    let curve_y = (1.0 - normalize_value(sample_curve(in.uv.x))) * in.size.y;
    if px.y > curve_y {
        color = mix(color, fill_color, fill_color.a);
    }

    // draw curve with anti-aliasing
    if min_dist < CURVE_WIDTH {
        let line_alpha = 1.0 - smoothstep(CURVE_WIDTH * 0.5, CURVE_WIDTH, min_dist);
        color = mix(color, curve_color, line_alpha);
    }

    let edge_alpha = 1.0 - smoothstep(-1.0, 1.0, d);
    color.a *= edge_alpha;

    return color;
}