latkit 0.2.1

Columnar data drawn as vertices, edges, lines, and areas on the GPU.
Documentation
// Strokes: antialiased segments with dashes, flowing comets, and arrowheads, in CSS pixels.

import package::lib::bind::frame;
import package::lib::mark::coverage;

struct Stroke {
    a: vec2f,
    b: vec2f,
    // Distance along the whole line at `a` and at `b`, CSS pixels: dashes and comets run on across
    // segments.
    phase: vec2f,
    // The same in length units, as far along as the line's own points: comets drawn in position
    // units run along it.
    along: vec2f,
    width: f32,
    color: vec4f,
    // Comet speed, negative backward, 0 for none; and how far the row's comets traveled.
    flow: f32,
    travel: f32,
    dash: bool,
    // Whether the segment ends in an arrowhead at `b`.
    arrow: bool,
}

// The arrowhead at `b` pointing along `dir`: a triangle `frame.arrow` long, as a signed distance.
fn head_distance(p: vec2f, b: vec2f, dir: vec2f, width: f32) -> f32 {
    let length = frame.arrow + width;
    let q = p - b;
    let along = dot(q, dir);
    let across = abs(dot(q, vec2f(-dir.y, dir.x)));
    let half = 0.5 * length * 0.9;
    // Inside where it lies behind the tip and within the widening flanks.
    let slope = half / length;
    let flank = (across - (-along) * slope) / sqrt(1.0 + slope * slope);
    return max(max(flank, along), -along - length);
}

// (distance outside the stroke's edge, distance along the line, fraction of the way from `a` to `b`)
// for pixel `p`.
fn stroke_distance(p: vec2f, s: Stroke) -> vec3f {
    let ab = s.b - s.a;
    let len = length(ab);
    let dir = ab / max(len, 1e-6);
    // An arrowhead's stroke stops where the head begins.
    let end = select(s.b, s.b - dir * (frame.arrow + s.width) * 0.8, s.arrow && len > frame.arrow);
    let ae = end - s.a;
    let t = clamp(dot(p - s.a, ae) / max(dot(ae, ae), 1e-6), 0.0, 1.0);
    var d = length(p - (s.a + ae * t)) - 0.5 * s.width;
    if s.arrow {
        d = min(d, head_distance(p, s.b, dir, s.width));
    }
    let along = clamp(dot(p - s.a, dir) / max(len, 1e-6), 0.0, 1.0);
    return vec3f(d, mix(s.phase.x, s.phase.y, along), along);
}

// How much of the pixel at `d`, from `stroke_distance`, the line of stroke `s` covers, dashed.
fn covered(d: vec3f, s: Stroke) -> f32 {
    var alpha = coverage(d.x);
    if s.dash {
        alpha *= step(0.5, fract(d.y / max(frame.dash, 1.0)));
    }
    return alpha;
}

// How much of the pixel at `d` a comet covers: comets `frame.flow_spacing` apart, moved on by stroke
// `s`'s travel, each a round head `frame.flow_width` wide, never narrower than the stroke, brightest
// at its middle, trailing a tail `frame.flow_length` long that thins and fades.
fn comet(d: vec3f, s: Stroke) -> f32 {
    if s.flow == 0.0 || frame.flow.a <= 0.0 {
        return 0.0;
    }
    // Where the pixel lies along the line in the comets' lengths, and CSS pixels per length: position
    // units where comets are part of the drawing, else the theme's.
    var along = d.y / frame.unit;
    var px = frame.unit;
    if frame.flow_units != 0u {
        along = mix(s.along.x, s.along.y, d.z);
        px = length(s.b - s.a) / max(abs(s.along.y - s.along.x), 1e-12);
    }
    let spacing = max(frame.flow_spacing, 1e-6);
    // How far the pixel leads the head behind it, the way comets run, and trails the head ahead.
    let lead = fract(sign(s.flow) * (along - s.travel) / spacing) * spacing;
    let trail = spacing - lead;
    let across = d.x + 0.5 * s.width;
    let cap = lead < trail;
    let fade = select(saturate(1.0 - trail / max(frame.flow_length, 1e-6)), 1.0, cap);
    let reach = 0.5 * max(frame.flow_width, s.width) * mix(0.4, 1.0, fade);
    let distance = select(across, length(vec2f(lead * px, across)), cap);
    return fade * fade * (1.0 - smoothstep(0.35 * reach, reach, distance));
}

// Stroke `s` at pixel `p`, comets over its line; a comet fades with its row.
fn stroke(p: vec2f, s: Stroke) -> vec4f {
    let d = stroke_distance(p, s);
    let line = s.color.a * covered(d, s);
    let head = comet(d, s) * frame.flow.a * s.color.a;
    let alpha = head + line * (1.0 - head);
    let color = (frame.flow.rgb * head + s.color.rgb * line * (1.0 - head)) / max(alpha, 1e-6);
    return vec4f(color, alpha);
}