From a4c3cb364499aa97ce0142cb2390d06c0828aafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Mon, 31 Aug 2026 00:08:33 +0100 Subject: [PATCH] zoom steps by a flat 0.1x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It multiplied by 1.25, which compounds: the same button was a 0.25x step at fit and a 1.75x step at 7x, so the first press did almost nothing and the last threw the picture across the frame. Added rather than multiplied, so every press is the same amount of change — which is what somebody nudging a crop into place is asking for, and it makes the number beside the buttons predictable, which a factor never is. Rounded to a tenth, because floating point turns 1 + 0.1 + 0.1 into 1.2000000000000002 and the label shows it. Co-Authored-By: Claude Opus 5 (1M context) --- web/MediaField.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/web/MediaField.tsx b/web/MediaField.tsx index 901fa37..5739ee3 100644 --- a/web/MediaField.tsx +++ b/web/MediaField.tsx @@ -187,12 +187,22 @@ export const MediaField = ({ to. That is also the behaviour somebody expects from a `+`: the middle of what I am looking at stays where it is. */ - const zoomBy = (factor: number) => { + const zoomBy = (step: number) => { if (!current || !natural || !onCropChange) return; const base = centredCrop(natural.w / natural.h, slotAspect); - // Fit is the floor — there is no more picture — and 8x is where a rendition runs out of pixels. - const zoom = Math.min(8, Math.max(1, (base.w / current.w) * factor)); + /* + A tenth per press, ADDED rather than multiplied. + + A factor compounds: the same button is a 0.25x step at fit and a 1.75x step at 7x, so the first + press does almost nothing and the last one throws the picture across the frame. A fixed step is + the same amount of change every time, which is what somebody nudging a crop into place is asking + for — and it makes the number beside the buttons predictable, which a factor never is. + + Fit is the floor — there is no more picture — and 8x is where a rendition runs out of pixels. + Rounded, because floating point turns 1 + 0.1 + 0.1 into 1.2000000000000002 and the label shows it. + */ + const zoom = Math.round(Math.min(8, Math.max(1, (base.w / current.w) + step)) * 10) / 10; const w = base.w / zoom; const h = base.h / zoom; @@ -348,7 +358,7 @@ export const MediaField = ({ size="sm" variant="ghost" className="size-6 p-0" - onClick={() => zoomBy(1 / 1.25)} + onClick={() => zoomBy(-0.1)} disabled={zoomLevel <= 1.001} title="Zoom out" > @@ -363,7 +373,7 @@ export const MediaField = ({ size="sm" variant="ghost" className="size-6 p-0" - onClick={() => zoomBy(1.25)} + onClick={() => zoomBy(0.1)} disabled={zoomLevel >= 7.99} title="Zoom in" >