Clip paths
The clipPath
element is an interesting one. Mainly because of how ridiculously complicated it is.
As of 2023, even browsers
struggle
with some edge cases. And regular SVG libraries barely support half of its features.
In this chapter, I will try to explain how it's even possible.
Allowed children
For starters, the clipPath
name misleads us into thinking that it contains a single path.
Instead, it can have any number of paths. Moreover, any basic shapes (circle
, rect
, etc.),
path
elements and text
elements are allowed. But no groups and images.
Yes, no groups. Unlike a mask
or a pattern
, a clipPath
is a list of elements, not a tree.
Weirdly enough, use
is allowed, but only when it directly (no use
-> use
-> path
)
references a basic shape, path
or text
.
Which is very strange because use
is usually instanced as a group, but not in clipPath
.
line
, while being a basic shape, isn't allowed either. Since it cannot be filled.
Same with paths that do not produce a fill-able shape.
Clip all
By default, a clipPath
clips the whole element and each clipPath
child element defines
a region that should be preserved.
Because of that, a clipPath
without children or with only malformed/unsupported children would
clip the whole element it is applied to.
No styles and no markers
Any style attributes like fill, stroke, opacity, filter, mask, etc.
on clipPath
children will be ignored.
All children must be parsed as fill="black" stroke="none"
.
The interesting part here is that markers in SVG are also part of the element style. Therefore markers should not be parsed and rendered inside clip paths as well.
On the other hand, clipPath
children are affected by display
and visibility
properties.
clip-rule
To complicate things for no reason, clipPath
ignores the fill-rule
property and
requires us to use clip-rule
instead. Why do we need such distinction? I have no idea.
And unlike other reference-able elements, like linearGradient
, mask
, filter
, etc.,
which you can usually find inside defs
,
clipPath
can search for clip-rule
in parent elements as well.
Meaning that in the case of:
<g clip-rule="evenodd">
<clipPath id="clip1">
<rect/>
</clipPath>
</g>
clip-rule
on g
does apply to rect
inside clipPath
.
Nested clip paths
What if I tell you that you can set a clip path on a clip path which also has a clip path on all of its children?
Yes, this is a valid SVG:
<clipPath id="clip1">
<rect/>
</clipPath>
<clipPath id="clip2">
<rect clip-path="url(#clip1)"/>
</clipPath>
<clipPath id="clip3" clip-path="url(#clip2)">
<rect/>
</clipPath>
In SVG, clipPath
is an endlessly recursive tree of paths that eventually produces an alpha mask.
Yes, an alpha mask. Not a path. Not a list of paths, but a mask.
The reason I'm trying to highlight this fact is because
a lot of 2D graphic libraries do provide a clip(path)
function,
like CanvasRenderingContext2D.clip()
in JavaScript.
And while it can be used in some cases it would be hard, if not impossible, to implement
a proper SVG clipPath
using such function.
How well nested clip paths are supported? Barely... Browsers are mostly fine, except a couple of bugs in Firefox. Inkscape, Batik and librsvg essentially do not support this at all.
Short-hands
While most SVG libraries struggle to implement even the SVG 1.1 spec,
the SVG authors decided that it isn't enough and added
clip-path
short-hands to SVG 2.
Now you can write just:
<rect fill="green" x="20" y="20" width="160" height="160" clip-path="circle()"/>
instead of:
<clipPath id="clip1">
<circle cx="100" cy="100" r="80"/>
</clipPath>
<rect fill="green" x="20" y="20" width="160" height="160" clip-path="url(#clip1)"/>
And as you can imagine, this one is extremely hard to implement. As of December 2023, only browsers even attempt to support it.
clipPath
vs mask
After reading all of the above you might be wondering what is the difference
between clipPath
and mask
then? Aren't they doing the same thing?
Sure, some simple cases like rectangular clips or single-path clips can be optimized,
but a proper clipPath
implementation would still have to use to an alpha mask. Right?
And honestly, I have no idea either. Especially since SVG 2 introduced mask-type=alpha
which makes mask
behave exactly like clipPath
.
Essentially, clipPath
is just a mask
subset instead of being it's own thing.
An 8-bit mask
An interesting tidbit is that the SVG 1.1 spec says that clipPath
is "conceptually" a 1-bit mask.
Which is technically not true, because you still need 8-bit to implement anti-aliasing.
The spec does clarify that, but the wording is still a bit confusing.
The reason I'm mentioning this at all is because Batik does implement clip paths as 1-bit masks. Meaning that clipping is never anti-aliased. It's the only SVG library I know that does this. And yes, at least the SVG 1.1 spec doesn't actually require clip paths to be anti-aliased.
Clip path transform
clipPath
has an interesting quirk in a way it handles invalid transforms, like scale(0 0)
.
In general, an invalid transform on an element in SVG makes the whole element invalid and it should
not be rendered.
So you might think that clip paths with invalid transforms should simply be ignored.
clip-path="url(#invalid-clip)"
would become clip-path="none"
.
But instead, the whole element should not be rendered.
No region
Unlike the mask
element, clipPath
doesn't have a region.
Meaning that it's up to the implementation to decide how big the alpha mask bitmap should be.