mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-08 19:42:05 +00:00
M8: dots halfway between pixels stay put across pans, bands and patches
The board's dot grid puts each mark on the device pixel nearest its page position, measured from the view's top-left. At a zoom of 1.0625 a step of 24 is 25.5 pixels, so every second mark falls halfway between two pixels, and a view a whole number of pixels on could put it on the other one: `round` sends halves away from zero, so a mark left of the view's origin went left and one right of it went right, and the origin `(px + offset) / scale` reached the half with another rounding error. A pan moved those marks by a pixel (up to 123 levels off). Positions are now rounded to 1/65536 px and then half up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
53259cf50e
commit
e39e563408
2 files changed
+34
-1
No files matched your search
@@ -57,7 +57,7 @@ pub(crate) fn draw(pix: &mut Pixmap, view: &View, d: &Dots) {
|
||||
let strong = |k: i64| k.rem_euclid(major) == 0;
|
||||
// The device-pixel span of the mark on line `k` along an axis `len` pixels long from `o`.
|
||||
let span = |k: i64, o: f64, len: i64| {
|
||||
let a = ((k as f64 * d.step * s - o).round() as i64).saturating_sub(lead);
|
||||
let a = (nearest_px(k as f64 * d.step * s - o) as i64).saturating_sub(lead);
|
||||
(a.max(0), a.saturating_add(side).min(len))
|
||||
};
|
||||
let row = w as usize * 4;
|
||||
@@ -92,6 +92,15 @@ pub(crate) fn draw(pix: &mut Pixmap, view: &View, d: &Dots) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The device pixel nearest `p` pixels from the view's top-left. A mark halfway between two pixels
|
||||
/// goes to the right (or lower) one on either side of the origin, and `p` is first rounded to
|
||||
/// 1/65536 px: a view a whole number of pixels off (a pan, a band, a patch) reaches it with another
|
||||
/// rounding error, and would otherwise put the marks that fall halfway on the other pixel.
|
||||
fn nearest_px(p: f64) -> f64 {
|
||||
const STEPS: f64 = 65_536.0;
|
||||
((p * STEPS).round() / STEPS + 0.5).floor()
|
||||
}
|
||||
|
||||
/// Which line covers each device pixel along an axis: 0 none, 1 a faded one, 2 a strong one.
|
||||
type Cover = Vec<u8>;
|
||||
|
||||
|
||||
@@ -134,3 +134,27 @@ fn graph_paper_is_strong_every_major_line() {
|
||||
assert_eq!(at(x, -195), strong, "a strong line at {x}");
|
||||
}
|
||||
}
|
||||
|
||||
/// At a zoom of 1.0625, a step of 24 is 25.5 pixels: every second dot falls halfway between two
|
||||
/// pixels. A view a whole number of pixels on (a pan, a band, a patch) has to put it on the same
|
||||
/// one, on whichever side of that view's origin it is.
|
||||
#[test]
|
||||
fn dots_halfway_between_pixels_stay_put() {
|
||||
let (d, page) = page_with_square();
|
||||
for lines in [false, true] {
|
||||
let opts = Options { dots: Some(Dots { lines, ..dots() }), ..Default::default() };
|
||||
let s = 1.0625;
|
||||
let full = View { origin: (0.0, 0.0), scale: s, width: 400, height: 300 };
|
||||
let a = render_view_single(&d, page, &full, &opts).unwrap();
|
||||
for dx in 1..100u32 {
|
||||
let dy = (dx * 37) % 100;
|
||||
let v = View { origin: (f64::from(dx) / s, f64::from(dy) / s), scale: s, width: 250, height: 180 };
|
||||
let b = render_view_single(&d, page, &v, &opts).unwrap();
|
||||
for y in 0..180 {
|
||||
for x in 0..250 {
|
||||
assert_eq!(rgba(&b, x, y), rgba(&a, x + dx, y + dy), "lines {lines}, a pan by ({dx}, {dy}), at {x}, {y}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user