Skip to main content

resamplescope/
graph.rs

1use imgref::ImgVec;
2use rgb::RGB8;
3
4use crate::analyze::FilterCurve;
5use crate::filters::KnownFilter;
6
7const WIDTH: usize = 600;
8const HEIGHT: usize = 300;
9const ZERO_X: f64 = 230.0;
10const UNIT_X: f64 = 90.0;
11const ZERO_Y: f64 = 220.0;
12const UNIT_Y: f64 = -200.0; // Negative: positive values go up
13
14const WHITE: RGB8 = RGB8 {
15    r: 255,
16    g: 255,
17    b: 255,
18};
19const BLACK: RGB8 = RGB8 { r: 0, g: 0, b: 0 };
20const GRID_GRAY: RGB8 = RGB8 {
21    r: 192,
22    g: 192,
23    b: 192,
24};
25const BORDER_GREEN: RGB8 = RGB8 {
26    r: 144,
27    g: 192,
28    b: 144,
29};
30const SCATTER_BLUE: RGB8 = RGB8 { r: 0, g: 0, b: 255 };
31const LINE_RED: RGB8 = RGB8 {
32    r: 224,
33    g: 64,
34    b: 64,
35};
36const REF_LIGHT: RGB8 = RGB8 {
37    r: 180,
38    g: 180,
39    b: 180,
40};
41
42fn xcoord(ix: f64) -> i32 {
43    (0.5 + ZERO_X + ix * UNIT_X) as i32
44}
45
46fn ycoord(iy: f64) -> i32 {
47    (0.5 + ZERO_Y + iy * UNIT_Y) as i32
48}
49
50fn set_pixel(buf: &mut [RGB8], x: i32, y: i32, color: RGB8) {
51    if x >= 0 && x < WIDTH as i32 && y >= 0 && y < HEIGHT as i32 {
52        buf[y as usize * WIDTH + x as usize] = color;
53    }
54}
55
56fn draw_line(buf: &mut [RGB8], x0: i32, y0: i32, x1: i32, y1: i32, color: RGB8) {
57    let dx = (x1 - x0).abs();
58    let dy = -(y1 - y0).abs();
59    let sx: i32 = if x0 < x1 { 1 } else { -1 };
60    let sy: i32 = if y0 < y1 { 1 } else { -1 };
61    let mut err = dx + dy;
62    let mut x = x0;
63    let mut y = y0;
64
65    loop {
66        set_pixel(buf, x, y, color);
67        if x == x1 && y == y1 {
68            break;
69        }
70        let e2 = 2 * err;
71        if e2 >= dy {
72            if x == x1 {
73                break;
74            }
75            err += dy;
76            x += sx;
77        }
78        if e2 <= dx {
79            if y == y1 {
80                break;
81            }
82            err += dx;
83            y += sy;
84        }
85    }
86}
87
88fn draw_dashed_line(buf: &mut [RGB8], x0: i32, y0: i32, x1: i32, y1: i32, color: RGB8) {
89    let dx = (x1 - x0).abs();
90    let dy = -(y1 - y0).abs();
91    let sx: i32 = if x0 < x1 { 1 } else { -1 };
92    let sy: i32 = if y0 < y1 { 1 } else { -1 };
93    let mut err = dx + dy;
94    let mut x = x0;
95    let mut y = y0;
96    let mut step = 0u32;
97
98    loop {
99        // 4 on, 4 off pattern matching gdImageDashedLine
100        if step % 8 < 4 {
101            set_pixel(buf, x, y, color);
102        }
103        step += 1;
104        if x == x1 && y == y1 {
105            break;
106        }
107        let e2 = 2 * err;
108        if e2 >= dy {
109            if x == x1 {
110                break;
111            }
112            err += dy;
113            x += sx;
114        }
115        if e2 <= dx {
116            if y == y1 {
117                break;
118            }
119            err += dx;
120            y += sy;
121        }
122    }
123}
124
125fn draw_grid(buf: &mut [RGB8]) {
126    // Dashed lines at half-integers
127    for i in -10..=10 {
128        let hx = xcoord(0.5 + i as f64);
129        draw_dashed_line(buf, hx, 0, hx, HEIGHT as i32 - 1, GRID_GRAY);
130        let hy = ycoord(0.5 + i as f64);
131        draw_dashed_line(buf, 0, hy, WIDTH as i32 - 1, hy, GRID_GRAY);
132    }
133
134    // Solid lines at integers
135    for i in -10..=10 {
136        let ix = xcoord(i as f64);
137        draw_line(buf, ix, 0, ix, HEIGHT as i32 - 1, GRID_GRAY);
138        let iy = ycoord(i as f64);
139        draw_line(buf, 0, iy, WIDTH as i32 - 1, iy, GRID_GRAY);
140    }
141
142    // Axes
143    let ax = xcoord(0.0);
144    draw_line(buf, ax, 0, ax, HEIGHT as i32 - 1, BLACK);
145    let ay = ycoord(0.0);
146    draw_line(buf, 0, ay, WIDTH as i32 - 1, ay, BLACK);
147}
148
149fn draw_border(buf: &mut [RGB8], color: RGB8) {
150    let w = WIDTH as i32;
151    let h = HEIGHT as i32;
152    draw_line(buf, 0, 0, w - 1, 0, color);
153    draw_line(buf, 0, h - 1, w - 1, h - 1, color);
154    draw_line(buf, 0, 0, 0, h - 1, color);
155    draw_line(buf, w - 1, 0, w - 1, h - 1, color);
156}
157
158fn plot_scatter(buf: &mut [RGB8], points: &[(f64, f64)], color: RGB8) {
159    for &(x, y) in points {
160        let px = xcoord(x);
161        let py = ycoord(y);
162        set_pixel(buf, px, py, color);
163    }
164}
165
166fn plot_connected(buf: &mut [RGB8], points: &[(f64, f64)], color: RGB8) {
167    let mut last: Option<(i32, i32)> = None;
168    for &(x, y) in points {
169        let px = xcoord(x);
170        let py = ycoord(y);
171        if let Some((lx, ly)) = last {
172            draw_line(buf, lx, ly, px, py, color);
173        }
174        last = Some((px, py));
175    }
176}
177
178fn plot_reference(buf: &mut [RGB8], filter: KnownFilter, color: RGB8) {
179    // Sample the reference filter densely across the visible range.
180    let x_min = -ZERO_X / UNIT_X; // leftmost visible logical x
181    let x_max = (WIDTH as f64 - ZERO_X) / UNIT_X; // rightmost visible logical x
182
183    let steps = WIDTH * 2;
184    let mut last: Option<(i32, i32)> = None;
185    for i in 0..=steps {
186        let x = x_min + (x_max - x_min) * i as f64 / steps as f64;
187        let y = filter.evaluate(x);
188        let px = xcoord(x);
189        let py = ycoord(y);
190        if let Some((lx, ly)) = last {
191            draw_line(buf, lx, ly, px, py, color);
192        }
193        last = Some((px, py));
194    }
195}
196
197/// Render a scope graph showing the reconstructed filter curve(s).
198pub fn render(
199    downscale: Option<&FilterCurve>,
200    upscale: Option<&FilterCurve>,
201    reference: Option<KnownFilter>,
202) -> ImgVec<RGB8> {
203    let mut buf = vec![WHITE; WIDTH * HEIGHT];
204
205    draw_grid(&mut buf);
206
207    if let Some(filter) = reference {
208        plot_reference(&mut buf, filter, REF_LIGHT);
209    }
210
211    if let Some(ds) = downscale {
212        plot_scatter(&mut buf, &ds.points, SCATTER_BLUE);
213    }
214
215    if let Some(us) = upscale {
216        plot_connected(&mut buf, &us.points, LINE_RED);
217    }
218
219    draw_border(&mut buf, BORDER_GREEN);
220
221    ImgVec::new(buf, WIDTH, HEIGHT)
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn render_empty_graph() {
230        let img = render(None, None, None);
231        assert_eq!(img.width(), WIDTH);
232        assert_eq!(img.height(), HEIGHT);
233        // Check corners are border color
234        assert_eq!(img.buf()[0], BORDER_GREEN);
235    }
236
237    #[test]
238    fn coordinate_system() {
239        assert_eq!(xcoord(0.0), 230);
240        assert_eq!(xcoord(1.0), 320);
241        assert_eq!(ycoord(0.0), 220);
242        assert_eq!(ycoord(1.0), 20);
243    }
244}