Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Tue, 03 Dec 2024 20:50:57 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.3 225069128 Pure CSS Halftone Effect in 3 Declarations https://frontendmasters.com/blog/pure-css-halftone-effect-in-3-declarations/ https://frontendmasters.com/blog/pure-css-halftone-effect-in-3-declarations/#comments Tue, 03 Dec 2024 20:50:56 +0000 https://frontendmasters.com/blog/?p=4594 About half a decade ago, I got an idea about how to create a halftone effect with pure CSS. My original idea (which Michelle Barker wrote about a couple of years ago) was a bit inefficient, but in the years that followed, I’ve managed to polish it and reduce it to a single <div>, no pseudos and just three CSS properties.

What’s a halftone effect?

If you don’t know what a halftone effect is, a very basic pattern looks like this:

The simplest possible halftone pattern

This is what we’ll be creating with a single <div> (no pseudo-elements) and only three CSS declarations. Afterwards, we’ll go through a bunch of variations and see some cooler-looking demos.

The 3 CSS Declarations

The first declaration is a background and it consists of two layers. One is the pattern – the dots in our most basic case. The other is the map – this decides where the dots are bigger and where they are smaller. In the most simple case, it’s a linear gradient. So what we have so far in terms of code looks like this:

background: 
  radial-gradient(closest-side, #000, #fff) 0/ 1em 1em space, 
  linear-gradient(90deg, #000, #fff);

We’ve made sure we have an integer number of dots along both axes by using the space value for background-repeat.

Taken separately, the two layers look like this:

the pattern and the map

Before we move any further, let’s take a closer look at these gradients. Each of the two layers goes from black, which can also be written as rgb(0%, 0%, 0%) or hsl(0, 0%, 0%) to white, which can also be written as rgb(100%, 100%, 100%) or hsl(0, 0%, 100%).

Dead in the middle we have grey, which is rgb(50%, 50%, 50%) or hsl(0, 0%, 50%). This is the 50% lightness grey or, in short, as we’ll be calling it from now on, the 50% grey.

Note that in the case of any grey, wherever it may be situated in between black and white, the saturation (the ‘S’ in HSL) is always 0%, while the hue (the ‘H’ in HSL) is irrelevant, so we just use 0. The only value that changes is the lightness (the ‘L’ in HSL), which goes from 0% for black to 100% for white.

Basically, going from 0% to 100% along the gradient line means going from 0% to 100% along the lightness axis of the HSL bicone.

HSL bicone slice showing the lightness axis (live demo)

So in general, any p% grey can be written as rgb(p%, p%, p%) or hsl(0, 0%, p%).

This can be seen in the interactive demo below where you can drag the bar along the entire lightness range.

Going back to our background with the pattern dots layer on top of the linear-gradient() map layer, we cannot see the map layer because it’s fully covered by the pattern layer. So the next step is to blend these two background layers using the multiply blend mode.

This means the second declaration is:

background-blend-mode: multiply

This works on a per pixel, per channel basis. We consider each layer to be a grid of pixels, we take every pair of corresponding pixels from the two layers and, for each of the three RGB channels, we multiply the corresponding channel values.

blending two layers at a pixel level

So for each pair of pixels, the result of this blending operation is an RGB value where each channel value is the result of multiplying the corresponding channel values from the two layers.

R = R₀·R₁
G = G₀·G₁
B = B₀·B₁

Note that what we’re multiplying is the decimal representation of percentage RGB values – that is, numbers in the [0, 1] interval. And when multiplying values in this interval, the result is always smaller or equal to the smallest of the two values multiplied.

In our case, both gradients go from black to white, all we have in between are greys, which have all three RGB channels equal. So if at some point, both pixels in the pair of corresponding ones from the two layers have rgb(50%, 50%, 50%), then the result of the multiply blend mode is .25 = .5·.5 for each channel.

We can see that the result of the multiply blend mode is always at least as dark as the darker of the two pixels whose RGB values we multiply. This is because the two RGB values are in the [0, 1] interval and, as mentioned before, multiplying such values always gives us a result that’s at most as big as the smallest of the two numbers multiplied. The smaller the channel values are, the darker the grey they represent is.

After blending our pattern and map layers, we can see how overall, the pattern dots are now darker on the left where the map is closer to black.

our two gradient layers, blended

Below, you can see two scaled up dots from different points along the gradient line of the map. The second dot is further to the right (lighter) than the first one. The dark red circles mark the 50% grey limit for each.

darker dot vs. lighter dot

For the darker dot, the 50% grey limit is a bigger circle than in the case of the lighter dot. Inside each dark red circle, we have greys darker than a 50% one. Outside, we have greys lighter than a 50% one. Keep this in mind for later.

The third and final declaration is a filter using a large contrast() value.

For those not familiar with how contrast() works, it does one of two things, depending on whether its argument is subunitary or not.

If its argument is subunitary, then it pushes every channel value towards .5 , the middle of the [0, 1] interval. A value of 1 means no change, while a value of 0 means the channel has been pushed all the way to .5.

This means that contrast(0) always gives us a 50% grey, regardless of the filter input.

You can see this in the interactive demo below – regardless of whether we apply our filter on a plain solid background box, opaque or semitransparent, a gradient or an image one, dragging the contrast down to 0 always turns it into a 50% grey with the same alpha as the input.

Note that contrast(100%) is the same as contrast(1)contrast(50%) is the same as contrast(.5) and so on.

If the argument of the contrast() function is greater than 1 however, then each channel value gets pushed towards either 0 or 1, whichever is closer. A contrast large enough can push the channel values all the way to 0 or 1.

If we have a large enough contrast, all channel values are either zeroed (0%) or maxed out (100%) meaning we can only get one of eight possible results.

8 possible RGB values where all channels are either zeroed or maxed out

Coming back to our halftone pattern, we use:

filter: contrast(16)

Here, all greys darker than a 50% one (grey or rgb(50%, 50%, 50%) or hsl(0, 0%, 50%)) get pushed to black and all the others to white.

Now remember how the 50% grey limit was a bigger circle if the dot was darker? That’s our limit for the contrast.

Inside that circle, we have greys darker than a 50% one, so they get pushed to black by large contrast vales. Outside it, the greys are lighter than a 50% one, so they get pushed to white by large contrast values.

Since the darker the dot, the bigger the 50% limit circle, this means the halftone dots in the darker area of the map are bigger.

So here’s the result we get after the third and final declaration:

the result so far

We’re starting to get somewhere, but what we have so far is not ideal. And it makes sense we aren’t there yet.

Since the left half of the map is darker than a 50% grey (the RGB channel values are below 50% or .5 in decimal representation of the percentage), blending any other layer with it using the multiply blend mode gives us a result that’s at least as dark.

This means the result of blending across the entire left half is a grey darker than a 50% one, so that large value contrast pushes everything in the left half to black.

The fix for this is pretty straightforward: we don’t make our gradients go all the way from black to white, but rather from mid greys to white. Furthermore, for best results, the map at its darkest should be a little bit brighter than a 50% grey, while the pattern can be a bit darker.

background: 
  radial-gradient(closest-side, #777, #fff) 0/ 1em 1em space, 
  linear-gradient(90deg, #888, #fff);

Much better!

Now one thing to note here is that the contrast value needs to be enough to compensate for the blur radius of our dots. So if we increase the pattern size (the background-size for the pattern layer), then we also need to increase the contrast value accordingly.

Let’s say we increase the background-size from 1em to 9em.

bigger dots, blurry edges

The dot edges are now blurry, so we also increase the contrast value from 16 to let’s say 80.

increased contrast, jagged edges

Unfortunately, this results in ugly edges.

A fix for this would be to then chain a slight blur and a contrast that’s enough to offset it. Generally, a contrast value that’s 2-3 times the blur value in pixels works pretty well.

filter: contrast(80) blur(2px) contrast(5)
tiny blur + contrast smoothing fix

An even better fix would involve using a custom SVG filter, but SVG filters are outside the scope of this article, so we’re not going there.

Variations

Now that we’ve gone through the basics, we can start making things more interesting in order to get a lot of cool results by varying at least one of the pattern or map layers.

background: 
  var(--pattern, radial-gradient(closest-side, #777, #fff) 0/ 1em 1em space)),
  var(--map, linear-gradient(90deg, #888, #fff));
  background-blend-mode: multiply;
  filter: contrast(16)

Pattern variations

In this part, we’re keeping the map gradient unchanged and keeping the same hex values for the pattern gradients, though the pattern gradients themselves change. Depending on the pattern, we might also adjust the contrast.

If you search for halftone patterns online, you’ll see that most of them don’t show a straight grid like we had above. So let’s fix that with a pattern made up of two layers.

--dot: radial-gradient(closest-side, #777, #fff calc(100%/sqrt(2)));
--pattern: var(--dot) 0 0/ 2em 2em, var(--dot) 1em 1em/ 2em 2em

In practice, I’d probably use a variable instead of 2em and compute the offsets for the second layer of dots to be half of that.

real halftone dots pattern

Also, since we’ve increased the size of the dots, we’ve also bumped up the contrast value from 16 to 24.

Another option would be to use a repeating-radial-gradient().

--pattern: repeating-radial-gradient(circle, #777, #fff, #777 1em)
halftone ripples

Something like this can even be animated or made interactive. We can place these halftone ripples at var(--x) var(--y) and change these custom properties on mousemove.

We don’t have to limit ourselves to radial gradients. Linear ones work just as well. We can use a repeating-linear-gradient(), for example:

--pattern: repeating-linear-gradient(#777, #fff, #777 1em)
thinning lines

We can also animate the gradient angle (like in the demo below on hover) or make it change as we move the cursor over the pattern.

We can also restrict the background-size of a linear-gradient():

--pattern: linear-gradient(45deg, #fff, #777) 0 / 1em 1em
triangles

Just like for the first dots pattern variation, here we’ve also bumped up the contrast.

We can also add one extra stop:

--pattern: linear-gradient(45deg, #fff, #777, #fff) 0 / 1em 1em
fragments

For both of the previous ones, the gradient angle can also be animated. This can be seen on hovering the panels in the demo below.

We can also play with conic gradients here. A simple repeating one produces rays that are thicker on the left than on the right.

--pattern: repeating-conic-gradient(#777, #fff, #777 2.5%)

Without any filter adjustment however, the edges of these rays look bad, and so does the middle.

rays, but with ugly edges

Using the tiny blur plus a contrast value that’s 2-3 times the blur tactic fixes the ray edges:

smooth ray edges, but faded pattern edges

… but the pattern’s edges are now faded! We have two possible fixes here.

The first would be to remove the filter from the element itself and apply it on another element stacked on top of it as a backdrop-filter.

The second would be to make the element extend outwards a bit using a negative margin and then clip its edges by the same amount using inset().

Things get a lot more fun if we limit the background-size of such a conic-gradient() pattern and then play with the start angle --a and the end percentage --p.

--pattern: 
  repeating-conic-gradient(var(--a), 
    #fff, #777, #fff var(--p)) 0/ 3em 3em

Map variations

In this part, we’re keeping the pattern constant and trying out different maps.

Our linear-gradient() map doesn’t necessarily need to go along the x axis – it can of course have a variable angle:

--map: linear-gradient(var(--a), #888, #fff)

The demo below shows this angle being animated on hover:

We can also add an extra stop:

--map: linear-gradient(var(--a), #fff, #888, #fff)

Again, hovering the demo below animates the map direction.

We can also make our gradient a repeating one:

--map: 
  repeating-linear-gradient(var(--a), #fff, #888, #fff var(--p))

Or we can switch to a radial-gradient():

--map: 
  radial-gradient(circle at var(--x) var(--y), #888, #fff)

In the demo below, the radial gradient’s position follows the cursor:

The radial gradient can be a repeating one too:

--map: 
  repeating-radial-gradient(circle at var(--x) var(--y), 
    #fff, #888, #fff var(--p))

Same thing goes for conic gradients.

--map: 
  conic-gradient(from var(--a) at var(--x) var(--y), 
    #fff, #888, #fff)

We can use a repeating one and control the number of repetitions as well.

--map: 
  repeating-conic-gradient(from var(--a) at var(--x) var(--y), 
    #fff, #888, #fff var(--p))

One thing that bugs me about some of the map variation demos, particularly about this last one, is the dot distortion. We can make it look less bad by sizing the element with the halftone background such that both its dimensions are multiples of the dot size and change the position in increments of the same dot size.

--d: 1em;
--pattern: 
  radial-gradient(closest-side, #777, #fff) 
    0/ var(--d) var(--d);
--map: 
  repeating-conic-gradient(from var(--a) 
    at round(var(--x), var(--d)) round(var(--y), var(--d)), 
    #fff, #888, #fff var(--p));
width: round(down, 100vw, var(--d));
height: round(down, 100vh, var(--d));

But it’s not enough. In order for our dots to always be perfectly round, we need an SVG filter solution. However, that’s outside the scope of this article, so we’re not discussing it here.

Even more interestingly, our map can be an image too. Taking any random image as it is won’t work well.

using a random image as it is for the map doesn’t work well

We need to bring its saturation down to zero and, for this particular technique, we need to make sure the lightness of its pixels is pretty much in the [50%, 100%] interval.

The filter() function could help here, but, sadly, for almost a decade now, Safari has remained the only browser implementing it. We could make the pattern and the map layer each be a pseudo of an element, blend them together and apply the contrast filter on the pseudo-elements’ parent. This way, the map pseudo could have a filter applied on it too. However, here we’re looking for solutions that don’t involve extra elements or pseudo-elements.

Something we can do is make the map be the result of multiple blended background layers. Making the background-color any grey and blending it with the map image using the luminosity blend mode gives us a result that has the luminosity of the map image on top, the saturation of the background-color below and, since this is a grey (its saturation is 0%), the hue becomes irrelevant.

Note that luminosity is not the same as lightness (which is the ‘L’ in HSL), though in a lot of cases, they’re close enough.

--pattern: 
  radial-gradient(closest-side, #777, #fff) 0/ 1em 1em space;
--map: url(my-image.jpg) 50%/ cover grey;
background: var(--pattern), var(--map);
background-blend-mode: 
  multiply /* between pattern & map */, 
  luminosity /* between map layers */;
filter: contrast(16)

We seem to be going in the right direction.

using a fully desaturated map obtained via blending

But it’s still not what we want, as this desaturated map is too dark, just like the first black to white map gradient we tried.

We can brighten our map using the screen blend mode. Think of this blend mode as being the same as multiply, only with the ends of the lightness interval reversed. multiply always produces a result that’s at least as dark as the darkest of its two inputs, screen always produces a result that’s at least as bright as the brightest of its two inputs.

In our case, if we use screen to blend the desaturated image we got at the previous step with a midway grey like #888, then the result is always at least as bright as #888. And it is #888 only where we blend it with pure black pixels. Wherever we blend it with pixels brighter than pure black, the result is brighter than #888. So basically, we get a map that’s #888 at its darkest, just like our base map gradient.

--pattern: 
  radial-gradient(closest-side, #777, #fff) 0/ 1em 1em space;
--map: 
  conic-gradient(#888 0 0), 
  url(my-image.jpg) 50%/ cover
  grey;
background: var(--pattern), var(--map);
background-blend-mode: 
  multiply /* between pattern & map */, 
  screen /* between map layers */, 
  luminosity /* between map layers */;
filter: contrast(16)

Much better!

using a fully desaturared and brightened map via blending (live demo)

Again, some of the dots aren’t fully round, but in order to get fully round dots, we’d need an SVG filter and that’s a way too big of a topic to discuss here.

Palette variations

The simplest possible variation would be having white halftone dots on a black background. To do this, we can simply chain invert(1) to our filter.

Or… we can do something else! We can use the screen blend mode we’ve used before to brighten the image map. As mentioned, this works like multiply, but with the ends of the lightness interval reversed. So let’s reverse them for both the pattern and the map.

background: 
  var(--pattern, 
    radial-gradient(closest-side, #888, #000) 0/ 1em 1em space), 
  var(--map, 
    linear-gradient(90deg, #777, #000));
background-blend-mode: screen;
filter: contrast(16)
inverted halftone dots pattern (live demo)

But we’re not limited to just black and white.

Remember the part about how contrast works? Large contrast values push all pixels of the filter input to one of 8 possible RGB values. So far, our filter input has been just greys, so they got pushed to either black or white. But we don’t necessarily need to have just greys there. We could tweak those values to either zero or max out a channel or two everywhere.

For example, if we max out one of the channels, then our black dots get that channel added to them. Maxing out the red channel gives us red dots, maxing out the blue channel gives us blue dots, maxing out both the red and blue channels gives us magenta dots.

Going the other way, if we zero one of the channels, then it gets subtracted out of the white background. Zeroing the blue channel gives us a yellow background (the red and green channels are still maxed out for the background and combined, they give yellow). Zeroing the red channel gives us a cyan background. Zeroing both the blue and green channels gives us a red background.

You can play with various scenarios in the interactive demo below:

We can of course also have more interesting palettes and we can even have halftone dots on top of image backgrounds using the pure CSS blending technique I detailed in a talk on the topic I used to give in 2020 or by using SVG filters. Both of these approaches however require more than just one element with no pseudos and three CSS properties, so we won’t be going into details about them here.

Combining these variations (and more!)

Varying more than one of the above can help with interesting results.

By using top to bottom linear gradients for both the pattern and the map, with the pattern one having its size limited to 10% of the element, we can get the effect below without needing to use a mask gradient with many irregulrly placed stops. Blending with some extra layers helps us with a nicer palette for the final result.

We can also animate a map’s background-position to get a blinds effect like below:

In the demo above, we’ve also blended the halftone pattern with an image. Here’s another such example (note that this doesn’t work in Firefox due to bug 1481498, which has everything to do with the text on the right side and nothing to do with the halftone part):

card with halftone effect (live demo)

Note that the code for all these demos so far is heavily commented, explaining the purpose of pretty much every CSS declaration in there.

The example below uses a repeating-radial-gradient() pattern and a conic-gradient() map, which funny enough, also creates a tiny heart in the middle.

For a bit of a different effect, here’s a rhombic halftone one created by using two blended layers for the map – two otherwise identical linear gradients going in different directions:

The demo below is a combination of two halftone patterns stacked one on top of the other, the top one being masked using a conic-gradient() checkerboard mask.

Here are a few more halftone samples as card backgrounds:

Even more such halftone samples can be found in this gallery:

We aren’t limited to 2D. We can also use such paterns in 3D and even animate them.

excavated cube with animated halftone (live demo)

Finally, even more demos showcasing halftone patterns can be found in this CodePen collection:

the CodePen collection

]]>
https://frontendmasters.com/blog/pure-css-halftone-effect-in-3-declarations/feed/ 8 4594
Creating Wavy Circles with Fancy Animations in CSS https://frontendmasters.com/blog/creating-wavy-circles-with-fancy-animations/ https://frontendmasters.com/blog/creating-wavy-circles-with-fancy-animations/#comments Fri, 15 Mar 2024 14:36:46 +0000 https://frontendmasters.com/blog/?p=1252 In a previous article, we created flower-like shapes using modern CSS (mask, trigonometric functions, etc). This article is a follow-up where we will create a similar shape and also introduce some fancy animations.

Article Series

Here is a demo of what we are building. Hover the image to see the animation

Cool right? If you check the HTML tab you won’t see a lengthy code structure. A single image element is all that we will be using to build that complex-looking effect. Don’t look at the CSS for now and let’s build this together.

You can also take a look at my online generator where you can easily generate the CSS code for those wavy circles.

Creating The Shape

It’s highly recommended that you read the previous article because I will be building on top of it. I will be reusing many tricks and the starting point of this article will be the last demo of the previous one.

And here is a figure to remind you the mask composition used to create the shape.

As you can see, a set of small circles is used in the “subtract” composition to create the inner curves, and another set of small circles is used in the “add” composition to create the outer curves. The idea is to move those circles in opposite directions to create and control our wavy circle.

Here is another figure to illustrate the trick

the differentd colored circles make the blob shape, and as they move around the blob changes shape.

The [1] above illustrates the initial shape where all the small circles are aligned in a way to create a bigger circle while touching each other. The red circles are the excluded ones and the blue circles are the added ones.

In [2] above we make the blue circles closer to the center while moving the red ones in the opposite direction. The result is weird because the circles no longer touch each other but if we increase their radius, we get a perfect result.

The idea is to move the circles and at the same time adjust their radius so they are always touching each other. Here is an interactive demo to better understand the movement of the circles. Use the slider to control the position of the circles.

Let’s write some code

Now that we have the geometry of shape in place, let’s translate this into code. It wasn’t an easy task at all. Finding the right formulas and translating everything into a CSS code was a bit tricky.

The first challenge is to find one variable that allows me to control the position of the small circles and at the same time their radius. I could have used multiple variables but having only one variable will make the shape easy to control as we only have to update one value and everything else will follow.

Initially, I thought about using a length variable which is logical since the radius can be expressed as a length and to move the circles I need a distance which is also a length. I wasn’t able to follow that root because finding the formulas and expressing them using CSS was almost impossible. Instead of a length I had to rely on an angle variable. It may sound wrong but it was indeed the right way to do it as I was able to find most of the formulas and write them using CSS.

Here is a figure to illustrate the angle I am referring to.

update to the angle between circles.

Let’s take two adjacent circles and draw a line between their center (illustrated in white). The [1] shows the initial shape where all the circles are perfectly aligned around the big circle. This will be the initial state so let’s consider we have an angle equal to 0deg. When we move the circles and get the shape in [2] the line will rotate a little and the angle of rotation will be our variable.

Don’t worry, I won’t start a boring geometry course. I just wanted to highlight the variable you need to adjust so you can visualize why it’s an angle and the angle of what. By the way, in the last interactive demo, you are adjusting that angle using the range slider.

Here is the full demo where you can play with the different values to control the shape.

If you want to dig into the math, check this question on StackExchange Mathematics. I struggled with some of the formulas so I had to ask some math gurus.

Introducing The Image Element

Let’s now consider an <img> element instead of a <div>.

It works fine but our goal is to have the image within the shape like the first example. To do this, we add some padding (or border) to leave space for the background and also add border-radius to round the image.

Now it’s perfect! I am using a padding value equal to .4*var(--r) but there is no particular logic behind it. It’s what gives me something that looks good to me. Feel free to update it if you want to increase or decrease the space around the image.

Adding The Animation

Let’s move to the interesting part which is the animation. We will first adjust the shape on hover and this is the easiest part because we already did the hard job by finding one variable to control the shape. All we have to do is to update that variable on hover.

img {
  --a: 28deg;
}
img:hover {
  --a: 10deg;
}

The above will simply change the shape but will not give us a smooth animation it is because by default we cannot animate CSS variables (custom properties). To do this we need to register them using @property.

@property --a {
  syntax: "<angle>";
  inherits: true;
  initial-value: 0deg;
}

Then add a transition like below

img {
  transition: --a .3s
}

Now we have a perfect hover effect!

Let’s tackle the rotation. It’s clear that we cannot rotate the whole element as the image needs to remain straight and only the shape needs to rotate. To do this, we will introduce a new angle variable and use it within the formulas that define the position of the circles.

If you look at the Sass loop that generates the code of the circles, you will notice that the increment $i is used to define an angle, and this angle is used to correctly place each circle. If we update that angle, we update the position. The idea is to update the angle of all the circles with the same value so they all move the same way to simulate a rotation.

In the demo above, you will see I am registering a new variable and applying an animation to it

@property --o {
  syntax: "<angle>";
  inherits: true;
  initial-value: 0deg;
}
img {
  animation: rotate 20s infinite linear;
}
@keyframes rotate {
  to { --o: 360deg; }
}

Then I am using that variable within the Sass loop to update the angle of the circles. Instead of 360deg*#{$i/$n}, we use 360deg*#{$i/$n} + var(--o) and instead of (360deg*#{$i} + 180deg)/#{$n} we use (360deg*#{$i} + 180deg)/#{$n} + var(--o).

The final touch is to increase the speed of the rotation on hover. For this, I am going to introduce the animation-composition property. It’s a pretty new propery so you may have not heard about it, but it’s a powerful property that I invite you to explore. Plus the browser support is pretty good.

I will update the code of the animation like below:

img {
  animation: 
    rotate 20s infinite linear,
    rotate 20s infinite linear paused;
  animation-composition: add
}
img:hover {
  animation-play-state: running;
}

I am applying the same animation twice while making the second one paused. On hover, both animations are running. Let’s have a look at the definition of animation-composition: add

The animation-composition CSS property specifies the composite operation to use when multiple animations affect the same property simultaneously.

Then:

add
The effect value builds on the underlying value of the property. This operation produces an additive effect.

We are using the same animation so we are affecting the same property (the variable --o) and the use of add will create an additive effect. It means that the element will rotate faster when both animations are running.

Try it!

The concept of animation-composition is not easy to grasp at first glance, but imagine that you add an animation on the top of another one. The first animation is rotating the element then we add another animation that will also rotate the element. If you rotate an element that is already rotating then you get a faster rotation. We can also decrease the rotation speed using the same technique. This time, the second animation needs to apply an opposite rotation using the reverse keywords.

Conclusion

We are done! We created a complex-looking effect using only CSS and a single HTML element. It was a good opportunity to explore modern CSS features such as mask, trigonometric functions, @property, etc

You are probably thinking it’s a bit too much, right? What’s the point of introducing all this complexity for a fancy effect that you will probably never use? The goal is not really to build the effect and use it but to push the limit of CSS and explore new features. In the end, you have a bunch of CSS tricks that you can use elsewhere.

We learned how to use mask-composite. We learned how to animate CSS variables using @property. We played with gradients. We discovered the animation-composition property. etc. One day, you will for sure need to use those CSS tricks!

Article Series

]]>
https://frontendmasters.com/blog/creating-wavy-circles-with-fancy-animations/feed/ 1 1252
Fading Out Text with Mask https://frontendmasters.com/blog/fading-out-text-with-mask/ https://frontendmasters.com/blog/fading-out-text-with-mask/#respond Thu, 25 Jan 2024 23:02:29 +0000 https://frontendmasters.com/blog/?p=656 If you’re going to fade out some text, I’m gonna call it and say the best way, while also being the easiest, is using a mask. Kilian Valkhof covered this recently, rejecting the idea of an overlay gradient, and also offering the idea of using background-clip. But mask is the clear winner. Let’s get that sucker un-prefixed already!

.fade-out-text {
  -webkit-mask-image: linear-gradient(to bottom, white, transparent);
  mask-image: linear-gradient(to bottom, white, transparent);
}

The gradient technique is such a classic though. I remember playing with that over a decade ago. Here’s a proper demo of masking though:

]]>
https://frontendmasters.com/blog/fading-out-text-with-mask/feed/ 0 656