Comment j'ai créé un jeu de puzzle CSS pur PlatoBlockchain Data Intelligence. Recherche verticale. Aï.

Comment j'ai créé un jeu de puzzle en CSS pur

J'ai récemment découvert la joie de créer des jeux uniquement en CSS. C'est toujours fascinant de voir comment HTML et CSS sont capables de gérer la logique d'un jeu en ligne entier, alors j'ai dû l'essayer ! Ces jeux reposent généralement sur le vieux Checkbox Hack où nous combinons l'état coché/décoché d'une entrée HTML avec le :checked pseudo-classe en CSS. Nous pouvons faire beaucoup de magie avec cette seule combinaison !

En fait, je me suis lancé le défi de construire un jeu entier sans Checkbox. Je ne savais pas si ce serait possible, mais c'est définitivement le cas, et je vais vous montrer comment.

En plus du jeu de réflexion que nous allons étudier dans cet article, j'ai réalisé une collection de jeux CSS purs, la plupart d'entre eux sans le Checkbox Hack. (Ils sont également disponibles sur CodePen.)

Vous voulez jouer avant de commencer ?

Personnellement, je préfère jouer au jeu en mode plein écran, mais vous pouvez y jouer ci-dessous ou ouvre le par ici.

Cool non ? Je sais, ce n'est pas le meilleur jeu de puzzle que vous ayez jamais vu ™, mais ce n'est pas mal du tout pour quelque chose qui n'utilise que CSS et quelques lignes de HTML. Vous pouvez facilement ajuster la taille de la grille, changer le nombre de cellules pour contrôler le niveau de difficulté et utiliser l'image que vous voulez !

Nous allons refaire cette démo ensemble, puis y mettre un peu plus d'éclat à la fin pour quelques coups de pied.

La fonctionnalité glisser-déposer

Alors que la structure du puzzle est assez simple avec CSS Grid, la possibilité de faire glisser et déposer des pièces de puzzle est un peu plus délicate. J'ai dû compter sur une combinaison de transitions, d'effets de survol et de sélecteurs frères pour le faire.

Si vous survolez la boîte vide dans cette démo, l'image se déplace à l'intérieur de celle-ci et y reste même si vous déplacez le curseur hors de la boîte. L'astuce consiste à ajouter une durée et un délai de transition importants - si importants que l'image met beaucoup de temps à revenir à sa position initiale.

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

En spécifiant uniquement le transition-delay est suffisant, mais l'utilisation de grandes valeurs à la fois pour le délai et la durée diminue les chances qu'un joueur voie l'image reculer. Si vous attendez 999s + 999s — qui est d'environ 30 minutes — alors vous verrez l'image bouger. Mais vous ne le ferez pas, n'est-ce pas ? Je veux dire, personne ne prendra autant de temps entre les tours à moins de s'éloigner du jeu. Donc, je considère cela comme une bonne astuce pour basculer entre deux états.

Avez-vous remarqué que le survol de l'image déclenche également les modifications ? C'est parce que l'image fait partie de l'élément box, ce qui n'est pas bon pour nous. Nous pouvons résoudre ce problème en ajoutant pointer-events: none à l'image, mais nous ne pourrons pas la faire glisser plus tard.

Cela signifie que nous devons introduire un autre élément à l'intérieur du .box:

Cet extra div (nous utilisons une classe de .a) prendra la même zone que l'image (grâce à CSS Grid et grid-area: 1 / 1) et sera l'élément qui déclenche l'effet de survol. Et c'est là que le sélecteur de frère entre en jeu :

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

Planant sur le .a L'élément déplace l'image, et puisqu'il occupe tout l'espace à l'intérieur de la boîte, c'est comme si nous survolions la boîte à la place ! Survoler l'image n'est plus un problème !

Faisons glisser et déposer notre image à l'intérieur de la boîte et voyons le résultat :

As-tu vu ça? Vous saisissez d'abord l'image et la déplacez dans la boîte, rien d'extraordinaire. Mais une fois que vous relâchez l'image, vous déclenchez l'effet de survol qui déplace l'image, puis nous simulons une fonction de glisser-déposer. Si vous relâchez la souris en dehors de la boîte, rien ne se passe.

Hmm, votre simulation n'est pas parfaite car nous pouvons également survoler la boîte et obtenir le même effet.

C'est vrai et nous rectifierons cela. Nous devons désactiver l'effet de survol et l'autoriser uniquement si nous libérons l'image à l'intérieur de la boîte. Nous allons jouer avec la dimension de notre .a élément pour que cela se produise.

Maintenant, le survol de la boîte ne fait rien. Mais si vous commencez à faire glisser l'image, le .a apparaît, et une fois relâché à l'intérieur de la boîte, on peut déclencher l'effet de survol et déplacer l'image.

Disséquons le 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;
}

Cliquer sur l'image déclenche le :active pseudo-classe qui rend le .a élément pleine chasse (il est initialement égal à 0). L'état actif restera infection jusqu'à ce que nous publions l'image. Si nous libérons l'image à l'intérieur de la boîte, le .a l'élément revient à width: 0, mais nous déclencherons l'effet de survol avant qu'il ne se produise et l'image tombera à l'intérieur de la boîte ! Si vous le relâchez en dehors de la boîte, rien ne se passe.

Il y a une petite bizarrerie : cliquer sur la case vide déplace également l'image et casse notre fonctionnalité. Actuellement, :active est lié au .box élément, donc cliquer dessus ou sur l'un de ses enfants l'activera; et en faisant cela, nous finissons par montrer le .a élément et déclenchant l'effet de survol.

Nous pouvons résoudre ce problème en jouant avec pointer-events. Il nous permet de désactiver toute interaction avec le .box tout en maintenant les interactions avec les éléments enfants.

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

Maintenant notre fonction glisser-déposer est parfaite. À moins que vous ne trouviez comment la pirater, la seule façon de déplacer l'image est de la faire glisser et de la déposer à l'intérieur de la boîte.

Construire la grille du puzzle

Assembler le puzzle va sembler facile par rapport à ce que nous venons de faire pour la fonction glisser-déposer. Nous allons nous appuyer sur la grille CSS et les astuces d'arrière-plan pour créer le puzzle.

Voici notre grille, écrite en Pug pour plus de commodité :

- 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") 

Le code peut sembler étrange mais il se compile en HTML simple :

<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>

Je parie que vous vous demandez ce qui se passe avec ces balises. Aucun de ces éléments n'a de signification particulière - je trouve juste que le code est beaucoup plus facile à écrire en utilisant <z> qu'un tas de <div class="z"> ou peu importe.

Voici comment je les ai cartographiés :

  • <g> est notre conteneur de grille qui contient N*N <z> éléments.
  • <z> représente nos éléments de grille. Il joue le rôle de .box élément que nous avons vu dans la section précédente.
  • <a> déclenche l'effet de survol.
  • <b> représente une partie de notre image. Nous appliquons le draggable car il ne peut pas être déplacé par défaut.

Très bien, enregistrons notre conteneur de grille sur <g>. C'est en Sass au lieu de 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);
}

Nous allons en fait faire de nos enfants de la grille - le <z> éléments - grilles ainsi et ont les deux <a> ainsi que <b> dans la même zone de grille :

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

Comme vous pouvez le voir, rien d'extraordinaire — nous avons créé une grille avec une taille spécifique. Le reste du CSS dont nous avons besoin est pour la fonction glisser-déposer, qui nous oblige à placer les pièces au hasard autour du tableau. Je vais me tourner vers Sass pour cela, encore une fois pour pouvoir parcourir et styliser toutes les pièces du puzzle avec une fonction :

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)
   }
}

Vous avez peut-être remarqué que j'utilise le Sass random() fonction. C'est ainsi que nous obtenons les positions aléatoires des pièces du puzzle. N'oubliez pas que nous allons désactiver cette position lorsque vous survolez le <a> élément après avoir glissé et déposé son correspondant <b> élément à l'intérieur de la cellule de la grille.

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

Dans cette même boucle, je définis également la configuration d'arrière-plan pour chaque pièce du puzzle. Tous partageront logiquement la même image comme arrière-plan, et sa taille devra être égale à la taille de toute la grille (définie avec la --s variable). En utilisant le même background-image et quelques calculs, nous mettons à jour le background-position pour ne montrer qu'une partie de l'image.

C'est ça! Notre jeu de puzzle CSS uniquement est techniquement terminé !

Mais on peut toujours faire mieux, non ? je t'ai montré comment faire une grille de formes de pièces de puzzle dans un autre article. Prenons cette même idée et appliquons-la ici, d'accord ?

Formes de pièces de puzzle

Voici notre nouveau jeu de puzzle. Même fonctionnalité mais avec des formes plus réalistes !

Voici une illustration des formes sur la grille :

Comment j'ai créé un jeu de puzzle en CSS pur

Si vous regardez attentivement, vous remarquerez que nous avons neuf formes de pièces de puzzle différentes : la quatre coins, quatre bordset un pour tout le reste.

La grille de pièces de puzzle que j'ai créée dans l'autre article auquel j'ai fait référence est un peu plus simple :

Nous pouvons utiliser la même technique qui combine des masques CSS et des dégradés pour créer les différentes formes. Au cas où vous ne seriez pas familier avec mask et les dégradés, je recommande fortement de vérifier ce cas simplifié pour mieux comprendre la technique avant de passer à la partie suivante.

Tout d'abord, nous devons utiliser des sélecteurs spécifiques pour cibler chaque groupe d'éléments partageant la même forme. Nous avons neuf groupes, nous allons donc utiliser huit sélecteurs, plus un sélecteur par défaut qui les sélectionne tous.

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 */

Voici une figure qui montre comment cela correspond à notre grille :

Comment j'ai créé un jeu de puzzle CSS pur PlatoBlockchain Data Intelligence. Recherche verticale. Aï.
Comment j'ai créé un jeu de puzzle en CSS pur

Abordons maintenant les formes. Concentrons-nous sur l'apprentissage d'une ou deux des formes car elles utilisent toutes la même technique - et de cette façon, vous avez des devoirs pour continuer à apprendre !

Pour les pièces du puzzle au centre de la grille, 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);

Le code peut sembler complexe, mais concentrons-nous sur un dégradé à la fois pour voir ce qui se passe :

Deux dégradés créent deux cercles (marqués en vert et violet dans la démo), et deux autres dégradés créent les fentes auxquelles les autres pièces se connectent (celui marqué en bleu remplit la majeure partie de la forme tandis que celui marqué en rouge remplit la partie supérieure). Une variable CSS, --r, définit le rayon des formes circulaires.

Comment j'ai créé un jeu de puzzle CSS pur PlatoBlockchain Data Intelligence. Recherche verticale. Aï.
Comment j'ai créé un jeu de puzzle en CSS pur

La forme des pièces du puzzle au centre (marquée 0 dans l'illustration) est le plus difficile à réaliser car il utilise quatre dégradés et comporte quatre courbures. Toutes les autres pièces jonglent avec moins de dégradés.

Par exemple, les pièces du puzzle le long du bord supérieur du puzzle (marquées 2 dans l'illustration) utilise trois dégradés au lieu de quatre :

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);

Nous avons supprimé le premier dégradé (du haut) et ajusté les valeurs du deuxième dégradé afin qu'il couvre l'espace laissé derrière. Vous ne remarquerez pas une grande différence dans le code si vous comparez les deux exemples. Il est à noter que l'on peut trouver différentes configurations de fond pour créer la même forme. Si vous commencez à jouer avec les dégradés, vous arriverez certainement à quelque chose de différent de ce que j'ai fait. Vous pouvez même écrire quelque chose de plus concis - si c'est le cas, partagez-le dans les commentaires !

En plus de créer les formes, vous constaterez également que j'augmente la largeur et/ou la hauteur des éléments comme ci-dessous :

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

Les pièces du puzzle doivent déborder de leur cellule de grille pour se connecter.

Comment j'ai créé un jeu de puzzle CSS pur PlatoBlockchain Data Intelligence. Recherche verticale. Aï.
Comment j'ai créé un jeu de puzzle en CSS pur

Démo finale

Voici à nouveau la démo complète. Si vous le comparez avec la première version, vous verrez la même structure de code pour créer la grille et la fonction glisser-déposer, plus le code pour créer les formes.

Améliorations possibles

L'article se termine ici mais nous pourrions continuer à améliorer notre puzzle avec encore plus de fonctionnalités ! Que diriez-vous d'une minuterie ? Ou peut-être une sorte de félicitations lorsque le joueur termine le puzzle ?

Je pourrais envisager toutes ces fonctionnalités dans une future version, donc gardez un oeil sur mon repo GitHub.

Emballage en place

Et CSS n'est pas un langage de programmation, ils disent. Ha!

Je n'essaie pas de déclencher un #HotDrama par là. Je le dis parce que nous avons fait des trucs logiques vraiment délicats et couvert beaucoup de propriétés et de techniques CSS en cours de route. Nous avons joué avec la grille CSS, les transitions, le masquage, les dégradés, les sélecteurs et les propriétés d'arrière-plan. Sans parler des quelques astuces Sass que nous avons utilisées pour rendre notre code facile à ajuster.

Le but n'était pas de construire le jeu, mais d'explorer le CSS et de découvrir de nouvelles propriétés et astuces que vous pourrez utiliser dans d'autres projets. Créer un jeu en ligne en CSS est un défi qui vous pousse à explorer les fonctionnalités CSS en détail et à apprendre à les utiliser. De plus, c'est juste très amusant d'avoir quelque chose avec quoi jouer quand tout est dit et fait.

Que CSS soit un langage de programmation ou non, cela ne change rien au fait que nous apprenons toujours en construisant et en créant des choses innovantes.

Horodatage:

Plus de Astuces CSS