Frontend Masters Boost RSS Feed https://frontendmasters.com/blog Helping Your Journey to Senior Developer Wed, 14 Aug 2024 22:50:07 +0000 en-US hourly 1 https://wordpress.org/?v=6.6.1 225069128 How to Get the Width/Height of Any Element in Only CSS https://frontendmasters.com/blog/how-to-get-the-width-height-of-any-element-in-only-css/ https://frontendmasters.com/blog/how-to-get-the-width-height-of-any-element-in-only-css/#respond Thu, 25 Jul 2024 14:14:28 +0000 https://frontendmasters.com/blog/?p=3119 Getting the dimension of an element using JavaScript is a trivial task. You barely even need to do anything. If you have a reference to an element, you’ve got the dimensions (i.e. el.offsetWidth / el.offsetHeight). But we aren’t so lucky in CSS. While we’re able to react to elements being particular sizes with @container queries, we don’t have access to a straight up number we could use to, for example, display on the screen.

It may sound impossible but it’s doable! There are no simple built-in functions for this, so get ready for some slightly hacky experimentation.

Note: At time of writing, only Chrome (and Edge) have the full support of the features we will be using so consider those browsers to read the article.

Let’s start with a demo:

This demo has a simple layout with elements that will all have different sizes. Each rectangular element displays it’s own width/height. You can resize the browser or adjust the content; the values will update automatically.

Don’t try to find the hidden JavaScript, it’s 100% CSS magic, powered mostly by scroll-driven animations.

Why Scroll-Driven Animations?

Scroll-Driven animations is one of the most popular new CSS features in 2024. It unlocked a lot of possibilities and solved some common problems.

How are these features relevant to this situation of figuring out an element’s dimensions, though?

The terms “scroll” and “animation” tend to bring to mind, uhh, animating stuff on scroll. To be fair, that is the main purpose:

It allows you to animate property values based on a progression along a scroll-based timeline instead of the default time-based document timeline. This means that you can animate an element by scrolling a scrollable element, rather than just by the passing of time.

MDN

But we can think about it differently and achieve more than a simple animation on scroll. If you keep reading the MDN page, it explains there are two types of “scroll-based timelines”. In our case, we will consider the “view progress timeline”.

You progress this timeline based on the change in visibility of an element (known as the subject) inside a scroller. The visibility of the subject inside the scroller is tracked as a percentage of progress.

MDN

With this type of scroll timeline, there are three relevant elements: the scroller which is the container having the scroll, the subject which is an element moving inside the container and the animation that will progress based on the position of the “subject” inside the “scroller”.

The three elements are linked with each other. To identify the progress of the animation we need to know the position of the subject inside the scroller and for this, we need to know the dimension of the scroller, the dimension of the subject, and the offset of the subject (the distance between the subject and the edges of the scroller).

So our equation contains four variables:

  1. Dimension of the scroller
  2. Dimension of the subject
  3. Progress of the animation
  4. Offset of the subject

If three variables are known, we can automatically find the missing one. In our case, the missing variable will be the “dimension of scroller” and that’s how we are going to find the width/height of any element (an element that will a be scroller).

How Does it Work?

Let’s dive into the theory and get to how scroll-driven animations are actually used to do this. It won’t be long and boring, I promise! I’ll be using width as the dimension being measured, but height would use the same logic just on the other axis.

Consider the following figure:

We have a container (the scroller) and an element inside it (the subject) placed on the left. There are two special positions within the container. The 0% position is when the element is at the right (inside the container) and the 100% position is when the element has exited the container from the left (outside the container).

The movement of the subject between 0% and 100% will define the percentage of the progression but our element will not move so the percentage will be fixed. Let’s call it P. We also know the width of the subject and we need to find the width of the scroller.

Remember the variables we talked about. Considering this configuration, we already know three of them: “the width of the subject”, “the offset of the subject” (fixed to the left edge), and the “progress of the animation” (since the subject is fixed). To make things easier, let’s consider that the width of the scroller is a multiplier of the width of the subject:

W = N * S.

The goal is to find the N or more precisely, we need to find the relation between the P and N. I said the P is fixed, but in reality it’s only fixed when the scroller width is fixed which is logical. But if the width of the scroller changes, the progress will also change, that’s why we need to find the formula between the progress and the width.

Let’s start with the case where the width of the scroller is equal to twice the width of the subject, we get the following:

The subject is in the middle between 0% and 100% so the progress in this case is 50%. For N = 2 we get P = 50%.

Let’s try for N = 3:

Now we have two extra slots in addition to the 0% and 100%. If we suppose that the subject can only be placed inside one of the 4 slots, we can have the following progress: 0%33.33%66.67%100%. But the subject is always placed at the before-the-last slot so the progress in this case is equal to 66.67% or, seen differently, it’s equal to 100% - 100%/3 (100%/3 is the progression step).

Are you seeing the pattern? If the width of the scroller is equal to N times the width of the subject we will have N+1 slots (including 0% and 100%) so the step between each slot is equal to 100%/N and the subject is located at the before-the-last slot so the progress is equal to 100% - 100%/N.

We have our equation: P = 100% - 100%/N so N = 100%/(100% - P).

If we convert the percentage to values between 0 and 1 we get N = 1/(1 - P) and the width we are looking for is equal to W = N * S = S/(1 - P).

Now If we consider a width for the subject equal to 1px, we get W = 1px/(1 - P) and without the unit, we have W = 1/(1 - P).

Let’s Write Some Code

Enough theory! Let’s transform all this into code. We start with this structure:

<div class="container"></div>
.container {
  overflow: auto;
  position: relative;
}
.container:before {
  content: "";
  position: absolute;
  left: 0;
  width: 1px;
}

The scroller element is the container and the subject element is a pseudo-element. I am using position: absolute so the subject doesn’t affect the width of the container (the value we need to calculate). Like described in the previous section, it’s placed at the left of the container with 1px of width.

Next, we define a named timeline linked to the pseudo-element (the subject)

.container {
  timeline-scope: --cx;
}
.container:before {
  view-timeline: --cx inline
}

The MDN description of the property:

The view-timeline CSS shorthand property is used to define a named view progress timeline, which is progressed through based on the change in visibility of an element (known as the subject) inside a scrollable element (scroller). view-timeline is set on the subject.

We consider the inline (horizontal) axis. We need to also use timeline-scope to give the container access to the view progress. By default, a named timeline is scoped to the element where it’s defined (and its descendants) but we can change this to make it available at any level.

Why not define the scope at the html level, then?

Enlarging the scope to all the elements may sound like a good idea, but it’s not. We may need to use the same code for different elements so limiting the scope allows us to reuse the same code and keep the same naming.

I won’t spend too much time detailing the scope feature but don’t forget about it. If the code doesn’t work as intended, it’s probably a scoping issue.

Now let’s define the animation:

@property --x {
  syntax: "<number>";
  inherits: true;
  initial-value: 0; 
}
.container {
  animation: x linear;
  animation-timeline: --cx;
  animation-range: entry 100% exit 100%; 
}
@keyframes x {
  0%   { --x: 0; }
  100% { --x: 1; }
}

We define a keyframes that animates a variable from 0 to 1. We have to register that variable with a number type to be able to animate it. We run the animation on the container with a linear easing and define the timeline using animation-timeline.

At this step, we told the browser to consider the named timeline defined on the pseudo-element (the subject) as the reference for the animation progress. And that progress will be stored in the --x variable. At 50%, we have --x: 0.5, at 70%, we have --x: 0.7, and so on.

The last step is to add the formula we identified earlier:

@property --w {
  syntax: "<integer>";
  inherits: true;
  initial-value: 0; 
}
.container {
  --w: calc(1/(1 - var(--x)));
}

The --w variable will contain the width in pixel of the container as a unitless value. It’s important to notice the “unitless” part. It gives us a lot of flexibility as we can integrate it within any formula. If you are a CSS hacker like me, you know what I mean!

What about that animation-range: entry 100% exit 100%;?

In addition to using a named timeline to define which element control the progress, we can also control the range of the animation. In other words, we can explicitly define where the 0% and 100% progress are located within the timeline.

Let’s get back to the first figure where I am showing the 0% and 100% progress.

The 0% is when the subject has completely entered the scroller from the right. We can express this using animation-range-start: entry 100%.

The 100% is when the subject has completely exited the scroller from the left. We can express this using animation-range-end: exit 100%.

Or using the shorthand:

animation-range: entry 100% exit 100%;

If you are new to scroll-driven animations, this part is not easy to grasp, so don’t worry if you don’t fully understand it. It requires some practice to build a mental model for it. Here is a good online tool that can help you visualize the different values.

Now, we do the same for the height and we are done. Here is the first demo again so you can inspect the full code.

Notice that I am using another pseudo-element to show the values. Let’s consider this as our first use case. Being able to get the width/height of any element and show them using only CSS is super cool!

.size::after {
  content: counter(w) "x" counter(h);
  counter-reset: w var(--w) h var(--h);
}

Are There Any Drawbacks?

Even if it seems to work fine, I still consider this as a “hack” to be used with caution. I am pretty sure it will fail in many situations so don’t consider this as a robust solution.

I also said “any element” in the title but in reality not all of them. It’s mandatory to be able to have a child element (the subject) so we cannot apply this trick to elements like <img> for example.

You also need to add overflow: auto (or hidden) to the container to make it the scroller for the subject. If you plan to have overflowing content then this solution will give you some trouble.

The value you will get using this method will include the padding but not the border! Pay attention to this part and compare the values you get with the ones of the Dev tools. You may need to perform another calculation to get the real dimension of the element by adding or subtracting specific amounts.

Another drawback is related to the use of 1px as our unit. We assumed that the size is a multiplier of 1px (which is true in most cases) but if your element is having a size like 185.15px, this trick won’t work. We can overcome this by using a smaller width for the subject (something like 0.01px) but I don’t think it is worth making this hack more complex.

A Few Use Cases

The first use case we saw is to show the dimension of the element which is a cool feature and can be a good one for debugging purposes. Let’s dig into more use cases.

Getting the Screen Dimension

We already have the viewport units vh and vw that works fine but this method can give us the unitless pixel values. You may ask how to do this since the viewport is not a real element. The solution is to rely on position: fixed applied to any element on the page. A fixed element is positioned relative to the viewport so its scroller will the viewport.

If you check the code, you will see that I am relying on the HTML pseudo-element for the subject and I don’t need to define any overflow or position on the HTML element. Plus the values are available globally since they are defined inside the HTML element!

For this particular case, I also have another CSS trick to get the screen dimension with an easier method:

Calculating the Scrollbar Width

There is a slight difference between the two screen width calculating methods above. The first demo will not include the scrollbar width if the page has a lot of content but the second one will. This means that If we combine both methods we can get the width of scrollbar!

Cool right? In addition to the screen dimension, you can also have the width of the scrollbar. Both values are available at root level so you can use them anywhere on the page.

Counting Stuff

All the calculations we did were based on the 1px size of the subject. If we change this to something else we can do some interesting counting. For example, if we consider 1lh (the height of the line box) we can count the number of lines inside a text.

Here is the version where you can edit the content. The number of lines will adjust based on the content you will enter.

Note how I am playing with the scope in this example. I am making the variable available at a higher level to be able to show the count inside a different element. Not only we can count the numbers of lines but we can also show the result anywhere on the page.

Can you think about something else to count? Share your example in the comment section.

Transferring Sizes

Being able to control the scope means that we can transfer the size of an element to another one on the page.

Here is an example where resizing the left element will also resize the right one!

Another important part of this trick is being able to get the width/height values as integer. This allows us to use them within any formula and append any unit to them.

Here is an example, where resizing the left element will rotate/scale the right one.

I have mapped the width with the rotation and the height with the scaling. Cool right? We can get the width/height of an element, have them as an integer, and transfer them to another element to do whatever we want. CSS is magic!

Conclusion

I hope you enjoyed this funny experiment. I still insist on the fact that it’s a hacky workaround to do something that was not possible using CSS. Use it for fun, use it to experiment with more CSS-only ideas but think twice before including this into a real project. Using one line of JavaScript code to get the dimension of an element is safer. Not all CSS-only tricks are a good replacement for JavaScript.

This said, if you find an interesting use case or you have another CSS-only experimentation where this trick can be useful, share it in the comment section.

I will end this article with a last demo where I am transforming the native progress element into a circular one. Can you figure out how it works? I am using the same technique. This time, I know both the width of the scroller and the subject and the missing variable is the progress. Try to dissect the code as homework 😜.

]]>
https://frontendmasters.com/blog/how-to-get-the-width-height-of-any-element-in-only-css/feed/ 0 3119
How Keyboard Navigation Works in a CSS Game https://frontendmasters.com/blog/how-keyboard-navigation-works-in-a-css-game/ https://frontendmasters.com/blog/how-keyboard-navigation-works-in-a-css-game/#comments Mon, 08 Jul 2024 14:34:58 +0000 https://frontendmasters.com/blog/?p=2936 We’re going to build a “Super CSS Mario” game where you can use a keyboard and the arrow keys to move Mario around. Go ahead and play it to check it out. Note there is no JavaScript at all, it’s just HTML and CSS.

Why bother with a CSS-only game?

Creating a CSS-only game is a fun exercise. Restricting yourself to only HTML & CSS allows you to discover and unlock CSS trickery you can add to your toolbox. That’s what happens for me!

You might think that limiting yourself to CSS for all the functionality of the game is useless. CSS is not designed for this sort of thing, it’s for layouts and design control. But doing unusual and unexpected things in CSS is a great way to practice, and will lead to a deeper understanding of the language, making you a better CSS developer all around.

Interactivity without a Mouse

Many pure CSS games you will see around are playable mostly with a mouse. They rely on interactive elements such as checkboxes and pseudo-classes like :hover:active, :checked, and so on. But with recent CSS features, a keyboard control game (beyond tabbing) is also doable using CSS!

Cool right? Stay with me if you want to know the secret behind creating this game (and a few others at the end).

At the time of writing this, only Chrome (and Edge) have the full support of the features we will be using so consider those browsers to read the article.

For the sake of simplicity, I will skip the aesthetic parts. I will mainly focus on the techniques used to build the game. The code of demos used in the article may differ slightly from the real code used in the game.

Let’s start with this basic setup:

We have a container with an overflowing content that will trigger both the vertical and horizontal scrolling. Nothing fancy so far but let’s not forget that, in addition to the mouse, we can scroll the container using the direction keys. Try it! Click the container (above) then use the keyboard to scroll inside it.

Now let’s add two more elements inside the overflowing div to have the following code:

<div class="container">
  <div>
    <div class="game">
      <div class="mario"></div>
    </div>
  </div>
</div>

Then we make the .game element sticky so it doesn’t move when we scroll the container:

The magic touch now is to introduce scroll-driven animations to move our character. We can scroll the outer container but everything else stays fixed. By adding scroll-driven animations we can control the movement of Mario as we want.

It may sound tricky, but the code is pretty simple:

.mario {
  position: relative;
  top: 0%;
  left: 0%;
  animation: 
    x linear,
    y linear;
  animation-timeline: 
    scroll(nearest inline),
    scroll(nearest block);
}
@keyframes x { to { left: 100% } }
@keyframes y { to { top: 100%  } }

We have two animations. Each controls the movement in one direction (horizontal or vertical). Then we link them with the scrolling of the outer container. The x animation will follow the “inline” scroll (horizontal) and the y animation will follow the ”block” scroll (vertical).

In other words, the scrolling will define the progress of the animation. Try it:

We have our keyboard control!

We can still use the mouse to manipulate the scrollbars but if we hide them, the illusion is perfect!

.container {
  scrollbar-width: none;
}

You still need to click inside the container to get the focus before using the keyboard. I add the tabindex attribute to the main container so you can get the focus using the “tab” key as well.

The game can be playable using only the keyboard. Here is the link for the full game again to test it. Either use the mouse or click “tab” to start the game.

Adding The Coins

Could we add coins on the screen and then have Mario “collect” them when they touch? Well… no, not really. CSS does not have “collision detection” (yet?). So let’s fake it!

Since we’re controlling the location of Mario with animations, we can know where he is located. We are going to rely on this information to simulate collision detection between Mario and the coins.

To start, let’s place a coin inside the game board:

<div class="container">
  <div>
    <div class="game">
      <div class="mario"></div>
      <div class="coin"></div>
    </div>
  </div>
</div>

And style it like this:

.coin {
  position: absolute;
  inset: 0;
}
.coin:before {
  content: "";
  position: absolute;
  width: 50px;
  left: calc(50% - 25px);
  top: calc(50% - 25px);
  aspect-ratio: 1;
}

The .coin container will fill the whole area of .game (we will see later why) and its pseudo-element is the visible coin:

The coin is placed at the center and to reach the center Mario needs to scroll half the distance vertically and horizontally which means it needs to reach half the x and y animations.

We use this information to define new animations that we link to the coin element like this:

.coin {
  animation: 
    c-x linear,
    c-y linear;
  animation-timeline: 
    scroll(nearest inline),
    scroll(nearest block);
}
@keyframes c-x {
  0% , 44%  {--c-x: 0}
  45%, 55%  {--c-x: 1}
  56%, 100% {--c-x: 0}
}
@keyframes c-y {
  0% , 44%  {--c-y: 0}
  45%, 55%  {--c-y: 1}
  56%, 100% {--c-y: 0}
}

This is the same animation configuration we used for Mario. One animation is linked to the horizontal scroll and another one is linked to the vertical scroll. Each animation will control a variable that will be either 0 or 1 based on the keyframes percentage.

The coin is placed at the center so we need the variable to turn 1 when the animation is around 50%. I am considering an offset of 5% to illustrate the idea but in the real code, I am using more accurate values.

Now, we will introduce another CSS feature: style queries. It allows us to conditionally apply specific CSS based on the value of custom properties (CSS variables). Style queries require a parent-child relation, so that’s why the real coin is the pseudo element of .coin container.

.coin {
  container-name: c;
}
@container c style(--c-x: 1) and style(--c-y: 1) {
  .coin:before {
     /* do what you want here */
  }
}

The previous animations will make both variables equal to 1 at 50% (when Mario is at the center) and the style query will apply a specific CSS when both variables are equal to 1.

In the below example, when Mario is above the coin, a red background will appear. We have our collision detection!

As I said previously, this is not super accurate. I am keeping this simple to illustrate the idea. In the real code, I am using more precise calculations to get a perfect collision detection.

What we did until now is good but we need better. The red color is only visible when Mario touches the coin but we need a way to maintain this state. In other words, if it turns red, it should stay red.

To achieve this, we have to introduce another animation and update the code like below:

.coin {
  container-name: c;
}
.coin:before {
  animation: touch .1s forwards linear var(--s, paused);
}
@keyframes touch {
  1%, 100% { background-color: red; }
}
@container c style(--c-x: 1) and style(--c-y: 1) {
  .coin:before {
     --s: running
  }
}

We define an animation that is “paused” initially and when the condition is met we make it “running”. I am using a small duration and a forwards configuration to make sure the red color stays even when Mario moves away from the coin.

With this configuration, we can add an animation that makes the coin disappear (instead of just the color change).

To add more coins, we add more .coin elements with different positions and animations. If you check the real code of the game you will find that I am defining different variables and using Sass to generate the code. I am using a grid system where I can control the number of columns and rows and I am defining another variable for the number of coins. Then with the help of the random() function from Sass I can randomly place the coins inside the grid.

The important thing to notice is how the HTML code is organized. We don’t do the following:

<div class="container">
  <div>
    <div class="game">
      <div class="mario"></div>
      <div class="coin"></div>
      <div class="coin"></div>
      <div class="coin"></div>
      ...
    </div>
  </div>
</div>

But rather the following:

<div class="container">
  <div>
    <div class="game">
      <div class="mario"></div>
      <div class="coin">     
        <div class="coin">
          <div class="coin">
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

The .coin elements should not be siblings but nested inside each other. I need this configuration to later calculate the score. For this reason, a .coin element needs to take the whole area of the game to make sure its descendants will have access to the same area and we can easily place all the coins following the same code structure.

There is probably a way to make the game work by having the .coin elements as siblings but I didn’t focus on the HTML structure too much.

Calculating The Score

To calculate the score, I will add a last element that should also be nested within all the .coin elements. The nested configuration is mandatory here to be able to query all the .coin elements.

<div class="container">
  <div>
    <div class="game">
      <div class="mario"></div>
      <div class="coin">     
        <div class="coin">
          <div class="coin">
           <div class="result"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

And here is the Sass code to illustrate how to calculate the score:

.result {
  $anim: ();
  @for $i from 1 to ($c+1) {
    --r#{$i}:0; 
    $anim: append($anim,'r#{$i} .1s forwards var(--s-r#{$i},paused)',comma);
    @container c#{$i} style(--c#{$i}-x: 1) and style(--c#{$i}-y: 1) {
       --s-r#{$i}: running
    }
    @keyframes r#{$i} {1%,to {--r#{$i}:1}}
  }
  $sum: ("var(--r1)");
  @for $i from 2 to ($c+1) {
    $sum: append($sum,'+ var(--r#{$i})' , space);
  }
  --sum: calc(#{$sum});
  animation: #{$anim};
}

For each coin, I will define one animation, one container query, and one @keyframe.

Notice how the configuration is similar to the one we used previously. When Mario touches the coin (--ci-x and --ci-y are equal to 1) we run an animation that will update the variable --ri from 0 to 1 and will maintain its value. In other words, a variable is incremented from 0 to 1 when a coin is touched and we have as many variables as coins in the game.

Then we define another variable that is the sum of all of them. That variable will contain the score of the game. To show the score we combine that variable with a counter and we rely on a pseudo-element like the below:

.result:before {
  content: "SCORE - " counter(r);
  counter-reset: r var(--sum);
}

Each time Mario collects a coin, the counter is reset with a new value, and the score is updated.

The Final Screen

To end the game, I will also rely on the sum variable and a style query. We test if the sum is equal to the number of coins. If that’s the case, we update some of the CSS to show the final screen.

The code will look like the below:

.result {
  container-name: r;
}
@container r style(--sum: #{$c}) {
  .result:after {
    /* the CSS of the final screen */
  }
}

For this style query, it’s important to register the sum variable using @property so that the browser can correctly evaluate its value and compare it with the number of coins.

@property --sum {
  syntax: '<integer>';
  inherits: true;
  initial-value: 0
}

We don’t need to do this with the other variables as there is no calculation to be done. They are either equal to 0 or 1.

What about the timer?

I deliberately skipped that part to make it your homework. The timer is closely related to the final screen and I let you dissect the original code to see how it works (when it starts, when it stops, etc). You will see that it’s the easiest part of the game. It’s also an opportunity to inspect the other parts of the code that I skipped.

We are done! Now, you know the secret behind my “Super CSS Mario” game. With a clever combination of scroll-driven animations and style queries, we can create a CSS-only game playable with keyboard navigation.

Take the time to digest what you have learned so far before moving to the next sections. I will share with you two more games but I will get faster with the explanation since the techniques used are almost the same. If you are struggling with some of the concepts, give it another read.

Super CSS Mario II

Let’s update the previous game and increase its difficulty by adding some enemies. In addition to collecting the coins, you need to also avoid the Goombas. Play “Super CSS Mario II”

Adding enemies to the game may sound tricky but it’s pretty easy since touching them will simply stop the game. The enemies will share the same code structure as the coins. The only difference is that all of them will control one variable. If one enemy is touched, the variable is updated from 0 to 1 and the game ends.

The HTML code looks like below:

<div class="container">
  <div>
    <div class="game">
      <div class="mario"></div>
      <div class="coin">     
        <div class="coin">
          <div class="coin">
            <div class="enemy">
              <div class="enemy">
                <div class="result"></div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Like the coins, I need to keep the nested structure to have the parent-child relation.

For the CSS code, I will have the following for the .result element.

.result {
  $anim: append($anim,'enemy .1s forwards var(--eeee,paused)',comma);
  --ee: 0;
  @for $i from 1 to ($e+1) {
    @container e#{$i} style(--e#{$i}-x: 1) and style(--e#{$i}-y: 1) {
      --eeee: running
    }
  }
  @keyframes enemy {1%,to {--ee:1}}
}

In addition to the previous animations defined for each coin, we add an extra animation that will control the variable --ee. All the style queries of the enemies will update the same animation which means if one of them is touched the variable will be equal to 1.

Then, for the final screen, we will have two conditions. Either the sum reaches the number of coins and you win or the enemy variable is equal to 1 and it’s a game over!

.result {
  container-name: r;
}
@container r style(--sum: #{$c}) {
  .result:after {
    /* you win */
  }
}
@container r style(--ee: 1) {
  .result:after {
    /* game over */
  }
}

Here is the Pen to see the full Sass code.

A CSS-only Maze game

One more game? Let’s go! This time it’s a maze game where the character needs to grab an object without touching the wall of the maze. Click to play the maze game.

The cool part about this game is that we have discrete movements, unlike the previous ones. It makes the game more realistic and similar to those retro games we enjoyed playing. The wall and the Dino are similar to the enemies and the coins of the previous game so I won’t detail them. I will focus on the movement and let you dissect the code of the other parts alone (here is the Pen).

Let’s start with the following demo:

Press the bottom arrow key to scroll and you will notice that the value will increment by a specific amount (it’s equal to 40 for me). If you keep tapping a lot of times, the value will keep increasing by the same amount.

This demonstrates that one click will always move the scroll by the same amount (as long as you don’t keep the key pressed). This information is what I need to create the discrete movement. If the game didn’t work well for you then it’s probably related to that value. In the Pen, you can update that value to match the one you get from the previous demo.

Now let’s suppose we want a maze with 10 columns and 5 rows. It means that we need 9 clicks to reach the last column and 4 clicks to reach the last row. The horizontal overflow needs to be equal to 360px=(40px*9) while the vertical overflow needs to be equal to 160px=(40px*4).

Let’s turn this into a code:

<div class="container">
  <div></div>
</div>
.container {
  width: 500px;  /* 50px * 10 */
  height: 250px; /* 50px * 5  */
}
.container div {
  width:  calc(100% + 40px*9);
  height: calc(100% + 40px*4);
}

The 50px I am using is an arbitrary value that will control the size of the grid.

Try to scroll the container using the keyboard and you will notice that you need exactly 9 clicks horizontally and 4 clicks vertically to scroll the whole content.

Then we can follow the same logic as the Mario game (the sticky container, the character, etc) but with a small difference: the x and y animations will animate integer variables instead of the top and left properties.

@property --x {
  syntax: '<integer>';
  inherits: true;
  initial-value: 0
}
@property --y {
  syntax: '<integer>';
  inherits: true;
  initial-value: 0
}
.character {
  width: 50px; /* the same value used to control the size of the grid */
  position: absolute;
  translate: calc(var(--x)*100%) calc(var(--y)*100%);
  animation: x linear,y linear;
  animation-timeline: scroll(nearest inline),scroll(nearest block);
}
@keyframes x { to { --x: 9 } }
@keyframes y { to { --y: 4 } }

We have a discrete keyboard movement using only CSS! Not only that, but thanks to the variable --x and --y we can know where our character is located within the grid.

You know the rest of the story, we apply style queries on those variables to know if the character hit a wall or if it reaches the Dino! I let you dissect the code as a small exercise and why not update it to create your own maze version? It could be a fun exercise to practice what we have learned together. Fork it and share your own maze version in the comment section.

Conclusion

I hope you enjoyed this CSS experimentation. It’s OK if you were a bit lost at times and didn’t fully understand all the tricks. What you need to remember is that scroll-driven animations allow us to link the scrolling progress to any kind of animation and style queries allow us to conditionally apply any kind of CSS based on the value of custom properties (CSS variables). Everything else depends on your creativity. I was able to create “Super CSS Mario” and a maze game but I am pretty sure you could do even better.

One day, someone will create a fully playable FPS using only CSS. Keyboard to move the character and mouse to kill enemies. Why not, nothing is impossible using CSS!

]]>
https://frontendmasters.com/blog/how-keyboard-navigation-works-in-a-css-game/feed/ 1 2936
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.

a ring of circles uses "exclude" to knock them out, then "intersect" to make a cog like shape then "add" to make the final blob shape.

As you can see, a set of small circles is used in the “exclude” 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
Creating Flower Shapes using CSS Mask & Trigonometric Functions https://frontendmasters.com/blog/creating-flower-shapes-using-css-mask-trigonometric-functions/ https://frontendmasters.com/blog/creating-flower-shapes-using-css-mask-trigonometric-functions/#respond Thu, 29 Feb 2024 15:04:12 +0000 https://frontendmasters.com/blog/?p=1021 Creating unusual shapes is always a fun exercise and a good way to practice your CSS skills. One might argue that SVG is better for this job, but nowadays we have a lot of new CSS tricks that allow us to create shapes with a clean and optimized code. Through this two-article series, we will explore what can be done with CSS nowadays.

Article Series

In this article, we are going to create flower-like shapes. We are going to rely on modern features like mask, trigonometric functions, CSS variables, etc.

four flower-like shapes with purple to pink gradients. two of them have petals and two of them have spikes. two of them are filled and two of them are outlined.

Before we start, you can take a look at my online generator for flower shapes to get an overview of what we are building here. You can easily define your settings and get the CSS code in no time. Some of the code we will be writing can be complex so it’s always good to have a generator to make our life easy. That said I invite you to keep reading to understand the logic behind the code you are copying and be able to tweak it if needed. 

The Geometry of A Flower Shape

The structure of the flower shape can be seen as small circles placed around a bigger one. 

Consider that the small circles touch each other without overlapping each other. This will make the calculation a bit challenging as we will need accurate formulas. Turns out this is a perfect use case for trigonometric functions in CSS.

The shape can be controlled using 2 variables; the number of small circles (N) and their radius (R). From there we can identify the size of the whole shape and the radius of the big circle.

Here is a figure to illustrate some of the values and from where we can extract the different formulas.

geometry of the circles showing the radius comparisons.

I won’t start a boring geometry course so I will jump straight to the formulas we need to use. The size of the element is equal to

2 * (X + R) = 2 * (R/sin(180deg/N) + R) = 2 * R * (1 + 1/sin(180deg/N))

and the radius of the big circle is equal to

C  = R/tan(180deg/N)

We have all that we need to start writing some code.

Coding The Shape

The main challenge is to rely on a single element. We are not going to consider a complex HTML structure where each circle is a different element. Instead, we will only use one element (and no pseudo-elements either!)

I mentioned mask, we’ll be using that, and gradients will allow us to do the shape drawing we want to do. Since it’s all about circles we are going to use radial-gradient(). We will also use a bit of Sass (for the looping feature) to control the number of circles. The number of gradients will depend on the number of circles and with Sass we can write a loop to generate the needed gradients.

Let’s start by setting the different variables and the shape size:

$n: 12; /* the number of circles/petals */

.flower {
  --r: 30px; /* the radius of the small circles */
  width: calc(2*var(--r)*(1 + 1/sin(180deg/#{$n})));
  aspect-ratio: 1;
  mask: radial-gradient(#000 calc(var(--r)/tan(180deg/#{$n})),#0000 0);
  background: /* you background coloration */
}

Nothing fancy so far, we simply translated the previous formulas using code. I also added the big circle so all that we are missing is the small ones. The Sass code to generate them will look like this:

$m: (); /* empty variable */
@for $i from 1 through ($n) { /* loop through the number of circles*/
  $m: append($m, 
    radial-gradient(50% 50%,#000 100%,#0000) no-repeat
    x y / calc(2*var(--r)) calc(2*var(--r)), 
  comma);
}

--r defines the radius so the size of each gradient will be equal to calc(2*var(--r)). Then we need to identify the position of each gradient (the x y).

Here as well, we need to consider some geometry formulas

x = calc(50% + 50%*cos(360deg*#{$i/$n})) 
y = calc(50% + 50%*sin(360deg*#{$i/$n}))

The final code will be:

$n: 6; /* the number of circles/petals */

.flower {
  --r: 30px; /* the radius of the small circles */
  width: calc(2*var(--r)*(1 + 1/sin(180deg/#{$n})));
  aspect-ratio: 1;
  $m: (); /* empty variable */
  @for $i from 1 through ($n) { /* loop through the number of circles*/
    $m: append($m, 
      radial-gradient(50% 50%,#000 100%,#0000) no-repeat
      calc(50% + 50%*cos(360deg*#{$i/$n})) 
      calc(50% + 50%*sin(360deg*#{$i/$n}))
     / calc(2*var(--r)) calc(2*var(--r)), 
    comma);
  }
  mask: radial-gradient(#000 calc(var(--r)/tan(180deg/#{$n})),#0000 0),{$m};
  background: /* you background coloration */
}

Note how the mask property takes the value generated using Sass in addition to the gradient that creates the big circle.

Our shape is done!

The size of the shape is controlled by the radius of the small circles but we can do the opposite which is probably more convenient since we generally want to control the width/height of our element.

.flower {
  --w: 200px; /* the size */
  --r: calc(var(--w)/(2*(1 + 1/sin(180deg/#{$n}))));
  width: var(--w);
  /* same as before */
}

We can even optimize the previous code a little and get rid the of --w variable. The latter is defining the width/height of the element and gradients can access such value using percentages we can write the code like below:

$n: 12; /* the number of circles/petals */

.flower {  
  width: 300px; /* the size */
  --r: calc(50%/(1 + 1/sin(180deg/#{$n}))); /* using percentage instead of --w */
  aspect-ratio: 1;
  $m: (); /* empty variable */
  @for $i from 1 through ($n) { /* loop through the number of circles*/
    $m: append($m, 
      radial-gradient(50% 50%,#000 100%,#0000) no-repeat
      calc(50% + 50%*cos(360deg*#{$i/$n})) 
      calc(50% + 50%*sin(360deg*#{$i/$n}))
     / calc(2*var(--r)) calc(2*var(--r)), 
    comma);
  }
  mask: radial-gradient(100% 100%,#000 calc(var(--r)/tan(180deg/#{$n})),#0000 0),#{$m};
}

Now by adjusting the width you control the size of the whole shape. Here is an interactive demo where you can resize the element and see how the shape adjusts automatically. Try it below with the resizer handle on the bottom right of the box:

Voilà! We did a nice flower shape without hack or complex code and you can easily control it by adjusting a few variables. You either use the above code or you consider my online generator to get the generated CSS without the variables and Sass.

Inverting the shape

Let’s try a different shape this time. It’s somehow the invert of the previous one where the circular part is going inside instead of outside. Well, a figure worth a thousand words.

spiky flower shape with purple to orange coloring

The code to get the above shape is almost the same as the previous one. We are going to introduce an extra gradient and play with mask-composite. The idea is to cut the small circles from the bigger one.

Here is a figure to illustrate the process:

multiple masking techniques used, exclude and include, to come to the final shape

We will perform two compositions. For the first one, we consider a gradient that fills the whole area and the set of small circles. We “exclude” one from another to get a shape where the small circles are now holes. In other words, the small circles are now the invisible part of our shape.

From there, we consider the gradient that creates the big circle and we do an “intersect” this time to show only the middle area and get our final shape.

The code of mask will look like the below

mask: 
 radial-gradient(100% 100%,#000 calc(var(--r)/tan(180deg/#{$n})),#0000 0) intersect,
 radial-gradient(#000 0 0) exclude,
 #{$m};

And here is the one of the previous shape to compare both

mask: 
 radial-gradient(100% 100%,#000 calc(var(--r)/tan(180deg/#{$n})),#0000 0),
 #{$m};

The only difference is an extra gradient (the one that will fill the whole shape) and the value of mask-composite. And since the remaining code is also the same, we can introduce a variable to control which one to use:

$n: 12; /* the number of circles/petals */

.flower {
  width: 300px;
  aspect-ratio: 1;
  --r: calc(50%/(1 + 1/sin(180deg/#{$n})));
  $m: (); /* empty variable */
  @for $i from 1 through ($n) { /* loop through the number of circles*/
    $m: append($m, 
      radial-gradient(50% 50%,#000 100%,#0000) no-repeat
      calc(50% + 50%*cos(360deg*#{$i/$n})) 
      calc(50% + 50%*sin(360deg*#{$i/$n}))
     / calc(2*var(--r)) calc(2*var(--r)), 
    comma);
  }
  mask: radial-gradient(100% 100%,#000 calc(var(--r)/tan(180deg/#{$n})),#0000 0) var(--alt,),#{$m};
}
.alt {
  --alt: intersect,radial-gradient(#000 0 0) exclude;
}

The var(--alt,) will default to nothing when the variable is not specified and we get the code of the first shape. By adding the extra gradient and the composition values, we get the second shape. Two different shapes with almost the same code.

The Border-Only Version

Now let’s tackle the border-only version of the previous shapes. We are also going to rely on mask-composite and the same code structure.

First of all, let’s introduce a new variable to control the border thickness and update the code that generates the small circles.

$n: 12; /* the number of circles/petals */

.flower {
  --b: 10px; /* the border thickness*/

  width: 300px;
  aspect-ratio: 1;
  --r: calc(50%/(1 + 1/sin(180deg/#{$n})));
  $m: (); /* empty variable */
  @for $i from 1 through ($n) { /* loop through the number of circles*/
    $m: append($m, 
      radial-gradient(50% 50%,#0000 calc(100% - var(--b)),#000 0 100%,#0000) no-repeat
      calc(50% + 50%*cos(360deg*#{$i/$n})) 
      calc(50% + 50%*sin(360deg*#{$i/$n}))
     / calc(2*var(--r)) calc(2*var(--r)), 
    comma);
  }
  mask: #{$m};
}

Nothing complex so far. Instead of full circles, we are getting a border-only version. This time we don’t want them to touch each other but rather overlap a bit to have a continuous shape.

We need to increase the radius a little from this

--r: calc(50%/(1 + 1/sin(180deg/#{$n})));

To this:

--r:calc((50% + var(--b)/(2*sin(180deg/#{$n})))/(1 + 1/sin(180deg/#{$n})));

Again some geometry stuff but you don’t really need to accurately understand all the formulas. I did the hard work to identify them and you only need to understand the main trick. In the end, all you have to do is update a few variables to control the shape or get the code from my generator.

The result so far:

Now, we use mask-composite with another gradient to hide some parts and get our final shapes. Here is a figure to illustrate the process for both shapes.

For the first shape:

And for the second one:

In both cases, I am introducing an extra gradient that will intersect with the small circles. The difference between each shape is the coloration of the extra gradient. In the first case, we have transparent inside and filling outside and for the second case, we have the opposite.

/* first shape */
mask:
 radial-gradient(100% 100%,#0000 calc(var(--r)/tan(180deg/#{$n}) - var(--b)/(2*tan(180deg/#{$n}))),#000 0) intersect, 
 #{$m};
/* second shape */
mask:
 radial-gradient(100% 100%,#000 calc(var(--r)/tan(180deg/#{$n}) - var(--b)/(2*tan(180deg/#{$n}))),#0000 0) intersect, 
 #{$m};

And here is the full code with both variations:

If you have some trouble visualizing how mask-composite works, I invite you to read the crash course written by Ana Tudor where you will find a more in-depth exploration.

One More Shape

Another flower? Let’s go!

blob like shape, sort of like a gear with smoothed over cogs.

This time, it’s your turn to figure out the code. Consider this as a small piece of homework to practice what we have learned together. As a hint, here is a figure that illustrates the mask-composite you need to perform, or maybe you will figure out another idea! If so don’t hesitate to share it in the comment section

Here is the code of my implementation but make a try before checking it so you can compare your method with mine. Take the time to study this last example because it will be the starting point of our second article.

Conclusion

I hope you enjoyed this little experimentation using CSS mask and trigonometric functions. You are probably not going to use such shapes in a real project but creating them is a good way to learn new features. If you have to remember one thing from this article it’s the use of mask-composite. It’s a powerful property that can help you create complex shapes.

It is worth noting that since we are using mask, we can easily apply our shape to image elements.

Don’t forget to bookmark my flower shapes generator so you can easily grab the code whenever you need it. I also have more CSS generators that invite you to check. Most of them rely on CSS mask as well and I have a detailed article linked to each one.

I will close this article with a few mesmerizing animations involving some flower shapes.

Article Series

]]>
https://frontendmasters.com/blog/creating-flower-shapes-using-css-mask-trigonometric-functions/feed/ 0 1021