Skip to content

fix(paint): wire style.clip-path into the paint pass - #348

Merged
LeadcodeDev merged 1 commit into
mainfrom
fix/clip-path-paint
Sep 26, 2026
Merged

LeadcodeDev merged 1 commit into
mainfrom
fix/clip-path-paint

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Closes #339.

The fault

CssStyle::clip_path was declared, exported in the JSON Schema with seven
variants and their documentation, and read by nothing. A scenario that set it
validated, rendered, and looked exactly as though the property were absent. The
three clip_path hits in the tree were all Skia's own Canvas::clip_path,
unrelated to the CSS property. Present since 676e49e.

What now works

kind Percentages resolve against
inset (+ optional radius) verticals on the height, horizontals on the width
circle sqrt(w² + h²) / √2 — the CSS reference
ellipse rx on the width, ry on the height
polygon x on the width, y on the height
path SVG data, offset by the box's own origin
none indistinguishable from the property being absent

circle uses the spec's reference length rather than a convenient min(w, h), so
a 50% circle on a non-square box matches what a browser draws.

The placement is the opposite of overflow: hidden, on purpose

clip-path masks the element itself — background, border and outer shadow
included — so the clip opens before the outset shadows and closes after the
inset ones.

overflow: hidden does the reverse here: it opens after the decorations,
deliberately sparing the node's own outset shadow, which
overflow_hidden_does_not_clip_own_outset_box_shadow already pins. Both
behaviours are now covered, so neither can drift into the other.

node-path is left out, but not left silent

Reading another node's geometry needs an id → resolved path lookup the paint
pass does not have — PaintContext has no node-id map, and #328's dependency
graph resolves scalar properties, not path geometry. #339 separates that variant
from the other five for exactly this reason.

It would have been easy to leave it returning None. That is the defect this PR
fixes, so instead it writes to stderr and names the workaround (repeat the same
kind: path on both nodes). Tracked separately for a follow-up.

Verification

Seven tests, six of which fail against the unwired property — including the
sample from the issue:

inset right:120 must remove x=330 — this is the sample from the issue
that stayed painted while clip-path was read by nothing

The seventh (clip_path_none_paints_exactly_as_no_clip_path_at_all) passes both
ways, which is correct: none means no clip.

End to end on a rendered still, a 400×400 violet square with
clip-path: inset{right: 120}:

sample before after
x=200 rgb(139, 92, 246) rgb(139, 92, 246)
x=330 rgb(139, 92, 246) rgb(0, 0, 0)

cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings
and cargo test --workspace (1521) all clean.

Why it read as working, and the doc fix for that

The issue makes the point precisely: the property's only mention anywhere in the
skills was a line in timeline-sequencing.md saying it cannot be animated —
which implies it can be set — while the exported schema advertised it globally
with all its variants. That is what a generator reads.

New rules/clip-path.md covers the six shapes, how each one's percentages
resolve, the local-coordinates trap on path (a path copied from an editor whose
viewBox does not start at the origin arrives offset), the eight-point polygon
for the chamfered frame #344 asks for, and that node-path is declared but inert.
Linked from SKILL.md's style table and its rules index.

Note on comments

Written comment-free, per the codebase-wide rule being applied in #345.

`CssStyle::clip_path` was declared, exported in the JSON Schema with seven
variants and their documentation, and read by nothing. A scenario that set it
validated, rendered, and looked exactly as though the property were absent. The
only three `clip_path` call sites in the tree were Skia's own `Canvas::clip_path`,
unrelated to the CSS property. Present since 676e49e.

Five variants now build a Skia path and clip with it: `Inset` (with optional
corner radius), `Circle`, `Ellipse`, `Polygon` and `Path`. `None` stays
indistinguishable from the property being absent.

Percentages resolve against the border box, per axis: `inset`'s verticals on the
height and horizontals on the width, `ellipse`'s `rx` on the width and `ry` on the
height, `polygon`'s `x`/`y` likewise. `circle`'s radius uses the CSS reference,
`sqrt(w² + h²) / √2`, rather than a convenient `min(w, h)` — a 50% circle on a
non-square box should match what a browser draws. `path`'s data is offset by the
box's own origin, so `M0 0` is the element's corner and not the video's.

The clip opens before the outset shadows and closes after the inset ones, so the
element's whole rendering is masked — background, border and shadow included.
That is CSS `clip-path`, and it is deliberately the opposite of the placement
`overflow: hidden` uses here: that one opens after the decorations, sparing the
node's own outset shadow, which an existing test pins.

`NodePath { id }` is left unimplemented: reading another node's geometry needs an
`id → resolved path` lookup the paint pass does not have, and #339 itself
separates that variant from the other five for this reason. It is not left
silent, though — a silent no-op is the defect being fixed here. It writes to
stderr and says what to do instead. Tracked separately.

Seven tests. Six fail against the unwired property, including the sample the
issue reports: a square with `clip-path: inset{right: 120}` painted at x=330
just as it did at x=200. End to end on a rendered still, x=330 goes from
rgb(139, 92, 246) to rgb(0, 0, 0) while x=200 stays violet.

`rules/clip-path.md` documents the six shapes, the local-coordinates trap on
`path`, the eight-point polygon for a chamfered frame, and that `node-path` is
declared but inert. Before this the property's only mention in the skills was a
line in `timeline-sequencing.md` saying it cannot be animated, while the exported
schema advertised it globally — which is what made it read as working.

Closes #339
@LeadcodeDev LeadcodeDev added the bug Something isn't working label Sep 26, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 26, 2026
@LeadcodeDev
LeadcodeDev merged commit 6c7d779 into main Sep 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

style.clip-path is declared and exported but read by nothing

1 participant