T26b Media: crop and annotate
ImageCropper crops a picture by a box in shares of it: a press holds a corner, a side or the body (crop::grip_at), a drag through gpui's drag moves them (crop::dragged), kept inside the picture with a least side; with an aspect only the corners size it and the box keeps its shape; the arrows move it. The picture sits inset by half a handle, the focus border outside it, so handles on its edge stay whole and it keeps its shape. ImageUpload (T06e) takes pictures only, from a drop or the dialog (forms::taken, DropZone::kinds), decodes the file for its shape, and with no crop yet reports the largest centered box of its aspect once. The annotator draws boxes, arrows, lines and numbered pins in chart colors through forms::pen; Select chooses a mark, Delete removes it, and Cmd-Z and Shift-Cmd-Z undo and redo through forms::History. theme.media() holds their sizes.
This commit is contained in:
1 parent
4acedc60c0
commit
5cd7ee0ea8
21 files changed
+2305
-65
No files matched your search
@@ -0,0 +1,333 @@
|
||||
/// A crop of a picture: its box in shares of the picture's width and height, each from 0 to 1.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Crop {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub w: f32,
|
||||
pub h: f32,
|
||||
}
|
||||
|
||||
impl Crop {
|
||||
/// The whole picture.
|
||||
pub const WHOLE: Self = Self {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
w: 1.0,
|
||||
h: 1.0,
|
||||
};
|
||||
|
||||
/// The largest box of `aspect`, its width over height in pixels, inside this one and centered on it, on a picture of `ratio`.
|
||||
pub fn fitted(self, aspect: f32, ratio: f32) -> Self {
|
||||
fitted(self, aspect / ratio)
|
||||
}
|
||||
|
||||
/// Fails loud on a box with no size or one that leaves its picture.
|
||||
pub(crate) fn checked(self) -> Self {
|
||||
let inside =
|
||||
|start: f32, side: f32| start >= 0.0 && side > 0.0 && start + side <= 1.0 + 1e-4;
|
||||
assert!(
|
||||
inside(self.x, self.w) && inside(self.y, self.h),
|
||||
"a crop past its picture: {self:?}"
|
||||
);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The sides a drag moves: none moves the whole box, one an edge, two a corner.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct Grip {
|
||||
pub left: bool,
|
||||
pub right: bool,
|
||||
pub top: bool,
|
||||
pub bottom: bool,
|
||||
}
|
||||
|
||||
impl Grip {
|
||||
pub(crate) fn corner(self) -> bool {
|
||||
(self.left || self.right) && (self.top || self.bottom)
|
||||
}
|
||||
}
|
||||
|
||||
/// What a press at `at`, in shares of the picture, holds: a side or corner within `reach` shares across and down, else the body when inside, else nothing. A shaped crop sizes by corners only, so its edges hold the body.
|
||||
pub(crate) fn grip_at(crop: Crop, at: (f32, f32), reach: (f32, f32), shaped: bool) -> Option<Grip> {
|
||||
let (x2, y2) = (crop.x + crop.w, crop.y + crop.h);
|
||||
let near = |a: f32, b: f32, r: f32| (a - b).abs() <= r;
|
||||
let around = at.0 >= crop.x - reach.0
|
||||
&& at.0 <= x2 + reach.0
|
||||
&& at.1 >= crop.y - reach.1
|
||||
&& at.1 <= y2 + reach.1;
|
||||
let inside = at.0 >= crop.x && at.0 <= x2 && at.1 >= crop.y && at.1 <= y2;
|
||||
let (left, top) = (near(at.0, crop.x, reach.0), near(at.1, crop.y, reach.1));
|
||||
let grip = Grip {
|
||||
left,
|
||||
right: !left && near(at.0, x2, reach.0),
|
||||
top,
|
||||
bottom: !top && near(at.1, y2, reach.1),
|
||||
};
|
||||
let sizes = grip != Grip::default() && (grip.corner() || !shaped);
|
||||
match (around, sizes, inside) {
|
||||
(true, true, _) => Some(grip),
|
||||
(_, _, true) => Some(Grip::default()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The crop after `grip` moves by `by`, in shares, kept inside the picture with sides of at least `least`. With `shape`, a width over height in shares, only corners resize and the box keeps that shape, anchored at the opposite corner.
|
||||
pub(crate) fn dragged(
|
||||
crop: Crop,
|
||||
grip: Grip,
|
||||
by: (f32, f32),
|
||||
least: f32,
|
||||
shape: Option<f32>,
|
||||
) -> Crop {
|
||||
let (mut x1, mut y1, mut x2, mut y2) = (crop.x, crop.y, crop.x + crop.w, crop.y + crop.h);
|
||||
if grip == Grip::default() {
|
||||
let dx = by.0.clamp(-x1, 1.0 - x2);
|
||||
let dy = by.1.clamp(-y1, 1.0 - y2);
|
||||
return Crop {
|
||||
x: x1 + dx,
|
||||
y: y1 + dy,
|
||||
..crop
|
||||
};
|
||||
}
|
||||
if shape.is_some() && !grip.corner() {
|
||||
return crop;
|
||||
}
|
||||
if grip.left {
|
||||
x1 = (x1 + by.0).clamp(0.0, x2 - least);
|
||||
}
|
||||
if grip.right {
|
||||
x2 = (x2 + by.0).clamp(x1 + least, 1.0);
|
||||
}
|
||||
if grip.top {
|
||||
y1 = (y1 + by.1).clamp(0.0, y2 - least);
|
||||
}
|
||||
if grip.bottom {
|
||||
y2 = (y2 + by.1).clamp(y1 + least, 1.0);
|
||||
}
|
||||
if let Some(shape) = shape {
|
||||
let tall = if grip.top { y2 } else { 1.0 - y1 };
|
||||
let wide = if grip.left { x2 } else { 1.0 - x1 };
|
||||
let w = (x2 - x1)
|
||||
.max(least * shape.max(1.0))
|
||||
.min(tall * shape)
|
||||
.min(wide);
|
||||
let h = w / shape;
|
||||
if grip.left {
|
||||
x1 = x2 - w;
|
||||
} else {
|
||||
x2 = x1 + w;
|
||||
}
|
||||
if grip.top {
|
||||
y1 = y2 - h;
|
||||
} else {
|
||||
y2 = y1 + h;
|
||||
}
|
||||
}
|
||||
Crop {
|
||||
x: x1,
|
||||
y: y1,
|
||||
w: x2 - x1,
|
||||
h: y2 - y1,
|
||||
}
|
||||
}
|
||||
|
||||
/// The largest box of `shape`, a width over height in shares, inside `crop` and centered on it.
|
||||
pub(crate) fn fitted(crop: Crop, shape: f32) -> Crop {
|
||||
let (w, h) = if crop.w / crop.h > shape {
|
||||
(crop.h * shape, crop.h)
|
||||
} else {
|
||||
(crop.w, crop.w / shape)
|
||||
};
|
||||
Crop {
|
||||
x: crop.x + (crop.w - w) / 2.0,
|
||||
y: crop.y + (crop.h - h) / 2.0,
|
||||
w,
|
||||
h,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Crop, Grip, dragged, fitted, grip_at};
|
||||
|
||||
const HALF: Crop = Crop {
|
||||
x: 0.25,
|
||||
y: 0.25,
|
||||
w: 0.5,
|
||||
h: 0.5,
|
||||
};
|
||||
|
||||
fn near(a: Crop, b: Crop) -> bool {
|
||||
[(a.x, b.x), (a.y, b.y), (a.w, b.w), (a.h, b.h)]
|
||||
.iter()
|
||||
.all(|(a, b)| (a - b).abs() < 1e-5)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_body_moves_whole_and_stops_at_the_picture_edge() {
|
||||
let moved = dragged(HALF, Grip::default(), (0.1, -0.4), 0.05, None);
|
||||
assert!(
|
||||
near(
|
||||
moved,
|
||||
Crop {
|
||||
x: 0.35,
|
||||
y: 0.0,
|
||||
..HALF
|
||||
}
|
||||
),
|
||||
"{moved:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_edge_or_corner_moves_its_sides_and_keeps_the_least_size() {
|
||||
let right = Grip {
|
||||
right: true,
|
||||
..Grip::default()
|
||||
};
|
||||
assert!(near(
|
||||
dragged(HALF, right, (0.1, 0.3), 0.05, None),
|
||||
Crop { w: 0.6, ..HALF }
|
||||
));
|
||||
assert!(near(
|
||||
dragged(HALF, right, (-0.9, 0.0), 0.05, None),
|
||||
Crop { w: 0.05, ..HALF }
|
||||
));
|
||||
let top_left = Grip {
|
||||
left: true,
|
||||
top: true,
|
||||
..Grip::default()
|
||||
};
|
||||
let grown = dragged(HALF, top_left, (-0.5, -0.1), 0.05, None);
|
||||
assert!(
|
||||
near(
|
||||
grown,
|
||||
Crop {
|
||||
x: 0.0,
|
||||
y: 0.15,
|
||||
w: 0.75,
|
||||
h: 0.6
|
||||
}
|
||||
),
|
||||
"{grown:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_shaped_crop_resizes_by_corners_only_and_keeps_its_shape_inside_the_picture() {
|
||||
let bottom_right = Grip {
|
||||
right: true,
|
||||
bottom: true,
|
||||
..Grip::default()
|
||||
};
|
||||
let square = dragged(HALF, bottom_right, (0.1, 0.0), 0.05, Some(1.0));
|
||||
assert!(
|
||||
near(
|
||||
square,
|
||||
Crop {
|
||||
w: 0.6,
|
||||
h: 0.6,
|
||||
..HALF
|
||||
}
|
||||
),
|
||||
"{square:?}"
|
||||
);
|
||||
let stopped = dragged(HALF, bottom_right, (0.25, 0.0), 0.05, Some(2.0));
|
||||
assert!(
|
||||
near(
|
||||
stopped,
|
||||
Crop {
|
||||
w: 0.75,
|
||||
h: 0.375,
|
||||
..HALF
|
||||
}
|
||||
),
|
||||
"{stopped:?}"
|
||||
);
|
||||
let edge = Grip {
|
||||
top: true,
|
||||
..Grip::default()
|
||||
};
|
||||
assert_eq!(dragged(HALF, edge, (0.0, -0.1), 0.05, Some(1.0)), HALF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_shape_takes_the_largest_centered_box_inside() {
|
||||
let wide = fitted(HALF, 2.0);
|
||||
assert!(
|
||||
near(
|
||||
wide,
|
||||
Crop {
|
||||
x: 0.25,
|
||||
y: 0.375,
|
||||
w: 0.5,
|
||||
h: 0.25
|
||||
}
|
||||
),
|
||||
"{wide:?}"
|
||||
);
|
||||
let tall = fitted(Crop::WHOLE, 0.5);
|
||||
assert!(
|
||||
near(
|
||||
tall,
|
||||
Crop {
|
||||
x: 0.25,
|
||||
y: 0.0,
|
||||
w: 0.5,
|
||||
h: 1.0
|
||||
}
|
||||
),
|
||||
"{tall:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_press_holds_the_nearest_corner_or_side_else_the_body_else_nothing() {
|
||||
let reach = (0.02, 0.02);
|
||||
let corner = grip_at(HALF, (0.26, 0.74), reach, false);
|
||||
assert_eq!(
|
||||
corner,
|
||||
Some(Grip {
|
||||
left: true,
|
||||
bottom: true,
|
||||
..Grip::default()
|
||||
})
|
||||
);
|
||||
let side = grip_at(HALF, (0.5, 0.24), reach, false);
|
||||
assert_eq!(
|
||||
side,
|
||||
Some(Grip {
|
||||
top: true,
|
||||
..Grip::default()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
grip_at(HALF, (0.5, 0.5), reach, false),
|
||||
Some(Grip::default())
|
||||
);
|
||||
assert_eq!(grip_at(HALF, (0.1, 0.5), reach, false), None);
|
||||
assert_eq!(
|
||||
grip_at(HALF, (0.5, 0.26), reach, true),
|
||||
Some(Grip::default()),
|
||||
"a shaped crop's edge holds its body"
|
||||
);
|
||||
assert_eq!(
|
||||
grip_at(HALF, (0.5, 0.24), reach, true),
|
||||
None,
|
||||
"and just outside it holds nothing"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "a crop past its picture")]
|
||||
fn a_crop_past_its_picture_fails_loud() {
|
||||
Crop {
|
||||
x: 0.8,
|
||||
y: 0.0,
|
||||
w: 0.5,
|
||||
h: 0.5,
|
||||
}
|
||||
.checked();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user