I just lately found the enjoyment of making CSS-only video games. It’s all the time fascinating how HTML and CSS are able to dealing with the logic of a complete on-line recreation, so I needed to strive it! Such video games normally depend on the ol’ Checkbox Hack the place we mix the checked/unchecked state of a HTML enter with the :checked
pseudo-class in CSS. We are able to do quite a lot of magic with that one mixture!
In truth, I challenged myself to construct a complete recreation with out Checkbox. I wasn’t certain if it will be doable, nevertheless it undoubtedly is, and I’m going to point out you the way.
Along with the puzzle recreation we’ll research on this article, I’ve made a set of pure CSS video games, most of them with out the Checkbox Hack. (They’re additionally obtainable on CodePen.)
Wish to play earlier than we begin?
I personally choose enjoying the sport in full display screen mode, however you possibly can play it under or open it up over right here.
Cool proper? I do know, it’s not the Finest Puzzle Sport You Ever Noticed™ nevertheless it’s additionally not dangerous in any respect for one thing that solely makes use of CSS and some traces of HTML. You’ll be able to simply regulate the scale of the grid, change the variety of cells to manage the issue degree, and use no matter picture you need!
We’re going to remake that demo collectively, then put a bit of additional sparkle in it on the finish for some kicks.
The drag and drop performance
Whereas the construction of the puzzle is pretty simple with CSS Grid, the power to pull and drop puzzle items is a bit trickier. I needed to counting on a mix of transitions, hover results, and sibling selectors to get it accomplished.
For those who hover over the empty field in that demo, the picture strikes inside it and stays there even if you happen to transfer the cursor out of the field. The trick is so as to add an enormous transition length and delay — so massive that the picture takes numerous time to return to its preliminary place.
img {
rework: translate(200%);
transition: 999s 999s; /* very gradual transfer on mouseout */
}
.field:hover img {
rework: translate(0);
transition: 0s; /* prompt transfer on hover */
}
Specifying solely the transition-delay
is sufficient, however utilizing massive values on each the delay and the length decreases the prospect {that a} participant ever sees the picture transfer again. For those who await 999s + 999s
— which is roughly half-hour — then you will note the picture transfer. However you received’t, proper? I imply, nobody’s going to take that lengthy between turns until they stroll away from the sport. So, I think about this a very good trick for switching between two states.
Did you discover that hovering the picture additionally triggers the adjustments? That’s as a result of the picture is a part of the field aspect, which isn’t good for us. We are able to repair this by including pointer-events: none
to the picture however we received’t be capable of drag it later.
Which means we’ve got to introduce one other aspect contained in the .field
:
That additional div
(we’re utilizing a category of .a
) will take the identical space because the picture (because of CSS Grid and grid-area: 1 / 1
) and would be the aspect that triggers the hover impact. And that’s the place the sibling selector comes into play:
.a {
grid-area: 1 / 1;
}
img {
grid-area: 1 / 1;
rework: translate(200%);
transition: 999s 999s;
}
.a:hover + img {
rework: translate(0);
transition: 0s;
}
Hovering on the .a
aspect strikes the picture, and since it’s taking on all house contained in the field, it’s like we’re hovering over the field as an alternative! Hovering the picture is now not an issue!
Let’s drag and drop our picture contained in the field and see the end result:
Did you see that? You first seize the picture and transfer it to the field, nothing fancy. However when you launch the picture you set off the hover impact that strikes the picture, after which we simulate a drag and drop characteristic. For those who launch the mouse outdoors the field, nothing occurs.
Hmm, your simulation isn’t excellent as a result of we will additionally hover the field and get the identical impact.
True and we’ll rectify this. We have to disable the hover impact and permit it provided that we launch the picture contained in the field. We are going to play with the dimension of our .a
aspect to make that occur.
Now, hovering the field does nothing. However if you happen to begin dragging the picture, the .a
aspect seems, and as soon as launched contained in the field, we will set off the hover impact and transfer the picture.
Let’s dissect the code:
.a {
width: 0%;
transition: 0s .2s; /* add a small delay to verify we catch the hover impact */
}
.field:lively .a { /* on :lively improve the width */
width: 100%;
transition: 0s; /* prompt change */
}
img {
rework: translate(200%);
transition: 999s 999s;
}
.a:hover + img {
rework: translate(0);
transition: 0s;
}
Clicking on the picture fires the :lively
pseudo-class that makes the .a
aspect full-width (it’s initially equal to 0
). The lively state will stay lively till we launch the picture. If we launch the picture contained in the field, the .a
aspect goes again to width: 0
, however we’ll set off the hover impact earlier than it occurs and the picture will fall contained in the field! For those who launch it outdoors the field, nothing occurs.
There’s a little quirk: clicking the empty field additionally strikes the picture and breaks our characteristic. Presently, :lively
is linked to the .field
aspect, so clicking on it or any of its youngsters will activate it; and by doing this, we find yourself displaying the .a
aspect and triggering the hover impact.
We are able to repair that by enjoying with pointer-events
. It permits us to disable any interplay with the .field
whereas sustaining the interactions with the kid parts.
.field {
pointer-events: none;
}
.field * {
pointer-events: preliminary;
}
Now our drag and drop characteristic is ideal. Except you’ll find the right way to hack it, the one solution to transfer the picture is to pull it and drop it contained in the field.
Constructing the puzzle grid
Placing the puzzle collectively goes to really feel simple peasy in comparison with what we simply did for the drag and drop characteristic. We’re going to depend on CSS grid and background methods to create the puzzle.
Right here’s our grid, written in Pug for comfort:
- let n = 4; /* variety of columns/rows */
- let picture = "https://picsum.images/id/1015/800/800";
g(model=`--i:url(${picture})`)
- for(let i = 0; i < n*n; i++)
z
a
b(draggable="true")
The code could look unusual nevertheless it compiles into plain HTML:
<g model="--i: url(https://picsum.images/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>
<!-- and so forth. -->
</g>
I guess you’re questioning what’s up with these tags. None of those parts have any particular that means — I simply discover that the code is way simpler to jot down utilizing <z>
than a bunch of <div class="z">
or no matter.
That is how I’ve mapped them out:
<g>
is our grid container that comprisesN*N
<z>
parts.<z>
represents our grid gadgets. It performs the function of the.field
aspect we noticed within the earlier part.<a>
triggers the hover impact.<b>
represents a portion of our picture. We apply thedraggable
attribute on it as a result of it can’t be dragged by default.
Alright, let’s register our grid container on <g>
. That is in Sass as an alternative of CSS:
$n : 4; /* variety of columns/rows */
g {
--s: 300px; /* dimension of the puzzle */
show: grid;
max-width: var(--s);
border: 1px strong;
margin: auto;
grid-template-columns: repeat($n, 1fr);
}
We’re truly going to make our grid youngsters — the <z>
parts — grids as nicely and have each <a>
and <b>
inside the identical grid space:
z {
aspect-ratio: 1;
show: grid;
define: 1px dashed;
}
a {
grid-area: 1/1;
}
b {
grid-area: 1/1;
}
As you possibly can see, nothing fancy — we created a grid with a selected dimension. The remainder of the CSS we’d like is for the drag and drop characteristic, which requires us to randomly place the items across the board. I’m going to show to Sass for this, once more for the comfort of with the ability to loop by way of and magnificence all of the puzzle items with a perform:
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: ground(($i - 0.001) / $n);
z:nth-of-type(#{$i}) b{
background-position: ($x / ($n - 1)) * 100% ($y / ($n - 1)) * 100%;
rework:
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 could have observed that I’m utilizing the Sass random()
perform. That’s how we get the randomized positions for the puzzle items. Bear in mind that we’ll disable that place when hovering over the <a>
aspect after dragging and dropping its corresponding <b>
aspect contained in the grid cell.
z a:hover ~ b {
rework: translate(0);
transition: 0s;
}
In that very same loop, I’m additionally defining the background configuration for each bit of the puzzle. All of them will logically share the identical picture because the background, and its dimension needs to be equal to the scale of the entire grid (outlined with the --s
variable). Utilizing the identical background-image
and a few math, we replace the background-position
to point out solely a chunk of the picture.
That’s it! Our CSS-only puzzle recreation is technically accomplished!
However we will all the time do higher, proper? I confirmed you the right way to make a grid of puzzle piece shapes in one other article. Let’s take that very same thought and apply it right here, we could?
Puzzle piece shapes
Right here’s our new puzzle recreation. Identical performance however with extra life like shapes!
That is an illustration of the shapes on the grid:
For those who look carefully you’ll discover that we’ve got 9 totally different puzzle-piece shapes: the 4 corners, the 4 edges, and one for every little thing else.
The grid of puzzle items I made within the different article I referred to is a bit more simple:
We are able to use the identical method that mixes CSS masks and gradients to create the totally different shapes. In case you’re unfamiliar with masks
and gradients, I extremely advocate checking that simplified case to raised perceive the method earlier than transferring to the following half.
First, we have to use particular selectors to focus on every group of parts that shares the identical form. Now we have 9 teams, so we’ll 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’s a determine that reveals how that maps to our grid:
Now let’s sort out the shapes. Let’s give attention to studying only one or two of the shapes as a result of all of them use the identical method — and that method, you could have some homework to continue to learn!
For the puzzle items within the heart of the grid, 0
:
masks:
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 could look advanced, however let’s give attention to one gradient at a time to see what’s occurring:
Two gradients create two circles (marked inexperienced and purple within the demo), and two different gradients create the the slots that different items connect with (the one marked blue fills up many of the form whereas the one marked crimson fills the highest portion). A CSS variable, --r
, units the radius of the round shapes.
The form of the puzzle items within the heart (marked 0
within the illustration) is the toughest to make because it makes use of 4 gradients and has 4 curvatures. All of the others items juggle fewer gradients.
For instance, the puzzle items alongside the highest fringe of the puzzle (marked 2
within the illustration) makes use of three gradients as an alternative of 4:
masks:
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 eliminated the primary (high) gradient and adjusted the values of the second gradient in order that it covers the house left behind. You received’t discover an enormous distinction within the code if you happen to examine the 2 examples. It needs to be famous that we will discover totally different background configurations to create the identical form. For those who begin enjoying with gradients you’ll for certain give you one thing totally different than what I did. You could even write one thing that’s extra concise — if that’s the case, share it within the feedback!
Along with creating the shapes, additionally, you will discover that I’m rising the width and/or the peak of the weather like under:
peak: calc(100% + var(--r));
width: calc(100% + var(--r));
The items of the puzzle must overflow their grid cell to attach.
Remaining demo
Right here is the total demo once more. For those who examine it with the primary model you will note the identical code construction to create the grid and the drag-and-drop characteristic, plus the code to create the shapes.
Doable enhancements
The article ends right here however we might hold enhancing our puzzle with much more options! How a couple of a timer? Or perhaps some form of congratulations when the participant finishes the puzzle?
I could think about all these options in a future model, so control my GitHub repo.
Wrapping up
And CSS ism’t a programming language, they are saying. Ha!
I’m not attempting to spark some #HotDrama by that. I say it as a result of we did some actually tough logic stuff and coated quite a lot of CSS properties and methods alongside the best way. We performed with CSS Grid, transitions, masking, gradients, selectors, and background properties. To not point out the few Sass methods we used to make our code simple to regulate.
The purpose was to not construct the sport, however to discover CSS and uncover new properties and methods that you should utilize in different tasks. Creating a web based recreation in CSS is a problem that pushes you to discover CSS options in nice element and discover ways to use them. Plus, it’s simply quite a lot of enjoyable that we get one thing to play with when all is claimed and accomplished.
Whether or not CSS is a programming language or not, doesn’t change the truth that we all the time study by constructing and creating modern stuff.