sequoia-export 0.1.3

Sequoia GUI framework — headless HiDPI PNG export (website-ready screenshots without a window)
Documentation
use sequoia_core::{BrushStop, Color, Fill, Rect};
use sequoia_export::{ExportOptions, render_to_image};
use sequoia_view::{Canvas, Column, Painter, View};

fn gray_stops() -> Vec<BrushStop> {
    (0..=6)
        .map(|i| {
            let v = (i as f32 / 6.0 * 255.0).round() as u8;
            BrushStop::new(i as f32 / 6.0, Color::rgb8(v, v, v))
        })
        .collect()
}

fn bar_view(pad: f32) -> impl View {
    let stops = gray_stops();
    Column::new()
        .padding_xy(pad, pad)
        .child(
            Canvas::new()
                .width(14.0)
                .height(198.0)
                .on_draw(move |g: &mut Painter| {
                    g.fill(
                        Rect::from_xywh(0.0, 0.0, 14.0, 198.0),
                        Fill::vertical(stops.clone()),
                    );
                }),
        )
}

fn center_luma(img: &image::RgbaImage, x: u32, y0: u32, y1: u32) -> Vec<f32> {
    (y0..y1)
        .map(|y| {
            let p = img.get_pixel(x, y).0;
            0.2126 * p[0] as f32 + 0.7152 * p[1] as f32 + 0.0722 * p[2] as f32
        })
        .collect()
}

fn max_notch(luma: &[f32]) -> f32 {
    luma.windows(3)
        .map(|w| (2.0 * w[1] - w[0] - w[2]).abs())
        .fold(0.0, f32::max)
}

#[test]
fn hue_strip_has_no_seams_on_fractional_origin() {
    for pad in [0.0f32, 10.5] {
        let opts = ExportOptions::new(120.0, 260.0).scale(1.0);
        let img = render_to_image(bar_view(pad), &opts).unwrap();

        let cx = (pad + 7.0).floor() as u32;
        let y0 = (pad + 2.0).ceil() as u32;
        let y1 = (pad + 196.0).floor() as u32;
        let luma = center_luma(&img, cx, y0, y1);
        let range = luma.iter().fold(0.0f32, |a, &b| a.max(b))
            - luma.iter().fold(255.0f32, |a, &b| a.min(b));
        assert!(
            range > 200.0,
            "pad {pad}: pasek nie wyrenderowany? zakres luminancji {range:.1}"
        );

        let notch = max_notch(&luma);
        if notch >= 12.0 {
            let mut spikes: Vec<(usize, f32)> = luma
                .windows(3)
                .enumerate()
                .map(|(i, w)| (i + 1, (2.0 * w[1] - w[0] - w[2]).abs()))
                .collect();
            spikes.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
            for (i, s) in spikes.iter().take(5) {
                let lo = i.saturating_sub(2);
                let hi = (i + 3).min(luma.len());
                eprintln!(
                    "pad {pad} spike row {i} ({s:.1}): {luma:?}",
                    luma = &luma[lo..hi]
                );
            }
        }
        assert!(notch < 4.0, "pad {pad}: szew gradientu (kolec {notch:.1})");
    }
}

#[test]
fn two_stop_gradient_is_exported() {
    let stops = vec![
        BrushStop::new(0.0, Color::BLACK),
        BrushStop::new(1.0, Color::WHITE),
    ];
    let view = Column::new().child(Canvas::new().width(200.0).height(8.0).on_draw(
        move |g: &mut Painter| {
            g.fill(
                Rect::from_xywh(0.0, 0.0, 200.0, 8.0),
                Fill::horizontal(stops.clone()),
            );
        },
    ));
    let opts = ExportOptions::new(220.0, 28.0).scale(1.0);
    let img = render_to_image(view, &opts).unwrap();
    let luma = |x: u32| {
        let p = img.get_pixel(x, 4).0;
        0.2126 * p[0] as f32 + 0.7152 * p[1] as f32 + 0.0722 * p[2] as f32
    };

    assert!(luma(0) < 15.0, "lewy koniec czarny, jest {}", luma(0));
    assert!(luma(199) > 245.0, "right end white, is {}", luma(199));
    let mid = luma(100);
    assert!(
        (170.0..=205.0).contains(&mid),
        "center of wedge ~188, is {mid:.1}"
    );
}