Files
valw/src/editor/view.rs
T

168 lines
5.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Fitting the image (or its crop) into the canvas: image pixels ↔ egui
//! points.
use crate::editor::shape::P;
use crate::frame::PixelRect;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Fit {
/// Points per image pixel.
pub scale: f32,
/// Where `origin` is drawn, in points.
pub offset: P,
/// The image pixel at the top-left of what is shown (the crop's corner).
pub origin: P,
}
impl Fit {
/// Fits the whole image; see `with_crop`.
#[cfg(test)]
pub fn new(image: (u32, u32), area: (f32, f32, f32, f32), pixels_per_point: f32) -> Fit {
let whole = PixelRect {
x: 0,
y: 0,
width: image.0,
height: image.1,
};
Fit::with_crop(whole, area, pixels_per_point)
}
/// Fits `crop` into `area` (x, y, w, h in points), centred, never above
/// 100 % (one image pixel per physical pixel).
pub fn with_crop(crop: PixelRect, area: (f32, f32, f32, f32), pixels_per_point: f32) -> Fit {
let (iw, ih) = (crop.width as f32, crop.height as f32);
let (x, y, w, h) = area;
let scale = (w / iw).min(h / ih).min(1.0 / pixels_per_point);
// Centred, then onto a physical pixel boundary so that at 100 % each
// image pixel is exactly one screen pixel (no half-texel blur).
let snap = |v: f32| (v * pixels_per_point).floor() / pixels_per_point;
Fit {
scale,
offset: (
snap(x + (w - iw * scale) / 2.0),
snap(y + (h - ih * scale) / 2.0),
),
origin: (crop.x as f32, crop.y as f32),
}
}
pub fn to_screen(self, p: P) -> P {
(
self.offset.0 + (p.0 - self.origin.0) * self.scale,
self.offset.1 + (p.1 - self.origin.1) * self.scale,
)
}
pub fn to_image(self, p: P) -> P {
(
(p.0 - self.offset.0) / self.scale + self.origin.0,
(p.1 - self.offset.1) / self.scale + self.origin.1,
)
}
}
/// `p` moved into `rect`.
pub fn clamp(p: P, rect: PixelRect) -> P {
(
p.0.clamp(rect.x as f32, (rect.x + rect.width) as f32),
p.1.clamp(rect.y as f32, (rect.y + rect.height) as f32),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn small_images_are_not_upscaled() {
let f = Fit::new((400, 300), (0.0, 50.0, 1000.0, 800.0), 1.0);
assert_eq!(f.scale, 1.0);
assert_eq!(f.offset, (300.0, 300.0), "centred");
}
#[test]
fn large_images_fit_the_area() {
let f = Fit::new((1920, 1080), (0.0, 40.0, 960.0, 1000.0), 1.0);
assert_eq!(f.scale, 0.5);
assert_eq!(f.offset, (0.0, 40.0 + (1000.0 - 540.0) / 2.0));
}
#[test]
fn hundred_percent_means_one_image_pixel_per_physical_pixel() {
let f = Fit::new((400, 300), (0.0, 0.0, 1000.0, 800.0), 2.0);
assert_eq!(
f.scale, 0.5,
"0.5 points per image px at 2× = 1 physical px"
);
}
#[test]
fn the_image_sits_on_physical_pixels() {
// 1001 - 400 is odd: centring alone lands on half a pixel.
let f = Fit::new((400, 300), (0.0, 0.0, 1001.0, 801.0), 1.0);
assert_eq!(f.offset, (300.0, 250.0));
let g = Fit::new((400, 300), (0.0, 0.0, 1001.0, 801.0), 1.5);
let px = (g.offset.0 * 1.5, g.offset.1 * 1.5);
assert_eq!(px, (px.0.round(), px.1.round()), "{g:?}");
}
#[test]
fn round_trip() {
let f = Fit::new((1920, 1080), (10.0, 40.0, 1000.0, 700.0), 1.25);
for p in [(0.0, 0.0), (1919.0, 1079.0), (333.5, 777.25)] {
let q = f.to_image(f.to_screen(p));
assert!(
(q.0 - p.0).abs() < 1e-3 && (q.1 - p.1).abs() < 1e-3,
"{p:?} {q:?}"
);
}
}
#[test]
fn clamp_keeps_points_on_the_image() {
let r = PixelRect {
x: 0,
y: 0,
width: 100,
height: 80,
};
assert_eq!(clamp((-5.0, 50.0), r), (0.0, 50.0));
assert_eq!(clamp((150.0, 90.0), r), (100.0, 80.0));
}
#[test]
fn a_crop_fills_the_area() {
let crop = PixelRect {
x: 100,
y: 50,
width: 200,
height: 100,
};
let f = Fit::with_crop(crop, (0.0, 0.0, 400.0, 400.0), 1.0);
assert_eq!(f.scale, 1.0);
assert_eq!(
f.to_screen((100.0, 50.0)),
(100.0, 150.0),
"the crop's corner is centred"
);
}
#[test]
fn round_trip_with_a_crop() {
let crop = PixelRect {
x: 640,
y: 360,
width: 640,
height: 360,
};
let f = Fit::with_crop(crop, (0.0, 30.0, 500.0, 400.0), 1.0);
for p in [(640.0, 360.0), (1279.0, 719.0), (800.5, 400.25)] {
let q = f.to_image(f.to_screen(p));
assert!(
(q.0 - p.0).abs() < 1e-3 && (q.1 - p.1).abs() < 1e-3,
"{p:?} {q:?}"
);
}
}
}