game – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Thu, 25 Jul 2024 13:31:50 +0000 en-US hourly 1 https://wordpress.org/?v=6.6.1 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 game – CSS-Tricks https://css-tricks.com 32 32 45537868 Pop(over) the Balloons https://css-tricks.com/popover-the-balloons/ https://css-tricks.com/popover-the-balloons/#comments Thu, 25 Jul 2024 13:31:46 +0000 https://css-tricks.com/?p=379281 I’ve always been fascinated with how much we can do with just HTML and CSS. The new interactive features of the Popover API are yet another example of just how far we can get with those two languages alone.

You …


Pop(over) the Balloons originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I’ve always been fascinated with how much we can do with just HTML and CSS. The new interactive features of the Popover API are yet another example of just how far we can get with those two languages alone.

You may have seen other tutorials out there showing off what the Popover API can do, but this is more of a beating-it-mercilessly-into-submission kind of article. We’ll add a little more pop music to the mix, like with balloons… some literal “pop” if you will.

What I’ve done is make a game — using only HTML and CSS, of course — leaning on the Popover API. You’re tasked with popping as many balloons as possible in under a minute. But be careful! Some balloons are (as Gollum would say) “tricksy” and trigger more balloons.

I have cleverly called it Pop(over) the Balloons and we’re going to make it together, step by step. When we’re done it’ll look something like (OK, exactly like) this:

Handling the popover attribute

Any element can be a popover as long as we fashion it with the popover attribute:

<div popover>...</div>

We don’t even have to supply popover with a value. By default, popover‘s initial value is auto and uses what the spec calls “light dismiss.” That means the popover can be closed by clicking anywhere outside of it. And when the popover opens, unless they are nested, any other popovers on the page close. Auto popovers are interdependent like that.

The other option is to set popover to a manual value:

<div popover=“manual”>...</div>

…which means that the element is manually opened and closed — we literally have to click a specific button to open and close it. In other words, manual creates an ornery popup that only closes when you hit the correct button and is completely independent of other popovers on the page.

Using the <details> element as a starter

One of the challenges of building a game with the Popover API is that you can’t load a page with a popover already open… and there’s no getting around that with JavaScript if our goal is to build the game with only HTML and CSS.

Enter the <details> element. Unlike a popover, the <details> element can be open by default:

<details open>
  <!-- rest of the game -->
</details>

If we pursue this route, we’re able to show a bunch of buttons (balloons) and “pop” all of them down to the very last balloon by closing the <details>. In other words, we can plop our starting balloons in an open <details> element so they are displayed on the page on load.

This is the basic structure I’m talking about:

<details open>
  <summary>🎈</summary>
  <button>🎈</button>
  <button>🎈</button>
  <button>🎈</button>
</details>

In this way, we can click on the balloon in <summary> to close the <details> and “pop” all of the button balloons, leaving us with one balloon (the <summary> at the end (which we’ll solve how to remove a little later).

You might think that <dialog> would be a more semantic direction for our game, and you’d be right. But there are two downsides with <dialog> that won’t let us use it here:

  1. The only way to close a <dialog> that’s open on page load is with JavaScript. As far as I know, there isn’t a close <button> we can drop in the game that will close a <dialog> that’s open on load.
  2. <dialog>s are modal and prevent clicking on other things while they’re open. We need to allow gamers to pop balloons outside of the <dialog> in order to beat the timer.

Thus we will be using a <details open> element as the game’s top-level container and using a plain ol’ <div> for the popups themselves, i.e. <div popover>.

All we need to do for the time being is make sure all of these popovers and buttons are wired together so that clicking a button opens a popover. You’ve probably learned this already from other tutorials, but we need to tell the popover element that there is a button it needs to respond to, and then tell the button that there is a popup it needs to open. For that, we give the popover element a unique ID (as all IDs should be) and then reference it on the <button> with a popovertarget attribute:

<!-- Level 0 is open by default -->
<details open>
  <summary>🎈</summary>
  <button popovertarget="lvl1">🎈</button>
</details>

<!-- Level 1 -->
<div id="lvl1" popover="manual">
  <h2>Level 1 Popup</h2>
</div>

This is the idea when everything is wired together:

Opening and closing popovers

There’s a little more work to do in that last demo. One of the downsides to the game thus far is that clicking the <button> of a popup opens more popups; click that same <button> again and they disappear. This makes the game too easy.

We can separate the opening and closing behavior by setting the popovertargetaction attribute (no, the HTML spec authors were not concerned with brevity) on the <button>. If we set the attribute value to either show or hide, the <button> will only perform that one action for that specific popover.

<!-- Level 0 is open by default -->
<details open>
  <summary>🎈</summary>
  <!-- Show Level 1 Popup -->
  <button popovertarget="lvl1" popovertargetaction="show">🎈</button>
  <!-- Hide Level 1 Popup -->
  <button popovertarget="lvl1" popovertargetaction="hide">🎈</button>
</details>

<!-- Level 1 -->
<div id="lvl1" popover="manual">
  <h2>Level 1 Popup</h2>
  <!-- Open/Close Level 2 Poppup -->
  <button popovertarget="lvl2">🎈</button>
</div>

<!-- etc. -->

Note, that I’ve added a new <button> inside the <div> that is set to target another <div> to pop open or close by intentionally not setting the popovertargetaction attribute on it. See how challenging (in a good way) it is to “pop” the elements:

Styling balloons

Now we need to style the <summary> and <button> elements the same so that a player cannot tell which is which. Note that I said <summary> and not <details>. That’s because <summary> is the actual element we click to open and close the <details> container.

Most of this is pretty standard CSS work: setting backgrounds, padding, margin, sizing, borders, etc. But there are a couple of important, not necessarily intuitive, things to include.

  • First, there’s setting the list-style-type property to none on the <summary> element to get rid of the triangular marker that indicates whether the <details> is open or closed. That marker is really useful and great to have by default, but for a game like this, it would be better to remove that hint for a better challenge.
  • Safari doesn’t like that same approach. To remove the <details> marker here, we need to set a special vendor-prefixed pseudo-element, summary::-webkit-details-marker to display: none.
  • It’d be good if the mouse cursor indicated that the balloons are clickable, so we can set cursor: pointer on the <summary> elements as well.
  • One last detail is setting the user-select property to none on the <summary>s to prevent the balloons — which are simply emoji text — from being selected. This makes them more like objects on the page.
  • And yes, it’s 2024 and we still need that prefixed -webkit-user-select property to account for Safari support. Thanks, Apple.

Putting all of that in code on a .balloon class we’ll use for the <button> and <summary> elements:

.balloon {
  background-color: transparent;
  border: none;
  cursor: pointer;
  display: block;
  font-size: 4em;
  height: 1em;
  list-style-type: none;
  margin: 0;
  padding: 0;
  text-align: center;
  -webkit-user-select: none; /* Safari fallback */
  user-select: none;
  width: 1em;
}

One problem with the balloons is that some of them are intentionally doing nothing at all. That’s because the popovers they close are not open. The player might think they didn’t click/tap that particular balloon or that the game is broken, so let’s add a little scaling while the balloon is in its :active state of clicking:

.balloon:active {
  scale: 0.7;
  transition: 0.5s;
}

Bonus: Because the cursor is a hand pointing its index finger, clicking a balloon sort of looks like the hand is poking the balloon with the finger. 👉🎈💥

The way we distribute the balloons around the screen is another important thing to consider. We’re unable to position them randomly without JavaScript so that’s out. I tried a bunch of things, like making up my own “random” numbers defined as custom properties that can be used as multipliers, but I couldn’t get the overall result to feel all that “random” without overlapping balloons or establishing some sort of visual pattern.

I ultimately landed on a method that uses a class to position the balloons in different rows and columns — not like CSS Grid or Multicolumns, but imaginary rows and columns based on physical insets. It’ll look a bit Grid-like and is less “randomness” than I want, but as long as none of the balloons have the same two classes, they won’t overlap each other.

I decided on an 8×8 grid but left the first “row” and “column” empty so the balloons are clear of the browser’s left and top edges.

/* Rows */
.r1 { --row: 1; }
.r2 { --row: 2; }
/* all the way up to .r7 */

/* Columns */
.c1 { --col: 1; }
.c2 { --col: 2; }
/* all the way up to .c7 */

.balloon {
  /* This is how they're placed using the rows and columns */
  top: calc(12.5vh * (var(--row) + 1) - 12.5vh);
  left: calc(12.5vw * (var(--col) + 1) - 12.5vw);
}

Congratulating The Player (Or Not)

We have most of the game pieces in place, but it’d be great to have some sort of victory dance popover to congratulate players when they successfully pop all of the balloons in time.

Everything goes back to a <details open> element. Once that element is not open, the game should be over with the last step being to pop that final balloon. So, if we give that element an ID of, say, #root, we could create a condition to hide it with display: none when it is :not() in an open state:

#root:not([open]) {
  display: none;
}

This is where it’s great that we have the :has() pseudo-selector because we can use it to select the #root element’s parent element so that when #root is closed we can select a child of that parent — a new element with an ID of #congrats — to display a faux popover displaying the congratulatory message to the player. (Yes, I’m aware of the irony.)

#game:has(#root:not([open])) #congrats {
  display: flex;
}

If we were to play the game at this point, we could receive the victory message without popping all the balloons. Again, manual popovers won’t close unless the correct button is clicked — even if we close its ancestral <details> element.

Is there a way within CSS to know that a popover is still open? Yes, enter the :popover-open pseudo-class.

The :popover-open pseudo-class selects an open popover. We can use it in combination with :has() from earlier to prevent the message from showing up if a popover is still open on the page. Here’s what it looks like to chain these things together to work like an and conditional statement.

/* If #game does *not* have an open #root 
 * but has an element with an open popover 
 * (i.e. the game isn't over),
 * then select the #congrats element...
 */
#game:has(#root:not([open])):has(:popover-open) #congrats {
  /* ...and hide it */
  display: none;
}

Now, the player is only congratulated when they actually, you know, win.

Conversely, if a player is unable to pop all of the balloons before a timer expires, we ought to inform the player that the game is over. Since we don’t have an if() conditional statement in CSS (not yet, at least) we’ll run an animation for one minute so that this message fades in to end the game.

#fail {
  animation: fadein 0.5s forwards 60s;
  display: flex;
  opacity: 0;
  z-index: -1;
}

@keyframes fadein {
  0% {
    opacity: 0;
    z-index: -1;
  }
  100% {
    opacity: 1;
    z-index: 10;
  }
}

But we don’t want the fail message to trigger if the victory screen is showing, so we can write a selector that prevents the #fail message from displaying at the same time as #congrats message.

#game:has(#root:not([open])) #fail {
  display: none;
}

We need a game timer

A player should know how much time they have to pop all of the balloons. We can create a rather “simple” timer with an element that takes up the screen’s full width (100vw), scaling it in the horizontal direction, then matching it up with the animation above that allows the #fail message to fade in.

#timer {
  width: 100vw;
  height: 1em;
}

#bar {
  animation: 60s timebar forwards;
  background-color: #e60b0b;
  width: 100vw;
  height: 1em;
  transform-origin: right;
}

@keyframes timebar {
  0% {
    scale: 1 1;
  }
  100% {
    scale: 0 1;
  }
}

Having just one point of failure can make the game a little too easy, so let’s try adding a second <details> element with a second “root” ID, #root2. Once more, we can use :has to check that neither the #root nor #root2 elements are open before displaying the #congrats message.

#game:has(#root:not([open])):has(#root2:not([open])) #congrats {
  display: flex;
}

Wrapping up

The only thing left to do is play the game!

Fun, right? I’m sure we could have built something more robust without the self-imposed limitation of a JavaScript-free approach, and it’s not like we gave this a good-faith accessibility pass, but pushing an API to the limit is both fun and educational, right?


I’m interested: What other wacky ideas can you think up for using popovers? Maybe you have another game in mind, some slick UI effect, or some clever way of combining popovers with other emerging CSS features, like anchor positioning. Whatever it is, please share!


Pop(over) the Balloons originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/popover-the-balloons/feed/ 2 379281
How I Made a Pure CSS Puzzle Game https://css-tricks.com/how-i-made-a-pure-css-puzzle-game/ https://css-tricks.com/how-i-made-a-pure-css-puzzle-game/#comments Fri, 09 Sep 2022 13:21:29 +0000 https://css-tricks.com/?p=372952 I recently discovered the joy of creating CSS-only games. It’s always fascinating how HTML and CSS are capable of handling the logic of an entire online game, so I had to try it! Such games usually rely on the ol’ …


How I Made a Pure CSS Puzzle Game originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I recently discovered the joy of creating CSS-only games. It’s always fascinating how HTML and CSS are capable of handling the logic of an entire online game, so I had to try it! Such games usually rely on the ol’ Checkbox Hack where we combine the checked/unchecked state of a HTML input with the :checked pseudo-class in CSS. We can do a lot of magic with that one combination!

In fact, I challenged myself to build an entire game without Checkbox. I wasn’t sure if it would be possible, but it definitely is, and I’m going to show you how.

In addition to the puzzle game we will study in this article, I have made a collection of pure CSS games, most of them without the Checkbox Hack. (They are also available on CodePen.)

Want to play before we start?

I personally prefer playing the game in full screen mode, but you can play it below or open it up over here.

Cool right? I know, it’s not the Best Puzzle Game You Ever Saw™ but it’s also not bad at all for something that only uses CSS and a few lines of HTML. You can easily adjust the size of the grid, change the number of cells to control the difficulty level, and use whatever image you want!

We’re going to remake that demo together, then put a little extra sparkle in it at the end for some kicks.

The drag and drop functionality

While the structure of the puzzle is fairly straightforward with CSS Grid, the ability to drag and drop puzzle pieces is a bit trickier. I had to relying on a combination of transitions, hover effects, and sibling selectors to get it done.

If you hover over the empty box in that demo, the image moves inside of it and stays there even if you move the cursor out of the box. The trick is to add a big transition duration and delay — so big that the image takes lots of time to return to its initial position.

img {
  transform: translate(200%);
  transition: 999s 999s; /* very slow move on mouseout */
}
.box:hover img {
  transform: translate(0);
  transition: 0s; /* instant move on hover */
}

Specifying only the transition-delay is enough, but using big values on both the delay and the duration decreases the chance that a player ever sees the image move back. If you wait for 999s + 999s — which is approximately 30 minutes — then you will see the image move. But you won’t, right? I mean, no one’s going to take that long between turns unless they walk away from the game. So, I consider this a good trick for switching between two states.

Did you notice that hovering the image also triggers the changes? That’s because the image is part of the box element, which is not good for us. We can fix this by adding pointer-events: none to the image but we won’t be able to drag it later.

That means we have to introduce another element inside the .box:

That extra div (we’re using a class of .a) will take the same area as the image (thanks to CSS Grid and grid-area: 1 / 1) and will be the element that triggers the hover effect. And that is where the sibling selector comes into play:

.a {
  grid-area: 1 / 1;
}
img {
  grid-area: 1 / 1;
  transform: translate(200%);
  transition: 999s 999s;
}
.a:hover + img {
  transform: translate(0);
  transition: 0s;
}

Hovering on the .a element moves the image, and since it is taking up all space inside the box, it’s like we are hovering over the box instead! Hovering the image is no longer a problem!

Let’s drag and drop our image inside the box and see the result:

Did you see that? You first grab the image and move it to the box, nothing fancy. But once you release the image you trigger the hover effect that moves the image, and then we simulate a drag and drop feature. If you release the mouse outside the box, nothing happens.

Hmm, your simulation isn’t perfect because we can also hover the box and get the same effect.

True and we will rectify this. We need to disable the hover effect and allow it only if we release the image inside the box. We will play with the dimension of our .a element to make that happen.

Now, hovering the box does nothing. But if you start dragging the image, the .a element appears, and once released inside the box, we can trigger the hover effect and move the image.

Let’s dissect the code:

.a {
  width: 0%;
  transition: 0s .2s; /* add a small delay to make sure we catch the hover effect */
}
.box:active .a { /* on :active increase the width */
  width: 100%;
  transition: 0s; /* instant change */
}
img {
  transform: translate(200%);
  transition: 999s 999s;
}
.a:hover + img {
  transform: translate(0);
  transition: 0s;
}

Clicking on the image fires the :active pseudo-class that makes the .a element full-width (it is initially equal to 0). The active state will remain active until we release the image. If we release the image inside the box, the .a element goes back to width: 0, but we will trigger the hover effect before it happens and the image will fall inside the box! If you release it outside the box, nothing happens.

There is a little quirk: clicking the empty box also moves the image and breaks our feature. Currently, :active is linked to the .box element, so clicking on it or any of its children will activate it; and by doing this, we end up showing the .a element and triggering the hover effect.

We can fix that by playing with pointer-events. It allows us to disable any interaction with the .box while maintaining the interactions with the child elements.

.box {
  pointer-events: none;
}
.box * {
  pointer-events: initial;
}

Now our drag and drop feature is perfect. Unless you can find how to hack it, the only way to move the image is to drag it and drop it inside the box.

Building the puzzle grid

Putting the puzzle together is going to feel easy peasy compared to what we just did for the drag and drop feature. We are going to rely on CSS grid and background tricks to create the puzzle.

Here’s our grid, written in Pug for convenience:

- let n = 4; /* number of columns/rows */
- let image = "https://picsum.photos/id/1015/800/800";

g(style=`--i:url(${image})`)
  - for(let i = 0; i < n*n; i++)
    z
      a
      b(draggable="true") 

The code may look strange but it compiles into plain HTML:

<g style="--i: url(https://picsum.photos/id/1015/800/800)">
 <z>
   <a></a>
   <b draggable="true"></b>
 </z>
 <z>
   <a></a>
   <b draggable="true"></b>
 </z>
 <z>
   <a></a>
   <b draggable="true"></b>
 </z>
  <!-- etc. -->
</g>

I bet you’re wondering what’s up with those tags. None of these elements have any special meaning — I just find that the code is much easier to write using <z> than a bunch of <div class="z"> or whatever.

This is how I’ve mapped them out:

  • <g> is our grid container that contains N*N <z> elements.
  • <z> represents our grid items. It plays the role of the .box element we saw in the previous section.
  • <a> triggers the hover effect.
  • <b> represents a portion of our image. We apply the draggable attribute on it because it cannot be dragged by default.

Alright, let’s register our grid container on <g>. This is in Sass instead of CSS:

$n : 4; /* number of columns/rows */

g {
  --s: 300px; /* size of the puzzle */

  display: grid;
  max-width: var(--s);
  border: 1px solid;
  margin: auto;
  grid-template-columns: repeat($n, 1fr);
}

We’re actually going to make our grid children — the <z> elements — grids as well and have both <a> and <b> within the same grid area:

z {
  aspect-ratio: 1;
  display: grid;
  outline: 1px dashed;
}
a {
  grid-area: 1/1;
}
b {
  grid-area: 1/1;
}

As you can see, nothing fancy — we created a grid with a specific size. The rest of the CSS we need is for the drag and drop feature, which requires us to randomly place the pieces around the board. I’m going to turn to Sass for this, again for the convenience of being able to loop through and style all the puzzle pieces with a function:

b {
  background: var(--i) 0/var(--s) var(--s);
}

@for $i from 1 to ($n * $n + 1) {
  $r: (random(180));
  $x: (($i - 1)%$n);
  $y: floor(($i - 0.001) / $n);
  z:nth-of-type(#{$i}) b{
    background-position: ($x / ($n - 1)) * 100% ($y / ($n - 1)) * 100%;
    transform: 
      translate((($n - 1) / 2 - $x) * 100%, (($n - 1)/2 - $y) * 100%) 
      rotate($r * 1deg) 
      translate((random(100)*1% + ($n - 1) * 100%)) 
      rotate((random(20) - 10 - $r) * 1deg)
   }
}

You may have noticed that I’m using the Sass random() function. That’s how we get the randomized positions for the puzzle pieces. Remember that we will disable that position when hovering over the <a> element after dragging and dropping its corresponding <b> element inside the grid cell.

z a:hover ~ b {
  transform: translate(0);
  transition: 0s;
}

In that same loop, I am also defining the background configuration for each piece of the puzzle. All of them will logically share the same image as the background, and its size should be equal to the size of the whole grid (defined with the --s variable). Using the same background-image and some math, we update the background-position to show only a piece of the image.

That’s it! Our CSS-only puzzle game is technically done!

But we can always do better, right? I showed you how to make a grid of puzzle piece shapes in another article. Let’s take that same idea and apply it here, shall we?

Puzzle piece shapes

Here’s our new puzzle game. Same functionality but with more realistic shapes!

This is an illustration of the shapes on the grid:

If you look closely you’ll notice that we have nine different puzzle-piece shapes: the four corners, the four edges, and one for everything else.

The grid of puzzle pieces I made in the other article I referred to is a little more straightforward:

We can use the same technique that combines CSS masks and gradients to create the different shapes. In case you are unfamiliar with mask and gradients, I highly recommend checking that simplified case to better understand the technique before moving to the next part.

First, we need to use specific selectors to target each group of elements that shares the same shape. We have nine groups, so we will use eight selectors, plus a default selector that selects all of them.

z  /* 0 */

z:first-child  /* 1 */

z:nth-child(-n + 4):not(:first-child) /* 2 */

z:nth-child(5) /* 3 */

z:nth-child(5n + 1):not(:first-child):not(:nth-last-child(5)) /* 4 */

z:nth-last-child(5)  /* 5 */

z:nth-child(5n):not(:nth-child(5)):not(:last-child) /* 6 */

z:last-child /* 7 */

z:nth-last-child(-n + 4):not(:last-child) /* 8 */

Here is a figure that shows how that maps to our grid:

Now let’s tackle the shapes. Let’s focus on learning just one or two of the shapes because they all use the same technique — and that way, you have some homework to keep learning!

For the puzzle pieces in the center of the grid, 0:

mask: 
  radial-gradient(var(--r) at calc(50% - var(--r) / 2) 0, #0000 98%, #000) var(--r)  
    0 / 100% var(--r) no-repeat,
  radial-gradient(var(--r) at calc(100% - var(--r)) calc(50% - var(--r) / 2), #0000 98%, #000) 
    var(--r) 50% / 100% calc(100% - 2 * var(--r)) no-repeat,
  radial-gradient(var(--r) at var(--r) calc(50% - var(--r) / 2), #000 98%, #0000),
  radial-gradient(var(--r) at calc(50% + var(--r) / 2) calc(100% - var(--r)), #000 98%, #0000);

The code may look complex, but let’s focus on one gradient at a time to see what’s happening:

Two gradients create two circles (marked green and purple in the demo), and two other gradients create the slots that other pieces connect to (the one marked blue fills up most of the shape while the one marked red fills the top portion). A CSS variable, --r, sets the radius of the circular shapes.

The shape of the puzzle pieces in the center (marked 0 in the illustration) is the hardest to make as it uses four gradients and has four curvatures. All the others pieces juggle fewer gradients.

For example, the puzzle pieces along the top edge of the puzzle (marked 2 in the illustration) uses three gradients instead of four:

mask: 
  radial-gradient(var(--r) at calc(100% - var(--r)) calc(50% + var(--r) / 2), #0000 98%, #000) var(--r) calc(-1 * var(--r)) no-repeat,
  radial-gradient(var(--r) at var(--r) calc(50% - var(--r) / 2), #000 98%, #0000),
  radial-gradient(var(--r) at calc(50% + var(--r) / 2) calc(100% - var(--r)), #000 98%, #0000);

We removed the first (top) gradient and adjusted the values of the second gradient so that it covers the space left behind. You won’t notice a big difference in the code if you compare the two examples. It should be noted that we can find different background configurations to create the same shape. If you start playing with gradients you will for sure come up with something different than what I did. You may even write something that’s more concise — if so, share it in the comments!

In addition to creating the shapes, you will also find that I am increasing the width and/or the height of the elements like below:

height: calc(100% + var(--r));
width: calc(100% + var(--r));

The pieces of the puzzle need to overflow their grid cell to connect.

Final demo

Here is the full demo again. If you compare it with the first version you will see the same code structure to create the grid and the drag-and-drop feature, plus the code to create the shapes.

Possible enhancements

The article ends here but we could keep enhancing our puzzle with even more features! How about a a timer? Or maybe some sort of congratulations when the player finishes the puzzle?

I may consider all these features in a future version, so keep an eye on my GitHub repo.

Wrapping up

And CSS isn’t a programming language, they say. Ha!

I’m not trying to spark some #HotDrama by that. I say it because we did some really tricky logic stuff and covered a lot of CSS properties and techniques along the way. We played with CSS Grid, transitions, masking, gradients, selectors, and background properties. Not to mention the few Sass tricks we used to make our code easy to adjust.

The goal was not to build the game, but to explore CSS and discover new properties and tricks that you can use in other projects. Creating an online game in CSS is a challenge that pushes you to explore CSS features in great detail and learn how to use them. Plus, it’s just a lot of fun that we get something to play with when all is said and done.

Whether CSS is a programming language or not, doesn’t change the fact that we always learn by building and creating innovative stuff.


How I Made a Pure CSS Puzzle Game originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/how-i-made-a-pure-css-puzzle-game/feed/ 9 372952
Generating (and Solving!) Sudokus in CSS https://css-tricks.com/generating-and-solving-sudokus-in-css/ https://css-tricks.com/generating-and-solving-sudokus-in-css/#comments Mon, 26 Apr 2021 14:05:00 +0000 https://css-tricks.com/?p=338362 I love to make CSS do stuff it shouldn’t. It’s the type of problem-solving brain training you’d get building a calculator in Minecraft, except you probably won’t get a job working with Minecraft Redstone no matter how good you …


Generating (and Solving!) Sudokus in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I love to make CSS do stuff it shouldn’t. It’s the type of problem-solving brain training you’d get building a calculator in Minecraft, except you probably won’t get a job working with Minecraft Redstone no matter how good you get at that, whereas CSS skills are worth actual money, and many generalist programmers are scared of CSS, so studying it can be a way to stand out from the pack. Also, when you’ve done the impossible with CSS, all normal CSS tasks seem easy.

I’ve read interesting discussions on the web about whether CSS is a Turing complete language and whether CSS and HTML qualify as programming languages. I haven’t decided, but I can say that in the quest to support common UI patterns in a standard way, some of the newer CSS features blur the line between styling and functionality.

Challenging ourselves to solve logical problems with only CSS and HTML can force us to spend quality time with some of the newish, programing-like features of CSS, such as custom properties and logical functions. It still wasn’t clear how these could be used to build a Sudoku solver using only CSS, but as crazy as the idea sounded, the constraint-based logic of Sudoku seemed like it might be compatible with the declarative nature of CSS, so I wasn’t shocked to find someone else claimed to have built a “CSS3 Sudoku solution solver.” As it turned out, this was more like a sudoku validator in CSS than a solver. It also used a tiny bit of JavaScript to work with textboxes.

After days of valiantly trying to build a full Sudoku solver and generator app in pure CSS, I learned three things.

  1. You can unit test Sass functions and mixins, which is awesome. If you’re heavily using and reusing these Sass features, which is what they are meant for, they become as mission-critical and as scary to change as any other part of your codebase. They deserve tests around them.
  2. Chrome DevTools shows an infinite spinner of death when you throw 50MB of Sass-generated CSS at it.
  3. Maybe it’s impossible to translate something like this Python script into pure CSS. Maybe.

However, we can achieve a Sudoku solver and generator app for 16-square Sudoku which you can play with below, then we’ll break down how its features work. Where is your god now, simple puzzle intended for young children?

The value selector

Since we’re experimenting with CSS, we are contractually obligated to include something visually interesting, though nothing too over-the-top as Sudoku players seem to appreciate a UI that stays out of the way. In my opinion, the way you select numbers on some of the Sudoku apps could be more intuitive, so I decided to apply the radial menu UI pattern, which dates all the way back to days of black and white Macintosh and is still popular in modern video games. Someone actually built a nice pure CSS library for radial menus, but I got a crush on React Planet as I love the way it captures both selecting an item with the circle around it, and how it attractively displays the available actions. I wanted to see if I could build a similar effect with just CSS.

I took some of the dashed circle code from this Pen and then made the numbers out of labels using the old border-radius: 50% trick, then I used absolute positioning to make the numbers “stick” to the correct point on the dashed circle even when the animation makes it change size.

.context .number.top {
  background: green;
  margin-left: auto;
  margin-right: auto;
  left: 0;
  right: 0;
  top: -12.5px;
}

.context .number.left {
  background: orange;
  margin-top: auto;
  margin-bottom: auto;
  top: 0;
  bottom: 0;
  left: -12.5px;
}

The animation fades the number picker in while making its z-index higher so it becomes clickable. We are also animating the top and left margin from 50% to zero so the circle expands from the center to fill the available space.

@keyframes bounce-out {
  0% {
    z-index: -1;
    width: 35%;
    height: 35%;
    margin-left: 50%;
    margin-top: 50%;
    opacity: 0;
  }
  100% {
    z-index: 2;
    opacity: 1;
    width: var(--circle-radius);
    height: var(--circle-radius);
  }
}

Then, to simulate bouncy physics similar to React Planet, I use a cubic-bezier() function on the animation. The website easings.net was a big help in making easing functions easy.

.context {
  animation: bounce-out cubic-bezier(.68,-0.6,.32, 2.5) .5s forwards;        
}

Both the selection of values and the behavior of opening the value selector for the selected square operate using radio button hacks, to remember which values were selected and achieve mutual exclusivity. CSS-Tricks has an excellent article on checkbox and radio button hacks, so I won’t repeat that information here, but I will show how we set CSS variables at the level of the Sudoku CSS grid based on checkboxes, as it’s central to the way this experiment works.

As we are using variables, we can get the same behavior when a value is set, regardless of whether it’s the user checking a box to specify a value, or the puzzle generator setting that same value for the square. There are many combinations of squares and values, so we are using Sass rather than writing all the combinations out by hand. We are also creating separate bit values for each value-square combination, and another custom property to tell us whether the square is unsolved. That’s because CSS gives us limited ability to compare one value to another (it’s possible but can be tricky). We are defining these values in a way that might look a bit odd at first, but will make life easier for us when it comes to validating whether a set of Sudoku square values is solvable or not.

@for $i from 1 through 16 {
  @for $j from 1 through 4 {
    #select-#{$j}-value-square-#{$i}:checked ~ .sudoku  {
       --square-#{$i}-unsolved: 0; 
       --square-#{$i}-equals-#{$j}: 1;
    }
  }
}

Validating the grid

Doctor Google tells us that even with only 16 squares, there are four billion possible combinations of four numbers. But a program that brute forces all these combinations and outputs the ones that are valid according to the rules of Sudoku shows that there are only 288 valid solutions in 4×4 Sudoku, which is a big difference from the number of possible valid solutions in a 9×9 grid. With only 288 possible solutions, this is where Sass can really come into its own. I’m still not sure if CSS is a Turing complete language, but Sass is, and it gives us some proper data structures, such as lists. With a bit of regex magic we can transform the list of valid 4×4 puzzles linked above into a Sass-powered two-dimensional list!

$solutions: ((1,2,3,4,3,4,1,2,2,1,4,3,4,3,2,1),(3,1,2,4,2,4,1,3,1,3,4,2,4,2,3,1),(1,2,3,4,3,4,1,2,2,3,4,1,4,1,2,3),/*...many lines later...*/(2,4,3,1,3,1,4,2,4,2,1,3,1,3,2,4),(4,3,2,1,2,1,4,3,3,4,1,2,1,2,3,4));

Sweet! If our CSS hack were a multi-tier application, this would be our database. Validation could have used the same approach of checking row and column values like the 9×9 validator we saw in the introduction, but since we know the answer it seems like we shouldn’t need to bother checking blocks and columns and rows. Instead, we can check whether the entered numbers could still be a valid puzzle or not. In pseudocode this might look something like:

foreach (s in squares) 
{
  if (solutionsContains(s.value, s.index) or s.isUnsolved())
  {
    showValidationError();
  } 
} 

Remember we created those weird variables whenever a square value is selected?

--square-#{$i}-unsolved: 0;        
--square-#{$i}-equals-#{$j}: 1;

So now we have answers to both questions in the condition in line 3 of the pseudocode above, but how can we do a logical OR operator in CSS? There’s a great article on CSS-Tricks about using calc() to simulate logic operators in CSS, and I’m not sure I would have even thought of some of the code in my Sudoku solver without it, but some of the formulas explained in that article get a bit unwieldy, especially if you want to do nested ANDs and ORs with more than two operands. For example, we need the CSS equivalent of this pseudocode:

if ((squareOneEqualsOne and squareTwoEqualsTwo /*...*/ and squareSixteenEqualsOne) or (squareOneEqualsOne and squareTwoEqualsThree /*...*/ and squareSixteenEqualsOne))
  {
    sudokuIsValid();
  } 
}

Well, that article showing how to do logic using calc() was written in 2019. Nowadays, in addition to calc(), we have the well-supported min() and max() math functions which meet our needs even better. If you Google “CSS min, max and clamp” (the last of which is just convenient sugar for a combination of min() and max()), you’ll find many examples are showing how they can be used to simplify fluid typography. That’s one compelling use case, but you can use these math functions anywhere you’d use a number, which adds a lot of power to CSS. For example, if you pass bit flag variables to CSS min(), that’s equivalent to AND. If you pass the same flags to CSS max(), that’s equivalent to OR. We can prove this using the following truth tables.

ABA AND Bmin(A, B)
0000
1000
0100
1111
ABA OR Bmax(A, B)
0000
1011
0111
1111

We can get pretty sophisticated with that, especially when you add the helpful fact that we are allowed to do anything calc() can do within min() and max(). CSS just took a step closer to being its own weird scripting language. Now we can implement the condition in our validation pseudocode above in CSS. (In practice, we generate this from Sass since it’s very repetitive.)

.sudoku { 
  --square-1-matches-puzzle-1: max(var(--square-1-unsolved), var(--square-1-equals-1, 0));
  --square-2-matches-puzzle-1: max(var(--square-2-unsolved), var(--square-2-equals-2, 0));
  /*...*/
  --square-16-matches-puzzle-1: max(var(--square-16-unsolved), var(--square-16-equals-1, 0));
  --puzzle-1-found: min(var(--square-1-matches-puzzle-1), 
  /*...*/ 
  var(--square-16-matches-puzzle-1));
  --solution-found: max(var(--puzzle-1-found), /*...*/ var(--puzzle-288-found));
}

By checking if each square is either unsolved or has a value that exists in the same position in one of our pre-calculated solutions from the Sass 2D list, we can produce a variable that tells us whether the currently defined squares exist in a valid 4×4 sudoku puzzle. Now as long as we can find something numeric that will drive a behavior in CSS, we can base that CSS behavior on --solution-found. For example, to make our grid turn red when it’s invalid we can put this in each square:

.square {
  color: rgb(calc(255 * (1 - var(--solution-found))), 0, 0);
}

Not every CSS property can be driven by a number, but many can, and both z-index and opacity are especially versatile CSS properties for this usage. Other behaviors can be trickier but often achievable. For example, I was a bit stuck thinking about how to trigger the shake animation for an invalid grid with just a numeric bit flag property so that the grid would shake any time it became invalid, but this is a great example of how hacking CSS forces you to read the specs and understand the edge cases for each property. I found my solution on this page about animation-duration.

A value of 0s, which is the default value, indicates that no animation should occur.

So we can base animation duration of the shake animation on --solution-found and remove the animation each time a number is clicked using the :active pseudo-class to make the animation replay any time the solution becomes invalid, and do nothing otherwise.

#select-#{$j}-value-square-#{$i}:active  { 
  animation: none;
}

 #select-#{$j}-value-square-#{$i}:checked ~ .sudoku {
  animation: shake cubic-bezier(.36,.07,.19,.97) calc((clamp(0, 1 - var(--solution-found), 1)) * 1s) forwards;
}

A pure CSS Sudoku app would probably be impossible if we didn’t have CSS custom properties, and they are more powerful than they may seem at first glance. The way they get reevaluated and update the UI whenever a property they depend on changes is like a simpler version of the reactivity you get from a fancy JavaScript framework like Vue. It’s fair to say reactivity is built right into the browser in the form of CSS variables!

Now that we have this approach for validation and our stylesheet knows the solution in its subconscious any time we set valid values in our Sudoku, we are close to implementing the solver!

Solving every 4×4 Sudoku

Remember when we introduced these intermediate variables?

.sudoku { 
  --puzzle-1-found: min(var(--square-1-matches-puzzle-1), 
  /*...*/ 
  var(--square-16-matches-puzzle-1));
}

That wasn’t only to make the validation code easier to write and understand. Knowing which of the 288 possible puzzle(s) are matched allows us to write the solver!

#no-solution {
  z-index: 1;
  color: red;
}
@for $solution-index from 1 through 288 {
label[for=solution-#{$solution-index}] {
  cursor: pointer;
  z-index: calc(var(--puzzle-#{$solution-index}-found) * #{$solution-index});
}

#solution-#{$solution-index}:checked ~ .sudoku  {
  @for $square from 1 through 16 {
    --square-#{$square}-solution:"#{nth(nth($solutions, $solution-index), $square)}";
    --square-#{$square}-color: grey;
    --auto-#{$square}: 1;
  }

I put the optional plural in the word “puzzle(s)” above because, if the user hasn’t filled out many squares, it’s possible there are multiple valid solutions. I dig solvers like this JavaScript one that can quickly produce a solution even if you haven’t specified enough values for a human to be able to solve it without guessing.

The trick in my CSS solver is that while the “solve” button looks like a single button, it’s actually 288 radio button labels stacked one on top of the other — but all of them look the same. Imagine a stack of cards: they all have the same design on the back, but different values on the front. The solver logic is putting the card with the solution on the top of the pile with z-index, so if you pick it up and read the other side of it, you will always have the correct solution. It still works if there are multiple correct solutions, because the solution that comes later in our list of valid answers will be placed on top, since we calculate the z-index by multiplying the flag by $solution-index. If no solutions are matched, the z-index of all the solve buttons will be zero and, since the disabled version of the button with the “invalid puzzle” message has z-index of one, it will appear on top. If puzzle number one is the solution, we will still see the puzzle one button, since the invalid button comes earlier in the HTML.

Stacking context can behave unexpectedly if you haven’t read up on it, so this is is a nice illustration of one of the non-obvious stacking behaviors.

Generating puzzles

We can think of the generating puzzles as another version of the solver with extra requirements.

  1. Some random squares need to be left unsolved when the puzzle generator button is pressed.
  2. The combination of randomly unsolved squares and a correct solution should be different each time the generator button is pressed.
  3. Pressing the solve button should reveal the complete solution.
  4. If the user manually solves the generated puzzle, we’d like to reward them with a victory screen that gives feedback about how fast they solved it.

CSS has no random() function (though Sass does), so it might not be obvious how we can get a different behavior each time we push the same button. But the solver explanation above was a bit of a spoiler as it already does something similar with a button that looks like a single element but is actually different depending on the current valid solution.

The question with the “generate” button is how we can get an unpredictable result each time we click. Full credit to Alvaro Montoro for his article on CSS-Tricks about how to generate seemingly random values with just CSS. The combination of radio button hacks and animating the stacking order seems to work nicely. I tried hard to see if I could do it without extra markup, but I concluded that this approach is the best and the simplest. To reuse the deck of cards analogy from the solver explanation, it’s like the deck of puzzle cards is invisibly shuffling all the time so that whenever you take a card, you discover it has a different face.

We can combine this pseudo randomness with the actual randomness offered by the Sass random() function to give our Sudoku game replay value.

@for $j from 0 through 287 {
  label[for=generate#{$j}] { 
    animation-delay: #{$j * .35s}; 
  }
  label[for=generate#{$j}]:active:after {
    z-index: 300;
    width: 100%;
  }
  #generate#{$j}:checked ~ .sudoku { 
    $blockCounts: (1: 2, 2: 2, 3: 3, 4: 2);
    $shuffleSquares: (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);

     @for $square from 1 through 16 {
       $index1: random(16);
       $index2: random(16);

       $temp: nth($shuffleSquares, $index1);
       $shuffleSquares: set-nth($shuffleSquares, $index1, nth($shuffleSquares, $index2));
       $shuffleSquares: set-nth($shuffleSquares, $index2, $temp);
     }

     @each $square in $shuffleSquares {
       $row: ceil($square/4);
      $column: 1 + ($square - 1) % 4;

       $block: if($row &lt; 3, 1, 3) + if($column < 3, 0, 1);
       $count: map-get($blockCounts, $block);
       $val: nth(nth($solutions, $j + 1), $square);

       --square-#{$square}-solution-#{$val}: 1;

       @if ($count > 0) {
         $blockCounts: map-merge($blockCounts, ($block: $count - 1));
        --square-#{$square}-unsolved: 0; 
        --square-#{$square}-equals-#{$val}: 1;

        @for $other-value from 1 through 4 {
          @if ($other-value != $val) {
            --square-#{$square}-equals-#{$other-value}: 0;   
          }
        }

        --square-#{$square}-color: grey;
        --auto-#{$square}: 1;
      }
    } 
  }
}

For each “block” (that’s Sudoku-speak for those 4×4 sections of the Sudoku grid with the thick border around them), we use Sass to randomly choose two out of four squares to solve, except for one “gimme” square which only has one unsolved square. Since the validation logic and solver logic uses the variables rather than being directly based on which values were checked using the value selector, the validation and solving logic both behave the same way. That means generated values are treated the same as if the user had individually selected each value.

The solving timer

Here’s the timer ticking through the first eleven seconds.

We’ll dive into the CSS for the solving timer in a moment, but let’s first show what one of the digits looks like without CSS overflow set to hidden, and with a green border around the element to show the part that would be visible to the user at each step of the animation.

We are using an infinitely repeating keyframes animation to shift the list of possible digits one character to the left at a desired interval (we use a monospaced font so that we can be sure each character will occupy the same exact width). The seconds digit will go from zero up to nine, and the next digit should only go up to five, increasing once per ten seconds before both digits of the seconds need to reset to zero.

Each digit is animating using the same technique you can use to animate a spritesheet in CSS, except instead of animatedly shifting an image background to achieve an animation effect, we are shifting a pseudo element containing the possible digits.

As with many tasks in CSS, there is more than one way to make an animated counter in CSS. But some don’t work cross-browser and really demand a preprocessor to keep the code succinct. I like my approach because it’s fairly short. CSS does the heavy lifting to figure out when and how to move to the next digit. All the markup needs to do is create a placeholder where each digit goes, giving us some freedom for how we present our timer.

Here’s the markup:

<div class="stopwatch">  
  <div class="symbol"></div>
  <div class="symbol">:</div>
  <div class="symbol"></div>
  <div class="symbol"></div>
</div>

…and the CSS:

.stopwatch {
  text-align: center;
  font-family: monospace;
  margin-bottom: 10px;
}

.symbol {
  width: 1ch;
  overflow: hidden;
  display: inline-flex;
  font-size: 5ch;
}

.symbol:nth-child(1)::after {
  animation: tens 3600s steps(6, end) infinite;
  content: '012345';
}

.symbol:nth-child(2)::after {
  animation: units 600s steps(10, end) infinite;
  content: '0123456789';
}

.symbol:nth-child(4)::after {
  animation: tens 60s steps(6, end) infinite;
  content: '012345';
}

.symbol:nth-child(5)::after {
  animation: units 10s steps(10, end) infinite;
  content: '0123456789';
}

@keyframes units {
  to {
    transform: translateX(-10ch);
  }
}

@keyframes tens {
  to {
    transform: translateX(-6ch);
  }
}

You might notice that the counter starts again from the beginning after an hour. That’s because all of the iteration counts are set to infinite . We could fix it, but I figure if someone spends an hour solving one of these, they have bigger problems than a children’s Sudoku puzzle. 😛

What would be unfair, though, is if we allowed the same timer to just keep ticking even when the user generates a fresh puzzle. Can we make it reset? It turns out we’ve solved that problem already in the first step in this article, where we removed, and conditionally re-added, the animation for our number selector using the :active pseudo-class. This time, it’s actually simpler because every time we hit the “generate” button, we want to remove the animation on all the digits to take them back to zero. Then the animation will start again when the radio button is no longer active. So, it’s only one line of CSS we need to make the timer reset each time we generate!

input[name=generate]:active ~ .stopwatch .symbol::after {
  animation: none;
}

Sudokumeter™️

Even when the puzzle is solved, I want to offer replay value by giving the player visual feedback on their time performance and challenging them to solve puzzles faster. I also want to reward you for making it this far through the article by giving you a minimalist circular gauge you could reuse in your own projects. Here’s a standalone Pen with the circular gauge for you to experiment:

We’re applying the same principles used in the win screen from the game except, in this Pen, the rating displayed is controlled with radio button hacks, whereas in the game it’s controlled by animation that slowly moves to a lower rating as time passes. The gauge in the game is hidden using zero opacity and is only displayed (and paused) when we detect that the puzzle has been manually solved.

Let’s explain how we create the illusion of a semi-circle that’s divided into two sides by color. It’s actually a full CSS circle with its bottom half hidden using overflow: hidden.

We apply the two colors using a pseudo-element that fills half of the <div>.

Then we cut a hole in the middle to make a donut, using another circle filled with the game’s background color, and center that inside the larger circle using flexbox.

Next, hide half of it by making the size of the container half as tall as the full circle and, again, using overflow: hidden.

Now, if we rotate our donut, it looks like the gauge is filling up with green or losing green, depending on whether we rotate our donut by negative or positive degrees!

We’d like to put labels on both ends of the gauge and a description in between them, and it turns out flexbox is an elegant solution:

#rating {
  font-size: 30px;
  display: flex;
  width: 300px;
  justify-content: space-between;
}

Here’s the markup:

<div id="rating">
  <div id="turtle">🐢</div>
  <div id="feedback"></div>
  <div id="rabbit">🐇</div>
</div>

That’s all we need to position our labels. If the rating <div> is the width of the diameter of our circle, flexbox will position the emoji labels at the ends and the description in the center of the circle!

As for controlling what the description says, it’s similar to the trick we used for our timer, except this time we do it vertically rather than horizontally since the feedback descriptions are of variable length. But they are always the same height.

Conclusion

I opened this article with questions about whether CSS is a programming language. It’s hard to argue the logic we were able to implement using just CSS wasn’t programming, but some of it is unusual usage of CSS to say the least. As with many things in the tech world, the answer seems to be “it depends,” and as much as we learned about CSS through this experiment, we’ve also illustrated that programming is as much about the mindset as the tech.

No CSS hacking article is complete without the disclaimer that although we’ve shown we can implement sophisticated logic in CSS and learn a lot in the process, most of the time we probably shouldn’t do this in production, because of maintainability, accessibility, and some other words ending with “ility.”

But we also saw that some things — such as what I think of as the built-in reactivity you get with CSS variables — are quite convenient in CSS but might require us to go through hoops in JavaScript, and probably use a framework. By pushing the limits of CSS we ended up creating a circular gauge that I believe could reasonably be used in a production app, and might even be the right thing compared to reaching for some JavaScript widget that might be heavy and do more than we really need.

On my wishlist for the CSS Sudoku app is a reset button. Currently, if you want to start a new Sudoku game, you have to refresh the page. That’s an inherent limitation of radio button hacks which makes CSS hacking different to conventional programming. At one stage, I believed I found a solution when I thought animations could be used to set CSS variables — but it turns out that is part of CSS Houdini and only supported in Chromium-based browsers. If and when that’s supported everywhere, it will open a Pandora’s box of hacks and be a lot of fun. In a future article, I may even explore why this innocuous feature we have in Chrome is a game changer for CSS hacking.

The jury is still out on whether a full 81-square Sudoku solver is possible in CSS, but if you’re curious to find out, leave your feedback in the comments. If enough people want it, we may go down that rabbit hole together and see what dark corners of CSS we can illuminate in the process.


Generating (and Solving!) Sudokus in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/generating-and-solving-sudokus-in-css/feed/ 6 338362
Recreating Game Elements for the Web: The Among Us Card Swipe https://css-tricks.com/recreating-game-elements-for-the-web-the-among-us-card-swipe/ Tue, 09 Feb 2021 16:13:48 +0000 https://css-tricks.com/?p=333769 As a web developer, I pay close attention to the design of video games. From the HUD in Overwatch to the catch screen in Pokemon Go to hunting in Oregon Trail, games often have interesting mechanics and satisfying interactions, …


Recreating Game Elements for the Web: The Among Us Card Swipe originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
As a web developer, I pay close attention to the design of video games. From the HUD in Overwatch to the catch screen in Pokemon Go to hunting in Oregon Trail, games often have interesting mechanics and satisfying interactions, many of which inspire my own coding games at Codepip.

Beyond that, implementing small slices of these game designs on a web stack is a fun, effective way to broaden your skills. By focusing on a specific element, your time is spent working on an interesting part without having to build out a whole game with everything that entails. And even in this limited scope, you often get exposed to new technologies and techniques that push on the boundaries of your dev knowledge.

As a case study for this idea, I’ll walk you through my recreation of the card swipe from Among Us. For anyone in the dark, Among Us is a popular multiplayer game. Aboard a spaceship, crewmates have to deduce who among them is an imposter. All the while, they complete mundane maintenance tasks and avoid being offed by the imposter.

The card swipe is the most infamous of the maintenance tasks. Despite being simple, so many players have struggled with it that it’s become the stuff of streams and memes.

Here’s my demo

This is my rendition of the card swipe task:

Next, I’ll walk you through some of the techniques I used to create this demo.

Swiping with mouse and touch events

After quickly wireframing the major components in code, I had to make the card draggable. In the game, when you start dragging the card, it follows your pointer’s position horizontally, but stays aligned with the card reader vertically. The card has a limit in how far past the reader it can be dragged to its left or right. Lastly, when you lift your mouse or finger, the card returns to its original position.

All of this is accomplished by assigning functions to mouse and touch events. Three functions are all that‘s needed to handle mouse down, mouse move, and mouse up (or touch start, touch move, and touch end if you‘re on a touchscreen device). Here’s the skeleton of that JavaScript code:

const card = document.getElementById('card');
const reader = document.getElementById('reader');
let active = false;
let initialX;

// set event handlers
document.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
document.addEventListener('touchstart', dragStart);
document.addEventListener('touchmove', drag);
document.addEventListener('touchend', dragEnd);

function dragStart(e) {
  // continue only if drag started on card
  if (e.target !== card) return;

  // get initial pointer position
  if (e.type === 'touchstart') {
    initialX = e.touches[0].clientX;
  } else {
    initialX = e.clientX;
  }

  active = true;
}

function drag(e) {
  // continue only if drag started on card
  if (!active) return;

  e.preventDefault();
  
  let x;

  // get current pointer position
  if (e.type === 'touchmove') {
    x = e.touches[0].clientX - initialX;
  } else {
    x = e.clientX - initialX;
  }

  // update card position
  setTranslate(x);
}

function dragEnd(e) {
  // continue only if drag started on card
  if (!active) return;

  e.preventDefault();
  
  let x;

  // get final pointer position
  if (e.type === 'touchend') {
    x = e.touches[0].clientX - initialX;
  } else {
    x = e.clientX - initialX;
  }

  active = false;
  
  // reset card position
  setTranslate(0);
}

function setTranslate(x) {
  // don't let card move too far left or right
  if (x < 0) {
    x = 0;
  } else if (x > reader.offsetWidth) {
    x = reader.offsetWidth;
  }

  // set card position on center instead of left edge
  x -= (card.offsetWidth / 2);
  
  card.style.transform = 'translateX(' + x + 'px)';
}

Setting status with performance.now()

Next, I had to determine whether the card swipe was valid or invalid. For it to be valid, you must drag the card across the reader at just the right speed. Didn’t drag it far enough? Invalid. Too fast? Invalid. Too slow? Invalid.

To find if the card has been swiped far enough, I checked the card’s position relative to the right edge of the card reader in the function dragEnd:

let status;

// check if card wasn't swiped all the way
if (x < reader.offsetWidth) {
  status = 'invalid';
}

setStatus(status);

To measure the duration of the card swipe, I set start and end timestamps in dragStart and dragEnd respectively, using performance.now().

function setStatus(status) {

  // status is only set for incomplete swipes so far
  if (typeof status === 'undefined') {

    // timestamps taken at drag start and end using performance.now()
    let duration = timeEnd - timeStart;

    if (duration > 700) {
      status = 'slow';
    } else if (duration < 400) {
      status = 'fast';
    } else {
      status = 'valid';
    }
  }

  // set [data-status] attribute on reader
  reader.dataset.status = status;
}

Based on each condition, a different value is set on the reader’s data-status attribute. CSS is used to display the relevant message and illuminate either a red or green light.

#message:after {
  content: "Please swipe card";
}

[data-status="invalid"] #message:after {
  content: "Bad read. Try again.";
}

[data-status="slow"] #message:after {
  content: "Too slow. Try again.";
}

[data-status="fast"] #message:after {
  content: "Too fast. Try again.";
}

[data-status="valid"] #message:after {
  content: "Accepted. Thank you.";
}

.red {
  background-color: #f52818;
  filter: saturate(0.6) brightness(0.7);
}

.green {
  background-color: #3dd022;
  filter: saturate(0.6) brightness(0.7);
}

[data-status="invalid"] .red,
[data-status="slow"] .red,
[data-status="fast"] .red,
[data-status="valid"] .green {
  filter: none;
}

Final touches with fonts, animations, and audio

With the core functionality complete, I added a few more touches to get the project looking even more like Among Us.

First, I used a free custom font called DSEG to imitate the segmented type from old LCDs. All it took was hosting the files and declaring the font face in CSS.

@font-face {
  font-family: 'DSEG14Classic';
  src: url('../fonts/DSEG14-Classic/DSEG14Classic-Regular.woff2') format('woff2'),
       url('../fonts/DSEG14-Classic/DSEG14Classic-Regular.woff') format('woff'),
       url('../fonts/DSEG14-Classic/DSEG14Classic-Regular.ttf') format('truetype');
}

Next, I copied the jitter animation of the text in the original. Game developers often add subtle animations to breath life into an element, like making a background drift or a character, well, breathe. To achieve the jitter, I defined a CSS animation:

@keyframes jitter {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(5px);
  }
}

At this point, the text glides smoothly back and forth. Instead, what I want is for it to jump back and forth five pixels at a time. Enter the steps() function:

#message {
  animation: jitter 3s infinite steps(2);
}

Finally, I added the same audio feedback as used in Among Us.

let soundAccepted = new Audio('./audio/CardAccepted.mp3');
let soundDenied = new Audio('./audio/CardDenied.mp3');

if (status === 'valid') {
  soundAccepted.play();
} else {
  soundDenied.play();
}

Sound effects are often frowned upon in the web development world. A project like this an opportunity to run wild with audio.

And with that, the we’re done! Here’s that demo again:

Try your own

Given how standardized the web has become in look and feel, this approach of pulling an element from a game and implementing it for the web is a good way to break out of your comfort zone and try something new.

Take this Among Us card swipe. In a small, simple demo, I tinkered with web fonts and animations in CSS. I monkeyed with input events and audio in JavaScript. I dabbled with an unconventional visual style.

Now it’s time for you to survey interesting mechanics from your favorite games and try your hand at replicating them. You might be surprised what you learn.


Recreating Game Elements for the Web: The Among Us Card Swipe originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
333769
Whack-a-Mole: The CSS Edition https://css-tricks.com/whack-a-mole-the-css-edition/ https://css-tricks.com/whack-a-mole-the-css-edition/#comments Wed, 06 Jan 2021 15:39:22 +0000 https://css-tricks.com/?p=331541 We’ve seen the checkbox hack and how it can be used to build a complete state machine in CSS. Today, we’ll take that line of thought a step further and build a simple game of Whack-A-Mole, where the player needs …


Whack-a-Mole: The CSS Edition originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
We’ve seen the checkbox hack and how it can be used to build a complete state machine in CSS. Today, we’ll take that line of thought a step further and build a simple game of Whack-A-Mole, where the player needs to react quickly to win … all without a touch of JavaScript.

This might seem a little silly in a language that doesn’t have any notion of timers or intervals, but the secret is that CSS does — it’s just packaged in a little feature called CSS Animations.

Take a look at this double-click handler. Note that CSS doesn’t know what a click is, much less a double click:

How does it work? Here’s a recording of the important elements, recolored and slowed down:

The idea is that when you click on the button for the first time, it moves the double-click element in place under the cursor, but it also causes a masking element to start moving up to cover it.

  • If the second click comes quickly enough after the first (as on the left side of the recording), it happens on the double-click (blue) element.
  • Otherwise (as on the right side of the recording), it happens on the masking (yellow) element, which has the effect of a single-click element.

(For an in-depth explanation, take a look at my write-up on the pure-CSS double-click handler here.)

There are two ideas in play here:

  1. Animations can be used to manage states according to a set pattern. (I’m using the term “state” loosely.)
  2. By changing an element’s position, we can change whether or not an user is allowed to take an action.

That’s all we need!

Now, instead of having our target scroll into view, we could instead use animation-timing-function: step-end to have it pop in and out, sort of like a mole in a hole:

I tried a few different options for moving the mole out of the way, and changing its absolute position — here left — seems to work the best. Using a translation would be convenient, but unfortunately leaves Firefox thinking the cursor is on the wrong element because changing transform in the Gecko layout engine doesn’t trigger a re-layout. (Normally, this would be a good thing for performance reasons, but not so much for our little demo!)

With a bit of styling, we can make it look more like a game element:

The “mole” here is basically a restyled CSS label that triggers the checkbox hack. So is the hole. When the mole’s animation takes it over the hole, clicking in the region triggers the mole’s radio input; when the mole is out of the way, clicking triggers the hole’s radio input.

The next step is to put a couple of holes next to each other and have the moles bounce around among them, each on a different negative animation-delay. With a state machine reading off which of the moles have been hit and which of the holes have been collapsed, it turns into a neat little game.

I used short Python scripts to generate both the state machine selectors and the mole keyframes. It gets a bit unwieldy for hand-coding otherwise. This is mostly because CSS also has no concept of random numbers, which leaves us no choice but to hardcode the “random” mole behavior.

There we have it: a complete reaction-based game, entirely in HTML and CSS.


Whack-a-Mole: The CSS Edition originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/whack-a-mole-the-css-edition/feed/ 3 331541
Multiplayer Tic Tac Toe with GraphQL https://css-tricks.com/multiplayer-tic-tac-toe-with-graphql/ Fri, 23 Aug 2019 14:35:35 +0000 https://css-tricks.com/?p=294362 GraphQL is a query language for APIs that is very empowering for front-end developers. As the GraphQL site explains it, you describe your data, ask for what you want, and get predictable results.

If you haven’t worked with it …


Multiplayer Tic Tac Toe with GraphQL originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
GraphQL is a query language for APIs that is very empowering for front-end developers. As the GraphQL site explains it, you describe your data, ask for what you want, and get predictable results.

If you haven’t worked with it before, GraphQL might be a little confusing to grok at first glance. So, let’s build a multiplayer tic-tac-toe game using it in order to demonstrate how it’s used and what we can do with it.

First thing we need is a backend for our APIs. We’re going to use Hasura GraphQL Engine along with a custom GraphQL server for this tutorial. We’ll look at the queries and mutations that the client-side needs to build the game. You can implement this kind of thing in whatever framework you wish, but we’re going with use React and Apollo for this tutorial.

Here’s what we’re making:

GitHub Repo

A brief GraphQL primer

GraphQL is a query language for APIs; a language with a syntax that defines a way to fetch data from the server. It works with any kind of API that is backed by a strong system that makes your codebase resilient. Some of the primary characteristics of GraphQL are:

  • The client can ask the server for what queries it supports (check out introspection for more).
  • The client must ask the server for exactly what it wants. It can’t ask for something like a wildcard (*) but rather exact fields. For example, to get a user’s ID and name, the GraphQL query would be something like:
    query {
      user {
        id
        name
      }
    }
  • Every query is made to a single endpoint and every request is a POST request.
  • Given a query, the structure of the response is enforced. For example, for the above query to get the id and name of a user object, a successful response would be something like:
    {
      "data": {
        "user": {
          "id": "AUTH:482919",
          "name": "Jon Doe"
        }
      }
    }

This series of articles is a great place to start if you want to know more about GraphQL.

Why are we using GraphQL, anyway?

We just discussed how GraphQL demands that the client must ask the server for exactly what it wants. That means there is no unnecessary data retrieved from the server, like in case of REST where you would receive a huge response even when you need one field. Getting what new need and only what we need optimizes responses so that they’re speedy and predictable.

The client can ask the server for its schema via introspection. This schema can be used to build queries dynamically using an API explorer like GraphiQL. It also enables linting and auto-completing because every query can be built with and cross-checked against the GraphQL schema. As a front-end developer, this greatly enhances the DX as there is much less human error involved.

Since there is a single endpoint and every request is a POST request, GraphQL can avoid a lot of boilerplate since it doesn’t have to track endpoints, request payloads and response types. Response caching is much easier because every query response can be expected to be of a certain structure.

Furthermore, GraphQL has a well-defined spec for implementing real-time subscriptions. You do not have to come up with your own implementation details for building real-time servers. Build a server that complies with GraphQL’s real-time spec and any client can start consuming the real-time GraphQL APIs with ease.

GraphQL Terminology

I will be using some GraphQL terms in this post, so it’s worth covering a few of them here in advance.

  • Query: A GraphQL query is one that simply fetches data from the server.
  • Mutation: This is a GraphQL query that changes something on the server and fetches some data.
  • Subscription: This is a GraphQL query that subscribes the client to some changes on the server.
  • Query variables: These allow us to add parameters to a GraphQL query.

Getting back to the backend

Now that we have a cursory understanding of GraphQL, let’s start with modeling a backend. Our GraphQL backend would be a combination of Hasura GraphQL Engine and a custom GraphQL server. We will not go into the subtleties of code in this case.

Since Tic Tac Toe is a multiplayer game, there is a need to store state in the database. We will use Postgres as our database and Hasura GraphQL Engine as a GraphQL server that allows us to CRUD the data in Postgres over GraphQL.

Apart from CRUD on the database, we would also want to run some custom logic in the form of GraphQL mutations. We will use a custom GraphQL server for that.

Hasura describes itself quite nicely in its README file:

GraphQL Engine is a blazing-fast GraphQL server that gives you instant, realtime GraphQL APIs over Postgres, with webhook triggers on database events, and remote schemas for business logic.

Going a little deeper, Hasura GraphQL Engine is an open-source server that sits on top of a Postgres database and allows you to CRUD the data over real-time GraphQL. It works with both new and existing Postgres databases. It also has an access control layer that you can integrate with any auth provider. In this post though, we will not implement auth for the sake of brevity.

Let’s start by deploying an instance of Hasura GraphQL Engine to Heroku’s free tier that comes with a fresh Postgres database. Go ahead, do it; it is free and you do not need to enter your credit card details :)

Once you deploy, you will land up on the Hasura console which is the admin UI to manage your backend. Note that the URL you are at, is your GraphQL Engine URL. Lets start with creating our required tables.

user

A user table will store our users. To create the table, go to the “Data” tab on top and click on the “Create Table” button.

This table has an id column which is the unique identifier of each user and a name column that stores the user’s name.

board

The board table will store the game boards where all the action happens. We’ll spin up a new board for each game that starts.

Lets look at the columns of this table:

  • id: A unique UUID for each board that is auto generated
  • user_1_id: The user_id of the first user. This user by default plays X in the game
  • user_2_id: The user_id of the second user. This user by default plays O.
  • winner: This is a text field that is set to X or O once the winner has been decided.
  • turn: This is a text field that can be X or O and it stores the current turn. It starts with X.

Since user_1_id and user_2_id store the user IDs, let’s add a constraint on the board table that ensures user_1_id and user_2_id to be present in table user.

Go to the “Modify” tab in the Hasura board table and add the foreign keys.

Adding the foreign key on user_1_id. We’ll need to add a new foreign key on user_2_id as well.

Now, based on these relationships, we need to create a connection between these tables so that we can query the user information while querying the board.

Go to the “Relationships” tab in Hasura and create relationships called user1 and user2 for user_1_id and user_2_id based suggested relations.

move

Finally, we need a move table that stores the moves made by users on a board.

Let’s look at the columns:

  • id: The unique identifier of each move that is auto generated
  • user_id: The ID of the user that made the move
  • board_id: The ID of the board that the move was made on
  • position: The position where the move was made (i.e. 1-9)

Since user_id and board_id are foreign keys to user and board table, respectively. We must create these foreign key constraints just like we did above. Next, create the relationships as user for the foreign key on user_id and board for the foreign key on board_id. Then, we’ll go back to the board table’s “Relationship” tab and create the suggested relationship to move table. Call it moves.

We need a custom server

Apart from storing data in the database, we also want to perform custom logic. The logic? Whenever a user makes a move, the move must be validated, made before the turn must is switched.

In order to do that, we must run an SQL transaction on the database. I have written a GraphQL server that does exactly that that I’ve deployed on Glitch.

Now we have two GraphQL servers. But GraphQL spec enforces that there must be only one endpoint. For this purpose, Hasura supports remote schemas — i.e. you can provide an external GraphQL endpoint to Hasura and it will merge this GraphQL server with itself and serve the combined schema under a single endpoint. Let’s add this custom GraphQL server to our Hasura Engine instance:

  1. Fork the GraphQL server.
  2. Add an environment variable that is the connection to your Postgres database. To do that, go to https://dashboard.heroku.com, choose your app, go to “Settings” and reveal config vars.

A few more steps from there:

  1. Copy the value for the DATABASE_URL.
  2. Go to the GraphQL server you forked and paste that value in the .env file (POSTGRES_CONNECTION_STRING=<value>).
  3. Click on the “Show Live” button on top and copy the opened URL.

We’ll add this URL to GraphQL engine by adding it as a remote schema. Go to the “Remote Schemas” tab on top and click on the “Add” option.

We are done with setting up our backend!

Let’s work on the front end

I will not be going into the details of front-end implementation since y’all would choose to implement it in the framework of your choice. I’ll go ahead and provide all the required queries and mutations that you would need to build the game. Using these with the front-end framework of your choice will allow you to build a fully functional multiplayer Tic Tac Toe.

Setup

Apollo Client is the go-to library for client-side GraphQL. They have awesome abstractions for React, Vue, Angular, iOS, Android etc. It helps you save a lot of boilerplate and the DX is smooth. You might want to consider using Apollo client over doing everything from scratch.

Let’s discuss the queries and mutations that the client would need for this game.

Insert user

In the app that I built, I generated a random username for each user and inserted this name into the database whenever they open the app. I also stored the name and generated a user ID in local storage so that the same user does not have different usernames. The mutation I used is:

mutation ($name:String) {
  insert_user (
    objects: {
      name: $name
    }
  ) {
    returning {
      id
    }
  }
}

This mutation inserts an entry into the user table and returns the generated id. If you observe the mutation closely, it uses $name. This is called the query variable. When you send this mutation to the server along with the variables { "name": "bazooka"}, the GraphQL server would replace $name from the query variables, which in this case would be “bazooka.”

If you wish, you can implement auth and insert into this table with the username or the nickname.

Load all boards

To load all the boards, we run a GraphQL subscription:

subscription {
  board (
    where: {
      _and: {
        winner: {
          _is_null: true
        },
        user_2_id: {
          _is_null: true
        }
      }
    }
    order_by: {
      created_at: asc }
  ) {
    id
    user1 {
      id
      name
    }
    user_2_id
    created_at
    winner
  }
}

This subscription is a live query that returns the id, user1 along with their id and name (from the relationship), user_2_id, winner and created_at. We have set a where filter which fetches us only the boards without a valid winner and where user_2_id is null which means the board is is open for a player to join. Finally, we order these boards by their created_at timestamp.

Creating a board

Users can create boards for other people to join. To do that, they have to insert an entry into the boards table.

mutation ($user_id: Int) {
  insert_board (
    objects: [{
      user_1_id: $user_id,
      turn: "x",
    }]
  ) {
    returning {
      id
    }
  }
}

Joining a board

To join a board, a user needs to update the user_2_id of the board with their own user_id. The mutation looks like:

mutation ($user_id: Int, $board_id: uuid!) {
  update_board (
    _set: {
      user_2_id: $user_id
    },
    where: {
      _and: {
        id: {
          _eq: $board_id
        },
        user_2_id: {
          _is_null: true
        },
        user_1_id: {
          _neq: $user_id
        }
      }
    }
  ) {
    affected_rows
    returning {
      id
    }
  }
}

In the above GraphQL mutation, we are setting the user_2_id of a board to a user_id. We have also added additional checks such that this action succeeds only if the joining player is not the creator and the board is not already full. After the mutation, we ask for the number of affected rows.

In my app, after joining a board, I would redirect users to /play?board_id=<board_id>.

Subscribing to the board

When both users are in game, we need real-time updates about the moves of each player. So we must subscribe to the given board that is being played on and also the moves (through the relationship).

subscription($board_id: uuid!) {
  board: board_by_pk (id: $board_id) {
    id
    moves (order_by: { id: desc}) {
      id
      position
      user {
        id
        name
      }
      user_id
    }
    user1 {
      id
      name
    }
    user2 {
      id
      name
    }
    turn
    winner
  }
}

The above query subscribes the client to the board that is being played. Whenever a new move is played, the client will be updated with it.

Making a move

To make a move, we will be using the make_move mutation from our custom GraphQL server.

mutation (
  $board_id: String!,
  $position: Int!,
  $user_id: Int!
) {
  make_move (
    board_id: $board_id,
    position: $position,
    user_id: $user_id
  ) {
    success
  }
}

This mutation takes a board_id, position and user_id from query variables. It validates the move, makes the move and also switches the turn. In the end, it returns whether this transaction was successful or not.

Tic Tac Whoa!

And now you have a working game of Tic Tac Toe! You can implement any real-time multiplayer game with GraphQL subscriptions using the concepts we covered. Let me know if you have any questions and I would be happy to answer.


Multiplayer Tic Tac Toe with GraphQL originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
294362
Building Battleship in CSS https://css-tricks.com/building-battleship-in-css/ https://css-tricks.com/building-battleship-in-css/#comments Thu, 16 Aug 2018 13:55:01 +0000 http://css-tricks.com/?p=274867 This is an experiment to see how far into an interactive experience I can get using only CSS. What better project to attempt than a game? Battleship seemed like a good challenge and a step up from the CSS games …


Building Battleship in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
This is an experiment to see how far into an interactive experience I can get using only CSS. What better project to attempt than a game? Battleship seemed like a good challenge and a step up from the CSS games I’ve seen so far because it has the complexity of multiple areas that have to interact with two players.

Wanna see the complete game?

View Repo View Demo

Oh, you wanna learn how it works? Let’s dig in.

I could tell right away there was going to be a lot of repetitive HTML and very long CSS selectors coming, so I set up Pug to compile HTML and Less to compile CSS. This is what all the code from here on is going to be written in.

Interactive elements in CSS

In order to get the game mechanics working, we need some interactive elements. We’re going to walk through each one.

HTML checkboxes and :checked

Battleship involves a lot of checking to see whether a field contains a ship or not, so we’re going to use a boatload of checkboxes.

[type*='checkbox'] {
  // inactive style

  &:checked {
    // active style
  }
}

To style checkboxes, we would first need to reset them using appearance: none; which is only poorly supported right now and needs browser prefixes. The better solution here is to add helper elements. <input> tags can’t have children, including pseudo elements (even though Chrome will render them anyway), so we need to work around that using the adjacent sibling selector.

[type*='checkbox'] {
  position: relative;
  opacity: none;

  + .check-helper {
    position: absolute;
    top: 0;
    left: 0;
    pointer-events: none;
    // further inactive styles
  }

  &:checked {
    + .check-helper {
      // active styles
    }
  }
}

If you use a <label> for the helper element, you will also extend the click area of the checkbox onto the helper, allowing you to position it more freely. Also, you can use multiple labels for the same checkbox. Multiple checkboxes for the same label are not supported, however, since you would have to assign the same ID for each checkbox.

Targets

We’re making a local multiplayer game, so we need to hide one player’s battlefield from the other and we need a pause mode allowing for a player to switch without glancing at the other player’s ships. A start screen explaining the rules would also be nice.

HTML already gives us the option to link to a given ID in the document. Using :target we can select the element that we just jumped to. That allows us to create an Single Page Application-like behavior in a completely static document (and without even breaking the back button).

- var screens = ['screen1', 'screen2', 'screen3'];
body
  nav
    each screen in screens
      a(href='#' + screen)

  each screen in screens
    .screen(id=screen)
      p #{screen}
.screen {
  display: none;

  &:target {
    display: block;
  }
}

Visibility and pointer events

Rendering elements inactive is usually done by using pointer-events: none; The cool thing about pointer-events is that you can reverse it on the child elements. That will leave only the selected child clickable, but the parent stays click-through. This will come in handy later in combination with checkbox helper elements.

The same goes for visibility: hidden; While display: none; and opacity: 0; make the element and all it’s children disappear, visibility can be reversed.

Note that a hidden visibility also disables any pointer events, unlike opacity: 0;, but stays in the document flow, unlike display: none;.

.foo {
  display: none; // invisible and unclickable
  .bar {
    display: block; // invisible and unclickable
  }
}

.foo {
  visibility: hidden; // invisible and unclickable
  .bar {
    visibility: visible; // visible and clickable
  }
}

.foo {
  opacity: 0;
  pointer-evens: none; // invisible and unclickable
  .bar {
    opacity: 1;
    pointer-events: all; // still invisible, but clickable
  }
}
CSS Rule Reversible opacity Reversible pointer events
display: none;
visibility: hidden;
opacity: 0;
pointer-events: none;

OK, now that we’ve established the strategy for our interactive elements, let’s turn to the setup of the game itself.

Setting up

We have some global static variables and the size of our battlefields to define before we actually start:

@gridSize: 12;
@zSea: 1;
@zShips: 1000;
@zAbove: 2000;
@seaColor: #123;
@enemyColor: #f0a;
@playerColor: #0c8;
@hitColor: #f27;

body {
  --grid-measurements: 70vw;
  @media (min-aspect-ratio: 1/2) {
    --grid-measurements: 35vh;
  }
}

The grid size is the size of the battlefield: 12×12 fields in this case. Next, we define some z-indexes and colors.

Here’s the Pug skeleton:

doctype html

head
  title Ships!
  link(rel="stylesheet", href="style.css")
  meta(charset="UTF-8")
  meta(name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no")
  meta(name="theme-color" content="#000000")

body

Everything HTML from this point on will be in the body.

Implementing the states

We need to build the states for Player 1, Player 2, pause, and a start screen. We’ll do this like it was explained above with target selectors. Here’s a little sketch of what we’re trying to achieve:

We have a few modes, each in its own container with an ID. Only one mode is to be displayed in the viewport—the others are hidden via display: none;, except for player modes. If one player is active, the other needs to be outside of the viewport, but still have pointer events so the players can interact with each other.

.mode#pause

each party in ['p1', 'p2']
  .mode(id=party)

.mode#start

.status
  each party  in ['p1', 'p2']
    a.player-link.switch(href='#' + party)
  a.status-link.playpause(href='#pause') End Turn

h1
  Ships!

The .status div contains the main navigation. Its entries will change depending on the active mode, so in order to select it properly, we’ll need put it after our .mode elements. The same goes for the <h1>, so it ends up at the end of the document (don’t tell the SEO people).

.mode {
  opacity: 0;
  pointer-events: none;

  &:target,
  &#start {
    opacity: 1;
    pointer-events: all;
    z-index: 1;
  }

  &#p1, &#p2 {
    position: absolute;
    transform: translateX(0);
    opacity: 1;
    z-index: 2;
  }

  &#p1:target {
    transform: translateX(50vw);

    +#p2 {
      transform: translateX(50vw);
      z-index: 2;
    }
  }

  &#p2 {
    transform: translateX(50vw);
    z-index: 1;
  }

&#pause:target {
    ~ #p1, ~ #p2 {
      opacity: 0;
    }
  }
}

#start {
  .mode:target ~ & {
    display: none;
  }
}

The .mode div never has pointer events and always is fully transparent (read: inactive), except for the start screen, which is enabled by default and the currently targeted screen. I don’t simply set it to display: none; because I still need it to be in the document flow. Hiding the visibility won’t work because I need to activate pointer events individually later on, when hitting enemy ships.

I need #p1 and #p2 to be next to each other because that’s what’s going to enable the interaction between one players hits and the other players ships.

Implementing the battlefields

We need two sets of two battlefields for a total of four battlefields. Each set contains one battlefield for the current player and another for the opposite player. One set is going to be in #p1 and the other one in #p2. Only one of the players will be in the viewport, but both retain their pointer events and their flow in the document. Here’s a little sketch:

Now we need lots of HTML. Each player needs two battlefields, which need to have 12×12 fields. That’s 576 fields in total, so we’re going to loop around a bit.

The fields are going to have their own class declaring their position in the grid. Also, fields in the first row or line get a position indicator, so you get to say something cool like “Fire at C6.”

each party in 'p1', 'p2']
  .mode(id=party)
    each faction in 'enemy', 'player']
      .battlefield(class=faction, class=party)
        each line in 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
          each col, colI in 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L']
            div(class='field-x' + (colI+1) + '-y' + line)
              if (col === 'A')
                .indicator-col #{line}
              if (line === 1)
                .indicator-line #{col}

The battlefield itself is going to be set in a CSS grid, with its template and measurements coming from the variables we set before. We’ll position them absolutely within our .mode divs and switch the enemy position with the player. In the actual board game, you have your own ships on the bottom as well. Note that we need to escape the calc on the top value, or Less will try to calculate it for you and fail.

.battlefield {
  position: absolute;
  display: grid;
  grid-template-columns: repeat(@gridSize, 1fr);
  width: var(--grid-measurements);
  height: var(--grid-measurements);
  margin: 0 auto 5vw;
  border: 2px solid;
  transform: translate(-50%, 0);
  z-index: @zSea;

  &.player {
    top: calc(var(--grid-measurements) ~"+" 150px);
    border-color: transparent;

    :target & {
      border-color: @playerColor;
    }
  }

  &.enemy {
    top: 100px;
    border-color: transparent;

    :target & {
      border-color: @enemyColor;
    }
  }
}

We want the tiles of the battlefield to be a nice checkerboard pattern. I wrote a mixin to calculate the colors, and since I like my mixins separated from the rest, this is going into a components.less file.

.checkerboard(@counter) when (@counter > 0) {
  .checkerboard(@counter - 2);

  &[class^='field-'][class$='-y@{counter}'] {
    &:nth-of-type(odd) {
      background-color: transparent;

      :target & {
      background-color: darken(@seaColor, 3%);
    }
  }

  &:nth-of-type(even) {
    background-color: transparent;

    :target & {
        background-color: darken(@seaColor, 4%);
      }
    }
  }
}

When we call it with .checkerboard(@gridSize);, it will iterate through every second line of the grid and set background colors for odd and even instances of the current element. We can color the remaining fields with an ordinary :odd and :even.

Next, we place the indicators outside of the battlefields.

[class^='field-'] {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  background-color: transparent;
  
  .checkerboard(@gridSize);
  :target &:nth-of-type(even) {
    background-color: darken(@seaColor, 2%);
  }

  :target &:nth-of-type(odd) {
    background-color: darken(@seaColor, 1%);
  }

  [class^='indicator-'] {
    display: none;

    :target & {
      position: absolute;
      display: flex;
      justify-content: center;
      width: calc(var(--grid-measurements)~"/"@gridSize);
      height: calc(var(--grid-measurements)~"/"@gridSize);
      color: lighten(@seaColor, 10%);
      pointer-events: none;
    }

    &.indicator-line {
      top: -1.5em;
      align-items: flex-start;
    }

    &.indicator-col {
      left: -2.3em;
      align-items: center;
    }
  }
}

Implementing the ships

Let’s get to the tricky part and place some ships. Those need to be clickable and interactive, so they’re going to be checkboxes. Actually, we need two checkboxes for one ship: miss and hit.

  • Miss is the bottom one. If nothing else is on that field, your shot hits the water and triggers a miss-animation. The exception is when a player clicks on their own battlefield. In that case, the ship animation plays.
  • When an own ships spawns, it activates a new checkbox. This one is called hit. It’s placed at the exact same coordinates as its corresponding ship, but in the other players attack field and above the checkbox helper for the miss. If a hit is activated, it displays a hit animation on the current player’s attack field as well as well as on the opponent’s own ship.

This is why we need to position our battlefields absolutely next to each other. We need them aligned at all times in order to let them interact with each other.

First, we’re going to set some styles that apply to both checkboxes. We still need the pointer events, but want to visually hide the checkbox and work with helper elements instead.

.check {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  
  + .check-helper {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
  }
}

We’ll also write some classes for our events for later use right now. This will also go into components.less:

.hit-obj {
  position: absolute;
  visibility: visible;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  animation: hit 1s forwards;
}

.ship-obj {
  position: absolute;
  left: 0;
  top: 0;
  width: 90%;
  height: 90%;
  border-radius: 15%;
  animation: setShip 0.5s forwards;
}

.miss-obj {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  animation: miss 1s forwards;
}

Spawning and missing ships

Those two events are basically the same. If you hit the sea in your own battlefield, you create a ship. If you hit the sea in the enemy battlefield, you trigger a miss. That happens by calling the respective class from our components.less file within a pseudo element of the helper class. We use pseudo elements here because we need to place two objects in one helper later on.

If you spawn a ship, you shouldn’t be able to un-spawn it, so we make it lose its pointer events after being checked. However, the next hit-checkbox gains it pointer events, enabling the enemy to hit spawned ships.

.check {
  &.ship {
    &:checked {
      pointer-events: none;
    }

    &:checked + .check-helper {
      :target .player & {
        &::after {
          content: "";
          .ship-obj; // set own ship
        }
      }

      :target .enemy & {
        &::after {
          content: "";
          .miss-obj; // miss enemy ship
        }
      }
    }        

    &:checked ~ .hit {
      pointer-events: all;
    }
  }
}

Hitting ships

That new hit checkbox is positioned absolutely on top of the other player’s attack field. For Player 1 that means by 50vw to the right and by the grid height + 50px margin to the top. It has no pointer events by default, they are going to be overwritten by those set in .ship:check ~ .hit, so only ships that are actually set, can be hit.

To display a hit event, we need two pseudo elements: one that confirms the hit on the attack field; and one that shows the victim where they have been hit. :checked + .check-helper::after calls a .hit-obj from components.less onto the attacker’s field and the corresponding ::before pseudo element gets translated back to the victim’s own battlefield.

Since the display of hit events isn’t scoped to the active player, we need to remove all unnecessary instances manually using display: none;.

.check {
  &.hit {
    position: absolute;
    top: ~"calc(-1 * (var(--grid-measurements) + 50px))";
    left: 50vw;
    width: 100%;
    height: 100%;
    pointer-events: none;

    #p2 &,
    #p1:target & {
      left: 0;
    }

    #p1:not(:target) & + .check-helper::before {
      left: 50vw;
    }

    &:checked {
      opacity: 1;
      visibility: hidden;
      pointer-events: none;

      + .check-helper {
        &::before {
          content: "";
          .hit-obj; // hit enemy ships
          top: ~"calc(-1 * (var(--grid-measurements) + 50px))";
      }

        &::after {
          content: "";
          .hit-obj; // hit own ships
          top: -2px;
          left: -2px;
        }

        #p1:target &::before,
        #p1:target ~ #p2 &::after,
        #p1:not(:target) &::after,
        #p2:target &::before {
          display: none;
        }
      }
    }

    #p1:target .battlefield.p1 &,
    #p2:target .battlefield.p2 & {
      display: none;
    }
  }
}

Animating the events

While we did style our miss, ship and hit objects, there’s nothing to be seen yet. That’s because we are still missing the animations making those objects visible. Those are simple keyframe animations that I put into a new Less file called animations.less.

@keyframes setShip {
  0% {
    transform: scale(0, 0);
    background-color: transparent;
  }

  100% {
    transform: scale(1, 1);
    background-color: @playerColor;
  }
}

@keyframes hit {
  0% {
    transform: scale(0, 0);
    opacity: 0;
    background-color: transparent;
  }

  10% {
    transform: scale(1.2, 1.2);
    opacity: 1;
    background-color: spin(@hitColor, 40);
    box-shadow: 0 0 0 0.5em var(--shadowColor);
  }

  100% {
    transform: scale(.7, .7);
    opacity: .7;
    background-color: @hitColor;
    box-shadow: 0 0 0 0.5em var(--shadowColor);
  }
}

@keyframes miss {
  0% {
    transform: scale(0, 0);
    opacity: 1;
    background-color: lighten(@seaColor, 50);
  }

  100% {
    transform: scale(1, 1);
    opacity: .8;
    background-color: lighten(@seaColor, 10);
  }
}

Add customizable player names

This isn’t really necessary for functionality, but it’s a nice little extra. Instead of being called “Player 1” and “Player 2,” you can enter your own name. We do this by adding two <input type="text"> to .status, one for each player. They have placeholders in case the players don’t want to enter their names and want to skip to the game right away.

.status
  input(type="text" placeholder="1st Player").player-name#name1
  input(type="text" placeholder="2nd Player").player-name#name2
  each party  in ['p1', 'p2']
      a.player-link.switch(href='#' + party)
  a.status-link.playpause(href='#pause') End Turn

Because we put them into .status, we can display them on every screen. On the start screen, we leave them as normal input fields, for the players to enter their names. We style their placeholders to look like the actual text input, so it doesn’t really matter if players enter their names or not.

.status {
  .player-name {
    position: relative;
    padding: 3px;
    border: 1px solid @enemyColor;
    background: transparent;
    color: @playerColor;

    &::placeholder {
      color: @playerColor;
      opacity: 1; // Reset Firefox user agent styles
    }
  }
}

On the other screens, we remove their typical input field styles as well as their pointer events, making they appear as normal, non-changeable text. .status also contains empty links to select players. We style those links to have actual measurements and display the name inputs without pointer events above them. Clicking a name triggers the link now, targeting the corresponding mode.

.status {
  .mode#pause:target ~ & {
    top: 40vh;
    width: calc(100% ~"-" 40px);
    padding: 0 20px;
    text-align: center;
    z-index: @zAbove;

    .player-name,
    .player-link {
      position: absolute;
      display: block;
      width: 80%;
      max-width: 500px;
      height: 40px;
      margin: 0;
      padding: 0;

      &:nth-of-type(even) {
        top: 60px;
      }
    }

    .player-name {
      border: 0;
      text-align: center;
      pointer-events: none;
    }
  }
}

The player screens only need to display the active player, so we remove the other one.

.status {
  .mode#p1:target ~ & #name2 {
    display: none;
  }
  
  .mode#p2:target ~ & #name1 {
    display: none;
  }
}

Some notes on the Internet Explorer and Edge: Microsoft browsers haven’t implemented the ::placeholder pseudo element. While they do support :-ms-input-placeholder for IE and ::-ms-input-placeholder, as well as the webkit-prefix for Edge, those prefixes only work if ::placeholder is not set. As far as I played around with placeholders, I only managed to style them properly in either the Microsoft browsers, or all the other ones. If someone else has a workaround, please share it!

Putting it all together

What we have so far is a functional, but not very handsome game. I use the start screen to clarify some basic rules. Since we don’t have a hard-coded win condition and nothing to prevent players to place their ships wildly all over the place, I created a “Play fair” note that encourages the good ol’ honor system.

.mode#start
  .battlefield.enemy
    ol
      li
        span You are this color.
      li
        span Your enemy is
        span this
        span color
      li
        span You may place your ships as follows:
        ul
          li 1 x 5 blocks
          li 2 x 4 blocks
          li 3 x 3 blocks
          li 4 x 2 blocks

I’m not going into the detail of how I went about getting things exactly to my liking since most of that is very basic CSS. You can go through the end result to pick them out.

When we finally connect all the pieces, we get this:

See the Pen CSS Game: Battleships by Daniel Schulz (@iamschulz) on CodePen.

Wrapping up

Let’s look back at what we’ve accomplished.

HTML and CSS may not be programming languages, but they are mighty tools in their own domain. We can manage states with pseudo classes and manipulate the DOM with pseudo elements.

While most of us use :hover and :focus all the time, :checked goes by largely unnoticed, only for styling actual checkboxes and radio buttons at best. Checkboxes are handy little tools that can help us to get rid of unnecessary JavaScript in our more simple front end features. I wouldn’t hesitate to build dropdown or off-canvas menus in pure CSS in real life projects, as long as the requirements don’t get too complicated.

I’d be a bit more cautious when using the :target selector. Since it uses the URL hash value, it’s only usable for a global value. I think I’d use it for, say, highlighting the current paragraph on a content page, but not for reusable elements like a slider or an accordion menu. It can also quickly get messy on larger projects, especially when other parts of it start controlling the hash value.

Building the game was a learning experience for me, dealing with pseudo selectors interacting with each other and playing around with lots of pointer events. If I had to build it again, I’d surely choose another path, which is a good outcome for me. I definitely don’t see it as a production-ready or even clean solution, and those super specific selectors are a nightmare to maintain, but it has some good parts in it that I can transition to real life projects.

Most importantly though, it was a fun thing to do.


Building Battleship in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/building-battleship-in-css/feed/ 2 274867
Creating a Parking Game With the HTML Drag and Drop API https://css-tricks.com/creating-a-parking-game-with-the-html-drag-and-drop-api/ https://css-tricks.com/creating-a-parking-game-with-the-html-drag-and-drop-api/#comments Thu, 08 Mar 2018 14:15:17 +0000 http://css-tricks.com/?p=267922 Among the many JavaScript APIs added in HTML5 was Drag and Drop (we’ll refer to it as DnD in this article) which brought native DnD support to the browser, making it easier for developers to implement this interactive feature into …


Creating a Parking Game With the HTML Drag and Drop API originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Among the many JavaScript APIs added in HTML5 was Drag and Drop (we’ll refer to it as DnD in this article) which brought native DnD support to the browser, making it easier for developers to implement this interactive feature into applications. The amazing thing that happens when features become easier to implement is that people start making all kinds of silly, impractical things with it, like the one we’re making today: a parking game!

DnD requires only a few things to work:

  • Something to drag
  • Somewhere to drop
  • JavaScript event handlers on the target to tell the browser it can drop

We’re going to start by creating our draggables.

Dragging

Both <img> and <a>(with the href attribute set) elements are draggable by default. If you want to drag a different element, you’ll need to set the draggable attribute to true.

We’ll start with the HTML that sets up the images for our four vehicles: fire truck, ambulance, car and bicycle.

<ul class="vehicles">
  <li>
    <!-- Fire Truck -->
    <!-- <code>img<code> elements don't need a <code>draggable<code> attribute like other elements -->
    <img id="fire-truck" alt="fire truck" src="https://cdn.glitch.com/20f985bd-431d-4807-857b-e966e015c91b%2Ftruck-clip-art-fire-truck4.png?1519011787956"/>
  </li>
  <li>
    <!-- Ambulance -->
    <img id="ambulance" alt="ambulance" src="https://cdn.glitch.com/20f985bd-431d-4807-857b-e966e015c91b%2Fambulance5.png?1519011787610">
  </li>
  <li>
    <!-- Car -->
    <img id="car" alt="car" src="https://cdn.glitch.com/20f985bd-431d-4807-857b-e966e015c91b%2Fcar-20clip-20art-1311497037_Vector_Clipart.png?1519011788408">
  </li>
  <li>
    <!-- Bike -->
    <img id="bike" alt="bicycle" src="https://cdn.glitch.com/20f985bd-431d-4807-857b-e966e015c91b%2Fbicycle-20clip-20art-bicycle3.png?1519011787816">
  </li>
</ul>

Since images are draggable by default, you’ll see dragging any one of them creates a ghost image.

Just adding a draggable attribute to an element that’s not an image or link is really all you need to make an element draggable in most browsers. To make elements draggable in all browsers, you need to define some event handlers. They are also useful for adding extra functionality like a border if an element is being dragged around or a sound if it stops being dragged. For these, you’re going to need some drag event handlers, so let’s look at those.

Drag Events

There are three drag-related events you can listen for but we’re only going to use two: dragstart and dragend.

  • dragstart – Triggered as soon as we start dragging. This is where we can define the drag data and the drag effect.
  • dragend – Triggered when a draggable element is dropped. This event is generally fired right after the drop zone’s drop event.

We’ll cover what the drag data and the drag effect is shortly.

let dragged; // Keeps track of what's being dragged - we'll use this later! 

function onDragStart(event) {
  let target = event.target;
  if (target && target.nodeName === 'IMG') { // If target is an image
    dragged = target;
    event.dataTransfer.setData('text', target.id);
    event.dataTransfer.dropEffect = 'move';
    // Make it half transparent when it's being dragged
    event.target.style.opacity = .3;
  }
}

function onDragEnd(event) {
  if (event.target && event.target.nodeName === 'IMG') {
    // Reset the transparency
    event.target.style.opacity = ''; // Reset opacity when dragging ends 
    dragged = null; 
  }
}

// Adding event listeners
const vehicles = document.querySelector('.vehicles');
vehicles.addEventListener('dragstart', onDragStart);
vehicles.addEventListener('dragend', onDragEnd);

There are a couple of things happening in this code:

  • We are defining the drag data. Each drag event has a property called dataTransfer that stores the event’s data. You can use the setData(type, data) method to add a dragged item to the drag data. We’re storing the dragged image’s ID as type 'text' in line 7.
  • We’re storing the element being dragged in a global variable. I know, I know. Global is dangerous for scoping but here’s why we do it: although you can store the dragged item using setData, you can’t retrieve it using event.dataTransfer.getData() in all browsers (except Firefox) because the drag data is protected mode. You can read more about it here. I wanted to mention defining the drag data just so you know about it.
  • We’re setting the dropEffect to move. The dropEffect property is used to control the feedback the user is given during a drag and drop operation. For example, it changes which cursor the browser displays while dragging. There are three effects: copy, move and link.
    • copy – Indicates that the data being dragged will be copied from its source to the drop location.
    • move – Indicates that the data being dragged will be moved.
    • link – Indicates that some form of relationship will be created between the source and drop locations.

Now we have draggable vehicles but nowhere to drop them:

See the Pen 1 – Can you park here? by Omayeli Arenyeka (@yelly) on CodePen.

Dropping

By default, when you drag an element, only form elements such as <input> will be able to accept it as a drop. We’re going to contain our “dropzone” in a <section> element, so we need to add drop event handlers so it can accept drops just like a form element.

First, since it’s an empty element we’re going to need to set a width, height and background color on it so we can see it on screen.

These are the parameters we have available for drop events:

  • dragenter – Triggered at the moment a draggable item enters a droppable area. At least 50% of the draggable element has to be inside the drop zone.
  • dragover – The same as dragenter but it is called repeatedly while the draggable item is within the drop zone.
  • dragleave – Triggered once a draggable item has moved away from a drop zone.
  • drop – Triggered when the draggable item has been released and the drop area agrees to accept the drop.
function onDragOver(event) {
  // Prevent default to allow drop
  event.preventDefault();
}

function onDragLeave(event) {
  event.target.style.background = '';
}

function onDragEnter(event) {
  const target = event.target;
  if (target) {
      event.preventDefault();
      // Set the dropEffect to move
      event.dataTransfer.dropEffect = 'move'
      target.style.background = '#1f904e';
  }
}

function onDrop(event) {
  const target = event.target;
  if ( target) {
    target.style.backgroundColor = '';
    event.preventDefault();
    // Get the id of the target and add the moved element to the target's DOM
    dragged.parentNode.removeChild(dragged);
    dragged.style.opacity = '';
    target.appendChild(dragged);
  }
}

const dropZone = document.querySelector('.drop-zone');
dropZone.addEventListener('drop', onDrop);
dropZone.addEventListener('dragenter', onDragEnter);
dropZone.addEventListener('dragleave', onDragLeave);
dropZone.addEventListener('dragover', onDragOver);

If you’re wondering why we keep calling event.preventDefault() it’s because by default the browser assumes any target is not a valid drop target. This isn’t true all the time for all browsers but it’s better to be safe than sorry! Calling preventDefault() on the dragenter, dragover and drop events, informs the browser that the current target is a valid drop target.

Now, we have a simple drag and drop application!

See the Pen 2 – Can you park here? by Omayeli Arenyeka (@yelly) on CodePen.

It’s fun, but not quite as frustrating as parking. We have to create some rules to make that happen.

Rules and Validation

I came up with some random parking rules, and I’d encourage you to create some of your own. Parking signs usually have days and times you can park as well as what types of vehicles are allowed to park at that moment in time. When we were creating our draggable objects, we had four vehicles: an ambulance, a fire truck, a regular car and a bicycle. So, we’re going to create rules for them.

  1. Ambulance parking only: Monday through Friday, 9pm to 3am.
  2. Fire truck parking only: All day during the weekend.
  3. Regular car parking: Monday through Friday, 3am to 3pm.
  4. Bicycle parking: Monday through Friday, 3pm to 9pm.

Now, we translate these rules to code. We’re going to be using two libraries to handle time and ranges: Moment and Moment-range.

The scripts are already available in Codepen to add to any new demo, but if you are developing outside of Codepen you can copy or link them up from here:

<script defer src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/moment-range/3.1.1/moment-range.js"></script>

Then, we create an object to store all the parking rules.

window['moment-range'].extendMoment(moment);

// The array of weekdays
const weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
const parkingRules =  {
  ambulance: {
    // The ambulance can only park on weekdays...
    days: weekdays,
    // ...from 9pm to 3am (the next day)
    times: createRange(moment().set('hour', 21), moment().add(1, 'day').set('hour', 3))
  },
  'fire truck': {
    // The fire truck can obnly park on Saturdays and Sundays, but all day
    days: ['Saturday', 'Sunday']
  },
  car: {
    // The car can only park on weekdays...
    days: weekdays,
    // ...from 3am - 3pm (the same day)
    times: createRange(moment().set('hour', 3), moment().set('hour', 15))
  },
  bicycle: {
    // The car can only park on weekdays...
    days: weekdays,
    // ...from 3pm - 9pm (the same day)
    times: createRange(moment().set('hour', 15), moment().set('hour', 21))
  }
};

function createRange(start, end) {
  if (start && end) {
    return moment.range(start, end);
  }
}

Each vehicle in the parkingRules object has a days property with an array of days it can park and a times property that is a time range. To get the current time using Moment, call moment(). To create a range using Moment-range, pass a start and end time to the moment.range function.

Now, in the onDragEnter and onDrop event handlers we defined earlier, we add some checks to make sure a vehicle can park. Our alt attribute on the img tag is storing the type of vehicle so we pass that to a canPark method which will return if the car can be parked. We also added visual cues (change in background) to tell the user whether a vehicle can be parked or not.

function onDragEnter(event) {
  const target = event.target;
  if (dragged && target) {
    const vehicleType = dragged.alt; // e.g bicycle, ambulance
    if (canPark(vehicleType)) {
      event.preventDefault();
      // Set the dropEffect to move
      event.dataTransfer.dropEffect = 'move';
      /* Change color to green to show it can be dropped /*
      target.style.background = '#1f904e';    
     }
    else {
      /* Change color to red to show it can't be dropped. Notice we
       * don't call event.preventDefault() here so the browser won't
       * allow a drop by default
       */
      target.style.backgroundColor = '#d51c00'; 
    }
  }
}

function onDrop(event) {
  const target = event.target;
  if (target) {
    const data = event.dataTransfer.getData('text');
    const dragged = document.getElementById(data);
    const vehicleType = dragged.alt;
    target.style.backgroundColor = '';
    if (canPark(vehicleType)) {
       event.preventDefault();
       // Get the ID of the target and add the moved element to the target's DOM
       dragged.style.opacity = '';
       target.appendChild(dragged);
    }
  }
}

Then, we create the canPark method.

function getDay() {
  return moment().format('dddd'); // format as 'monday' not 1
}

function getHours() {
  return moment().hour();
}

function canPark(vehicle) {
  /* Check the time and the type of vehicle being dragged
   * to see if it can park at this time
   */
  if (vehicle && parkingRules[vehicle]) {
    const rules = parkingRules[vehicle];
    const validDays = rules.days;
    const validTimes = rules.times;
    const curDay = getDay();
    if (validDays) {
      /* If the current day is included on the parking days for the vehicle
       * And if the current time is within the range
       */
      return validDays.includes(curDay) && (validTimes ? validTimes.contains(moment()) : true); 
      /* Moment.range has a contains function that checks
       * to see if your range contains a moment. 
         https://github.com/rotaready/moment-range#contains
        */
    }
  }
  return false;
}

Now, only cars that are allowed to park can park. Lastly, we add the rules to the screen and style it.

Here’s the final result:

See the Pen 3 – Can you park here? by Omayeli Arenyeka (@yelly) on CodePen.

There are lots of ways this could be improved:

  • Auto-generate the HTML for the rules list from the parkingRules object!
  • Add some sound effects!
  • Add ability to drag back vehicles to original point without a page refresh.
  • All those pesky global variables.

But I’ll let you handle that.

If you’re interested in learning more about the DnD API and some critiques of it, here’s some good reading:


Creating a Parking Game With the HTML Drag and Drop API originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/creating-a-parking-game-with-the-html-drag-and-drop-api/feed/ 4 267922
The Evolution of Trust https://css-tricks.com/the-evolution-of-trust/ Mon, 07 Aug 2017 12:31:12 +0000 http://css-tricks.com/?p=257481 Nicky Case’s games are a damn treasure in this world. Most importantly, they are fun and compelling to play. They also make gameplay the vehicle for education on tricky, intricate, and important issues. Issues that would be much harder to …


The Evolution of Trust originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Nicky Case’s games are a damn treasure in this world. Most importantly, they are fun and compelling to play. They also make gameplay the vehicle for education on tricky, intricate, and important issues. Issues that would be much harder to learn about by just reading. They are also a masterclass in design: clear calls to action, clear onboarding, meaningful interactions and animations, and good copy.

This latest one is no different.

To Shared LinkPermalink on CSS-Tricks


The Evolution of Trust originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
257481
Pong with SVG.js https://css-tricks.com/pong-svg-js/ https://css-tricks.com/pong-svg-js/#comments Fri, 07 Apr 2017 14:33:30 +0000 http://css-tricks.com/?p=253412 Everybody loves the vintage game Pong, right? We sure do. What’s more fun? Building it yourself!

That’s why we decided to create one with SVG.js – to highlight some aspects of our library. It might seem like a complex idea …


Pong with SVG.js originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Everybody loves the vintage game Pong, right? We sure do. What’s more fun? Building it yourself!

That’s why we decided to create one with SVG.js – to highlight some aspects of our library. It might seem like a complex idea for a small tutorial, but as you’ll see, it’s simpler than it sounds. Let’s dive into it!

Here’s the finished product:

See the Pen Fully functional Pong game with effects by Wout Fierens (@wout) on CodePen.

Getting started

SVG.js is available through Github, npm, bower or CDN.js. There are plenty of options for getting your hands on SVG.js, so use whatever you are most comfortable with.

Start out by creating a new HTML document and include the library. Create an empty <div> to serve as a wrapper for the SVG document, and give it an id attribute. Something like pong should be suitable for this project:

<div id="pong"></div>

Next, initialize the SVG.js instance by referencing the wrapper. At this point, it’s also a good idea to define a width and height for the game which makes it easier to modify them later on.

// define width and height
var width = 450, height = 300

// create SVG document and set its size
var draw = SVG('pong').size(width, height)

Now you’re ready to start building the game.

Drawing Game Elements

The Background

The background should cover the whole document, so we’re using a <rect> and give it a neutral grayish color. First, we’ll draw the left player in green. Then, we’ll draw the right one by cloning the left one and coloring it pink.

// draw background
var background = draw.rect(width, height).fill('#E3E8E6')

We’ll also need a vertical, dashed line in the middle to distinguish the player fields.

// draw line
var line = draw.line(width/2, 0, width/2, height)
line.stroke({ width: 5, color: '#fff', dasharray: '5,5' })

See the Pen Pong Background by Wout Fierens (@wout) on CodePen.

Paddles and the Ball

Pong wouldn’t be Pong without paddles and a ball. First, we’ll draw the left player in green. Then, we’ll draw the right one by cloning the left one and coloring it pink.

var paddleWidth = 20, paddleHeight = 100

// create and position left paddle
var paddleLeft = draw.rect(paddleWidth, paddleHeight)
paddleLeft.x(0).cy(height/2).fill('#00ff99')

// create and position right paddle
var paddleRight = paddleLeft.clone()
paddleRight.x(width-paddleWidth).fill('#ff0066')

For the ball we’re going to use a circle with a diameter of 20 and place it in the center of the court:

// define ball size
var ballSize = 20

// create ball
var ball = draw.circle(ballSize)
ball.center(width/2, height/2).fill('#7f7f7f')

See the Pen Pong Paddels and Ball by Wout Fierens (@wout) on CodePen.

Score board

Finally, we will need a scoreboard which we will add at the top of the court.

// define initial player score
var playerLeft = playerRight = 0

// create text for the score, set font properties
var scoreLeft = draw.text(playerLeft+'').font({
  size: 32,
  family: 'Menlo, sans-serif',
  anchor: 'end',
  fill: '#fff'
}).move(width/2-10, 10)

// cloning rocks!
var scoreRight = scoreLeft.clone()
  .text(playerRight+'')
  .font('anchor', 'start')
  .x(width/2+10)

That’s all! Now we have all game elements, let’s move on to game logic.

See the Pen Pong Scoreboard by Wout Fierens (@wout) on CodePen.

Game logic

We’ll start out by writing an update function which will update the state of our game and game elements.

// random velocity for the ball at start
var vx = Math.random() * 500 - 250
  , vy = Math.random() * 500 - 250

// update is called on every animation step
function update(dt) {
  // move the ball by its velocity
  ball.dmove(vx*dt, vy*dt)

  // get position of ball
  var cx = ball.cx()
    , cy = ball.cy()

  // check if we hit top/bottom borders
  if ((vy < 0 && cy <= 0) || (vy > 0 && cy >= height)) {
    vy = -vy
  }

  // check if we hit left/right borders
  if ((vx < 0 && cx <= 0) || (vx > 0 && cx >= width)) {
    vx = -vx
  }
}

When we run this, nothing will happen, because we didn’t call the update function yet. This will be done using JavaScript’s native requestAnimationFrame feature, which will allow us to do smooth animations. To make this work, a handler is registered to periodically call our update function:

var lastTime, animFrame;

function callback(ms) {
  // we get passed a timestamp in milliseconds
  // we use it to determine how much time has passed since the last call

  if (lastTime) {
    update((ms-lastTime)/1000) // call update and pass delta time in seconds
  }

  lastTime = ms
  animFrame = requestAnimationFrame(callback)
}

callback()

Yay! The ball is jumping around! But, our paddles are still pretty useless at the moment. So, let’s do something about that and insert paddle collision detection. We’ll only need it on the x-axis:

var paddleLeftY = paddleLeft.y()
  , paddleRightY = paddleRight.y()

// check if we hit the paddle
if ((vx < 0 && cx <= paddleWidth && cy > paddleLeftY && cy < paddleLeftY + paddleHeight) ||
   (vx > 0 && cx >= width - paddleWidth && cy > paddleRightY && cy < paddleRightY + paddleHeight)) {
  // depending on where the ball hit we adjust y velocity
  // for more realistic control we would need a bit more math here
  // just keep it simple
  vy = (cy - ((vx < 0 ? paddleLeftY : paddleRightY) + paddleHeight/2)) * 7 // magic factor

  // make the ball faster on hit
  vx = -vx * 1.05
} else ...

Better, now the ball is aware of the paddles. But a few other things are still missing:

  • the score is not updating when the border is hit
  • paddles are not moving
  • the ball should reset when a point was scored

Let’s work through this list from top to bottom.

See the Pen Pong jumping ball by Wout Fierens (@wout) on CodePen.

Update the score

To update our score, we need to hook into collision detection to the left or right wall:

// check if we hit left/right borders
if ((vx < 0 && cx <= 0) || (vx > 0 && cx >= width)) {
  // when x-velocity is negative, its a point for player 2, else player 1
  if (vx < 0) { ++playerRight }
  else { ++playerLeft }

  vx = -vx

  scoreLeft.text(playerLeft + '')
  scoreRight.text(playerRight + '')
}

See the Pen Pong jumping ball by Wout Fierens (@wout) on CodePen.

Moving the user-controlled paddle

The right paddle will be controlled by the keyboard, and that’s a piece of cake with SVG.js:

// define paddle direction and speed
var paddleDirection = 0  // -1 is up, 1 is down, 0 is still
  , paddleSpeed = 5      // pixels per frame refresh

// detect if up and down arrows are prssed to change direction
SVG.on(document, 'keydown', function(e) {
  paddleDirection = e.keyCode == 40 ? 1 : e.keyCode == 38 ? -1 : 0
});

// make sure the direction is reset when the key is released
SVG.on(document, 'keyup', function(e) {
  paddleDirection = 0
})

So what are we doing here? First, we call SVG.on, which lets us bind an event listener to any node (not only SVG.js objects). We’ll listen to the keydown event to detect if either the up (38) or the down (40) key is pressed. If that’s the case, the paddleDirection will be set to -1 or 1 respectively. If another key is pressed, the paddleDirection will be 0. Lastly, when any key is released, the paddleDirection will be reset to 0.

The update function will do the actual work of moving the paddle, based on the user input. So we’ll add the following code to the update function:

// move player paddle
var playerPaddleY = paddleRight.y();

if (playerPaddleY <= 0 && paddleDirection == -1) {
  paddleRight.cy(paddleHeight / 2)
} else if (playerPaddleY >= height-paddleHeight && paddleDirection == 1) {
  paddleRight.y(height - paddleHeight)
} else {
  paddleRight.dy(paddleDirection * paddleSpeed)
}

We prevent the paddle from exiting the court by testing its y position. Otherwise, the paddle will be moved by a relative distance using dy().

See the Pen Pong user controlled paddle by Wout Fierens (@wout) on CodePen.

Moving the AI paddle

A good opponent will make the game worthwhile. So we’ll have the AI player follow the ball, with a predefined difficulty level. The higher the difficulty, the faster the AI paddle will respond.

First define the difficulty value, defining the AI’s speed:

var difficulty = 2

Then add the following code to the update function:

// get position of ball and paddle
var paddleRightCy = paddleRight.cy()

// move the left paddle in the direction of the ball
var dy = Math.min(difficulty, Math.abs(cy - paddleRightCy))
paddleRightCy += cy > paddleRightCy ? dy : -dy

// constraint the move to the canvas area
paddleRight.cy(Math.max(paddleHeight/2, Math.min(height-paddleHeight/2, paddleRightCy)))

See the Pen Pong user controlled paddle by Wout Fierens (@wout) on CodePen.

Score!

Wait, this isn’t right! The game goes on even after one of the players scored. Time to include a reset function to move all game elements to their initial position using animations:

function reset() {
  // reset speed values
  vx = 0
  vy = 0

  // position the ball back in the middle
  ball.animate(100).center(width / 2, height / 2)

  // reset the position of the paddles
  paddleLeft.animate(100).cy(height / 2)
  paddleRight.animate(100).cy(height / 2)
}

The reset function should be called if one of the players misses the ball. To make that happen, change the failure detection by removing the vx = -vx line and adding the reset() call:

// check if a player missed the ball
if ((vx < 0 && cx <= 0) || (vx > 0 && cx >= width)) {
  // when x-velocity is negative, its a point for player 2, else player 1
  if (vx < 0) {
    ++playerRight
  } else {
    ++playerLeft
  }

  // update score
  scoreLeft.text(playerLeft)
  scoreRight.text(playerLeft)

  reset()
}

We also need to make sure the initial vx and vy values are set to 0. So the game does not start without our input. To be able to indicate the first serve, we’ll add a click listener to the SVG document:

draw.on('click', function() {
  if (vx === 0 && vy === 0) {
    vx = Math.random() * 500 - 250
    vy = Math.random() * 500 - 250
  }
})

See the Pen Pong with start and reset by Wout Fierens (@wout) on CodePen.

Encore

Of course, there is a lot left to improve on the game, but the purpose of this tutorial is to teach about SVG and in particular about SVG.js. We want to leave you with some visual effects to spice up the game.

Ball color

It would be nice to have the color of the ball change while approaching the opposite opponent. This is done by leveraging the power of the morph method on the SVG.Color class. We’ll detect the position of the ball, and gradually assign the color of the opposite opponent, based on the position of the ball on the x-axis.

We’ll start by initializing a new instance of SVG.Color:

var ballColor = new SVG.Color('#ff0066')

Next, we’ll define the target color by calling the morph() method:

ballColor.morph('#00ff99')

This will set a start color, being #ff0066 and an end color, being #00ff99. Using the at() method on SVG.Color, we can tween the color based on a given position between 0 and 1. So by adding the following code to our update function, we can change the color of the ball while it moves:

ball.fill(ballColor.at(1/width*ball.x()))

That wasn’t hard at all, right?

Boom!

Imagine a huge color blast when the opponent missed the ball. That would make it even more fun to win a point. To achieve this, we’ll use a radial gradient. It will appear where the ball has hit the wall and then quickly fade out. Once faded out, the object carrying the gradient will be deleted from the scene. To achieve this, we’ll add another function called boom including the required logic:

function boom() {
  // detect winning player
  var paddle = vx > width/2 ? paddleLeft : paddleRight

  // create the gradient
  var gradient = draw.gradient('radial', function(stop) {
    stop.at(0, paddle.attr('fill'), 1)
    stop.at(1, paddle.attr('fill'), 0)
  })

  // create circle to carry the gradient
  var blast = draw.circle(300)
  blast.center(ball.cx(), ball.cy()).fill(gradient)

  // animate to invisibility
  blast.animate(1000, '>').opacity(0).after(function() {
    blast.remove()
  })
}

See the Pen Fully functional Pong game with effects by Wout Fierens (@wout) on CodePen.

Conclusion

That’s it! You just created a working Pong game using SVG.js. In the next tutorial, we will cover how to convert this blob of code into a reusable SVG.js plug-in, while adding new features and easy configuration to the game.


Written by Ulrich-Matthias Schäfer & Wout Fierens.


Pong with SVG.js originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/pong-svg-js/feed/ 1 253412
Using GSAP to Animate Game UI with Canvas https://css-tricks.com/using-gsap-animate-game-ui-canvas/ https://css-tricks.com/using-gsap-animate-game-ui-canvas/#comments Mon, 23 Jan 2017 12:18:06 +0000 http://css-tricks.com/?p=250106 The year was 1995; Toy Story hit the theaters, kids were obsessively collecting little cardboard circles and Kiss From a Rose was being badly sung by everyone. I was a gangly ten-year-old, and like any other relatively tall kid I …


Using GSAP to Animate Game UI with Canvas originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
The year was 1995; Toy Story hit the theaters, kids were obsessively collecting little cardboard circles and Kiss From a Rose was being badly sung by everyone. I was a gangly ten-year-old, and like any other relatively tall kid I was often addressed to by “you must be so good at basketball!”. So I practiced and practiced spending hours on the court of my elementary school. Eventually, I realized, much to the dismay of aunts and other cheek-pinchers alike, that while occupying vertical real estate might give you an advantage in the art of basketball, it does not ensure it.

Fast forward 21 years later. Now a tall and gangly developer, still bad at basketball, I was faced with a project: Designing and implementing a full motion video web basketball game for the NBA’s Detroit Pistons. Throwing balls around is one thing; throwing pixels around — now that’s finally a basketball challenge I can ace!

While developing the game I used many neat things like canvas, SVG and CSS animations, gesture recognition and a video stream that’s dynamically constructed on the fly. It’s really amazing what we can do with just a browser these days. Go ahead, give it a spin.

In this article, I want to focus and show you how I implemented the animation for the Superpower Gauge using vanilla JS in conjunction with GSAP. This is the motion reference I used while implementing the animation, created in After Effects:

In 1on1, once the user succeeds making a move, they’re awarded combo points. The gauge sits at the top left corner of the screen, and its task is to convey to the user the amount of their combo points as denoted by the number of red segments. At certain times in the game, the Superpower Gauge becomes active, notifying the user they can click it to make their in-game avatar perform a special move.

The basic structure of the Superpower is achieved with one Canvas element and a bit of simple geometry:

See the Pen Pistons Superpower: Structure by Opher Vishnia (@OpherV) on CodePen.

Essentially, there are two main components here — the central image and the gauge segments. The image is the easy part, it’s just a trivial use of canvas’ drawImage. The gauge segments is where things get interesting. I defined a general options defined with some properties to play with later on like the number of segments, radius, width and so on. Then I iterate over an array of segment objects and use their properties (strokeStyle, lineWidth) to draw the actual segments with the canvas arc function. So far so good — but where’s the animation?

I was debating whether to use a canvas animation framework but ultimately decided against it. This is because I needed to use several types of animations in the project: Canvas, SVG and CSS/DOM, and no one framework does it all. In addition, all of the animations had to run smoothly on top of playing video, on both desktop and mobile with varying capabilities and network conditions. This means that performance was nothing if not paramount, and I wanted to know exactly which code powers the animation. Luckily GSAP (aka Greensock AKA TweenMax AKA TweenLite) allows me to do just that.

GSAP is cool. It enables you to animate pretty much anything! The trick is that the animation API accepts not only DOM/SVG objects but also arbitrary JS data structures, whose properties you can then “animate”.

Animate all the thing! Meme

The basic idea is that you use GSAP to change the properties of these objects over time. These values specify how the UI looks at any given point in time. On each requestAnimationFrame you make a draw call to the canvas to draw the state of the UI based on those objects.

function render() { 
 //draw the animation state
 drawComboGui();
 
 //draw the image
 ctx.drawImage(...);
 //render on the next frame as well
 window.requestAnimationFrame(render)
}
render();

Here’s a breakdown of the different animations implemented:

Gauge fills up

See the Pen Pistons Superpower: Gauge fill by Opher Vishnia (@OpherV) on CodePen.

Let’s discuss the anatomy of this animation. At idle state, all yet-to-be filled segments of the gauge are gray and thin, and filled segments are red and slightly thicker. Once the next segment of the gauge fills up, all previous active segments change color to white, grow in thickness and start glowing. The following segment is then filled, and lastly, all the segments stop glowing and return to their original, active width.

Remember that array of segment objects? Here’s where they come into play with GSAP. The function addActiveSegment is the heart of the magic, where we use TweenMax.fromTo to animate properties like lineWidth and anglePercent. The GSAP colorProps plugin allows us to make smooth transition in color properties like strokeStyle and activeStrokeStyle. I’m using the delay property to time the various components of this animation.

TweenMax.fromTo(segments[index], expandAnimLength, {
  anglePercent: 0,
  colorProps:{strokeStyle: options.activeStrokeStyle},
}, 
{
  anglePercent: 1,
  colorProps:{strokeStyle: options.activeStrokeStyle},
  ease: Power0.easeIn,
  delay: growAnimLength
});

Like I mentioned earlier, the render function then calls drawComboGui on each requestAnimationFrame, ideally 60 times a second. In drawComboGui first we clear the canvas from any previous data drawn onto it before drawing the current state.

To create the glow effect, I drew two segments on top of each other. The bottom one uses shadowBlur on the canvas path, and the top one has no shadow blur. This makes the blurred element “peek” behind the non-blurred one, resulting in the glow effect.

There are several extra animations for the Superpower gauge. The Superpower enabled and Superpower disabled are very simple in concept to the gauge fill up discussed here. They are implemented by animating the width of the image and the active segments.

superpower charged
Superpower charged
Superpower discharged
Superpower discharged

The Superpower charged and Superpower discharged animation require other techniques like animating image sprites and applying a blur-on-the-fly filter. It’s a bit out-of-scope right now, but it will be discussed in a future article!

There’s one major gotcha when it comes to implementing UI for games. Games are extremely stateful. In any given moment the state of the game, and by proxy, its UI can change. In the Superpower Gauge’s particular case  —  it means that at every moment the gauge might fill up, become enabled, be discharged or become disabled. This can happen even while in the middle of an animation! What do you do when that happens, though?

You have two options — one is to stop whatever animation is currently playing and abruptly transition to the new animation. The problem with this approach is that the experience for the user is very jarring, detaching them from the game and ultimately conveying more noise than information. This is the exact opposite of what a good interface should do.

The other option is to queue up the animations, so each animation is fired before the last one starts. This gets a little tricky, since an animation might be comprised of smaller sub-animations, but thanks to GSAP’s Timeline feature, the task of herding all these states and animations becomes much more manageable. Instead of calling TweenMax.to, you initialize a Timeline instance object and use it to make to calls. By default, these to calls define new animations to start at the end of the timeline forming an animation queue, but this is highly configurable! You could, for example, define an animation to start at an offset relative to the timeline end, or at an absolute position on the timeline. This also allows you avoid having to use the delay property to calculate queuing of animations, which tends to get cumbersome when dealing with multiple animations.

Here’s an implementation of the gauge fill up animation using GSAP’s Timeline. Try to click the “Add Segment” button while an animation is already in progress.

See the Pen Pistons Superpower: Gauge fill with Timeline by Opher Vishnia (@OpherV) on CodePen.

I hope this helps you tackle some challenges and issues you encounter in your game/site/project. If you have any questions or if you’d like to know how I tackled other UI elements in the game feel free to hit me up on Twitter!

Epilogue

While I still get assaulted once in awhile by low hanging branches and my hoop-shooting skills leave much to be desired,  when it comes to quickly pressing key combos, Andre Drummond has nothing on me.


Using GSAP to Animate Game UI with Canvas originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/using-gsap-animate-game-ui-canvas/feed/ 5 250106
Screamy Bird https://css-tricks.com/screamy-bird/ Wed, 18 May 2016 14:20:29 +0000 http://css-tricks.com/?p=241774 During San Francisco’s Stupid Shit No One Needs and Terrible Ideas “Hackathon”, Glen Chiaccheri made a game where you have to literally scream your way through it.

Make sure to check it out in Firefox, as Chrome and …


Screamy Bird originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
During San Francisco’s Stupid Shit No One Needs and Terrible Ideas “Hackathon”, Glen Chiaccheri made a game where you have to literally scream your way through it.

Make sure to check it out in Firefox, as Chrome and Safari don’t support the getUserMedia API fully. This is the API that allows web developers to request access to the user’s microphone, which you can find more info about on HTML5Rocks.

To Shared LinkPermalink on CSS-Tricks


Screamy Bird originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
241774