SVG Essentials/Clipping and Masking

From WikiContent

Jump to: navigation, search
SVG Essentials

Sometimes you don't want to see an entire picture; for example, you might wish to draw a picture as though it were seen through binoculars or a keyhole; everything outside the boundary of the eyepieces or keyhole will be invisible. Or, you might want to set a mood by showing an image as though viewed through a translucent curtain. SVG accomplishes such effects with clipping and masking.

Clipping to a Path

When you create an SVG document, you establish its viewport by specifying the width and height of the area you're interested in. This automatically becomes your clipping area; anything drawn outside these limits will not be displayed. You can establish a clipping area of your own with the <clipPath> element. Here's the simplest case: establishing a rectangular clip path. Inside the <clipPath> element will be the <rect> we wish to clip to. The rectangle itself is not displayed; we only love it for its coordinates. Thus, we are free to add any fill or stroke styles we wish to the elements within the <clipPath>. On the object to be clipped we add a clip-path style property whose value references the <clipPath> element. Note that the property is hyphenated and not capitalized; the element is capitalized and not hyphenated. In Example 9-1, the object being clipped is a small version of the cat picture from Chapter 1, producing Figure 9-1.

Example 9-1. Clipping to a rectangular path

<defs>
<clipPath id="rectClip">
    <rect id="rect1" x="15" y="15"
       width="40" height="45"
       style="stroke: gray; fill: none;"/>
</clipPath>
</defs>

<!-- clip to rectangle -->
<use xlink:href="minicat.svg#cat" style="clip-path: url(#rectClip);"/>

<!--
  for reference, show entire picture with clipping area outlined -->
<g transform="translate(100,0)">
    <use xlink:href="#rect1"/>    <!-- show clip rectangle -->
    <use xlink:href="minicat.svg#cat"/>
</g>

Figure 9-1. Simple rectangular clipping

Simple rectangular clipping

As the name <clipPath> implies, you can clip to any arbitrary path. Indeed, the <clipPath> element can contain any number of basic shapes, <path> elements, or <text> elements. Example 9-2 shows a group of shapes clipped to a curved path and the same group of shapes clipped by text.

Example 9-2. Complex clip paths

<defs>
<clipPath id="curveClip">
    <path id="curve1"
        d="M5 55 C 25 5, 45 -25, 75 55, 85 85, 20 105, 40 55 Z"
        style="stroke: black; fill: none;"/>
</clipPath>

<clipPath id="textClip">
    <text id="text1" x="20" y="20" transform="rotate(60)"
        style="font-size: 48pt; stroke: black; fill: none;">
    CLIP
    </text>
</clipPath>

<g id="shapes">
<rect x="0" y="50" width="90" height="60" style="fill: #999;"/>
<circle cx="25" cy="25" r="25" style="fill: #666;"/>
<polygon points="30 0 80 0 80 100" style="fill: #ccc;"/>
</g>
</defs>

<!-- draw with curved clip-path -->
<use xlink:href="#shapes" style="clip-path: url(#curveClip);" />

<g transform="translate(100,0)">
    <use xlink:href="#shapes"/>
    <use xlink:href="#curve1"/>   <!-- show clip path -->
</g>

<!-- draw with text as clip-path -->
<g transform="translate(0,150)">
    <use xlink:href="#shapes" style="clip-path: url(#textClip);"/>
</g>

<g transform="translate(100,150)">
    <use xlink:href="#shapes"/>
    <use xlink:href="#text1"/>
</g>

To help you see the areas better, the preceding SVG draws the clipping path above the entire figure; you see this in the right half of Figure 9-2.

The coordinates for the preceding clip paths have been specified in user coordinates. If you wish to express coordinates in terms of the object bounding box, then set clipPathUnits to objectBoundingBox (the default is userSpaceOnUse). Example 9-3 uses a clip path that will produce a circular (or oval) window on any object that it's applied to.

Example 9-3. clipPathUnits using objectBoundingBox

<defs>
<clipPath id="circularPath" clipPathUnits="objectBoundingBox">
    <circle cx="0.5" cy="0.5" r="0.5"/>
</clipPath>

<g id="shapes">
<rect x="0" y="50" width="100" height="50" style="fill: #999;"/>
<circle cx="25" cy="25" r="25" style="fill: #666;"/>
<polygon points="30 0 80 0 80 100" style="fill: #ccc;"/>
</g>

<g id="words">
<text  x="0"  y="19" style="font-size: 12;">
<tspan x="0"  y="19">If you have form'd a circle</tspan>
<tspan x="12" y="33">to go into,</tspan>
<tspan x="0"  y="47">Go into it yourself</tspan>
<tspan x="12" y="61">and see how you would do.</tspan>
<tspan x="50" y="80">&#xad;William Blake</tspan>
</text>
</g>
</defs>

<use xlink:href="#shapes" style="clip-path: url(#circularPath);" />
<use xlink:href="#words" transform="translate(110,0)"
    style="clip-path: url(#circularPath);"/>

Figure 9-2. Complex path clipping

Complex path clipping

In Figure 9-3, the geometric figures happen to have a square bounding box, so the clipping appears circular. The text is bounded by a rectangular area, so the clipping area appears to be an oval.

Figure 9-3. Use of a circular/oval clipping path

Use of a circular/oval clipping path

Note

Specify a clipping rectangle for <marker> and <symbol> tags with the clip style property. Its value is four whitespace-separated numbers specifying the rectangle's top, right, bottom, and left bounds. This sets the clip rectangle to match the marker or symbol's viewBox, rather than its viewport.

Masking

A mask in SVG is the exact opposite of the mask you wear to a costume party. With a costume party mask, the parts that are opaque hide your face; the parts that are translucent let people see your face dimly, and the holes (which are transparent) let people see your face clearly. An SVG mask, on the other hand, transfers its transparency to the object that it masks. Where the mask is opaque, the pixels of the masked object are opaque. Where the mask is translucent, so is the object, and the transparent parts of the mask make the corresponding parts of the masked object invisible.

You use the <mask> element to create a mask. You may specify the mask's dimensions with the x, y, width, and height attributes. These dimensions are in terms of the masked objectBoundingBox. If you want the dimensions to be in terms of user space coordinates, set maskUnits to userSpaceOnUse.

Between the beginning <mask> and ending </mask> tags are any basic shapes, text, or paths that you wish to use as the mask. The coordinates on these elements are expressed in user coordinate space by default. If you wish to use the object bounding box for the contents of the mask, set maskContentUnits to objectBoundingBox. (The default is userSpaceOnUse.)

The question then becomes: how does SVG determine the transparency, or alpha value of the mask? We know that each pixel is described by four values: its red, green, and blue color value, and its opacity. While at first glance it would seem logical to use only the opacity value, SVG decides to use all the information available to it rather than throwing away three-fourths of a pixel's information. SVG uses this formula:

( 0.2125 * red value +
0.7154 * green value +
0.0721 * blue value ) *
 opacity value
         

where all of the values are floating point numbers in the range zero to one. You may be surprised that the proportions aren't equal, but if you look at fully saturated red, green, and blue, the green appears to be the brightest, red darker, and blue the darkest. (You can see this in Figure 9-4.) The darker the color, the smaller the resulting alpha value will be, and the less opaque the masked object will be.

Figure 9-4. Effect of color values on transparency

Effect of color values on transparency

Example 9-4 creates black text and a black circle masked by a totally opaque red, green, blue, and white square. The text and circle are grouped together, and the group uses a mask style property to reference the appropriate mask.

Example 9-4. Masking with opaque colors

<defs>
<mask id="redmask" x="0" y="0" width="1" height="1"
    maskContentUnits="objectBoundingBox">
    <rect x="0" y="0" width="1" height="1" style="fill: #f00;"/>
</mask>

<mask id="greenmask" x="0" y="0" width="1" height="1"
    maskContentUnits="objectBoundingBox">
    <rect x="0" y="0" width="1" height="1" style="fill: #0f0;"/>
</mask>

<mask id="bluemask" x="0" y="0" width="1" height="1"
    maskContentUnits="objectBoundingBox">
    <rect x="0" y="0" width="1" height="1" style="fill: #00f;"/>
</mask>

<mask id="whitemask" x="0" y="0" width="1" height="1"
    maskContentUnits="objectBoundingBox">
    <rect x="0" y="0" width="1" height="1" style="fill: #fff;"/>
</mask>
</defs>

<!-- display the colors to show relative brightness (luminance) -->
<rect x="10" y="10" width="50" height="50" style="fill: #f00;"/>
<rect x="70" y="10" width="50" height="50" style="fill: #0f0;"/>
<rect x="130" y="10" width="50" height="50" style="fill: #00f;"/>
<rect x="190" y="10" width="50" height="50"
    style="fill: #fff; stroke: black;"/>

<g style="mask: url(#redmask);
    font-size: 14pt; text-anchor: middle;">
<circle cx="35" cy="115" r="25"  style="fill: black;"/>
<text x="35" y="80">Red</text>
</g>

<g style="mask: url(#greenmask);
    font-size: 14pt; text-anchor: middle;">
<circle cx="95" cy="115" r="25" style="fill: black;"/>
<text x="95" y="80">Green</text>
</g>

<g style="mask: url(#bluemask);
    font-size: 14pt; text-anchor: middle;">
<circle cx="155" cy="115" r="25" style="fill: black;"/>
<text x="155" y="80">Blue</text>
</g>

<g style="mask: url(#whitemask);
    font-size: 14pt; text-anchor: middle;">
<circle cx="215" cy="115" r="25" style="fill: black;"/>
<text x="215" y="80">White</text>
</g>

Note

Figuring out the interaction between color, opacity, and final alpha value is not exactly intuitive. If you fill and/or stroke the mask contents in white, the "color factor" adds up to 1.0, and the opacity will then be the only factor that controls the mask's alpha value. Example 9-5 is written this way, and the result is in Figure 9-5.

Figure 9-5. Alpha value equal to opacity

Alpha value equal to opacity

Example 9-5. Mask alpha using opacity only

<defs>
<mask id="fullmask" x="0" y="0" width="1" height="1"
    maskContentUnits="objectBoundingBox">
    <rect x="0" y="0" width="1" height="1"
        style="fill-opacity: 1.0; fill: white;"/>
</mask>

<mask id="three-fourths" x="0" y="0" width="1" height="1"
    maskContentUnits="objectBoundingBox">
    <rect x="0" y="0" width="1" height="1"
        style="fill-opacity: 0.75; fill: white;"/>
</mask>

<mask id="one-half" x="0" y="0" width="1" height="1"
    maskContentUnits="objectBoundingBox">
    <rect x="0" y="0" width="1" height="1"
        style="fill-opacity: 0.5; fill: white;"/>
</mask>

<mask id="one-fourth" x="0" y="0" width="1" height="1"
    maskContentUnits="objectBoundingBox">
    <rect x="0" y="0" width="1" height="1"
        style="fill-opacity: 0.25; fill: white;"/>
</mask>
</defs>

<g style="font-size: 14pt; text-anchor:middle; fill:black;">
    <g style="mask: url(#fullmask);">
    <circle cx="35" cy="35" r="25"/>
    <text x="35" y="80">100%</text>
    </g>

    <g style="mask: url(#three-fourths);">
    <circle cx="95" cy="35" r="25"/>
    <text x="95" y="80">75%</text>
    </g>

    <g style="mask: url(#one-half);">
    <circle cx="155" cy="35" r="25"/>
    <text x="155" y="80">50%</text>
    </g>

    <g style="mask: url(#one-fourth);">
    <circle cx="215" cy="35" r="25"/>
    <text x="215" y="80">25%</text>
    </g>
</g>

Case Study -- Masking a Graphic

Example 9-6 adds a JPG image to the image that was constructed in Section 8.10. As you can see in Figure 9-6 (reduced to save space and in grayscale to avoid using color ink), the image obscures the curve inside the main ellipse, and the blue sky intrudes horribly on the pale red section.

Example 9-6. Unmasked image

<defs>
    <font-face font-family="bakbatn">
        <font-face-src>
            <font-face-uri xlink:href="kfont.svg#kfont-defn"/>
        </font-face-src>     
    </font-face>
</defs>

<!-- draws ellipse and text -->
<use xlink:href="ksymbol.svg#ksymbol"/>

<image xlink:href="kwanghwamun.jpg" x="72" y="92"
    width="160" height="120"/>

Figure 9-6. Unmasked image

Unmasked image

The solution is to fade out the edges of the picture, which is easily done by using a radial gradient as a mask. Here's the code to be added to the <defs> section of the document:

<radialGradient id="fade">
    <stop offset="0%" style="stop-color: white; stop-opacity: 1.0;"/>
    <stop offset="85%" style="stop-color: white; stop-opacity: 0.5;"/>
    <stop offset="100%" style="stop-color: white; stop-opacity: 0.0;"/>
</radialGradient>
<mask id="fademask">
    <rect x="72" y="92" width="160" height="120"
        style="fill: url(#fade);"/>
</mask>

Then add a mask reference to the <image> tag, resulting in Figure 9-7:

<image xlink:href="kwanghwamun.jpg" x="72" y="92"
    width="160" height="120"
    style="mask: url(#fademask);"/>

Figure 9-7. Masked image

Masked image

Using less of the picture can substantially improve the graphic as a whole.

Personal tools