Fix canvas stretching on HiDPI screens and window resize - #2
Merged
Conversation
A 4032x3024 photo came out visibly too wide - 85% too wide on a 1920x1080 screen at 2x, and wrong by some amount on every display with a pixel ratio above 1. The engine was never at fault: `ops::fit_within` returns an aspect-correct frame, and the preview it hands back has the right shape. The layer that got it wrong was the layout. `#preview-canvas` is a flex item of `.canvas-frame`, and its two axes were being sized by two mechanisms that never spoke to each other: the width by flex-shrink against `max-width: 100%`, the height by the default `align-items: stretch` against the frame's line. Nothing tied them to the canvas's own aspect ratio. That only stayed hidden at a pixel ratio of 1, where the backing store happens to equal the CSS box, so both axes land on the same scale by coincidence. Above 1 the backing store is measured in device pixels and the CSS box in CSS pixels, the coincidence breaks, and the picture stretches. So the canvas is now given an explicit display size instead: - `previewBox()` reports the box in both units - CSS pixels for layout, device pixels for the render request - rather than conflating them. It also measures with `getBoundingClientRect()`, since `clientHeight` rounds to whole pixels while the padding it subtracts does not, which could overstate the box by most of a pixel. - `layoutCanvas()` fits the frame that actually arrived into the box that actually exists and sets `style.width`/`style.height` from it. Fitting the received frame rather than the requested one is self-correcting: a resize mid-render costs a little sharpness, never a stretched image. - The window `resize` handler re-fits immediately, then re-renders on the existing debounce to recover the resolution the new box is worth. `.canvas-frame` loses its `max-width`/`max-height` so it wraps the canvas exactly. The crop overlay is stretched across that box with `inset: 0`, so slack between the two put the selection somewhere the image was not - the crop tool was misaligned by the same amount the image was stretched. Verified in Chromium across 70 viewport/pixel-ratio/image-size combinations: worst aspect-ratio error drops from 100.6% to 0.21% (integer rounding of the frame's device dimensions, sub-pixel on screen), with the overlay landing on the image to within 0.000px and no case overflowing the viewport. Also checked that dragging the window through a range of shapes keeps the image proportioned with no re-render in between. Engine suite still 38/38. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A8jTvVudtvCzWAqgG2MkJf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR fixes canvas rendering issues where images appeared stretched on HiDPI screens and during window resizing. The root cause was conflating CSS pixels (layout space) with device pixels (rendering resolution), and relying on CSS
max-width/max-heightwhich size axes independently and destroy aspect ratios.Key Changes
Enhanced
previewBox()function: Now returns both CSS dimensions and device pixel dimensions separately, with detailed documentation explaining the distinction. Also added border width to padding calculations to accurately measure the available space.New
fitWithin()utility: Implements aspect-ratio-preserving scaling (mirrors the engine'sops::fit_within), ensuring layout and render math agree on what "fits".New
layoutCanvas()function: Explicitly sets canvas display size via JavaScript instead of relying on CSS flex andmax-width/max-height. This prevents the independent axis sizing that was causing stretching. Includes self-correction for window resize timing mismatches.Updated canvas styling: Removed
max-width,max-height, andwidth: auto/height: autofrom#preview-canvas. Addedflex: 0 0 autoandalign-self: flex-startto prevent flex from independently sizing axes. Updated.canvas-frameto removemax-width/max-heightconstraints.Integrated layout into frame rendering:
layoutCanvas()is called inonFrame()after drawing and in the windowresizeevent handler to keep the canvas properly fitted to its container.Implementation Details
The fix addresses a critical issue where on HiDPI screens (2x pixel ratio), the canvas backing store (device pixels) was being laid out using CSS pixel constraints, causing misalignment. For example, a 4032x3024 photo on a 1920x1080 screen at 2x was being drawn 85% too wide.
By explicitly calculating and applying CSS dimensions to the canvas element, the image now maintains its aspect ratio and fits properly within the viewport, with the crop overlay correctly aligned.
https://claude.ai/code/session_01A8jTvVudtvCzWAqgG2MkJf