henad-app 0.3.0

Desktop and web app for Henad, a parallel agent-based modelling engine.
Documentation
// Instanced edges, drawn as one line per edge, with both ends read from the agent layer's position lanes.
// A directed edge can also get a filled arrowhead at its target end.
//
// The lanes are bound as storage buffers. Each edge looks up its two nodes by index.

#import world::{HIDDEN, is_placed, to_clip}

struct Uniforms {
    world: vec2<f32>,
    // Alpha at the source and target ends. On a directed graph, the source end fades.
    alpha: vec2<f32>,
    // Points per clip unit on each axis. Arrowheads are shaped in points, where both axes have the same scale.
    points_per_clip: vec2<f32>,
    // Length and half width of an arrowhead in points. A zero length draws no arrowhead.
    arrow: vec2<f32>,
    // Distance in points between an arrowhead's tip and the centre of its node.
    arrow_gap: f32,
}

@group(0) @binding(0) var<uniform> u: Uniforms;
@group(0) @binding(1) var<storage, read> pos_x: array<f32>;
@group(0) @binding(2) var<storage, read> pos_y: array<f32>;

struct VertexInput {
    @location(0) src: u32,
    @location(1) dst: u32,
    @location(2) color: vec4<f32>,
}

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) color: vec4<f32>,
}

// The two ends of an edge, in points relative to the centre of the rect.
struct Span {
    tail: vec2<f32>,
    head: vec2<f32>,
    // Whether both ends are placed. Hiding only one end would draw a line off towards it.
    placed: bool,
}

fn span_of(in: VertexInput) -> Span {
    let a = vec2<f32>(pos_x[in.src], pos_y[in.src]);
    let b = vec2<f32>(pos_x[in.dst], pos_y[in.dst]);
    return Span(
        to_clip(a, u.world) * u.points_per_clip,
        to_clip(b, u.world) * u.points_per_clip,
        is_placed(a) && is_placed(b),
    );
}

// Returns the arrowhead length on an edge that is `len` points long.
// The head shrinks on a short edge.
fn head_length(len: f32) -> f32 {
    return min(u.arrow.x, max(len - 2.0 * u.arrow_gap, 0.0));
}

fn to_output(p: vec2<f32>, color: vec4<f32>) -> VertexOutput {
    var out: VertexOutput;
    out.clip_position = vec4<f32>(p / u.points_per_clip, 0.0, 1.0);
    out.color = color;
    return out;
}

@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexInput) -> VertexOutput {
    let span = span_of(in);
    let at_target = vertex_index == 1u;
    let color = vec4<f32>(in.color.rgb, in.color.a * select(u.alpha.x, u.alpha.y, at_target));
    if !span.placed {
        var hidden: VertexOutput;
        hidden.clip_position = HIDDEN;
        hidden.color = color;
        return hidden;
    }
    if !at_target {
        return to_output(span.tail, color);
    }
    // With an arrowhead, the line stops at the head's base so the two do not overlap.
    let run = span.head - span.tail;
    let len = length(run);
    var back = 0.0;
    if u.arrow.x > 0.0 {
        back = min(u.arrow_gap + head_length(len), len);
    }
    return to_output(span.head - run / max(len, 1e-6) * back, color);
}

// Three vertices per edge, the tip followed by the two corners of the base.
@vertex
fn vs_arrow(@builtin(vertex_index) vertex_index: u32, in: VertexInput) -> VertexOutput {
    let span = span_of(in);
    // The head is opaque whatever the line's alpha. It shows the direction.
    let color = vec4<f32>(in.color.rgb, 1.0);
    if !span.placed {
        var hidden: VertexOutput;
        hidden.clip_position = HIDDEN;
        hidden.color = color;
        return hidden;
    }
    let run = span.head - span.tail;
    let len = length(run);
    let along = run / max(len, 1e-6);
    let across = vec2<f32>(-along.y, along.x);
    let head = head_length(len);
    let width = u.arrow.y * head / max(u.arrow.x, 1e-6);

    let tip = span.head - along * u.arrow_gap;
    let base = tip - along * head;
    var p = tip;
    if vertex_index == 1u {
        p = base + across * width;
    } else if vertex_index == 2u {
        p = base - across * width;
    }
    return to_output(p, color);
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    return in.color;
}