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émN*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 odraggable
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:
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:
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.
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.
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.