css+paint: implement transform:rotate() and the standalone rotate property - #245
Merged
Merged
Conversation
…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>
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
rotate/scale/skew/matrixas unsupportedtransformfunctions alongside the already-implementedtranslate. Read the actual CSS Transforms mechanics before assuming scope:rotatealone (never mixed with the others) is tractable because this engine's existingfilter/opacity/mask-imagegroup-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 pinnedgo-imagesdependency already ships an arbitrary-angle, bilinear, resize-to-fitRotatefunction.go-images'Rotateis 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.rotate-(--angle)/.rotate-90utilities) turned out to use CSS Transforms Level 2's newer, INDEPENDENTrotateproperty, not the classictransform: rotate()function this engine's own pre-existing doc comment anticipated — modern Tailwind (v4) compilesrotate-*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 owncase, reusing the sameRotateDegfield and paint path; kept thetransform: rotate()parser too, as the same now-evidenced capability under CSS's older syntax.paintBoxonly takes the new rotate path when none of those apply (disclosed inRotateDeg's own doc comment).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 newTestApplyTransformRotate.Test plan
go build ./...,go vet ./...,go test ./...all greenpaintstayed at 100.0% on the first attempt)css/props_test.go(TestApplyTransformRotate,TestApplyStandaloneRotateProperty— every value/unit,rotateZ, cascade replacement,none, no-op-on-invalid) andpaint/rotate_test.go(hand-built box trees mirroring the existingbackdrop_filter_test.goconvention: 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)cssandpaintsetInterval-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