lumen-engine 0.2.2

Core composition model and rendering orchestration for Lumen.
Documentation
struct Paint {
    colors: array<vec4<f32>, 8>,
    offsets: array<vec4<f32>, 8>,
    start: vec2<f32>,
    end: vec2<f32>,
    center: vec2<f32>,
    radius: vec2<f32>,
    angle: f32,
    kind: u32,
    units: u32,
    spread: u32,
    interpolation: u32,
    stop_count: u32,
    _pad0: u32,
    _pad1: u32,
}

// TODO: move common shader structs/functions such as Paint into a shared WGSL
// include once the shader assembly path can concatenate common modules.
struct PathParams {
    fill_paint: Paint,
    stroke_paint: Paint,
    position: vec2<f32>,
    bounds_min: vec2<f32>,
    bounds_size: vec2<f32>,
    stroke_width: f32,
    flags: u32,
    point_count: u32,
    _pad0: u32,
    _pad1: u32,
    _pad2: u32,
}

struct PathPoints {
    points: array<vec2<f32>>,
}

@group(0) @binding(0) var<uniform> params: PathParams;
@group(0) @binding(1) var<storage, read> path_points: PathPoints;
@group(0) @binding(2) var output_tex: texture_storage_2d<rgba8unorm, write>;

fn spread_t(paint: Paint, value: f32) -> f32 {
    if (paint.spread == 1u) {
        return fract(value);
    }
    if (paint.spread == 2u) {
        let repeated = fract(value * 0.5) * 2.0;
        return select(repeated, 2.0 - repeated, repeated > 1.0);
    }
    return clamp(value, 0.0, 1.0);
}

fn paint_space_point(paint: Paint, pixel: vec2<f32>, local01: vec2<f32>) -> vec2<f32> {
    return select(local01, pixel, paint.units == 1u);
}

fn interpolate_color(paint: Paint, t_raw: f32) -> vec4<f32> {
    let t = spread_t(paint, t_raw);
    let count = max(paint.stop_count, 1u);
    var color = paint.colors[0];
    for (var i = 1u; i < 8u; i = i + 1u) {
        if (i < count) {
            let left = paint.offsets[i - 1u].x;
            let right = paint.offsets[i].x;
            let span = max(right - left, 0.00001);
            let amount = clamp((t - left) / span, 0.0, 1.0);
            color = select(color, mix(paint.colors[i - 1u], paint.colors[i], amount), t >= left);
        }
    }
    return color;
}

fn sample_paint(paint: Paint, pixel: vec2<f32>, local01: vec2<f32>) -> vec4<f32> {
    let p = paint_space_point(paint, pixel, local01);
    if (paint.kind == 1u) {
        let axis = paint.end - paint.start;
        return interpolate_color(paint, dot(p - paint.start, axis) / max(dot(axis, axis), 0.00001));
    }
    if (paint.kind == 2u) {
        let delta = (p - paint.center) / max(paint.radius, vec2<f32>(0.00001));
        return interpolate_color(paint, length(delta));
    }
    if (paint.kind == 3u) {
        let angle = atan2(p.y - paint.center.y, p.x - paint.center.x) / 6.28318530718 + 0.5;
        return interpolate_color(paint, fract(angle - paint.angle / 360.0));
    }
    return paint.colors[0];
}

fn distance_to_segment(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>) -> f32 {
    let ab = b - a;
    let h = clamp(dot(p - a, ab) / max(dot(ab, ab), 0.0001), 0.0, 1.0);
    return length(p - (a + ab * h));
}

fn polygon_contains(p: vec2<f32>) -> bool {
    var inside = false;
    let count = params.point_count;
    if (count < 3u) {
        return false;
    }

    var j = count - 1u;
    for (var i = 0u; i < count; i = i + 1u) {
        let pi = path_points.points[i] + params.position;
        let pj = path_points.points[j] + params.position;
        let denominator = pj.y - pi.y;
        let safe_denominator = select(denominator, 0.000001, abs(denominator) < 0.000001);
        let crosses = ((pi.y > p.y) != (pj.y > p.y)) &&
            (p.x < (pj.x - pi.x) * (p.y - pi.y) / safe_denominator + pi.x);
        if (crosses) {
            inside = !inside;
        }
        j = i;
    }
    return inside;
}

fn path_edge_distance(p: vec2<f32>) -> f32 {
    let count = params.point_count;
    if (count < 2u) {
        return 1000000.0;
    }

    var min_distance = 1000000.0;
    var previous = path_points.points[count - 1u] + params.position;
    for (var i = 0u; i < count; i = i + 1u) {
        let current = path_points.points[i] + params.position;
        min_distance = min(min_distance, distance_to_segment(p, previous, current));
        previous = current;
    }
    return min_distance;
}

@compute @workgroup_size(8, 8, 1)
fn cs_main(@builtin(global_invocation_id) id: vec3<u32>) {
    let dims = textureDimensions(output_tex);
    if (id.x >= dims.x || id.y >= dims.y) {
        return;
    }

    let pixel = vec2<f32>(f32(id.x) + 0.5, f32(id.y) + 0.5);
    let fill_enabled = (params.flags & 1u) != 0u;
    let stroke_enabled = (params.flags & 2u) != 0u;
    let distance = path_edge_distance(pixel);
    let fill_alpha = select(0.0, 1.0, fill_enabled && polygon_contains(pixel));
    let stroke_alpha = select(
        0.0,
        clamp(0.5 - (distance - params.stroke_width * 0.5), 0.0, 1.0),
        stroke_enabled,
    );
    let bounds_origin = params.position + params.bounds_min;
    let local01 = clamp((pixel - bounds_origin) / max(params.bounds_size, vec2<f32>(1.0)), vec2<f32>(0.0), vec2<f32>(1.0));
    let fill = sample_paint(params.fill_paint, pixel, local01) * fill_alpha;
    let stroke = sample_paint(params.stroke_paint, pixel, local01) * stroke_alpha;
    let color = mix(fill, stroke, stroke_alpha);
    textureStore(output_tex, vec2<i32>(id.xy), color);
}