Skip to content

Scale CSS transform translation by viewport scale without double-scaling percentages - #623

Merged
nicoburns merged 1 commit into
mainfrom
devin/1786112632-scale-transform-translation
Aug 7, 2026
Merged

Scale CSS transform translation by viewport scale without double-scaling percentages#623
nicoburns merged 1 commit into
mainfrom
devin/1786112632-scale-transform-translation

Conversation

@nicoburns

@nicoburns nicoburns commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary

Fixes absolute-length transform translations being unscaled at hidpi (e.g. translate(100px) at hidpi_scale=2 moved only 100 device px = 50 CSS px), without introducing the percentage double-scaling that #622 does.

The stored node.transform() is treated as a device-pixel-space matrix everywhere (hit testing inverts it on device-px points; blitz-paint composes it after box_position * scale). Previously set_transform passed a reference box already scaled to device px, which made percentage translates/origins resolve correctly but left absolute lengths unscaled.

The fix resolves the transform entirely in CSS pixels (unscaled reference box), then conjugates it into device space — S · T · S⁻¹ — which scales only the translation components:

// set_transform: reference_box now uses unscaled layout size
resolve_2d_transform(s.get_box(), reference_box).map(|t| {
    let [m11, m12, m21, m22, m41, m42] = t.as_coeffs();
    Affine::new([m11, m12, m21, m22, m41 * scale, m42 * scale])
})

resolve_2d_transform itself is unchanged and stays pure CSS-px (a scale param was previously removed in 0e359fe because in-function scaling caused exactly this class of double-scaling bug).

Alternative to #622, which multiplies by scale after resolving percentages against the already-scaled reference box, double-scaling translate: 50%, translate(50%, 50%), and the default transform-origin: 50% 50% (displacing every rotate/scale transform at hidpi).

Tests (transform_viewport_scale.rs) cover px and percentage translates (property and function forms), unitless scale factors, and default/absolute transform-origins at scale 1 and 2.

Link to Devin session: https://dioxus.staging.devinenterprise.com/sessions/1d6103743b204723b3b711924b665735
Requested by: @nicoburns

WPT results

No changes in test results compared to main.

Generated by the WPT workflow.

@nicoburns nicoburns self-assigned this Aug 7, 2026
@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

Tested hidpi transform rendering headlessly at the rendered-pixel level (CPU image renderer, no window). A test page with 100×100 boxes using translate(100px,50px), translate: 50% 50%, rotate(90deg) (default origin), scale(0.5) (default origin), plus untransformed controls was rendered at viewport scale 1 and 2, and per-box pixel bounding boxes were measured programmatically.

Result: all boxes' rendered bboxes at scale 2 are exactly 2× their scale-1 bboxes (0px error). Percentage translates and the default transform-origin: 50% 50% are scaled exactly once (no #622-style double-scaling), and untransformed content is unaffected.

main vs fixed at scale 2
Left: main at scale 2 — red translate(100px,50px) box under-scaled (misses the black expected-position outline by 100/50 device px). Right: this PR — red box lands exactly on the expected 2× position.

Measured bounding boxes (device px)
box transform scale 1 scale 2 (this PR) scale 2 (main)
red translate(100px,50px) 120,250–220,350 240,500–440,700 140,450–340,650 ❌
green translate: 50% 50% 350,250–450,350 700,500–900,700 ✅ 700,500–900,700
magenta rotate(90deg) def. origin 20,400–120,500 40,800–240,1000 ✅ same
orange scale(0.5) def. origin 325,425–375,475 650,850–750,950 ✅ same
blue/cyan none (controls) 20,20–120,120 / 120,560–220,660 40,40–240,240 / 240,1120–440,1320 ✅ same

The transformed red box's scale-2 x-range (240–440) coincides exactly with the untransformed cyan control laid out at the same CSS x-coords, confirming transforms track layout.

scale 1 vs scale 2

cargo test -p blitz-tests --test transform_viewport_scale: 8/8 pass.

Written by Devin

@nicoburns
nicoburns merged commit a5e9b4e into main Aug 7, 2026
15 checks passed
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.

1 participant