Skip to content

fix(painting): build the ring without a path-ops difference - #3

Merged
rehmatsg merged 1 commit into
mainfrom
fix/web-ring-path-difference
Sep 2, 2026
Merged

fix(painting): build the ring without a path-ops difference#3
rehmatsg merged 1 commit into
mainfrom
fix/web-ring-path-difference

Conversation

@rehmatsg

@rehmatsg rehmatsg commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

The bug

On the Pages gallery the beam does not hug the border — it floods the whole card face. iOS, macOS and the goldens are all correct, so this reads as a web-only rendering bug rather than a renderer quality difference.

BeamRingGeometry builds the ring by subtracting the content box from the outer shape:

Path.combine(PathOperation.difference, outer, inner)

PathOperation.difference silently returns the first operand on Flutter web (CanvasKit). So ring and halo(reach) come back as the filled shape, and every layer meant to be confined to the border — stroke, bloom, the pulse glows — paints across the entire child.

Evidence

A minimal probe, no package code, same widget tree rendered both ways:

probe Skia (test renderer) CanvasKit (web)
combine(difference, rrect, rrect) ring solid fill
combine(difference, superellipse, superellipse) ring solid fill
combine(difference, hand-built arc path ×2) ring solid fill
combine(intersect, …) correct correct
single path, PathFillType.evenOdd ring ring

intersect is fine, so it is difference specifically, and the path construction style is irrelevant.

The fix

A _subtract helper that expresses the ring as one even-odd path instead of a path-ops difference. For the built-in shapes inner is always simple and nested inside outer, so even-odd describes exactly the same region — no path-ops pass, identical output on every backend.

Verification

  • dart format, flutter analyze clean.
  • flutter testall 1197 pass, every golden unchanged, so Skia/Impeller output is pixel-identical.
  • Rebuilt example for web and screenshotted it against the same gallery rendered through the Skia test renderer at identical size: they now match.

Known gap

A custom BeamPathContour can be self-intersecting (the star contour in surface_golden_test.dart is), where even-odd and non-zero disagree and the shortcut would punch a hole of its own. Those keep the path-ops route, so custom contours still render wrong on web. Fixing that needs an inverse clip via a dstOut mask layer, which costs a saveLayer and would break the budget table in save_layer_budget_test.dart — left out of this change deliberately.

No regression test is added: the bug is renderer-specific and invisible to the Skia-backed suite. Catching it would need browser tests (flutter test --platform chrome), which the repo does not set up yet.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Fixes a Flutter web rendering bug where PathOperation.difference returned the filled outer shape instead of a border ring. Built-in rings and halos now use an even-odd path, keeping strokes and glows on the border while preserving existing Skia and Impeller output.

Scope

  • Custom self-intersecting BeamPathContour values still use path subtraction and remain affected on web.
  • flutter analyze passes, and all 1,197 tests pass with unchanged goldens.

Written for commit bc24105. Summary will update on new commits.

Review in cubic

`Path.combine(PathOperation.difference, ...)` is unreliable on Flutter
web (CanvasKit): it silently returns the first operand. `ring` and
`halo` were built with it, so on web both came back as the filled shape
instead of a ring, and every ring-confined layer — stroke, bloom, the
pulse glows — washed across the whole child instead of hugging the
border. The effect was plainly visible on the Pages gallery: card
interiors flooded with colour where iOS and the goldens show a thin
edge glow.

For the built-in shapes `inner` is always simple and nested inside
`outer`, so the even-odd fill of the two contours in one path describes
exactly the same region with no path-ops pass, and renders identically
on every backend — every golden passes unchanged.

A custom `BeamPathContour` can be self-intersecting (the star contour
is), where even-odd and non-zero disagree and the shortcut would punch
a hole of its own, so those keep the path-ops route. Custom contours
therefore still render wrong on web; that needs an inverse clip and
costs a `saveLayer`, so it is left alone here.

Verified by rebuilding `example` for web and comparing against the same
gallery rendered through the Skia test renderer at identical size.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rehmatsg
rehmatsg merged commit 85be4f8 into main Sep 2, 2026
5 checks passed
@rehmatsg
rehmatsg deleted the fix/web-ring-path-difference branch September 2, 2026 19:10
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