An awesome, cross-browser Star Wars circular wipe transition? CSS houdini vs clippath vs mask
Tags: css, webdev
Published: 2023-01-16 13:00:00


Speaking of wipe animations, Will Boyd covered [making CSS animations of some Star Wars wipe transitions](https://codersblock.com/blog/star-wars-scene-transition-effects-in-css/) recently. All of the animations were based on CSS masks.

CSS Masks have some issues. I will offer a couple of alternative solutions to one of his wipe animations - Will refers to it as an iris wipe transition - I will call it a circular wipe transition. I guess others may refer to it as a radial wipe transition.

Tom-a-toe, tomat-oe. 🍅😁

## What is the issue with CSS Masks?

CSS masks are a bit iffy.

<img src="https://caniuse.bitsofco.de/static/v1/css-masks-1673711900658.jpg" width="800" height="366" alt="Data on support for the css-masks feature across the major browsers from caniuse.com">

According to caniuse, masks are [partially implemented in Chromium browsers](https://caniuse.com/css-masks). You still need to use a vendor prefix.

The advice is to favour using the `-webkit-mask-image` property. The rest of [the spec](https://www.w3.org/TR/css-masking-1/) is not fully implemented. That is quite vague.

I guess that this means that you can't specify properties such as `mask-size`, `mask-position`, and `mask-repeat`. From what I seen you can use the prefixed version of these properties in some contexts in Chrome, but it may not behave as you expect.

Will Boyd animates `-webkit-mask-position` successfully for his [horizontal wipe transition example](https://codersblock.com/blog/star-wars-scene-transition-effects-in-css/#horizontal-wipe-transition)  - from `-webkit-mask-position: left;` to `-webkit-mask-position: right;`. So, you can animate the position of a mask, but to varying degrees of success I guess. The same goes for the size of a mask.

If you use a gradient as the mask source, you cannot animate it as a gradual transition where it is based on interpolated values. It is a discrete type of animation. It will jump from one state to another. That is how it is specified, sothat is not an implementation blooper. I doubt that you would want that type of animation though!

CSS Houdini does open the door to animating a gradient mask source as a gradual transition - you can use the [CSS Properties and Values API](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Properties_and_Values_API/guide). However, Houdini is only supported in Chrome and Edge right now.

The bottom line is that you can't specify and animate masks freely, and expect it to work across all browsers.

Let's get into the circular wipe transition.

## Using CSS Masks to create the circular wipe transition

Will Boyd uses the CSS Properties and Values API to animate the gradient mask as below:

```css
@property --radius {
  syntax: '<percentage>';
  inherits: true;
  initial-value: -5%;
}

@keyframes scene-transition {
  to { --radius: 105%; }
}

.scene-2 {
  background-image: url(scene-2.jpg);
  z-index: -1;
  -webkit-mask-image: radial-gradient(
    circle,
    #fff calc(var(--radius) - 5%),
    transparent calc(var(--radius) + 5%)
  );
}

.scenes:is(:hover, :focus) .scene-2 {
  z-index: 1;
  animation: scene-transition 2s linear forwards;
}
```

Here is the codepen:

{% codepen https://codepen.io/lonekorean/pen/yLvjbrr %}

As stated, this only works in Chrome and Edge at the moment.

There is a fallback included for this animation, the images will be swapped using `z-index`. You can try this in Firefox or Safari to see that happening.

You could treat this as a progressive enhancement. If you *must* have a consistent outcome, you got to look at other solutions. Read on if you are interested in that.

## Is there a cross-browser solution for a circular wipe transition?

For this effect, you could animate the `-webkit-mask-size` property instead of using the CSS Properties and Values API. Set the initial mask size to zero and then tween it to 100% on hover. I tried it and it doesn't appear to work properly in Chromium browsers, but in any case we would like to see alternatives that have more predictable outcomes.

### Use `clip-path` instead of `mask`

You can use `clip-path` instead of `-webkit-mask-image`. The limitation is that you cannot have semi-transparent areas, you will need forgo the featured edges. The advantage is that the  `clip-path`  property has [evergreen support for basic shapes](https://caniuse.com/mdn-css_properties_clip-path_basic_shape) and you can animate the shape in a gradual transition.

We will use a circle shape through the [`circle()` function.](https://developer.mozilla.org/en-US/docs/Web/CSS/basic-shape/circle) We will change the radius of the circle from zero to a larger size to expand the circle to reveal the second scene image.

The simplest way to visualize the animation is by opening [clippy CSS clip-path maker](https://bennettfeely.com/clippy/), and play with the circle shape. You can play the video demo below to grasp the idea.

<video src="https://www.roboleary.net/assets/img/blog/2023-01-16-awesome-cross-browser-starwars-circular-wipe-transition-css-houdini-clipath-mask/clippath-demo.mp4" controls></video>

If you look at the bottom of the video (probably covered by the controls), you can see the CSS snippet change. The first value in `circle()` - the radius - is reduced from 50% to 0%. This makes the image disappear.

We want to do it the opposite way round for our animation, go from 0% to 85%. The value is bigger than 50% because we want to reveal the entire image and we need a circle that exceeds the bounds of the image for that to happen.

Here is the CSS:

```css
.scene-2 {
  clip-path: circle(0% at 50% 50%);
  background-image: url("img/scene-2.jpg");
}

@keyframes scene-transition {
  100% {
    clip-path: circle(85% at 50% 50%);
  }
}

.scenes:is(:hover, :focus) .scene-2 {
  animation: scene-transition 2s linear forwards;
}
```

This definitely works in Chrome and Firefox, and should work in Safari (tell me if it doesn't):

{% codepen https://codepen.io/robjoeol/pen/YzjQmoG %}

This appears to be an evergreen solution. If you need some transparency, then look into the next method.

### Use a SVG mask

If you want some semi-transparent areas, then you can opt for a SVG Mask. It is better supported than CSS Masks.

It is a bit more work, but you can get a good result that should work across all browsers.

I found it easiest to create a SVG with a `viewBox` that has the same dimensions as the *scene-2.jpg*, which is 1920px by 820px. We give the `viewBox` a width of 1920 units and height of 820 units i.e  `viewBox="0 0 1920 820"`.

I created a `mask` element that contains a `circle` element. We are provided the same set of values as we did for the `clip-path`. We position in the center, so the `cx` and `cy` values are 50%.  The mask radius - the `r` attribute -  is give a value of 410, which is 50% of the height.  

We create an `image` element for *scene-2.jpg*, that covers the entire canvas.  We apply the mask to it. If you want soft edges, you can create a `radialGradient` and apply that to `circle` contained in the mask.  It sounds a bit weird to explain it all. Probably best to look at the markdown below:

```svg
<svg xmlns="http://www.w3.org/2000/svg" class="scene-2 scene" viewBox="0 0 1920 820">
  <defs>
    <radialGradient id="myGradient">
      <stop offset="70%" stop-color="white" />
      <stop offset="100%" stop-color="transparent" />
    </radialGradient>
    <mask id="imageMask">
      <circle id="circleMask" cx="50%" cy="50%" r="410" fill="url('#myGradient')" />
    </mask>
  </defs>
  <image width="100%" height="100%" preserveAspectRatio="xMidYMid slice" href="https://raw.githubusercontent.com/robole/artifice/main/star-wars-circular-wipehttps://www.roboleary.net/assets/img/scene-2.jpg" mask="url(#imageMask)"></image>
</svg>
```

This is how the SVG will look:

<img src="https://www.roboleary.net/assets/img/blog/2023-01-16-awesome-cross-browser-starwars-circular-wipe-transition-css-houdini-clipath-mask/mask-svg.jpg" alt="preview of svg image revealing the star wars scene masked in a circular shape" width="1480" height="633" loading="lazy">

For the animation, we change the size of the `circle` element that is contained in the `mask`. We want to set the intial size to be zero and then increase it to expand beyond the boundaries of the canvas.  We can use a [`scale()` transformation](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/scale) to do this.

```css
.scenes:is(:hover, :focus) #circleMask {
  animation: scene-transition 2s linear forwards;
}

#circleMask {
  transform: scale(0);
  transform-origin: center;
}

@keyframes scene-transition {
  to {
    transform: scale(4);
  }
}
```

Below is a short video demo of the code in Chrome:

<video src="https://www.roboleary.net/assets/img/blog/2023-01-16-awesome-cross-browser-starwars-circular-wipe-transition-css-houdini-clipath-mask/svg-mask-demo-chome.mp4" controls></video>

I tested the code in Firefox and Chrome and it runs smoothly.

Here is [the codepen](https://codepen.io/robjoeol/full/dyjzvdb) if you want to try it out. For some reason the code *does not run as expected in a codepen on Chrome*. Maybe this is caused by having the code inside an `iframe`. You can grab the code from my [*artifice* GitHub repository](https://github.com/robole/artifice) and try out the code locally to see this anomaly for yourself.

## Conclusion

I would be wary with animating masks. I would favour animating `clip-path` when you do not need semi-transparency - the browser support is much better. If you want an animation with semi-transparency, you could use a SVG mask instead of a CSS mask. This works well but it requires decent SVG knowledge and a bit more effort.