Investigating Masking in Google Web Designer 

Masking routinely tops the list of requested features in Google Web Designer. We investigated different approaches to implementing masking in HTML5, and we concluded that the limitations of existing browsers make effective masking infeasible at this time. We recognize that this is a disappointing result, so we’re using this space to explain (and show!) what we tried, and why we think masking is not yet ready for production.

What is masking? 

A mask defines a region of the plane where content (the maskee) is rendered; outside of this region, the content is invisible. For example, a circular mask may be used to create a spotlight effect, by only revealing content within the "light". A complete masking solution would meet the following criteria: 


  1. Everything is maskable. The maskee may include text, images, animation, SVG, canvases, videos, custom elements, and even other masks. 


  2. A mask can have any shape. It may be as simple as a circle, or it may be multiple disconnected regions with complicated geometry. 


  3. Masks can be animated. The mask can be translated, rotated, and scaled; it can be morphed into new shapes; and in the most general case, the mask can be a video. 


  4. Masks stack and overlap. Placing one mask inside another is equivalent to intersecting the two masks. If two independent masks overlap, the output of one is rendered atop the other, with no other interference. 


  5. Events are masked. Events originating outside the mask are blocked, and otherwise they pass through to the maskee without modification. 


What did we try? 


There is no way to achieve all of the above properties on any particular browser, much less every browser we support (i.e., Chrome, Firefox, Safari, and IE 10+). We don’t require perfection, however; an approximate solution can still be useful in practice. We initially experimented with CSS3 masking. Smooth mask animation is only possible if the mask is a CSS basic-shape, but otherwise all masking behaviors, such as those listed in the section above, are supported. However, CSS3 masking is not yet available on all major browsers.

Another way to implement masking, which is already supported by all major browsers, is to represent a mask as an element styled with overflow: hidden. This exhibits all masking behaviors, although the mask’s shape is equivalent to the element’s border, which effectively limits masks to rectangles, circles, ellipses, and capsules. Despite this limitation, overflow-based masking would still be useful for basic effects like wipes and reveals. Moreover, on browsers that support CSS3 masking, the mask’s shape could essentially be arbitrary, although animation would still be limited to the element’s transform. Unfortunately, we discovered that even in simple cases, overflow-based masking produced blurring and flickering artifacts on some browsers.

Finally, it’s worth noting that in simple cases, masking-like effects can be achieved simply by overlaying an image with transparent regions, which is already possible in Google Web Designer. This technique, however, is really the opposite of masking -- instead of rendering only within the mask region, it paints over everything that is not in the mask region.


CSS3 masking 

We started by exploring CSS3 masking, which comes in two forms: clipping (clip-path) and masking (image-mask). Clipping crops the rendering of an element and its children to a binary mask defined either through an inlined SVG, or a “basic shape” consisting of a circle, and ellipse, or a polygon. Masking crops the element’s content to an image’s boundary, and then applies an alpha channel determined by the image content. As of this writing, browser support for CSS3 masking is as follows:

  • Clipping is supported on Chrome, Safari, and Firefox, although Firefox’s support for basic shapes is turned off by default. 
  • Masking is supported in Chrome, Safari, and Firefox, but Firefox currently does not allow control over the image’s size or position. 
  • IE does not currently support CSS3 masking, but future support is likely, at least for Edge. 

 We tested CSS3 clipping and masking in several different scenarios, and here’s what we found (demo):

Clipping. Clip paths defined as SVG elements offer the most flexible representation of a mask’s shape, but CSS animations applied to SVG clipping elements are ignored at render time. Basic shapes offer less expressive mask geometry, but at least they can be animated: for circles and ellipses, the centers and radii are interpolated, and for polygons, control points locations are linearly interpolated (assuming that each keyframe has the same number of control points). One quirk is that in some browsers, overflow: hidden must be set on the mask element, or else the clipping path will be ignored if the maskee contains 3D transforms or has animated transforms (2D or 3D).

Masking.  Masking allows essentially arbitrary mask geometry, but animation of the mask shape is limited to defining CSS keyframes for mask-position and mask-size, allowing the mask to be translated and (non-uniformly) scaled. However, the mask is always rendered with positions and sizes rounded to the nearest integer, resulting in jagginess artifacts when the mask is animated slowly (demo).

In summary, smoothly animatable masks are only possible using clip-path with a basic shape — and even then, currently only in Chrome and Safari.


Overflow-based masking 


An alternative to CSS3 masking is to set an element's overflow to “hidden”, and then add the maskee as a child element:

<div id=”mask”>
 <div id=”maskee”></div>
</div>

This crops the maskee to the bounds of the parent element. Because the maskee is a child of the mask, however, moving the mask will also move the maskee. To allow them to move independently, we can insert an intermediate element that cancels changes to the mask.

<div id=”mask”>
 <div id=”maskCancel”>
   <div id=”maskee”></div>
 </div>
</div>

For example, if mask is moved ten pixels to the left, maskCancel is moved ten pixels to the right, giving the appearance that maskee remains stationary while the mask reveals a different portion of it. More generally, changes to the mask configuration can be canceled as follows:


  1. The intermediate element is forced to the same size as the mask, by setting its width and height to 100%. 
  2. If the mask’s left and top are (x, y), the intermediate element’s left and top are (-x, -y). 
  3. The intermediate element’s transform origin is identical to that of the mask, and its transform is the inverse of the mask’s transform. 
Similarly, if the mask is animated using CSS3 keyframes, additional keyframes rules must be added to the intermediate layer to cancel this animation. Assuming that animation is limited to transform (per existing best practices), this means:


  1. Animation of the mask’s transform-origin is copied to the intermediate element. 
  2. The inverse of the mask’s transform animation is applied to the intermediate element. 
(2) is by far the most complicated part of the process, and as it need not be understood to evaluate the practical effectiveness of overflow-based masking, we discuss it in the Appendix at the end of this post.

We built a proof-of-concept implementation of overflow-based masking, and discovered that even in simple of cases, animating the mask transform produces blurring and flickering artifacts on some browsers. For example, here is a screenshot of Chrome’s output, using a circular mask that translates back and forth (demo):





It appears as if the maskee is first being rendered with the intermediate layer’s transform applied, and then re-rendered with the mask’s transform, resulting in blurring and flickering artifacts. This behavior is not consistent across browsers: as of this writing, the artifacts appear in Chrome in all cases, they appear in Firefox if the animation includes scale or rotation (demo), and they do not appear in IE or Safari. However, on all browsers, no artifacts are present if overflow is set to visible, but the mask/intermediate layer/maskee construct is otherwise left unmodified.

Animation of an element’s left, top, width, and height (LTWH) is generally discouraged, and by default GWD uses LTWH for static layout and translation/scale for animation. Still, in light of these rendering artifacts, it’s worth considering whether LTWH makes more sense for masking. One problem with LTWH animation is that it has worse performance than transform animation: dozens of objects can easily be animated at 60Hz using transforms, but will slow down to 10Hz or worse if LTWH is used. If a document only has one or two masks and isn’t too complicated, however, perhaps the performance penalty is acceptable. A second problem is that LTWH values are rounded to the nearest integer at render time, which avoids the rendering artifacts discussed above, but at the expense of creating clunky, stair-step-like animations when objects animate slowly (demo).


Stay tuned 


We would love to add masking to Google Web Designer, but as an HTML5 authoring tool, we're limited by what browsers are able to support. Still, the web is constantly evolving, and browsers are continually improving, so we still hope and expect to ultimately be able to add masking to our suite of tools.


Posted by Lucas, Software Engineer




Appendix: Inverting animation for overflow-based masking 


If a mask is rotated or translated, then an inverse animation can easily be created by reversing the order of each key’s transforms and negating the values of each transform channel. For example, if the mask’s transform is

translate(tx, ty) rotate(rz) scale(sx, sy), 

then the intermediate element’s transform is

scale(1/sx, 1/sy) rotate(-rz) translate(-tx, -ty). 

If a mask is also scaled, then the inverse scale animation is defined in terms of a reciprocal. For example, a simple linear scaling from s1 to s2 over a time interval T can be written as

linearScale.png

and the inverse scaling animation is then

linearScaleInverse.png

Functions like this cannot be exactly represented in CSS3, which defines animation curves as 2D cubic Bezier segments where the first dimension is time and the second dimension is normalized value. While we could simply disallow scaling, this would make it impossible to achieve wipe and reveal effects. Instead, we generate samples of the ideal inverse scale animation

inverseScaleSamples.png

and fit an approximating animation curve. For simplicity, in our experiments we constructed inverse animations as piecewise linear functions, although more compact results can be obtained by fitting general cubic Bezier curves. To determine an acceptable error tolerance for the approximation, consider an absolute error e resulting in a net scale at the maskee of

scaleError.png

At a distance d from the transform origin, this yields a positional error of sed, and so given a maximum mask size of M, the maximum error E is

maxScaleError.png

Positional discrepancies can therefore be kept below half a pixel by requiring

errorBound.png

Intuitively, the smaller the scale value, the larger the error we can tolerate, because only a narrow slice of content will be visible through the mask. (It is also possible to invert animation of uniform scale by animating z translation, but this technique does not generalize to animations that include non-uniform scaling, and it alters the meaning of existing z translations authored in the maskee.)