This is a follow up to my previous post looking into clip paths. Last time around, we dug into the fundamentals of clipping and how to get started. We looked at some ideas to exemplify what we can do with clipping. We’re going to take things a step further in this post and look at different examples, discuss alternative techniques, and consider how to approach our work to be cross-browser compatible.
One of the biggest drawbacks of CSS clipping, at the time of writing, is browser support. Not having 100% browser coverage means different experiences for viewers in different browsers. We, as developers, can’t control what browsers support — browser vendors are the ones who implement the spec and different vendors will have different agendas.
One thing we can do to overcome inconsistencies is use alternative technologies. The feature set of CSS and SVG sometimes overlap. What works in one may work in the other and vice versa. As it happens, the concept of clipping exists in both CSS and SVG. The SVG clipping syntax is quite different, but it works the same. The good thing about SVG clipping compared to CSS is its maturity level. Support is good all the way back to old IE browsers. Most bugs are fixed by now (or at least one hope they are).
This is what the SVG clipping support looks like:
This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.
Desktop
Chrome
Opera
Firefox
IE
Edge
Safari
4
9
3
9
12
3.2
Mobile / Tablet
iOS Safari
Opera Mobile
Opera Mini
Android
Android Chrome
Android Firefox
3.2
10
all
4.4
67
60
Clipping as a transition
A neat use case for clipping is transition effects. Take The Silhouette Slideshow demo on CodePen:
See the Pen Silhouette zoom slideshow by Mikael Ainalem (@ainalem) on CodePen.
A “regular” slideshow cycles though images. Here, to make it a bit more interesting, there’s a clipping effect when switching images. The next image enters the screen through a silhouette of of the previous image. This creates the illusion that the images are connected to one another, even if they are not.
The transitions follow this process:
Identify the focal point (i.e., main subject) of the image
Create a clipping path for that object
Cut the next image with the path
The cut image (silhouette) fades in
Scale the clipping path until it’s bigger than the viewport
Complete the transition to display the next image
Repeat!
Let’s break down the sequence, starting with the first image. We’ll split this up into multiple pens so we can isolate each step.
For this image, we then want to create a mask of the focal point — in this case, the person’s silhouette. If you’re unsure how to go about creating a clip, check out my previous article for more details because, generally speaking, making cuts in CSS and SVG is fundamentally the same:
Import an image into the SVG editor
Draw a path around the object
Convert the path to the syntax for SVG clip path. This is what goes in the SVG’s <defs> block.
Paste the SVG markup into the HTML
If you’re handy with the editor, you can do most of the above in the editor. Most editors have good support for masks and clip paths. I like to have more control over the markup, so I usually do at least some of the work by hand. I find there’s a balance between working with an SVG editor vs. working with markup. For example, I like to organize the code, rename the classes and clean up any cruft the editor may have dropped in there.
Mozilla Developer Network does a fine job of documenting SVG clip paths. Here’s a stripped-down version of the markup used by the original demo to give you an idea of how a clip path fits in:
Let’s use a colored rectangle as a placeholder for the next image in the slideshow. This helps to clearly visualize the shape that part that’s cut out and will give a clearer idea of the shape and its movement.
Note that this examples is not supported by Firefox at the time of writing because is lacks support for scaling clip paths. I hope this is something that will be addressed in the near future.
Clipping to emerge foreground objects into the background
Another interesting use for clipping is for revealing and hiding effects. We can create parts of the view where objects are either partly or completely hidden making for a fun way to make background images interact with foreground content. For instance, we could have objects disappear behind elements in the background image, say a building or a mountain. It becomes even more interesting when we pair that idea up with animation or scrolling effects.
See the Pen Parallax clip by Mikael Ainalem (@ainalem) on CodePen.
This example uses a clipping path to create an effect where text submerges into the photo — specifically, floating behind mountains as a user scrolls down the page. To make it even more interesting, the text moves with a parallax effect. In other words, the different layers move at different speeds to enhance the perspective.
We start with a simple div and define a background image for it in the CSS:
Don’t pay too much attention to the + 5 used when calculating the distance. It’s only there as a sloppy way to offset the element. The important part is where things are divided by 10, which creates the parallax effect. Scrolling a certain amount will proportionally move the element and the clip path. Template literals convert the calculated value to a string which is used for the transform property value as an offset to the SVG nodes.
Combining clipping and masking
Clipping and masking are two interesting concepts. One lets you cut out pieces of content whereas the other let’s you do the opposite. Both techniques are useful by themselves but there is no reason why we can’t combine their powers!
When combining clipping and masking, you can split up objects to create different visual effects on different parts. For example:
See the Pen parallax logo blend by Mikael Ainalem (@ainalem) on CodePen.
I created this effect using both clipping and masking on a logo. The text, split into two parts, blends with the background image, which is a beautiful monochromatic image of the New York’s Statue of Liberty. I use different colors and opacities on different parts of the text to make it stand out. This creates an interesting visual effect where the text blends in with the background when it overlaps with the statue — a splash of color to an otherwise grey image. There is, besides clipping and masking, a parallax effect here as well. The text moves in a different speed relative to the image when the user hovers or moves (touch) over the image.
To illustrate the behavior, here is what we get when the masked part is stripped out:
See the Pen parallax logo blend by Mikael Ainalem (@ainalem) on CodePen.
Wrapping up
Clipping is a fun way to create interactions and visual effects. It can enhance slide-shows or make objects stand out of images, among other things. Both SVG and CSS provide the ability to apply clip paths and masks to elements, though with different syntaxes. We can pretty much cut any web content nowadays. It is only your imagination that sets the limit.
If you happen to create anything cool with the things we covered here, please share them with me in the comments!
The post Using CSS Clip Path to Create Interactive Effects, Part II appeared first on CSS-Tricks.
I first got the idea to CSS something of the kind when I saw this gradient infinity logo by Infographic Paradise:
The original gradient infinity.
After four hours and some twenty minutes, of which over four hours were spent on tweaking positioning, edges and highlights… I finally had the result below:
My version of the rainbow gradient infinity.
The gradient doesn’t look like in the original illustration, as I chose to generate the rainbow logically instead of using the Dev Tools picker or something like that, but other than that, I think I got pretty close—let’s see how I did that!
Markup
As you’ve probably already guessed from the title, the HTML is just one element:
<div class='∞'></div>
Styling
Deciding on the approach
The first idea that might come to mind when seeing the above would be using conic gradients as border images. Unfortunately, border-image and border-radius don’t play well together, as illustrated by the interactive demo below:
See the Pen by thebabydino (@thebabydino) on CodePen.
Whenever we set a border-image, border-radius just gets ignored, so using the two together is sadly not an option.
So the approach we take here is using conic-gradient() backgrounds and then getting rid of the part in the middle with the help of a mask. Let’s see how that works!
Creating the two ∞ halves
We first decide on an outer diameter.
$do: 12.5em;
We create the two halves of the infinity symbol using the ::before and ::after pseudo-elements of our .∞ element. In order to place these two pseudo-elements next to one another, we use a flex layout on their parent (the infinity element .∞). Each of these has both the width and the height equal to the outer diameter $do. We also round them with a border-radius of 50% and we give them a dummy background so we can see them.
We’ve also placed the .∞ element in the middle of its parent (the body in this case) both vertically and horizontally by using the flexbox approach.
See the Pen by thebabydino (@thebabydino) on CodePen.
How conic-gradient() works
In order to create the conic-gradient() backgrounds for the two haves, we must first understand how the conic-gradient() function works.
If inside the conic-gradient() function we have a list of stops without explicit positions, then the first is taken to be at 0% (or 0deg, same thing), the last is taken to be at 100% (or 360deg), while all those left are distributed evenly in the [0%, 100%] interval.
See the Pen by thebabydino (@thebabydino) on CodePen.
If we have just 2 stops, it’s simple. The first is at 0%, the second (and last) at 100% and there are no other stops in between.
If we have 3 stops, the first is at 0%, the last (third) at 100%, while the second is dead in the middle of the [0%, 100%] interval, at 50%.
If we have 4 stops, the first is at 0%, the last (fourth) at 100%, while the second and third split the [0%, 100%] interval into 3 equal intervals, being positioned at 33.(3)% and 66.(6)% respectively.
If we have 5 stops, the first is at 0%, the last (fifth) at 100%, while the second, third and fourth split the [0%, 100%] interval into 4 equal intervals being positioned at 25%, 50% and 75% respectively.
If we have 6 stops, the first is at 0%, the last (sixth) at 100%, while the second, third, fourth and fifth split the [0%, 100%] interval into 5 equal intervals being positioned at 20%, 40%, 60% and 80% respectively.
In general, if we have n stops, the first is at 0%, the last at 100%, while the ones in between split the [0%, 100%] interval into n-1 eqial intervals spanning 100%/(n-1) each. If we give the stops 0-based indices, then each one of them is positioned at i*100%/(n-1).
For the first one, i is 0, which gives us 0*100%/(n-1) = 0%.
For the last (n-th) one, i is n-1, which gives us (n-1)*100%/(n-1) = 100%.
Here, we choose to use 9 stops which means we split the [0%, 100%] interval into 8 equal intervals.
Alright, but how do we get the stop list?
The hsl() stops
Well, for simplicity, we choose to generate it as a list of HSL values. We keep the saturation and the lightness fixed and we vary the hue. The hue is an angle value that goes from 0 to 360, as we can see here:
Visual representation of the hue scale from 0 to 360 (saturation and lightness being kept constant).
With this in mind, we can construct a list of hsl() stops with fixed saturation and lightness and varying hue if we know the start hue$hue-start, the hue range$hue-range (this is the end hue minus the start hue) and the number of stops$num-stops.
Let’s say we keep the saturation and the lightness fixed at 85% and 57%, respectively (arbitrary values that can probably be tweaked for better results) and, for example, we might go from a start hue of 240 to an end hue of 300 and use 4 stops.
In order to generate this list of stops, we use a get-stops() function that takes these three things as arguments:
We create the list of stops $list which is originally empty (and which we’ll return at the end after we populate it). We also compute the span of one of the equal intervals our stops split the full start to end interval into ($unit).
@function get-stops($hue-start, $hue-range, $num-stops) { $list: (); $unit: $hue-range/($num-stops - 1); /* populate the list of stops $list */ @return $list
}
In order to populate our $list, we loop through the stops, compute the current hue, use the current hue to generate the hsl() value at that stop and then then add it to the list of stops:
@for $i from 0 to $num-stops { $hue-curr: $hue-start + $i*$unit; $list: $list, hsl($hue-curr, 85%, 57%);
}
We can now use the stop list this function returns for any kind of gradient, as it can be seen from the usage examples for this function shown in the interactive demo below (navigation works both by using the previous/next buttons on the sides as well as the arrow keys and the PgDn/ PgUp keys):
See the Pen by thebabydino (@thebabydino) on CodePen.
Note how, when our range passes one end of the [0, 360] interval, it continues from the other end. For example, when the start hue is 30 and the range is -210 (the fourth example), we can only go down to 0, so then we continue going down from 360.
Conic gradients for our two halves
Alright, but how do we determine the $hue-start and the $hue-range for our particular case?
In the original image, we draw a line in between the central points of the two halves of the loop and, starting from this line, going clockwise in both cases, we see where we start from and where we end up in the [0, 360] hue interval and what other hues we pass through.
We start from the line connecting the central points of the two halves and we go around them in the clockwise direction.
To simplify things, we consider we pass through the whole [0, 360] hue scale going along our infinity symbol. This means the range for each half is 180 (half of 360) in absolute value.
Keywords to hue values correspondence for saturation and lightness fixed at 100% and 50% respectively.
On the left half, we start from something that looks like it’s in between some kind of cyan (hue 180) and some kind of lime (hue 120), so we take the start hue to be the average of the hues of these two (180 + 120)/2 = 150.
The plan for the left half.
We get to some kind of red, which is 180 away from the start value, so at 330, whether we subtract or add 180:
So… do we go up or down? Well, we pass through yellows which are around 60 on the hue scale, so that’s going down from 150, not up. Going down means our range is negative (-180).
The plan for the right half.
On the right half, we also start from the same hue in between cyan and lime (150) and we also end at the same kind of red (330), but this time we pass through blues, which are around 240, meaning we go up from our start hue of 150, so our range is positive in this case (180).
As far as the number of stops goes, 9 should suffice.
Now update our code using the values for the left half as the defaults for our function:
@function get-stops($hue-start: 150, $hue-range: -180, $num-stops: 9) { /* same as before */
} .∞ { display: flex; &:before, &:after { /* same as before */ background: conic-gradient(get-stops()); } &:after { background: conic-gradient(get-stops(150, 180)); }
}
And now our two discs have conic-gradient() backgrounds:
See the Pen by thebabydino (@thebabydino) on CodePen.
However, we don’t want these conic gradients to start from the top.
For the first disc, we want it to start from the right—that’s at 90° from the top in the clockwise (positive) direction. For the second disc, we want it to start from the left—that’s at 90° from the top in the other (negative) direction, which is equivalent to 270° from the top in the clockwise direction (because negative angles don’t appear to work from some reason).
Angular offsets from the top for our two halves.
Let’s modify our code to achieve this:
.∞ { display: flex; &:before, &:after { /* same as before */ background: conic-gradient(from 90deg, get-stops()); } &:after { background: conic-gradient(from 270deg, get-stops(150, 180)); }
}
So far, so good!
See the Pen by thebabydino (@thebabydino) on CodePen.
From 🥧 to 🍩
The next step is to cut holes out of our two halves. We do this with a mask or, more precisely, with a radial-gradient() one. This cuts out Edge support for now, but since it’s something that’s in development, it’s probably going to be a cross-browser solution at some point in the not too far future.
Remember that CSS gradient masks are alpha masks by default (and only Firefox currently allows changing this via mask-mode), meaning that only the alpha channel matters. Overlaying the mask over our element makes every pixel of this element use the alpha channel of the corresponding pixel of the mask. If the mask pixel is completely transparent (its alpha value is 0), then so will the corresponding pixel of the element.
See the Pen by thebabydino (@thebabydino) on CodePen.
In order to create the mask, we compute the outer radius $ro (half the outer diameter $do) and the inner radius $ri (a fraction of the outer radius $ro).
$ro: .5*$do;
$ri: .52*$ro;
$m: radial-gradient(transparent $ri, red 0);
We then set the mask on our two halves:
.∞ { /* same as before */ &:before, &:after { /* same as before */ mask: $m; }
}
See the Pen by thebabydino (@thebabydino) on CodePen.
This looks perfect in Firefox, but the edges of radial gradients with abrupt transitions from one stop to another look ugly in Chrome and, consequently, so do the inner edges of our rings.
Close-up of the inner edge of the right half in Chrome.
The fix here would be not to have an abrupt transition between stops, but spread it out over a small distance, let’s say half a pixel:
$m: radial-gradient(transparent calc(#{$ri} - .5px), red $ri);
We now got rid of the jagged edges in Chrome:
Close-up of the inner edge of the right half in Chrome after spreading out the transition between stops over half a pixel.
The following step is to offset the two halves such that they actually form an infinity symbol. The visible circular strips both have the same width, the difference between the outer radius $ro and the inner radius $ri. This means we need to shift each laterally by half this difference $ri - $ri.
.∞ { /* same as before */ &:before, &:after { /* same as before */ margin: 0 (-.5*($ro - $ri)); }
}
See the Pen by thebabydino (@thebabydino) on CodePen.
Intersecting halves
We’re getting closer, but we still have a very big problem here. We don’t want the right part of the loop to be completely over the left one. Instead, we want the top half of the right part to be over that of the left part and the bottom half of the left part to be over that of the right part.
So how do we achieve that?
We take a similar approach to that presented in an older article: using 3D!
In order to better understand how this works, consider the two card example below. When we rotate them around their x axes, they’re not in the plane of the screen anymore. A positive rotation brings the bottom forward and pushes the top back. A negative rotation brings the top forward and pushes the bottom back.
See the Pen by thebabydino (@thebabydino) on CodePen.
Note that the demo above doesn’t work in Edge.
So if we give the left one a positive rotation and the right one a negative rotation, then the top half of the right one appears in front of the top half of the left one and the other way around for the bottom halves.
Addiing perspective makes what’s closer to our eyes appears bigger and what’s further away appears smaller and we use way smaller angles. Without it, we have the 3D plane intersection without the 3D appearance.
Note that both our halves need to be in the same 3D context, something that’s achieved by setting transform-style: preserve-3d on the .∞ element.
.∞ { /* same as before */ transform-style: preserve-3d; &:before, &:after { /* same as before */ transform: rotatex(1deg); } &:after { /* same as before */ transform: rotatex(-1deg); }
}
And now we’re almost there, but not quite:
See the Pen by thebabydino (@thebabydino) on CodePen.
Fine tuning
We have a little reddish strip in the middle because the gradient ends and the intersection line don’t quite match:
Close-up of small issue at the intersection of the two halves.
A pretty ugly, but efficient fix is to add a 1px translation before the rotation on the right part (the ::after pseudo-element):
See the Pen by thebabydino (@thebabydino) on CodePen.
This still isn’t perfect though. Since the inner edges of our two rings are a bit blurry, the transition in between them and the crisp outer ones looks a bit odd, so maybe we can do better there:
A quick fix here would be to add a radial-gradient() cover on each of the two halves. This cover is transparent white for most of the unmasked part of the two halves and goes to solid white along both their inner and outer edges such that we have nice continuity:
$gc: radial-gradient(#fff $ri, rgba(#fff, 0) calc(#{$ri} + 1px), rgba(#fff, 0) calc(#{$ro} - 1px), #fff calc(#{$ro} - .5px)); .∞ { /* same as before */ &:before, &:after { /* same as before */ background: $gc, conic-gradient(from 90deg, get-stops()); } &:after { /* same as before */ background: $gc, conic-gradient(from 270deg, get-stops(150, 180)); }
}
The benefit becomes more obvious once we add a dark background to the body:
See the Pen by thebabydino (@thebabydino) on CodePen.
Now it looks better even when zooming in:
No more sharp contrast between inner and outer edges.
The final result
Finally, we add some prettifying touches by layering some more subtle radial gradient highlights over the two halves. This was the part that took me the most because it involved the least amount of logic and the most amount of trial and error. At this point, I just layered the original image underneath the .∞ element, made the two halves semi-transparent and started adding gradients and tweaking them until they pretty much matched the highlights. And you can see when I got sick of it because that’s when the position values become rougher approximations with fewer decimals.
Another cool touch would be drop shadows on the whole thing using a filter on the body. Sadly, this breaks the 3D intersection effect in Firefox, which means we cannot add it there, too.
See the Pen by thebabydino (@thebabydino) on CodePen.
Spicing it up with animation!
When I first shared this demo, I got asked about animating it. I initially thought this would be complicated, but then it hit me that, thanks to Houdini, it doesn’t have to be!
As mentioned in my previous article, we can animate in between stops, let’s say from a red to a blue. In our case, the saturation and lightness components of the hsl() values used to generate the rainbow gradient stay constant, all that changes is the hue.
For each and every stop, the hue goes from its initial value to its initial value plus 360, thus passing through the whole hue scale in the process. This is equivalent to keeping the initial hue constant and varying an offset. This offset --off is the custom property we animate.
Sadly, this means support is limited to Blink browsers with the Experimental Web Platform features flag enabled.
The Experimental Web Platform features flag enabled in Chrome.
Still, let’s see how we put it all into code!
For starters, we modify the get-stops() function such that the current hue at any time is the initial hue of the current stop $hue-curr plus our offset --off:
Let’s say I told you we can get the results below with just one HTML element and five CSS properties for each. No SVG, no images (save for the background on the root that’s there just to make clear that our one HTML element has some transparent parts), no JavaScript. What would you think that involves?
The desired results.
Well, this article is going to explain just how to do this and then also show how to make things fun by adding in some animation.
CSS-ing the Gradient Rays
The HTML is just one <div>.
<div class='rays'></div>
In the CSS, we need to set the dimensions of this element and we need to give it a background so that we can see it. We also make it circular using border-radius:
And… we’ve already used up four out of five properties to get the result below:
See the Pen by thebabydino (@thebabydino) on CodePen.
So what’s the fifth? mask with a repeating-conic-gradient() value!
Let’s say we want to have 20 rays. This means we need to allocate $p: 100%/20 of the full circle for a ray and the gap after it.
Dividing the disc into rays and gaps (live).
Here we keep the gaps in between rays equal to the rays (so that’s .5*$p for either a ray or a space), but we can make either of them wider or narrower. We want an abrupt change after the ending stop position of the opaque part (the ray), so the starting stop position for the transparent part (the gap) should be equal to or smaller than it. So if the ending stop position for the ray is .5*$p, then the starting stop position for the gap can’t be bigger. However, it can be smaller and that helps us keep things simple because it means we can simply zero it.
How repeating-conic-gradient() works (live).
$nr: 20; // number of rays
$p: 100%/$nr; // percent of circle allocated to a ray and gap after .rays { /* same as before */ mask: repeating-conic-gradient(#000 0% .5*$p, transparent 0% $p);
}
Note that, unlike for linear and radial gradients, stop positions for conic gradients cannot be unitless. They need to be either percentages or angular values. This means using something like transparent 0 $p doesn’t work, we need transparent 0% $p (or 0deg instead of 0%, it doesn’t matter which we pick, it just can’t be unitless).
Gradient rays (live demo, no Edge support).
There are a few things to note here when it comes to support:
Edge doesn’t support masking on HTML elements at this point, though this is listed as In Development and a flag for it (that doesn’t do anything for now) has already shown up in about:flags. The Enable CSS Masking flag in Edge.
conic-gradient() is only supported natively by Blink browsers behind the Experimental Web Platform features flag (which can be enabled from chrome://flags or opera://flags). Support is coming to Safari as well, but, until that happens, Safari still relies on the polyfill, just like Firefox. The Experimental Web Platform features flag enabled in Chrome.
WebKit browsers still need the -webkit- prefix for mask properties on HTML elements. You’d think that’s no problem since we’re using the polyfill which relies on -prefix-free anyway, so, if we use the polyfill, we need to include -prefix-free before that anyway. Sadly, it’s a bit more complicated than that. That’s because -prefix-free works via feature detection, which fails in this case because all browsers do support mask unprefixed… on SVG elements! But we’re using mask on an HTML element here, so we’re in the situation where WebKit browsers need the -webkit- prefix, but -prefix-free won’t add it. So I guess that means we need to add it manually:
$nr: 20; // number of rays
$p: 100%/$nr; // percent of circle allocated to a ray and gap after
$m: repeating-conic-gradient(#000 0% .5*$p, transparent 0% $p); // mask .rays { /* same as before */ -webkit-mask: $m; mask: $m;
}
I guess we could also use Autoprefixer, even if we need to include -prefix-free anyway, but using both just for this feels a bit like using a shotgun to kill a fly.
Adding in Animation
One cool thing about conic-gradient() being supported natively in Blink browsers is that we can use CSS variables inside them (we cannot do that when using the polyfill). And CSS variables can now also be animated in Blink browsers with a bit of Houdini magic (we need the Experimental Web Platform features flag to be enabled for that, but we also need it enabled for native conic-gradient() support, so that shouldn’t be a problem).
In order to prepare our code for the animation, we change our masking gradient so that it uses variable alpha values:
And we animate it from the CSS using a keyframe animation:
.rays { /* same as before */ animation: p .5s linear infinite alternate
} @keyframes p { to { --p: #{$p} } }
The result is more interesting in this case:
Alternating ray size animation (live demo, only works in Blink browsers with the Experimental Web Platform features flag enabled).
But we can still spice it up a bit more by flipping the whole thing horizontally in between every iteration, so that it’s always flipped for the reverse ones. This means not flipped when --p goes from 0% to $p and flipped when --p goes back from $p to 0%.
The way we flip an element horizontally is by applying a transform: scalex(-1) to it. Since we want this flip to be applied at the end of the first iteration and then removed at the end of the second (reverse) one, we apply it in a keyframe animation as well—in one with a steps() timing function and double the animation-duration.
$t: .5s; .rays { /* same as before */ animation: p $t linear infinite alternate, s 2*$t steps(1) infinite;
} @keyframes p { to { --p: #{$p} } } @keyframes s { 50% { transform: scalex(-1); } }
Now we finally have a result that actually looks pretty cool:
Alternating ray size animation with horizontal flip in between iterations (live demo, only works in Blink browsers with the Experimental Web Platform features flag enabled).
CSS-ing Gradient Rays and Ripples
To get the rays and ripples result, we need to add a second gradient to the mask, this time a repeating-radial-gradient().
How repeating-radial-gradient() works (live).
$nr: 20;
$p: 100%/$nr;
$stop-list: #000 0% .5*$p, transparent 0% $p;
$m: repeating-conic-gradient($stop-list), repeating-radial-gradient(closest-side, $stop-list); .rays-ripples { /* same as before */ mask: $m;
}
Sadly, using multiple stop positions only works in Blink browsers with the same Experimental Web Platform features flag enabled. And while the conic-gradient() polyfill covers this for the repeating-conic-gradient() part in browsers supporting CSS masking on HTML elements, but not supporting conic gradients natively (Firefox, Safari, Blink browsers without the flag enabled), nothing fixes the problem for the repeating-radial-gradient() part in these browsers.
This means we’re forced to have some repetition in our code:
$nr: 20;
$p: 100%/$nr;
$stop-list: #000, #000 .5*$p, transparent 0%, transparent $p;
$m: repeating-conic-gradient($stop-list), repeating-radial-gradient(closest-side, $stop-list); .rays-ripples { /* same as before */ mask: $m;
}
We’re obviously getting closer, but we’re not quite there yet:
Intermediary result with the two mask layers (live demo, no Edge support).
To get the result we want, we need to use the mask-composite property and set it to exclude:
Note that mask-composite is only supported in Firefox 53+ for now, though Edge should join in when it finally supports CSS masking on HTML elements.
XOR rays and ripples (live demo, Firefox 53+ only).
If you think it looks like the rays and the gaps between the rays are not equal, you’re right. This is due to a polyfill issue.
Adding in Animation
Since mask-composite only works in Firefox for now and Firefox doesn’t yet support conic-gradient() natively, we cannot put CSS variables inside the repeating-conic-gradient() (because Firefox still falls back on the polyfill for it and the polyfill doesn’t support CSS variable usage). But we can put them inside the repeating-radial-gradient() and even if we cannot animate them with CSS keyframe animations, we can do so with JavaScript!
Because we’re now putting CSS variables inside the repeating-radial-gradient(), but not inside the repeating-conic-gradient() (as the XOR effect only works via mask-composite, which is only supported in Firefox for now and Firefox doesn’t support conic gradients natively, so it falls back on the polyfill, which doesn’t support CSS variable usage), we cannot use the same $stop-list for both gradient layers of our mask anymore.
But if we have to rewrite our mask without a common $stop-list anyway, we can take this opportunity to use different stop positions for the two gradients:
// for conic gradient
$nc: 20;
$pc: 100%/$nc;
// for radial gradient
$nr: 10;
$pr: 100%/$nr;
The CSS variable we animate is an alpha --a one, just like for the first animation in the rays case. We also introduce the --c0 and --c1 variables because here we cannot have multiple positions per stop and we want to avoid repetition as much as possible:
$m: repeating-conic-gradient(#000 .5*$pc, transparent 0% $pc) exclude, repeating-radial-gradient(closest-side, var(--c0), var(--c0) .5*$pr, var(--c1) 0, var(--c1) $pr); body { --a: 0; /* layout, backgrounds and other irrelevant stuff */
} .xor { /* same as before */ --c0: #{rgba(#000, var(--a))}; --c1: #{rgba(#000, calc(1 - var(--a)))}; mask: $m;
}
The alpha variable --a is the one we animate back and forth (from 0 to 1 and then back to 0 again) with a little bit of vanilla JavaScript. We start by setting a total number of frames NF the animation happens over, a current frame index f and a current animation direction dir:
const NF = 50; let f = 0, dir = 1;
Within an update() function, we update the current frame index f and then we set the current progress value (f/NF) to the current alpha --a. If f has reached either 0 of NF, we change the direction. Then the update() function gets called again on the next refresh.
(function update() { f += dir; document.body.style.setProperty('--a', (f/NF).toFixed(2)); if(!(f%NF)) dir *= -1; requestAnimationFrame(update)
})();
And that’s all for the JavaScript! We now have an animated result:
Ripple alpha animation, linear (live demo, only works in Firefox 53+).
This is a linear animation, the alpha value --a being set to the progress f/NF. But we can change the timing function to something else, as explained in an earlier article I wrote on emulating CSS timing functions with JavaScript.
For example, if we want an ease-in kind of timing function, we set the alpha value to easeIn(f/NF) instead of just f/NF, where we have that easeIn() is:
function easeIn(k, e = 1.675) { return Math.pow(k, e)
}
The result when using an ease-in timing function can be seen in this Pen (working only in Firefox 53+). If you’re interested in how we got this function, it’s all explained in the previously linked article on timing functions.
The exact same approach works for easeOut() or easeInOut():
function easeOut(k, e = 1.675) { return 1 - Math.pow(1 - k, e)
}; function easeInOut(k) { return .5*(Math.sin((k - .5)*Math.PI) + 1)
}
Since we’re using JavaScript anyway, we can make the whole thing interactive, so that the animation only happens on click/tap, for example.
In order to do so, we add a request ID variable (rID), which is initially null, but then takes the value returned by requestAnimationFrame() in the update() function. This enables us to stop the animation with a stopAni() function whenever we want to:
/* same as before */ let rID = null; function stopAni() { cancelAnimationFrame(rID); rID = null
}; function update() { /* same as before */ if(!(f%NF)) { stopAni(); return } rID = requestAnimationFrame(update)
};
On click, we stop any animation that may be running, reverse the animation direction dir and call the update() function:
addEventListener('click', e => { if(rID) stopAni(); dir *= -1; update()
}, false);
Since we start with the current frame index f being 0, we want to go in the positive direction, towards NF on the first click. And since we’re reversing the direction on every click, it results that the initial value for the direction must be -1 now so that it gets reversed to +1 on the first click.
The result of all the above can be seen in this interactive Pen (working only in Firefox 53+).
We could also use a different alpha variable for each stop, just like we did in the case of the rays:
In the update() function, the only difference from the first animated demo is that we don’t change the value of just one CSS variable—we now have four to take care of: --a0, --a1, --a2, --a3. We do this within a loop, using the ease-in function for the ones at even indices and the ease-out function for the others. For the first two, the progress is given by f/NF, while for the last two, the progress is given by 1 - f/NF. Putting all of this into one formula, we have:
(function update() { f += dir; for(var i = 0; i < 4; i++) { let j = ~~(i/2); document.body.style.setProperty( `--a${i}`, TFN[i%2 ? 'ease-out' : 'ease-in'](j + Math.pow(-1, j)*f/NF).toFixed(2) ) } if(!(f%NF)) dir *= -1; requestAnimationFrame(update)
})();
The result can be seen below:
Multiple ripple alpha animations (live demo, only works in Firefox 53+).
Just like for conic gradients, we can also animate the stop position between the opaque and the transparent part of the masking radial gradient. To do so, we use a CSS variable --p for the progress of this stop position:
The JavaScript is almost identical to that for the first alpha animation, except we don’t update an alpha --a variable, but a stop progress --p variable and we use an ease-in-out kind of function:
/* same as before */ function easeInOut(k) { return .5*(Math.sin((k - .5)*Math.PI) + 1)
}; (function update() { f += dir; document.body.style.setProperty('--p', easeInOut(f/NF).toFixed(2)); /* same as before */
})();
Alternating ripple size animation (live demo, only works in Firefox 53+).
We can make the effect more interesting if we add a transparent strip before the opaque one and we also animate the progress of the stop position --p0 where we go from this transparent strip to the opaque one:
In the JavaScript, we now need to animate two CSS variables: --p0 and --p1. We use an ease-in timing function for the first and an ease-out for the second one. We also don’t reverse the animation direction anymore:
const NF = 120, TFN = { 'ease-in': function(k, e = 1.675) { return Math.pow(k, e) }, 'ease-out': function(k, e = 1.675) { return 1 - Math.pow(1 - k, e) } }; let f = 0; (function update() { f = (f + 1)%NF; for(var i = 0; i < 2; i++) document.body.style.setProperty(`--p${i}`, TFN[i ? 'ease-out' : 'ease-in'](f/NF); requestAnimationFrame(update)
})();
This gives us a pretty interesting result:
Double ripple size animation (live demo, only works in Firefox 53+).
The post 1 HTML Element + 5 CSS Properties = Magic! appeared first on CSS-Tricks.