Skip to content

css+paint: implement transform:rotate() and the standalone rotate property - #245

Merged
tannevaled merged 1 commit into
mainfrom
transform-rotate
Sep 28, 2026
Merged

tannevaled merged 1 commit into
mainfrom
transform-rotate

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Summary

  • FIDELITY.md already flagged rotate/scale/skew/matrix as unsupported transform functions alongside the already-implemented translate. Read the actual CSS Transforms mechanics before assuming scope: rotate alone (never mixed with the others) is tractable because this engine's existing filter/opacity/mask-image group-buffer machinery (render a box's subtree offscreen, transform it, composite back) is architecturally the exact shape a "rotate the pixels" implementation needs, and the pinned go-images dependency already ships an arbitrary-angle, bilinear, resize-to-fit Rotate function.
  • Verified the sign convention BEFORE writing paint code: go-images' Rotate is counter-clockwise-positive (matching scikit-image); CSS is clockwise-positive. Negated the angle and proved it with an isolated four-colour-border repro, checking the exact predicted edge mapping before trusting the implementation.
  • The real confirmed trigger (tailwindcss.com's rotate-(--angle)/.rotate-90 utilities) turned out to use CSS Transforms Level 2's newer, INDEPENDENT rotate property, not the classic transform: rotate() function this engine's own pre-existing doc comment anticipated — modern Tailwind (v4) compiles rotate-* utilities to the standalone property, confirmed directly from the site's own compiled CSS bundle. Caught by re-rendering the real page and seeing the diagonal "P3 colors" swatch labels still perfectly horizontal, not by trusting passing unit tests alone. Added the standalone property as its own case, reusing the same RotateDeg field and paint path; kept the transform: rotate() parser too, as the same now-evidenced capability under CSS's older syntax.
  • Scoped independently of the existing filter/opacity/mask-image group pass — no confirmed trigger combines rotate with any of them, so paintBox only takes the new rotate path when none of those apply (disclosed in RotateDeg's own doc comment).
  • Updated a pre-existing test (TestApplyTransformTranslate) whose own expectation — transform:rotate(10deg) is a complete no-op — was written when rotate was genuinely unrecognised, and moved it into its own new TestApplyTransformRotate.

Test plan

  • go build ./..., go vet ./..., go test ./... all green
  • Coverage floors held (paint stayed at 100.0% on the first attempt)
  • 26 new tests: css/props_test.go (TestApplyTransformRotate, TestApplyStandaloneRotateProperty — every value/unit, rotateZ, cascade replacement, none, no-op-on-invalid) and paint/rotate_test.go (hand-built box trees mirroring the existing backdrop_filter_test.go convention: 90° clockwise verified by exact edge-colour mapping, negative-angle mirror, zero-rotation no-op, outside-clip no-op, and the disclosed non-combination with opacity)
  • Git-stash-confirmed: reverting the production files alone fails to even compile in both css and paint
  • Verified directly on the real live page: tailwindcss.com's "P3 colors" diagonal swatch labels now render tilted -45° and reading correctly uphill left-to-right
  • Full 10-page bench comparison: every page's webengine-side render is byte-identical to round 144's committed one except three, each individually confirmed as already-documented noise (GitHub's commit count ticking, HN's rotating front page, tailwindcss.com's sponsor-logo carousel) — not this round's own effect. A genuinely new, unrelated finding surfaced while investigating an alarming-looking SSIM drop on example.com (a brand-new setInterval-driven animation this engine can't simulate, confirmed via git-stash to be pre-existing, not a regression) — documented as a new Known-gaps entry rather than silently absorbed as noise

🤖 Generated with Claude Code

…perty

FIDELITY.md already flagged rotate/scale/skew/matrix as unsupported
transform functions alongside the already-implemented translate. Read
the actual spec mechanics before assuming scope: rotate alone (never
mixed with the others) is tractable because this engine's existing
filter/opacity/mask-image group-buffer machinery (render a box's
subtree offscreen, transform it, composite back) is architecturally
the exact shape a "rotate the pixels" implementation needs, and the
pinned go-images dependency already ships an arbitrary-angle, bilinear,
resize-to-fit Rotate function.

Verified the sign convention before writing paint code: go-images'
Rotate is counter-clockwise-positive (matching scikit-image); CSS is
clockwise-positive. Negated the angle and proved it with an isolated
four-colour-border repro, checking the exact predicted edge mapping.

The real confirmed trigger (tailwindcss.com's rotate-(--angle)/rotate-90
utilities) turned out to use CSS Transforms Level 2's newer, independent
`rotate` property, not the classic `transform: rotate()` function this
engine's own pre-existing doc comment anticipated - modern Tailwind (v4)
compiles rotate-* utilities to the standalone property, confirmed
directly from the site's own compiled CSS. Caught by re-rendering the
real page and seeing the diagonal "P3 colors" labels still horizontal,
not by trusting passing unit tests alone. Added the standalone property
as its own case, reusing the same RotateDeg field and paint path; kept
the transform() parser too as the same now-evidenced capability under
CSS's older syntax.

Scoped independently of the existing group pass (rotate needs the
box's own exact rectangle and a growing buffer; the group pass keeps
full canvas width and only crops vertically) - no confirmed trigger
combines rotate with filter/opacity/mask-image, so paintBox only takes
the rotate path when none of those apply.

Updated a pre-existing test whose own expectation (transform:rotate(10deg)
is a no-op) was written when rotate was genuinely unrecognised, and is
no longer accurate now that it is a real, recognised value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 4838978 into main Sep 28, 2026
7 checks passed
@tannevaled
tannevaled deleted the transform-rotate branch September 28, 2026 18:35
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