iced_plot 0.4.0

A GPU-accelerated plotting widget for Iced.
Documentation
// Vertex shader
const QUAD_POS: array<vec2<f32>, 4> = array<vec2<f32>, 4>(
    vec2<f32>(-1.0, -1.0),  // bottom-left
    vec2<f32>(1.0, -1.0),   // bottom-right
    vec2<f32>(-1.0, 1.0),   // top-left
    vec2<f32>(1.0, 1.0),    // top-right
);
const CIRCLE_RADIUS: f32 = 1.0;
const EMPTY_CIRCLE_INNER: f32 = 0.7;
const STAR_ANGLE_MULT: f32 = 5.0;
const STAR_INNER_SCALE: f32 = 0.3;

struct CameraUniform {
    view_proj: mat4x4<f32>,
    pixel_to_clip: vec4<f32>, // (2/width, 2/height, _, _) - for screen-space sizing
    pixel_to_world: vec4<f32>, // (world_per_pixel_x, world_per_pixel_y, _, _) - for world-space patterns
};

@group(0) @binding(0)
var<uniform> camera: CameraUniform;

struct VertexInput {
    @location(0) position: vec2<f32>,
    @location(1) color: vec4<f32>,
    @location(2) marker_type: u32,
    @location(3) size: f32,
    @location(4) size_mode: u32,
};

struct VertexOutput {
    @builtin(position) clip_position: vec4<f32>,
    @location(0) color: vec4<f32>,
    @interpolate(flat) @location(1) marker_type: u32,
    @location(2) size: f32,
    @location(3) local_pos: vec2<f32>,
};

@vertex
fn vs_main(
    @builtin(vertex_index) vertex_index: u32,
    model: VertexInput,
) -> VertexOutput {
    var out: VertexOutput;

    // Generate quad vertices for each marker
    let local_pos = QUAD_POS[vertex_index];

    var center_pos = model.position;
    var half_world = 0.0;
    if (model.size_mode == 1u) {
        half_world = model.size * 0.5;
        center_pos = center_pos + vec2<f32>(half_world, half_world);
    }
    // Center in clip space
    let center_clip = camera.view_proj * vec4<f32>(center_pos, 0.0, 1.0);

    // Interpret model.size as pixels or world units depending on size_mode
    var half_size_px_x = model.size;
    var half_size_px_y = model.size;
    if (model.size_mode == 1u) {
        half_size_px_x = half_world / camera.pixel_to_world.x;
        half_size_px_y = half_world / camera.pixel_to_world.y;
    }
    let offset_clip = vec4<f32>(local_pos.x * half_size_px_x * camera.pixel_to_clip.x * center_clip.w,
                                local_pos.y * half_size_px_y * camera.pixel_to_clip.y * center_clip.w,
                                0.0, 0.0);
    out.clip_position = center_clip + offset_clip;
    out.color = model.color;
    out.marker_type = model.marker_type;
    out.size = model.size;
    out.local_pos = local_pos;

    return out;
}

// Fragment shader
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    let dist = length(in.local_pos);

    // Different marker shapes
    switch in.marker_type {
        case 0u { // Filled Circle
            if dist <= CIRCLE_RADIUS {
                return vec4<f32>(in.color.rgb, 1.0);
            }
        }
        case 1u { // Empty Circle (ring)
            if dist >= EMPTY_CIRCLE_INNER && dist <= CIRCLE_RADIUS {
                return vec4<f32>(in.color.rgb, 1.0);
            }
        }
        case 2u { // Square
            if abs(in.local_pos.x) <= CIRCLE_RADIUS && abs(in.local_pos.y) <= CIRCLE_RADIUS {
                return vec4<f32>(in.color.rgb, 1.0);
            }
        }
        case 3u { // Star
            let angle = atan2(in.local_pos.y, in.local_pos.x);
            let star_dist = CIRCLE_RADIUS - STAR_INNER_SCALE * abs(sin(angle * STAR_ANGLE_MULT));
            if dist <= star_dist {
                return vec4<f32>(in.color.rgb, 1.0);
            }
        }
        case 4u { // Triangle
            let x = in.local_pos.x;
            let y = in.local_pos.y;
            // Equilateral triangle pointing up: base from (-1, -0.866) to (1, -0.866), apex at (0, 0.866)
            // Height/base ratio of √3/2 ≈ 0.866 (truly equilateral), centered at y=0
            if y >= -0.866 && y <= 0.866 {
                // Calculate the fraction from base to apex (0 at base, 1 at apex)
                let fraction = (y + 0.866) / 1.732;
                // Width decreases linearly from 2 at base to 0 at apex
                let half_width = 1.0 * (1.0 - fraction);
                let left_bound = -half_width;
                let right_bound = half_width;
                if x >= left_bound && x <= right_bound {
                    return vec4<f32>(in.color.rgb, 1.0);
                }
            }
        }
        default {
            return vec4<f32>(in.color.rgb, 1.0);
        }
    }

    // Transparent for areas outside the marker
    return vec4<f32>(0.0, 0.0, 0.0, 0.0);
}