mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
They acted inside a circle fitted to the bounds' shorter side, so on a wide or tall image only a central disc changed. Like Photoshop's, they now act inside the ellipse inscribed in the bounds (selection or layer), reaching all four edges; square bounds are unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c408c83a9f
commit
16774eea6f
+36
-19
@@ -28,32 +28,47 @@ pub(crate) fn centre(b: Rect) -> (f32, f32, f32) {
|
||||
(b.x0 as f32 + w / 2.0, b.y0 as f32 + h / 2.0, w.min(h) / 2.0)
|
||||
}
|
||||
|
||||
/// The bounds' centre and half width and height: Twirl, Pinch and Spherize act inside the ellipse
|
||||
/// inscribed in the bounds, as Photoshop's do, so on a wide or tall image the effect reaches all
|
||||
/// four edges instead of stopping at a circle fitted to the shorter side (#5643). A point maps to
|
||||
/// unit-circle coordinates `(u, v)` and back.
|
||||
fn ellipse(b: Rect) -> (f32, f32, f32, f32) {
|
||||
let (w, h) = (b.width() as f32, b.height() as f32);
|
||||
(b.x0 as f32 + w / 2.0, b.y0 as f32 + h / 2.0, w / 2.0, h / 2.0)
|
||||
}
|
||||
|
||||
pub(crate) fn twirl(src: &Image, out: Rect, ctx: &Ctx, angle: f32) -> Vec<f32> {
|
||||
let (cx, cy, rr) = centre(ctx.bounds);
|
||||
let (cx, cy, hx, hy) = ellipse(ctx.bounds);
|
||||
let a = angle.to_radians();
|
||||
remap(src, out, ctx, Edge::Repeat, |x, y| {
|
||||
let (dx, dy) = (x - cx, y - cy);
|
||||
let r = (dx * dx + dy * dy).sqrt();
|
||||
if r >= rr || rr <= 0.0 {
|
||||
if hx <= 0.0 || hy <= 0.0 {
|
||||
return (x, y);
|
||||
}
|
||||
let t = a * (1.0 - r / rr);
|
||||
let (u, v) = ((x - cx) / hx, (y - cy) / hy);
|
||||
let r = (u * u + v * v).sqrt();
|
||||
if r >= 1.0 {
|
||||
return (x, y);
|
||||
}
|
||||
let t = a * (1.0 - r);
|
||||
let (s, c) = t.sin_cos();
|
||||
(cx + dx * c - dy * s, cy + dx * s + dy * c)
|
||||
(cx + (u * c - v * s) * hx, cy + (u * s + v * c) * hy)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn pinch(src: &Image, out: Rect, ctx: &Ctx, amount: f32) -> Vec<f32> {
|
||||
let (cx, cy, rr) = centre(ctx.bounds);
|
||||
let (cx, cy, hx, hy) = ellipse(ctx.bounds);
|
||||
let a = (amount / 100.0).clamp(-1.0, 1.0);
|
||||
remap(src, out, ctx, Edge::Repeat, |x, y| {
|
||||
let (dx, dy) = (x - cx, y - cy);
|
||||
let r = (dx * dx + dy * dy).sqrt();
|
||||
if r >= rr || r <= 0.0 {
|
||||
if hx <= 0.0 || hy <= 0.0 {
|
||||
return (x, y);
|
||||
}
|
||||
let rs = rr * (r / rr).powf(1.0 - a * 0.5);
|
||||
(cx + dx * rs / r, cy + dy * rs / r)
|
||||
let (u, v) = ((x - cx) / hx, (y - cy) / hy);
|
||||
let r = (u * u + v * v).sqrt();
|
||||
if r >= 1.0 || r <= 0.0 {
|
||||
return (x, y);
|
||||
}
|
||||
let k = r.powf(1.0 - a * 0.5) / r;
|
||||
(cx + u * k * hx, cy + v * k * hy)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,20 +79,22 @@ fn sphere_1d(rn: f32, a: f32) -> f32 {
|
||||
}
|
||||
|
||||
pub(crate) fn spherize(src: &Image, out: Rect, ctx: &Ctx, amount: f32, mode: SpherizeMode) -> Vec<f32> {
|
||||
let b = ctx.bounds;
|
||||
let (cx, cy, rr) = centre(b);
|
||||
let (hx, hy) = (b.width() as f32 / 2.0, b.height() as f32 / 2.0);
|
||||
let (cx, cy, hx, hy) = ellipse(ctx.bounds);
|
||||
let a = (amount / 100.0).clamp(-1.0, 1.0);
|
||||
remap(src, out, ctx, Edge::Repeat, |x, y| {
|
||||
let (dx, dy) = (x - cx, y - cy);
|
||||
match mode {
|
||||
SpherizeMode::Normal => {
|
||||
let r = (dx * dx + dy * dy).sqrt();
|
||||
if r >= rr || r <= 0.0 {
|
||||
if hx <= 0.0 || hy <= 0.0 {
|
||||
return (x, y);
|
||||
}
|
||||
let rs = rr * sphere_1d(r / rr, a);
|
||||
(cx + dx * rs / r, cy + dy * rs / r)
|
||||
let (u, v) = (dx / hx, dy / hy);
|
||||
let r = (u * u + v * v).sqrt();
|
||||
if r >= 1.0 || r <= 0.0 {
|
||||
return (x, y);
|
||||
}
|
||||
let k = sphere_1d(r, a) / r;
|
||||
(cx + u * k * hx, cy + v * k * hy)
|
||||
}
|
||||
SpherizeMode::HorizontalOnly => {
|
||||
if dx.abs() >= hx {
|
||||
|
||||
@@ -414,6 +414,24 @@ fn twirl_keeps_centre_and_outside() {
|
||||
assert_ne!(out.pixel(20, 12), s.pixel(20, 12));
|
||||
}
|
||||
|
||||
/// #5643: on a wide image Twirl, Pinch and Spherize fill the ellipse inscribed in the bounds, as
|
||||
/// Photoshop's do, not just a circle fitted to the height: a point near the left and right edges
|
||||
/// on the middle row changes, the corners don't, and a square still uses its inscribed circle.
|
||||
#[test]
|
||||
fn round_distortions_fill_the_inscribed_ellipse() {
|
||||
let b = Rect::new(0, 0, 80, 40);
|
||||
let s = pattern(SampleType::F32, b);
|
||||
for p in [FilterParams::Twirl { angle: 180.0 }, FilterParams::Pinch { amount: 80.0 }, FilterParams::Spherize { amount: 80.0, mode: SpherizeMode::Normal }] {
|
||||
let out = apply(&s, &p, b, b, None);
|
||||
// 30 px left of centre on the middle row: outside the old 20 px circle, inside the ellipse.
|
||||
assert_ne!(out.pixel(10, 21), s.pixel(10, 21), "{p:?}");
|
||||
assert_ne!(out.pixel(69, 18), s.pixel(69, 18), "{p:?}");
|
||||
for (x, y) in [(0, 0), (79, 0), (0, 39), (79, 39)] {
|
||||
assert_eq!(out.pixel(x, y), s.pixel(x, y), "{p:?}: corner ({x}, {y}) is outside the ellipse");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motion_blur_spreads_along_angle_only() {
|
||||
let mut s = flat(SampleType::F32, R, [0.0, 0.0, 0.0, 1.0]);
|
||||
|
||||
Reference in New Issue
Block a user