Como fiz um jogo de quebra-cabeça CSS puro PlatoBlockchain Data Intelligence. Pesquisa vertical. Ai.

Como eu fiz um jogo de quebra-cabeça CSS puro

Recentemente descobri a alegria de criar jogos somente CSS. É sempre fascinante como HTML e CSS são capazes de lidar com a lógica de um jogo online inteiro, então eu tive que experimentar! Esses jogos geralmente contam com o velho Checkbox Hack, onde combinamos o estado marcado/desmarcado de uma entrada HTML com o :checked pseudoclasse em CSS. Podemos fazer muita mágica com essa combinação!

Na verdade, eu me desafiei a construir um jogo inteiro sem Checkbox. Eu não tinha certeza se seria possível, mas definitivamente é, e eu vou te mostrar como.

Além do jogo de quebra-cabeça que estudaremos neste artigo, fiz uma coleção de jogos CSS puros, a maioria deles sem o Checkbox Hack. (Eles também estão disponíveis no CodePen.)

Quer jogar antes de começar?

Eu pessoalmente prefiro jogar o jogo no modo de tela cheia, mas você pode jogar abaixo ou abra aqui.

Legal certo? Eu sei, não é o melhor jogo de quebra-cabeça que você já viu ™, mas também não é ruim para algo que usa apenas CSS e algumas linhas de HTML. Você pode ajustar facilmente o tamanho da grade, alterar o número de células para controlar o nível de dificuldade e usar a imagem que quiser!

Nós vamos refazer essa demo juntos, então colocar um pouco de brilho extra no final para dar uns chutes.

A funcionalidade de arrastar e soltar

Embora a estrutura do quebra-cabeça seja bastante direta com CSS Grid, a capacidade de arrastar e soltar peças do quebra-cabeça é um pouco mais complicada. Eu tive que confiar em uma combinação de transições, efeitos de foco e seletores de irmãos para fazer isso.

Se você passar o mouse sobre a caixa vazia nessa demonstração, a imagem se moverá dentro dela e permanecerá lá mesmo se você mover o cursor para fora da caixa. O truque é adicionar uma grande duração e atraso de transição - tão grande que a imagem leva muito tempo para retornar à sua posição inicial.

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

Especificando apenas o transition-delay é suficiente, mas usar grandes valores tanto no atraso quanto na duração diminui a chance de um jogador ver a imagem se mover para trás. Se você esperar 999s + 999s — que é aproximadamente 30 minutos — então você verá a imagem se mover. Mas você não vai, certo? Quero dizer, ninguém vai demorar tanto entre os turnos, a menos que saia do jogo. Então, considero isso um bom truque para alternar entre dois estados.

Você notou que passar o mouse sobre a imagem também aciona as mudanças? Isso porque a imagem faz parte do elemento caixa, o que não é bom para nós. Podemos corrigir isso adicionando pointer-events: none para a imagem, mas não poderemos arrastá-la mais tarde.

Isso significa que temos que introduzir outro elemento dentro do .box:

Esse extra div (estamos usando uma classe de .a) ocupará a mesma área que a imagem (graças ao CSS Grid e grid-area: 1 / 1) e será o elemento que acionará o efeito hover. E é aí que o seletor de irmãos entra em ação:

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

Pairando no .a O elemento move a imagem e, como está ocupando todo o espaço dentro da caixa, é como se estivéssemos pairando sobre a caixa! Passar o mouse sobre a imagem não é mais um problema!

Vamos arrastar e soltar nossa imagem dentro da caixa e ver o resultado:

Você viu aquilo? Você primeiro pega a imagem e a move para a caixa, nada extravagante. Mas uma vez que você libera a imagem, você aciona o efeito de foco que move a imagem e, em seguida, simulamos um recurso de arrastar e soltar. Se você soltar o mouse fora da caixa, nada acontece.

Hmm, sua simulação não é perfeita porque também podemos passar a caixa e obter o mesmo efeito.

Verdade e vamos corrigir isso. Precisamos desabilitar o efeito hover e permitir apenas se liberarmos a imagem dentro da caixa. Vamos brincar com a dimensão do nosso .a elemento para que isso aconteça.

Agora, pairar sobre a caixa não faz nada. Mas se você começar a arrastar a imagem, o .a elemento aparece, e uma vez liberado dentro da caixa, podemos acionar o efeito hover e mover a imagem.

Vamos dissecar o código:

.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;
}

Clicar na imagem dispara o :active pseudo-classe que torna o .a elemento de largura total (é inicialmente igual a 0). O estado ativo permanecerá ativo até liberarmos a imagem. Se liberarmos a imagem dentro da caixa, o .a elemento volta para width: 0, mas acionaremos o efeito hover antes que aconteça e a imagem cairá dentro da caixa! Se você soltá-lo fora da caixa, nada acontece.

Há uma pequena peculiaridade: clicar na caixa vazia também move a imagem e quebra nosso recurso. Atualmente, :active está ligado ao .box elemento, então clicar nele ou em qualquer um de seus filhos irá ativá-lo; e fazendo isso, acabamos mostrando o .a elemento e acionando o efeito hover.

Podemos consertar isso brincando com pointer-events. Ele nos permite desabilitar qualquer interação com o .box enquanto mantém as interações com os elementos filhos.

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

Atual nosso recurso de arrastar e soltar é perfeito. A menos que você possa descobrir como cortá-la, a única maneira de mover a imagem é arrastá-la e soltá-la dentro da caixa.

Construindo a grade do quebra-cabeça

Montar o quebra-cabeça vai parecer fácil em comparação com o que acabamos de fazer para o recurso de arrastar e soltar. Vamos confiar na grade CSS e nos truques de fundo para criar o quebra-cabeça.

Aqui está nossa grade, escrita em Pug por conveniência:

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

O código pode parecer estranho, mas compila em HTML simples:

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

Aposto que você está se perguntando o que há com essas tags. Nenhum desses elementos tem qualquer significado especial - eu só acho que o código é muito mais fácil de escrever usando <z> do que um monte de <div class="z"> como queiras.

É assim que eu os mapeei:

  • <g> é o nosso contêiner de grade que contém N*N <z> elementos.
  • <z> representa nossos itens de grade. Ele desempenha o papel de .box elemento que vimos na seção anterior.
  • <a> aciona o efeito de foco.
  • <b> representa uma parte da nossa imagem. Aplicamos o draggable atributo nele porque não pode ser arrastado por padrão.

Tudo bem, vamos registrar nosso contêiner de grade no <g>. Isso está em Sass em vez 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);
}

Na verdade, vamos tornar nossos filhos da grade - o <z> elementos — grades também e têm ambos <a> e <b> dentro da mesma área de grade:

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

Como você pode ver, nada extravagante - criamos uma grade com um tamanho específico. O resto do CSS que precisamos é para o recurso de arrastar e soltar, que exige que coloquemos aleatoriamente as peças ao redor do tabuleiro. Vou recorrer ao Sass para isso, novamente pela conveniência de poder percorrer e estilizar todas as peças do quebra-cabeça com uma função:

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

Você deve ter notado que estou usando o Sass random() função. É assim que obtemos as posições aleatórias para as peças do quebra-cabeça. Lembre-se que vamos desabiltar essa posição ao passar o mouse sobre o <a> elemento depois de arrastar e soltar seu correspondente <b> elemento dentro da célula da grade.

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

Nesse mesmo loop, também estou definindo a configuração de fundo para cada peça do quebra-cabeça. Todos eles compartilharão logicamente a mesma imagem como plano de fundo, e seu tamanho deve ser igual ao tamanho de toda a grade (definida com o --s variável). Usando o mesmo background-image e um pouco de matemática, atualizamos o background-position para mostrar apenas uma parte da imagem.

É isso! Nosso jogo de quebra-cabeça somente CSS está tecnicamente pronto!

Mas sempre podemos fazer melhor, certo? Eu te mostrei como fazer uma grade de formas de peças de quebra-cabeça em outro artigo. Vamos pegar essa mesma ideia e aplicá-la aqui, certo?

Formas de peças de quebra-cabeça

Aqui está o nosso novo jogo de quebra-cabeça. Mesma funcionalidade, mas com formas mais realistas!

Esta é uma ilustração das formas na grade:

Como eu fiz um jogo de quebra-cabeça CSS puro

Se você olhar de perto, notará que temos nove formas diferentes de peças de quebra-cabeça: o quatro cantos, quatro arestas e um para todo o resto.

A grade de peças do quebra-cabeça que fiz no outro artigo ao qual me referi é um pouco mais direta:

Podemos usar a mesma técnica que combina máscaras CSS e gradientes para criar as diferentes formas. Caso você não esteja familiarizado com mask e gradientes, eu recomendo verificar aquele caso simplificado para entender melhor a técnica antes de passar para a próxima parte.

Primeiro, precisamos usar seletores específicos para direcionar cada grupo de elementos que compartilha a mesma forma. Temos nove grupos, então usaremos oito seletores, além de um seletor padrão que seleciona todos eles.

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

Aqui está uma figura que mostra como isso é mapeado para nossa grade:

Como fiz um jogo de quebra-cabeça CSS puro PlatoBlockchain Data Intelligence. Pesquisa vertical. Ai.
Como eu fiz um jogo de quebra-cabeça CSS puro

Agora vamos lidar com as formas. Vamos nos concentrar em aprender apenas uma ou duas das formas porque todas usam a mesma técnica - e dessa forma, você tem alguns deveres de casa para continuar aprendendo!

Para as peças do quebra-cabeça no centro da grade, 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);

O código pode parecer complexo, mas vamos nos concentrar em um gradiente de cada vez para ver o que está acontecendo:

Dois gradientes criam dois círculos (marcados em verde e roxo na demonstração), e dois outros gradientes criam os slots aos quais outras peças se conectam (o marcado em azul preenche a maior parte da forma enquanto o marcado em vermelho preenche a parte superior). Uma variável CSS, --r, define o raio das formas circulares.

Como fiz um jogo de quebra-cabeça CSS puro PlatoBlockchain Data Intelligence. Pesquisa vertical. Ai.
Como eu fiz um jogo de quebra-cabeça CSS puro

A forma das peças do quebra-cabeça no centro (marcadas 0 na ilustração) é o mais difícil de fazer, pois usa quatro gradientes e tem quatro curvaturas. Todas as outras peças fazem malabarismos com menos gradientes.

Por exemplo, as peças do quebra-cabeça ao longo da borda superior do quebra-cabeça (marcadas 2 na ilustração) usa três gradientes em vez de quatro:

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

Removemos o primeiro gradiente (superior) e ajustamos os valores do segundo gradiente para que ele cubra o espaço deixado para trás. Você não notará uma grande diferença no código se comparar os dois exemplos. Deve-se notar que podemos encontrar diferentes configurações de fundo para criar a mesma forma. Se você começar a brincar com gradientes, com certeza vai encontrar algo diferente do que eu fiz. Você pode até escrever algo mais conciso — se sim, compartilhe nos comentários!

Além de criar as formas, você também descobrirá que estou aumentando a largura e/ou a altura dos elementos como abaixo:

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

As peças do quebra-cabeça precisam transbordar sua célula de grade para se conectar.

Como fiz um jogo de quebra-cabeça CSS puro PlatoBlockchain Data Intelligence. Pesquisa vertical. Ai.
Como eu fiz um jogo de quebra-cabeça CSS puro

Demonstração final

Aqui está a demonstração completa novamente. Se você comparar com a primeira versão, verá a mesma estrutura de código para criar a grade e o recurso de arrastar e soltar, além do código para criar as formas.

Possíveis melhorias

O artigo termina aqui, mas poderíamos continuar aprimorando nosso quebra-cabeça com ainda mais recursos! Que tal um temporizador? Ou talvez algum tipo de parabéns quando o jogador terminar o quebra-cabeça?

Posso considerar todos esses recursos em uma versão futura, então fique de olho no meu repositório do GitHub.

Resumindo

E CSS não é uma linguagem de programação, eles dizem. Ah!

Eu não estou tentando provocar algum #HotDrama com isso. Digo isso porque fizemos algumas coisas de lógica realmente complicadas e cobrimos muitas propriedades e técnicas de CSS ao longo do caminho. Brincamos com CSS Grid, transições, mascaramento, gradientes, seletores e propriedades de plano de fundo. Sem mencionar os poucos truques do Sass que usamos para tornar nosso código fácil de ajustar.

O objetivo não era construir o jogo, mas explorar CSS e descobrir novas propriedades e truques que você pode usar em outros projetos. Criar um jogo online em CSS é um desafio que o leva a explorar os recursos do CSS em detalhes e aprender como usá-los. Além disso, é muito divertido termos algo para brincar quando tudo estiver dito e feito.

Se CSS é uma linguagem de programação ou não, não muda o fato de que sempre aprendemos construindo e criando coisas inovadoras.

Carimbo de hora:

Mais de Truques CSS