Fix panning going dead when zoomed in on a portrait photo

Reported: fully zoomed in, couldn't pan left/right at all — stuck
dead-center.

My previous fix pinned the zoom's transform-origin at a fixed 50% 50%
(the frame's own center) specifically to fix a *different* bug (the head
of a portrait being unreachable). That traded one bug for another: with a
fixed center, object-position's own pan formula only has room to move in
whichever axis has overflow at zoom=1. For a portrait photo in this
landscape frame, width is the *exact*-fit axis at zoom=1 — literally zero
horizontal slack — so there was nothing for a fixed-center zoom to
magnify away from, and horizontal dragging computed a division by
(essentially) zero, guarded down to a hard no-op.

The actual fix: transform-origin should match object-position (X% Y%),
not stay fixed at center. Re-derived from there — with a matching origin,
box position (X%, Y%) always shows exactly natural-image point
(X/100*naturalW, Y/100*naturalH), at any zoom, in either axis, with no
zero-slack case at all. Rewrote the drag-sensitivity and reference-
rectangle math to match this (both simplify to plain linear formulas).

Verified in the standalone test harness with the same portrait test image
that reproduces the zero-slack case: dragging now works at any zoom level
in both axes, and reaching an image edge (the original bug) still works
too — tested both scenarios explicitly before deploying, since the first
fix silently broke the second.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 11:17:10 -07:00
parent c20078890e
commit f79d56a82d
2 changed files with 43 additions and 38 deletions
+9 -6
View File
@@ -19,12 +19,15 @@ function photo_crop_style(array $row): string {
$y = max(0, min(100, $y));
$zoom = max(1, min(3, $zoom));
// transform-origin is deliberately left at its CSS default (50% 50% —
// the box's own center), not tied to $x/$y: object-position places the
// unzoomed crop, and the scale transform then magnifies around whatever
// is currently centered, matching the crop editor exactly.
// transform-origin matches object-position — see photo-crop.js's file
// header for why this (rather than a fixed 50% 50%) is required: a
// fixed center has zero pan room on whichever axis has no overflow at
// zoom=1 (common for a portrait photo in a landscape box), so panning
// that axis goes dead the moment you zoom in. Matching the origin to
// (X%, Y%) pins that natural-image point to the same box position at
// any zoom, with no such zero-slack case in either axis.
return sprintf(
'object-position:%.2f%% %.2f%%;transform:scale(%.3f);',
$x, $y, $zoom
'object-position:%.2f%% %.2f%%;transform:scale(%.3f);transform-origin:%.2f%% %.2f%%;',
$x, $y, $zoom, $x, $y
);
}