Skip to content

Fix profile picture crop drift for off-centre crops#143

Merged
vincentmakes merged 1 commit into
mainfrom
claude/fix-crop-zoom-mismatch-t2b6Z
Apr 22, 2026
Merged

Fix profile picture crop drift for off-centre crops#143
vincentmakes merged 1 commit into
mainfrom
claude/fix-crop-zoom-mismatch-t2b6Z

Conversation

@vincentmakes

Copy link
Copy Markdown
Owner

Description

Fixes a geometry bug in applyProfilePictureCrop that caused rendered profile pictures to drift and appear "more zoomed in" than the cropper box when crops were off-centre.

The previous implementation combined object-position: P% Q% with a matching transform-origin: P% Q% and transform: scale(zoom). This is only geometrically correct for centred crops. For off-centre crops, object-position anchors the chosen image point at the element's P% position (not the container's centre), and the matching transform-origin keeps it there through the scale, causing the rendered crop to sit off-axis, get clipped on one side, and show image content from outside the crop box.

The fix: Replace with the correct model using translate(tx%, ty%) scale(z) where:

  • tx% = -zoom·(W/L)·offsetX
  • ty% = -zoom·(H/L)·offsetY
  • L = min(W, H)

This makes the crop centre land exactly at the container centre for any aspect ratio. The helper now defers via a one-shot load listener when naturalWidth/naturalHeight aren't yet available, using addEventListener (not .onload) to avoid clobbering caller-wired load handlers.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Checklist

Required for all code changes

  • I have tested my changes locally (npm test passes)
  • Version has been bumped in all 3 files (package.json, package-lock.json, version.json)
  • CHANGELOG.md has been updated with a new entry under the correct version

If adding or changing user-visible strings

  • N/A — no user-visible strings changed

If documentation-only change

  • N/A — code changes included

Test Plan

Added comprehensive unit tests in tests/frontend.test.js covering:

  • Null/invalid crop clears styles
  • Identity crop (offsets 0, zoom 1) clears styles
  • Centred crop with zoom > 1 is a pure scale
  • Landscape off-centre crop uses W/L factor on tx
  • Portrait off-centre crop uses H/L factor on ty
  • Square image off-centre on both axes
  • Deferred application when image not yet loaded, applies on load event

All tests verify the transform formula against the documented geometry contract.

https://claude.ai/code/session_01V6g4sngEajmbmKFm5GCsNg

applyProfilePictureCrop was combining object-position: P% Q% with a
matching transform-origin and scale(). That only rendered correctly for
centred crops — for any offset, object-position aligned the crop centre
with the element's P% point (not its centre), and the matching
transform-origin kept it pinned there under scale, so the rendered
picture appeared shifted and "more zoomed in" compared to the cropper
box, with non-crop image content bleeding in on the opposite edge.

Replace with translate+scale around the element centre:
  tx% = -zoom · (W / L) · offsetX
  ty% = -zoom · (H / L) · offsetY
where L = min(W, H). This puts the crop centre exactly at the container
centre for any aspect ratio. The helper now needs naturalWidth/Height,
so it defers via a one-shot addEventListener('load', …) when the image
isn't loaded yet — using addEventListener rather than .onload so callers
that wire their own pic.onload display toggle aren't clobbered.

Stored crop encoding (offsetX, offsetY, zoom) is unchanged; existing
data renders correctly without migration.
@vincentmakes vincentmakes merged commit 4fb3ce2 into main Apr 22, 2026
3 checks passed
@vincentmakes vincentmakes deleted the claude/fix-crop-zoom-mismatch-t2b6Z branch April 22, 2026 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants