Logotipo de Zephyrnet

Cómo hice un juego de rompecabezas CSS puro

Fecha:

Recientemente descubrí la alegría de crear juegos solo con CSS. Siempre es fascinante cómo HTML y CSS son capaces de manejar la lógica de un juego en línea completo, ¡así que tenía que probarlo! Dichos juegos generalmente se basan en el viejo Checkbox Hack donde combinamos el estado marcado/no marcado de una entrada HTML con el :checked pseudo-clase en CSS. ¡Podemos hacer mucha magia con esa combinación!

De hecho, me desafié a mí mismo a crear un juego completo sin Checkbox. No estaba seguro de si sería posible, pero definitivamente lo es, y les mostraré cómo hacerlo.

Además del juego de rompecabezas que estudiaremos en este artículo, he hecho una colección de juegos CSS puros, la mayoría de ellos sin Checkbox Hack. (También están disponibles en CodePen.)

¿Quieres jugar antes de que empecemos?

Personalmente, prefiero jugar el juego en modo de pantalla completa, pero puedes jugarlo a continuación o ábrelo por aquí.

¿Guay, verdad? Lo sé, no es el Mejor Juego de Rompecabezas que hayas Visto™ pero tampoco está nada mal para ser algo que solo usa CSS y unas pocas líneas de HTML. ¡Puede ajustar fácilmente el tamaño de la cuadrícula, cambiar la cantidad de celdas para controlar el nivel de dificultad y usar la imagen que desee!

Vamos a rehacer esa demostración juntos, luego le pondremos un poco más de brillo al final para darle un poco de diversión.

La funcionalidad de arrastrar y soltar

Si bien la estructura del rompecabezas es bastante sencilla con CSS Grid, la capacidad de arrastrar y soltar las piezas del rompecabezas es un poco más complicada. Tuve que confiar en una combinación de transiciones, efectos de desplazamiento y selectores de hermanos para hacerlo.

Si pasa el cursor sobre el cuadro vacío en esa demostración, la imagen se mueve dentro y permanece allí incluso si mueve el cursor fuera del cuadro. El truco consiste en agregar una gran duración y retraso de transición, tan grande que la imagen tarde mucho tiempo en volver a su posición 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 solo el transition-delay es suficiente, pero el uso de valores grandes tanto en la demora como en la duración disminuye la posibilidad de que un jugador vea que la imagen retrocede. si esperas 999s + 999s —que es aproximadamente 30 minutos—, luego verá que la imagen se mueve. Pero no lo harás, ¿verdad? Quiero decir, nadie se tomará tanto tiempo entre turnos a menos que abandone el juego. Entonces, considero que este es un buen truco para cambiar entre dos estados.

¿Notaste que al pasar el mouse sobre la imagen también se activan los cambios? Eso es porque la imagen es parte del elemento de caja, lo cual no es bueno para nosotros. Podemos arreglar esto agregando pointer-events: none a la imagen pero no podremos arrastrarla más tarde.

Eso significa que tenemos que introducir otro elemento dentro del .box:

Ese extra div (estamos usando una clase de .a) ocupará la misma área que la imagen (gracias a CSS Grid y grid-area: 1 / 1) y será el elemento que dispare el efecto hover. Y ahí es donde entra en juego el selector de hermanos:

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

Flotando en el .a El elemento mueve la imagen y, dado que ocupa todo el espacio dentro del cuadro, ¡es como si estuviéramos pasando el cursor sobre el cuadro! ¡Pasar la imagen ya no es un problema!

Arrastremos y sueltemos nuestra imagen dentro del cuadro y veamos el resultado:

¿Viste eso? Primero tomas la imagen y la mueves a la caja, nada especial. Pero una vez que sueltas la imagen, activas el efecto de desplazamiento que mueve la imagen y luego simulamos una función de arrastrar y soltar. Si sueltas el ratón fuera de la caja, no pasa nada.

Hmm, tu simulación no es perfecta porque también podemos desplazar el cuadro y obtener el mismo efecto.

Cierto y vamos a rectificar esto. Necesitamos deshabilitar el efecto de desplazamiento y permitirlo solo si liberamos la imagen dentro del cuadro. Jugaremos con la dimensión de nuestro .a elemento para que eso suceda.

Ahora, pasar el cursor sobre la caja no hace nada. Pero si empiezas a arrastrar la imagen, el .a Aparece el elemento, y una vez liberado dentro del cuadro, podemos activar el efecto de desplazamiento y mover la imagen.

Analicemos el 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;
}

Al hacer clic en la imagen se dispara el :active pseudo-clase que hace el .a elemento de ancho completo (inicialmente es igual a 0). El estado activo permanecerá lector activo hasta soltar la imagen. Si soltamos la imagen dentro de la caja, la .a elemento vuelve a width: 0, ¡pero activaremos el efecto de desplazamiento antes de que suceda y la imagen caerá dentro del cuadro! Si lo sueltas fuera de la caja, no pasa nada.

Hay una pequeña peculiaridad: hacer clic en el cuadro vacío también mueve la imagen y rompe nuestra característica. Corrientemente, :active está vinculado a la .box elemento, por lo que al hacer clic en él o en cualquiera de sus elementos secundarios se activará; y al hacer esto, terminamos mostrando el .a elemento y activando el efecto de desplazamiento.

Podemos arreglar eso jugando con pointer-events. Nos permite deshabilitar cualquier interacción con el .box manteniendo las interacciones con los elementos secundarios.

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

Ahora nuestra función de arrastrar y soltar es perfecta. A menos que pueda encontrar cómo piratearlo, la única forma de mover la imagen es arrastrarla y soltarla dentro del cuadro.

Construyendo la cuadrícula del rompecabezas

Armar el rompecabezas será muy fácil en comparación con lo que acabamos de hacer con la función de arrastrar y soltar. Vamos a confiar en la cuadrícula CSS y los trucos de fondo para crear el rompecabezas.

Aquí está nuestra cuadrícula, escrita en Pug para mayor comodidad:

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

El código puede parecer extraño, pero se compila en HTML simple:


 
   
   
 
 
   
   
 
 
   
   
 
  

Apuesto a que te estás preguntando qué pasa con esas etiquetas. Ninguno de estos elementos tiene un significado especial; solo encuentro que el código es mucho más fácil de escribir usando que un montón de

o lo que sea.

Así es como los he mapeado:

  • es nuestro contenedor de cuadrícula que contiene N*N elementos.
  • representa nuestros elementos de cuadrícula. Hace el papel de la .box elemento que vimos en el apartado anterior.
  • activa el efecto de desplazamiento.
  • representa una porción de nuestra imagen. Aplicamos el draggable atributo en él porque no se puede arrastrar de forma predeterminada.

Muy bien, registremos nuestro contenedor de cuadrícula en . Esto está en Sass en lugar 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);
}

De hecho, vamos a hacer que nuestros hijos de la cuadrícula, el elementos - cuadrículas también y tienen ambos y dentro de la misma área de cuadrícula:

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

Como puede ver, nada especial: creamos una cuadrícula con un tamaño específico. El resto del CSS que necesitamos es para la función de arrastrar y soltar, que requiere que coloquemos las piezas al azar alrededor del tablero. Voy a recurrir a Sass para esto, nuevamente por la conveniencia de poder recorrer y diseñar todas las piezas del rompecabezas con una función:

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

Es posible que haya notado que estoy usando el Sass random() función. Así es como obtenemos las posiciones aleatorias para las piezas del rompecabezas. Recuerda que lo haremos inhabilitar esa posición cuando se desplaza sobre el elemento después de arrastrar y soltar su correspondiente elemento dentro de la celda de la cuadrícula.

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

En ese mismo ciclo, también estoy definiendo la configuración de fondo para cada pieza del rompecabezas. Todos ellos compartirán lógicamente la misma imagen que el fondo, y su tamaño debe ser igual al tamaño de toda la cuadrícula (definida con el --s variable). usando el mismo background-image y algo de matemáticas, actualizamos el background-position para mostrar sólo una parte de la imagen.

¡Eso es todo! ¡Nuestro juego de rompecabezas solo con CSS está técnicamente terminado!

Pero siempre podemos hacerlo mejor, ¿verdad? te mostré cómo hacer una cuadrícula de formas de piezas de rompecabezas en otro artículo. Tomemos esa misma idea y apliquémosla aquí, ¿de acuerdo?

Formas de piezas de rompecabezas

Aquí está nuestro nuevo juego de rompecabezas. ¡Misma funcionalidad pero con formas más realistas!

Esta es una ilustración de las formas en la cuadrícula:

Si miras de cerca, notarás que tenemos nueve formas diferentes de piezas de rompecabezas: la cuatro esquinas, la cuatro bordesy uno para todo lo demás.

La cuadrícula de piezas del rompecabezas que hice en el otro artículo al que me referí es un poco más sencilla:

Podemos usar la misma técnica que combina máscaras CSS y degradados para crear las diferentes formas. En caso de que no esté familiarizado con mask y degradados, recomiendo comprobar ese caso simplificado para comprender mejor la técnica antes de pasar a la siguiente parte.

Primero, necesitamos usar selectores específicos para apuntar a cada grupo de elementos que comparten la misma forma. Tenemos nueve grupos, por lo que usaremos ocho selectores, además de un selector predeterminado que los selecciona a todos.

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

Aquí hay una figura que muestra cómo eso se asigna a nuestra cuadrícula:

Ahora vamos a abordar las formas. Centrémonos en aprender solo una o dos de las formas porque todas usan la misma técnica, y de esa manera, ¡tienes tarea para seguir aprendiendo!

Para las piezas del rompecabezas en el centro de la cuadrícula, 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);

El código puede parecer complejo, pero concentrémonos en un gradiente a la vez para ver qué sucede:

Dos degradados crean dos círculos (marcados en verde y morado en la demostración), y otros dos degradados crean las ranuras a las que se conectan otras piezas (el marcado en azul llena la mayor parte de la forma mientras que el marcado en rojo llena la parte superior). Una variable CSS, --r, establece el radio de las formas circulares.

La forma de las piezas del rompecabezas en el centro (marcadas 0 en la ilustración) es el más difícil de hacer ya que usa cuatro degradados y tiene cuatro curvaturas. Todas las demás piezas hacen malabares con menos gradientes.

Por ejemplo, las piezas del rompecabezas a lo largo del borde superior del rompecabezas (marcadas 2 en la ilustración) utiliza tres gradientes en lugar de cuatro:

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

Eliminamos el primer degradado (superior) y ajustamos los valores del segundo degradado para que cubra el espacio que queda. No notará una gran diferencia en el código si compara los dos ejemplos. Cabe destacar que podemos encontrar distintas configuraciones de fondo para crear una misma forma. Si comienzas a jugar con degradados, seguro que obtendrás algo diferente a lo que hice. Incluso puede escribir algo que sea más conciso; si es así, ¡compártalo en los comentarios!

Además de crear las formas, también encontrará que estoy aumentando el ancho y/o la altura de los elementos como se muestra a continuación:

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

Las piezas del rompecabezas deben desbordar su celda de cuadrícula para conectarse.

demostración final

Aquí está la demostración completa de nuevo. Si lo compara con la primera versión, verá la misma estructura de código para crear la cuadrícula y la función de arrastrar y soltar, además del código para crear las formas.

Posibles mejoras

El artículo termina aquí, ¡pero podríamos seguir mejorando nuestro rompecabezas con aún más funciones! ¿Qué tal un temporizador? ¿O tal vez algún tipo de felicitación cuando el jugador termina el rompecabezas?

Puedo considerar todas estas características en una versión futura, así que mantener un ojo en mi repositorio de GitHub.

Terminando

Y CSS no es un lenguaje de programación, ellos dicen. ¡Decir ah!

No estoy tratando de provocar algo de #HotDrama con eso. Lo digo porque hicimos algunas cosas de lógica realmente complicadas y cubrimos muchas propiedades y técnicas de CSS en el camino. Jugamos con CSS Grid, transiciones, máscaras, degradados, selectores y propiedades de fondo. Sin mencionar los pocos trucos de Sass que usamos para hacer que nuestro código sea fácil de ajustar.

El objetivo no era construir el juego, sino explorar CSS y descubrir nuevas propiedades y trucos que puedes usar en otros proyectos. Crear un juego en línea en CSS es un desafío que lo empuja a explorar las funciones de CSS con gran detalle y aprender a usarlas. Además, es muy divertido tener algo con lo que jugar cuando todo está dicho y hecho.

Si CSS es un lenguaje de programación o no, no cambia el hecho de que siempre aprendemos construyendo y creando cosas innovadoras.

punto_img

Información más reciente

punto_img