facett-flow/src/flow.wgsl
struct Point { pos: vec2<f32>, radius: f32, kind: f32 }
struct Link { a_index: u32, b_index: u32, phase: f32, weight: f32 }
struct Uniforms {
viewport: vec4<f32>, // physical width/height, world centre
motion: vec4<f32>, // pixels/world, seconds, animation enabled, edge opacity
input_col: vec4<f32>,
middle_col: vec4<f32>,
output_col: vec4<f32>,
edge_col: vec4<f32>,
pulse_col: vec4<f32>,
detail: vec4<f32>, // curve segment count
}
@group(0) @binding(0) var<uniform> u: Uniforms;
@group(0) @binding(1) var<storage,read> points: array<Point>;
@group(0) @binding(2) var<storage,read> links: array<Link>;
struct Vary { @builtin(position) pos: vec4<f32>, @location(0) local: vec2<f32>, @location(1) colour: vec4<f32> }
fn corner(v:u32)->vec2<f32> {
let corners=array<vec2<f32>,6>(vec2(-1.0,-1.0),vec2(1.0,-1.0),vec2(-1.0,1.0),vec2(-1.0,1.0),vec2(1.0,-1.0),vec2(1.0,1.0));
return corners[v];
}
fn project(p:vec2<f32>)->vec2<f32> {return (p-u.viewport.zw)*u.motion.x+u.viewport.xy*0.5;}
fn clip(p:vec2<f32>)->vec4<f32> {return vec4(2.0*p.x/u.viewport.x-1.0,1.0-2.0*p.y/u.viewport.y,0.0,1.0);}
fn hidden()->Vary {return Vary(vec4(2.0,2.0,0.0,1.0),vec2(0.0),vec4(0.0));}
fn curve(a:vec2<f32>, b:vec2<f32>, t:f32)->vec2<f32> {
let delta=b-a;
var bend=vec2(delta.x*0.42,0.0);
if abs(delta.y)>abs(delta.x) {bend=vec2(0.0,delta.y*0.42);}
let c=a+bend; let d=b-bend; let s=1.0-t;
return s*s*s*a+3.0*s*s*t*c+3.0*s*t*t*d+t*t*t*b;
}
fn outside(a:vec2<f32>,b:vec2<f32>)->bool {
let lo=min(a,b); let hi=max(a,b);
return hi.x < -16.0 || hi.y < -16.0 || lo.x>u.viewport.x+16.0 || lo.y>u.viewport.y+16.0;
}
@vertex fn node_vs(@builtin(vertex_index) v:u32,@builtin(instance_index) i:u32)->Vary {
let n=points[i];
// The host draws the hub outline. An opaque GPU hub would hide its packets.
if n.kind>2.5 {return hidden();}
let p=project(n.pos); let r=clamp(n.radius*u.motion.x,1.2,14.0);
if outside(p-vec2(r),p+vec2(r)) {return hidden();}
var col=u.middle_col; if n.kind<0.5 {col=u.input_col;} else if n.kind>1.5 {col=u.output_col;}
return Vary(clip(p+corner(v)*r),corner(v),col);
}
@fragment fn node_fs(v:Vary)->@location(0) vec4<f32> {
let d=length(v.local); let alpha=1.0-smoothstep(0.65,1.0,d);
return vec4(v.colour.rgb,v.colour.a*alpha);
}
@vertex fn edge_vs(@builtin(vertex_index) v:u32,@builtin(instance_index) i:u32)->Vary {
let e=links[i]; let a=project(points[e.a_index].pos); let b=project(points[e.b_index].pos);
if outside(a,b) {return hidden();}
// Zoom-dependent segment count; the host bounds interpolation error.
let segment=v/6u; let c=corner(v%6u);
let p=curve(a,b,f32(segment)/u.detail.x); let q=curve(a,b,f32(segment+1u)/u.detail.x);
let direction=normalize(q-p+vec2(0.0001,0.0)); let normal=vec2(-direction.y,direction.x);
let centre=mix(p,q,(c.x+1.0)*0.5); let half_width=0.65;
let pos=centre+normal*c.y*half_width;
var col=u.edge_col; col.a*=u.motion.w;
return Vary(clip(pos),vec2((f32(segment)+(c.x+1.0)*0.5)/u.detail.x,e.phase),col);
}
@fragment fn edge_fs(v:Vary)->@location(0) vec4<f32> {
let head=fract(u.motion.y*0.18+v.local.y);
let behind=fract(head-v.local.x);
let trail=(1.0-smoothstep(0.0,0.16,behind))*u.motion.z;
return vec4(mix(v.colour.rgb,u.pulse_col.rgb,trail),v.colour.a*(0.5+2.0*trail));
}
@vertex fn pulse_vs(@builtin(vertex_index) v:u32,@builtin(instance_index) i:u32)->Vary {
let e=links[i]; let a=project(points[e.a_index].pos); let b=project(points[e.b_index].pos);
if outside(a,b) || u.motion.z<0.5 {return hidden();}
let t=fract(u.motion.y*0.18+e.phase); let p=curve(a,b,t); let c=corner(v);
return Vary(clip(p+c*2.8),c,u.pulse_col);
}
@fragment fn pulse_fs(v:Vary)->@location(0) vec4<f32> {
let a=(1.0-smoothstep(0.0,1.0,length(v.local)))*0.8;
return vec4(v.colour.rgb,v.colour.a*a);
}