motionloom 0.1.0

MotionLoom DSL parser and renderer for video effects, scene graphs, motion graphics, and world graphs.
Documentation

struct BatchParams {
    canvas: vec4<f32>,
    count: vec4<f32>,
};

struct Primitive {
    info: vec4<f32>,
    bounds: vec4<f32>,
    shape: vec4<f32>,
    style: vec4<f32>,
    color: vec4<f32>,
    inv0: vec4<f32>,
    inv1: vec4<f32>,
    paint: vec4<f32>,
    paint_bounds: vec4<f32>,
    gradient: vec4<f32>,
    stop_offsets0: vec4<f32>,
    stop_offsets1: vec4<f32>,
    stop_color0: vec4<f32>,
    stop_color1: vec4<f32>,
    stop_color2: vec4<f32>,
    stop_color3: vec4<f32>,
    stop_color4: vec4<f32>,
    stop_color5: vec4<f32>,
    stop_color6: vec4<f32>,
    stop_color7: vec4<f32>,
    line: vec4<f32>,
};

struct PrimitiveBuffer {
    items: array<Primitive>,
};

struct TileRangeBuffer {
    items: array<vec4<u32>>,
};

struct TileIndexBuffer {
    items: array<u32>,
};

@group(0) @binding(0) var base_tex: texture_2d<f32>;
@group(0) @binding(1) var out_tex: texture_storage_2d<rgba8unorm, write>;
@group(0) @binding(2) var<uniform> params: BatchParams;
@group(0) @binding(3) var<storage, read> primitive_buffer: PrimitiveBuffer;
@group(0) @binding(4) var<storage, read> tile_range_buffer: TileRangeBuffer;
@group(0) @binding(5) var<storage, read> tile_index_buffer: TileIndexBuffer;

fn rounded_rect_sdf(p: vec2<f32>, rect: vec4<f32>, radius: f32) -> f32 {
    let half_size = max(rect.zw * 0.5, vec2<f32>(0.0001, 0.0001));
    let center = rect.xy + half_size;
    let r = clamp(radius, 0.0, min(half_size.x, half_size.y));
    let q = abs(p - center) - half_size + vec2<f32>(r, r);
    return length(max(q, vec2<f32>(0.0, 0.0))) + min(max(q.x, q.y), 0.0) - r;
}

fn segment_distance(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 cross2(a: vec2<f32>, b: vec2<f32>) -> f32 {
    return a.x * b.y - a.y * b.x;
}

fn triangle_coverage(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>, c: vec2<f32>) -> f32 {
    let area = cross2(b - a, c - a);
    if (abs(area) <= 0.0001) {
        return 0.0;
    }
    let e0 = cross2(b - a, p - a);
    let e1 = cross2(c - b, p - b);
    let e2 = cross2(a - c, p - c);
    var same_side = false;
    if (area > 0.0) {
        same_side = e0 >= 0.0 && e1 >= 0.0 && e2 >= 0.0;
    } else {
        same_side = e0 <= 0.0 && e1 <= 0.0 && e2 <= 0.0;
    }
    if (!same_side) {
        return 0.0;
    }
    // Filled paths are triangulated before reaching this shader. Applying AA to
    // every triangle edge makes internal triangulation seams visible as moire /
    // wave bands on flat anime fills. Keep triangle interiors hard-filled here;
    // visible contour quality should be handled by the path outline/stroke.
    return 1.0;
}

fn inside_coverage(dist: f32) -> f32 {
    return clamp(0.5 - dist, 0.0, 1.0);
}

fn over(base: vec4<f32>, src_rgb: vec3<f32>, src_a: f32) -> vec4<f32> {
    let a = clamp(src_a, 0.0, 1.0);
    let out_a = a + base.a * (1.0 - a);
    if (out_a <= 0.000001) {
        return vec4<f32>(0.0, 0.0, 0.0, 0.0);
    }
    let rgb = (src_rgb * a + base.rgb * base.a * (1.0 - a)) / out_a;
    return vec4<f32>(rgb, out_a);
}

fn blend_over(base: vec4<f32>, src_rgb: vec3<f32>, src_a: f32, mode: f32) -> vec4<f32> {
    let blend_mode = i32(round(mode));
    if (blend_mode == 0) {
        return over(base, src_rgb, src_a);
    }

    let a = clamp(src_a, 0.0, 1.0);
    if (a <= 0.0) {
        return base;
    }

    var blended = src_rgb;
    if (blend_mode == 1) {
        blended = src_rgb * base.rgb;
    } else if (blend_mode == 2) {
        let one = vec3<f32>(1.0, 1.0, 1.0);
        blended = one - (one - src_rgb) * (one - base.rgb);
    } else if (blend_mode == 3) {
        blended = min(src_rgb + base.rgb, vec3<f32>(1.0, 1.0, 1.0));
    }

    let out_a = a + base.a * (1.0 - a);
    if (out_a <= 0.000001) {
        return vec4<f32>(0.0, 0.0, 0.0, 0.0);
    }
    let rgb = (blended * a + base.rgb * base.a * (1.0 - a)) / out_a;
    return vec4<f32>(rgb, out_a);
}

fn stop_offset(p: Primitive, index: i32) -> f32 {
    if (index == 0) { return p.stop_offsets0.x; }
    if (index == 1) { return p.stop_offsets0.y; }
    if (index == 2) { return p.stop_offsets0.z; }
    if (index == 3) { return p.stop_offsets0.w; }
    if (index == 4) { return p.stop_offsets1.x; }
    if (index == 5) { return p.stop_offsets1.y; }
    if (index == 6) { return p.stop_offsets1.z; }
    return p.stop_offsets1.w;
}

fn stop_color(p: Primitive, index: i32) -> vec4<f32> {
    if (index == 0) { return p.stop_color0; }
    if (index == 1) { return p.stop_color1; }
    if (index == 2) { return p.stop_color2; }
    if (index == 3) { return p.stop_color3; }
    if (index == 4) { return p.stop_color4; }
    if (index == 5) { return p.stop_color5; }
    if (index == 6) { return p.stop_color6; }
    return p.stop_color7;
}

fn sample_gradient_stops(p: Primitive, t_in: f32) -> vec4<f32> {
    let count = i32(clamp(round(p.paint.z), 0.0, 8.0));
    if (count <= 0) {
        return vec4<f32>(0.0, 0.0, 0.0, 0.0);
    }
    let t = clamp(t_in, 0.0, 1.0);
    if (t <= stop_offset(p, 0)) {
        return stop_color(p, 0);
    }
    for (var i = 1; i < 8; i = i + 1) {
        if (i < count && t <= stop_offset(p, i)) {
            let a_off = stop_offset(p, i - 1);
            let b_off = stop_offset(p, i);
            let span = max(b_off - a_off, 0.000001);
            let local_t = clamp((t - a_off) / span, 0.0, 1.0);
            let a = stop_color(p, i - 1);
            let b = stop_color(p, i);
            return a + (b - a) * local_t;
        }
    }
    return stop_color(p, max(count - 1, 0));
}

fn sample_shape_paint(p: Primitive, local: vec2<f32>) -> vec4<f32> {
    let paint_kind = i32(round(p.paint.x));
    if (paint_kind == 0) {
        return p.color;
    }

    let units = i32(round(p.paint.y));
    let min_p = p.paint_bounds.xy;
    let max_p = p.paint_bounds.zw;
    let size = max(max_p - min_p, vec2<f32>(0.0001, 0.0001));
    var sample_p = local;
    if (units == 0) {
        sample_p = (local - min_p) / size;
    }

    if (paint_kind == 1) {
        let start = p.gradient.xy;
        let end = p.gradient.zw;
        let dir = end - start;
        let len2 = max(dot(dir, dir), 0.000001);
        return sample_gradient_stops(p, dot(sample_p - start, dir) / len2);
    }

    if (paint_kind == 2) {
        let center = p.gradient.xy;
        var delta = sample_p - center;
        if (units == 0) {
            let aspect = select(1.0, size.y / size.x, size.x > size.y);
            delta.x = delta.x / max(aspect, 0.0001);
        }
        return sample_gradient_stops(p, length(delta) / max(p.gradient.z, 0.0001));
    }

    return p.color;
}

fn line_taper_pressure(p: Primitive, t: f32) -> f32 {
    var pressure = 1.0;
    if (p.line.z > 0.0001) {
        pressure = min(pressure, clamp(t / p.line.z, 0.0, 1.0));
    }
    if (p.line.w > 0.0001) {
        pressure = min(pressure, clamp((1.0 - t) / p.line.w, 0.0, 1.0));
    }
    return pressure;
}

fn primitive_coverage(p: Primitive, local: vec2<f32>) -> vec2<f32> {
    let shape_kind = i32(round(p.info.x));
    var coverage = 0.0;
    var replace = 0.0;

    if (shape_kind == 7) {
        coverage = 1.0;
        replace = 1.0;
    } else if (shape_kind == 1) {
        let dist = rounded_rect_sdf(local, p.shape, p.style.x);
        coverage = inside_coverage(dist);
    } else if (shape_kind == 2) {
        let outer = rounded_rect_sdf(local, p.shape, p.style.x);
        let sw = max(p.style.y, 0.0);
        let inner = rounded_rect_sdf(
            local,
            vec4<f32>(p.shape.x + sw, p.shape.y + sw, max(p.shape.z - sw * 2.0, 0.0), max(p.shape.w - sw * 2.0, 0.0)),
            max(p.style.x - sw, 0.0)
        );
        coverage = inside_coverage(outer) * (1.0 - inside_coverage(inner));
    } else if (shape_kind == 3) {
        let dist = length(local - p.shape.xy) - p.shape.z;
        coverage = inside_coverage(dist);
    } else if (shape_kind == 4) {
        let dist = length(local - p.shape.xy) - p.shape.z;
        let sw = max(p.style.y, 0.0);
        coverage = inside_coverage(dist) * (1.0 - inside_coverage(dist + sw));
    } else if (shape_kind == 5) {
        let dist = rounded_rect_sdf(local, p.shape, p.style.x);
        let outside = max(dist, 0.0);
        let blur = max(p.style.z, 1.0);
        let sigma = max(blur * 0.42, 1.0);
        let inside = smoothstep(0.0, blur * 0.7, max(-dist, 0.0));
        let outside_falloff = exp(-(outside * outside) / (2.0 * sigma * sigma));
        coverage = min(max(inside, outside_falloff), 0.86);
    } else if (shape_kind == 6) {
        let dist = length(local - p.shape.xy) - p.shape.z;
        let outside = max(dist, 0.0);
        let blur = max(p.style.z, 1.0);
        let sigma = max(blur * 0.42, 1.0);
        let inside = smoothstep(0.0, blur * 0.7, max(-dist, 0.0));
        let outside_falloff = exp(-(outside * outside) / (2.0 * sigma * sigma));
        coverage = min(max(inside, outside_falloff), 0.86);
    } else if (shape_kind == 8) {
        let ab = p.shape.zw - p.shape.xy;
        let h = clamp(dot(local - p.shape.xy, ab) / max(dot(ab, ab), 0.0001), 0.0, 1.0);
        let global_t = p.line.x + (p.line.y - p.line.x) * h;
        let pressure = line_taper_pressure(p, global_t);
        let half_width = max(p.style.y * pressure * 0.5, 0.01);
        let dist = segment_distance(local, p.shape.xy, p.shape.zw) - half_width;
        coverage = inside_coverage(dist);
    } else if (shape_kind == 9) {
        coverage = triangle_coverage(local, p.shape.xy, p.shape.zw, p.style.xy);
    }

    return vec2<f32>(clamp(coverage, 0.0, 1.0), replace);
}

@compute @workgroup_size(16, 16, 1)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
    let x = gid.x;
    let y = gid.y;
    if (x >= u32(params.canvas.x) || y >= u32(params.canvas.y)) {
        return;
    }

    let pos = vec2<i32>(i32(x), i32(y));
    let px = f32(x) + 0.5;
    let py = f32(y) + 0.5;
    var out_color = textureLoad(base_tex, pos, 0);
    let primitive_count = u32(round(params.count.x));
    let tile_size = max(u32(round(params.count.y)), 1u);
    let tiles_x = max(u32(round(params.count.z)), 1u);
    let tiles_y = max(u32(round(params.count.w)), 1u);
    let tile_x = min(x / tile_size, tiles_x - 1u);
    let tile_y = min(y / tile_size, tiles_y - 1u);
    let tile = tile_range_buffer.items[tile_y * tiles_x + tile_x];
    let tile_start = tile.x;
    let tile_count = tile.y;

    for (var local_i = 0u; local_i < tile_count; local_i = local_i + 1u) {
        let primitive_index = tile_index_buffer.items[tile_start + local_i];
        if (primitive_index >= primitive_count) {
            continue;
        }
        let p = primitive_buffer.items[primitive_index];
        if (px < p.bounds.x || py < p.bounds.y || px >= p.bounds.x + p.bounds.z || py >= p.bounds.y + p.bounds.w) {
            continue;
        }
        let local = vec2<f32>(
            p.inv0.x * px + p.inv0.y * py + p.inv0.z,
            p.inv1.x * px + p.inv1.y * py + p.inv1.z
        );
        let cover_replace = primitive_coverage(p, local);
        let coverage = cover_replace.x;
        if (coverage <= 0.0) {
            continue;
        }
        let paint_color = sample_shape_paint(p, local);
        if (cover_replace.y > 0.5) {
            out_color = vec4<f32>(paint_color.rgb, paint_color.a * p.style.w);
        } else {
            out_color = blend_over(out_color, paint_color.rgb, paint_color.a * p.style.w * coverage, p.info.y);
        }
    }

    textureStore(out_tex, pos, out_color);
}