Introduction
One of the problems with the standards-based Web is that it's hard to use SVG's features to enhance HTML content. For example, there is no reasonable way to clip an HTML element to a non-rectangular region, or to apply an alpha mask to an HTML element, or to apply image processing effects such as color channel manipulation to HTML elements. SVG has these features but they can only be applied to SVG elements. You can embed the HTML in a <foreignObject>, but that has major limitations; in particular, you can't apply effects to HTML elements via stylesheets without changing the markup --- ripping out content and putting it inside ugly SVG fragments --- and breaking most CSS layouts in the process.
One approach to solving this is to create new CSS properties for the desired effects. Dave Hyatt's been doing this a bit in Webkit, and the approach has its place. However for complex effects such as clipping to arbitrary shapes and custom image processing, CSS isn't really up to the task. One problem is that CSS isn't good at manipulating structured values like shapes and filter processing stacks; they're cumbersome to write in CSS expression syntax, or else they require new custom CSS syntax (e.g. @-rules), and there's no standard DOM to let scripts manipulate components of these structured values. Another issue is that we should
try to avoid duplicating specification and implementation of complex features.
So I've been experimenting with better ways to apply SVG effects to HTML content. The first step is to make SVG's 'clip-path',
'mask' and
'filter' properties work when applied to HTML content.
clip-path
Here's some XHTML markup that clips elements to a shape composed of a circle next to
a rectangle.
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:svg="http://www.w3.org/2000/svg">
<body style="background:#ccc; font-size:30px;">
<style>
p { width:300px; border:1px solid black; display:inline-block; margin:1em; }
iframe { width:300px; height:300px; border:none; }
b { outline:1px dotted blue; }
</style>
<p class="target" style="background:lime;">
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. Ut enim ad minim veniam.</p>
<iframe class="target" src="http://mozilla.org"/>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing
<b class="target">elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua.</b> Ut enim ad minim veniam.</p>
<style>.target { clip-path: url(#c1); }</style>
<svg:svg height="0">
<svg:clipPath id="c1" clipPathUnits="objectBoundingBox">
<svg:circle cx="0.25" cy="0.25" r="0.25" id="circle"/>
<svg:rect x="0.5" y="0.2" width="0.5" height="0.8"/>
</svg:clipPath>
</svg:svg>
</body>
</html>
So we have a block element, an <iframe>, and an inline element all clipped by the same clipPath. The clipPath shape coordinates are relative to the bounding-box of the clipped object.
Here's the rendering:
You can't see it in this example, but hit-testing is affected by clip-path as you'd expected; mouse events in the clipped-out area pass through to the element(s) underneath.
One of the tricky integration points is handling elements which generate multiple CSS boxes, such as the inline element here. I don't expect clipping and masking to find much use there, but filters can be useful for inlines. The approach that makes most sense is to apply the effects to the whole element at once, and make the SVG "object bounding box" be the rectangle union of all border-boxes for the element and its geometric descendants, plus any outlines on the element or descendants. This is useful and also seems in accord with the spirit of the SVG spec.
clipPath, mask and filter content can also use "userSpaceOnUse" units. In SVG,
"user space" is established by the SVG viewport containing the affected content. We don't have such a viewport for non-SVG content, so I make "user space" be the rectangle which is the union of all border-boxes for the affected element. User space is in CSS pixel units, so clipPath, mask and filter content can specify lengths in CSS pixels as well as percentages relative to the size of the affected element.
Scripting
It's easy to add some dynamism to the above example:
<button onclick="toggleRadius()">Toggle radius</button><br/>
<script>
function toggleRadius() {
var circle = document.getElementById("circle");
circle.r.baseVal.value = 0.40 - circle.r.baseVal.value;
}
</script>
The SVG DOM isn't as clean as it could be, but it's adequate.
mask
Replace the 'clip-path' chunk above with 'mask':
<style>.target { mask: url(#m1); }</style>
<svg:svg height="0">
<svg:mask id="m1" maskUnits="objectBoundingBox" maskContentUnits="objectBoundingBox">
<svg:linearGradient id="g" gradientUnits="objectBoundingBox" x2="0" y2="1">
<svg:stop stop-color="white" offset="0"/>
<svg:stop stop-color="white" stop-opacity="0" offset="1"/>
</svg:linearGradient>
<svg:circle cx="0.25" cy="0.25" r="0.25" id="circle" fill="white"/>
<svg:rect x="0.5" y="0.2" width="0.5" height="0.8" fill="url(#g)"/>
</svg:mask>
</svg:svg>
Now the rectangle is being painted with a translucent gradient and the shape is being used
as an alpha mask instead of just clipping.
Here's the rendering:
As per the SVG spec, masks do not affect hit-testing, so events will be received even where content is masked out. It would be smart to implement the SVG
pointer-events property for non-HTML content to give authors a way to control this.
filter
Replace the 'mask' chunk with some filters:
<style>
p.target { filter:url(#f3); }
body:hover p.target { filter:url(#f5); }
b.target { filter:url(#f1); }
body:hover b.target { filter:url(#f4); }
iframe.target { filter:url(#f2); }
body:hover iframe.target { filter:url(#f3); }
</style>
<svg:svg height="0">
<svg:filter id="f1">
<svg:feGaussianBlur stdDeviation="3"/>
</svg:filter>
<svg:filter id="f2">
<svg:feColorMatrix values="0.3333 0.3333 0.3333 0 0
0.3333 0.3333 0.3333 0 0
0.3333 0.3333 0.3333 0 0
0 0 0 1 0"/>
</svg:filter>
<svg:filter id="f3">
<svg:feConvolveMatrix filterRes="100 100" style="color-interpolation-filters:sRGB"
order="3" kernelMatrix="0 -1 0 -1 4 -1 0 -1 0" preserveAlpha="true"/>
</svg:filter>
<svg:filter id="f4">
<svg:feSpecularLighting surfaceScale="5" specularConstant="1"
specularExponent="10" lighting-color="white">
<svg:fePointLight x="-5000" y="-10000" z="20000"/>
</svg:feSpecularLighting>
</svg:filter>
<svg:filter id="f5">
<svg:feColorMatrix values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 1 0 0 0" style="color-interpolation-filters:sRGB"/>
</svg:filter>
</svg:svg>
Filters are cool so I decided to go a bit over the top here. The block element gets an edge-detection convolution filter. The <iframe> gets converted to grayscale.
The inline element is blurred. When you hover over the body, the filters change; the <iframe> gets the edge detection filter, the block gets a color channel transformation that takes the alpha channel to the green channel, creating a punch-out effect, and the inline element gets a 3D lighting effect.
Here are the renderings:
Filters do not affect hit-testing.
Discussion
All these effects are fully live. You can select, zoom, change the DOM, invoke contenteditable, etc, and everything works fine. Effects are applied to the drawing of the selection, and even to the drawing of the caret while it's in an affected element.
The effects following the SVG composition model; if an element has a filter, clipping and masking, then the filter is applied first followed by the clipping and/or masking. They behave like CSS 'opacity' in that they induce a pseudo-stacking-context for the element. They do not affect layout, although 'filter' can produce drawing outside the bounds of the element (e.g., shadows). Currently, in my implementation, 'filter' can actually affect layout because when 'filter' paints outside the element we may show scrollbars so that the user can scroll to see the overflowing paint, but I think this should be considered a bug.
Since we don't yet support remote content references in Gecko, you have to put the effects fragments inline in your document. This is ugly and also means you can't use these effects in non-XML HTML. Once we implement remote content references, these problems will go away; author CSS style sheets will be able to reference effects in auxiliary SVG documents. Many effects can be stored in a single SVG document, amortizing the cost of an extra resource. Authors will even be able to use tools like
Inkscape to generate SVG effects and reference them from CSS.
At a spec level, there's very little that needs to be said. No new syntax is required.
A specification needs to be written documenting decisions on the issues I've mentioned above (not necessarily the same decisions I made :-) ).
In lieu of a spec, we have to decide how much of this to take in Gecko and when. It's nice that there's no new syntax, but that also means there's no convenient way to use -moz prefixing to isolate the non-standard features. We could create new
'-moz-filter'/'-moz-clip-path'/'-moz-mask' properties that behave like the existing properties except they also apply to HTML. Maybe it's not worth it. Something for discussion.
I'm making tryserver builds right now, and I'll update this post with a link when they're ready. Here's a link to my Mercurial repository. (Unfortunately, I accidentally pushed copies of NSPR and NSS to that repo, so don't blindly pull from it unless you're OK with that.) Some demos:
At some point I'll post a followup talking about the other work I've done on this branch that leads up to these features, with some more details about the implementation. I'm also planning to add some more features soon.
Aside
A nice side effect of providing better SVG-HTML integration is that it gives SVG a leg up on the Web. You can't do these effects using Flash or Silverlight, and since they're not standards they probably won't ever be invited to this party.
Update Mac build, Linux build, Windows build.
Belorussian translation