Crea formularios de miedo para Halloween con CSS y JavaScript

Crea formularios de miedo para Halloween con CSS y JavaScript con este tutorial paso a paso, y sorprende a tus usuarios.

Halloween está a la vuelta de la esquina y debido a ello muchos sitios actualizan su página web vistiéndola para la ocasión.

Sin ir más lejos, el mismo Google modifica su “Doodle” para convertirlo en un juego colaborativo de fantasmas y arañas.

Si a tí también te gustaría incluir algún elemento escalofriante en tu web, pero no se te ocurre nada original, en este post te propongo una idea que quizá te guste.

Por supuesto no voy a plantear nada tan espectacular como el juego de Google, pero aun así, estoy seguro de que te parecerá divertido.

La idea es convertir los formularios en monstruos que sorprendan al usuario.

Sorprende a tus usuarios convirtiendo los campos de texto en criaturas misteriosas.
Sorprende a tus usuarios convirtiendo los campos de texto en criaturas misteriosas.

Monstruos escondidos en campos de formularios

Los formularios web siempre se me han antojado aburridos y, en cierto modo, hasta fastidiosos. Su interacción suele ser tosca y poco amigable.

Así que pensé, ¿por qué no darle una vuelta de tuerca en motivo de Halloween? y se me ocurrió convertirlos en monstruos animados.

Me puse manos a la obra y este fue el resultado, puedes verlo pinchando en la imagen que aparece a continuación.

Haz click en la imagen para ver el resultado en una ventana nueva.
Haz click en la imagen para ver el resultado en una ventana nueva.

¿Qué te parece? No me negarás que añade un extra en la experiencia del usuario. Puede que incluso motive al visitante a compartir tu formulario y viralizar tu sitio web.

Como ves, el objetivo es mantener el suspense hasta que se intenta rellenar cualquier campo, entonces éste cobra vida, y se muestra con apariencia de monstruo.

Además, para hacer la interacción un poco más interesante, decidí añadir un indicador de vida a cada “input”, como si de un enemigo se tratara.

Si quiere derrotarlos, el usuario deberá rellenar los campos con información para el envío.

Para terminar, también incluí una animación de confeti en el momento de enviar el formulario.

La librería que he usado para este efecto la analicé hace un tiempo en el siguiente artículo

Disparar confetti con JavaScript y CanvasConfetti

¿Te animas a implementarlo en tu proyecto? Si es así, vayamos directamente al tutorial paso a paso.

Crear formularios de miedo animados

Antes de empezar, te sugiero que acudas al repositorio del proyecto donde encontrarás el código fuente completo. Te puede ser de utilidad si te pierdes en algún punto de la explicación.

https://github.com/Danivalldo/libreriasjs/tree/master/Monster-form

En primer lugar, quiero destacar que los recursos para construir los monstruos los obtuve de la web de Kenney

Una fantástica página, con muchos recursos gratuitos para crear interfaces y videojuegos. En concreto me descargue el siguiente pack.

https://www.kenney.nl/assets/monster-builder-pack

Acto seguido generé un proyecto nuevo con Vite e instalé las siguientes dependencias:

npm i canvas-confetti sass

Con canvas-confetti añadimos el efecto final de confeti y con el preprocesador SASS trabajaremos los estilos CSS de forma más cómoda.

Comenzamos creando la estructura HTML base del proyecto.

Toda la aplicación se engloba dentro de dos contenedores anidados con los nombres de clase “.main-container” y “.form-wrapper” respectivamente.

<div class="main-container">
  <div class="form-wrapper">
  </div>
</div>

Iniciamos form-wrapper con un titular y unos párrafos de introducción.

<h1>
  Monster<img
    src="./imgs/pumpkin-halloween-svgrepo-com.svg"
    alt=""
  /><span>Form</span>
</h1>
<p class="intro">
  <b>¡Cuidado!</b> En este formulario se esconden terribles monstruos.
</p>
<p>
  Derrótalos rellenando el formulario para liberar al mundo de estos
  aterradores seres.
</p>
<p>
  ¡Adelante, héroe del teclado, prepárate para una aventura de datos
  aterradoramente divertida!
</p>

Presta especial atención a los nombres de clase CSS más tarde serán necesarios para aplicar los estilos correctamente.

Seguimos ampliando el contenido con el formulario. Dentro de la etiqueta form incluiremos tres campos de texto con sus etiquetas label. 

Cada input se enmarca dentro de una etiqueta “span” con distintos elementos que conformarán el diseño de los monstruos (ojos, boca, brazos y barra de vida).

Este es va a ser el campo “nombre”.

<form>
  <label for="">Nombre *:</label>
  <span data-monster class="input-monster">
    <div class="life-bar">
      <span class="inner-bar"></span>
    </div>
    <div class="eye-contianer"></div>
    <div class="right-arm-container"></div>
    <div class="left-arm-container"></div>
    <div class="mouth-contianer"></div>
    <input
      type="text"
      placeholder="Nombre"
      name="name"
      autocomplete="off"
    />
  </span>
</form>

Fíjate cómo he incluído un atributo data-monster y una clase “input-monster” en el span. Esto será clave para asignar el comportamiento y los estilos posteriormente.

Seguimos con los otros dos campos monstruos.

<label for="">Email *:</label>
<span data-monster class="input-monster">
  <div class="life-bar">
    <span class="inner-bar"></span>
  </div>
  <div class="eye-contianer" data-eye="2"></div>
  <div class="right-arm-container" data-r-arm="2"></div>
  <div class="left-arm-container" data-l-arm="2"></div>
  <div class="mouth-contianer" data-mouth="2"></div>
  <input
    type="email"
    placeholder="Email"
    name="user-email"
    autocomplete="off"
  />
</span>
<label for="">Descripción *:</label>
<span data-monster class="input-monster">
  <div class="life-bar">
    <span class="inner-bar"></span>
  </div>
  <div class="eye-contianer" data-eye="3"></div>
  <div class="right-arm-container" data-r-arm="3"></div>
  <div class="left-arm-container" data-l-arm="3"></div>
  <div class="mouth-contianer" data-mouth="3"></div>
  <textarea rows="5" placeholder="Descripción"></textarea>
</span>
<span data-monster class="input-monster no-shadow">
  <span
    data-tooltip="Derrota a todos los monstruos para enviar el formulario"
    ><input type="submit" value="Enviar" disabled="true"
  /></span>
</span>

Se trata del campo “email” y el área de texto “descripción”. De nuevo atiende a los atributos data para identificar qué tipo de bocas, brazos y ojos asignaremos a cada criatura.

El último elemento del formulario es un “input” de tipo “submit”, en él incluimos una información en forma de tooltip.

<span data-monster class="input-monster no-shadow">
  <span
    data-tooltip="Derrota a todos los monstruos para enviar el formulario"
    ><input type="submit" value="Enviar" disabled="true"
  /></span>
</span>

Cerramos el archivo index.html con un pie de formulario con información adicional.

<p class="post-form-content">
  ¿Te gustó? Puedes seguir mi blog para más contenido como este.
  <a href="https://libreriasjs.com">libreriasjs.com</a>
  <br />
  Gracias a
  <a href="https://www.kenney.nl/" target="_blank" rel="nofollow"
    >Kenney</a
  >
  por esos increíbles recursos gratuitos..
</p>

Saltamos ahora a la definición de estilos CSS. He dividido esta parte en distintos archivos para una mayor claridad.

El archivo principal llamado “style.scss” importa una fuente de Google Fonts, incluye todos los demás archivos y declara estilos genéricos.

@import url("https://fonts.googleapis.com/css2?family=Lora:wght@500&display=swap");
@import url("./styles/main-container.scss");
@import url("./styles/input-monster.scss");
@import url("./styles/tooltip.scss");
:root {
  --mainColor: #5b48d9;
  --secondaryColor: #04d9c4;
  --mainColorDarker: #16103c;
}
body {
  margin: 0;
  padding: 0;
  color: hsl(247, 71%, 93%);
  overflow-x: hidden;
  background-color: var(--mainColor);
  font-family: "Lora", serif;
  background-image: url("./assets/imgs/pattern.svg");
}
a {
  color: var(--secondaryColor);
}
b {
  color: var(--secondaryColor);
}
p.post-form-content {
  font-size: 0.8rem;
}

Vamos a analizar el código que da estilos al contenedor principal y al “form-wrapper”. Encontrarás estos estilos en el archivo “styles/main-container.scss”.

Para entender bien esta parte, hay que tener en cuenta que el “main-container” puede tener dos estados, el normal y el “modo batalla”.

Este es el conjunto de instrucciones CSS cuando se encuentra en modo normal.

.main-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 100vh;
  padding: 0 20px;
  transition: background-color 0.3s ease;
  .form-wrapper {
    transition: transform 0.3s ease;
    perspective: 300px;
    width: 100%;
    max-width: 460px;
    margin: auto auto;
    .intro {
      font-size: 1.2rem;
      text-align: center;
    }
    h1 {
      color: #fff;
      text-align: center;
      display: flex;
      justify-content: center;
      align-items: center;
      gap: 5px;
      font-size: 50px;
      text-shadow: 0 4px 0px rgba(55, 42, 133, 0.6);
      span {
        color: var(--secondaryColor);
      }
      img {
        width: 40px;
        filter: drop-shadow(0 4px 0px rgba(55, 42, 133, 0.6));
      }
    }
    label {
      color: #fff;
      font-weight: 400;
      font-size: 0.8rem;
      margin-bottom: 10px;
      display: block;
    }
  }
}

A continuación te resumo brevemente qué hacen estas líneas, con todo, te animo a leer detenidamente cada instrucción para entender su propósito.

  • El contenedor principal ocupa todo el alto de pantalla y se muestra como “flex” centrando su contendio.
  • El marco del formulario está preparado para dotar de perspectiva los elementos hijo, dando sensación de espacio 3d.
  • Se dan estilos genéricos al titular h1, a los párrafos con la clase “intro” y a las etiquetas label de los campos del formulario.

Cuando se agrega la clase “battle-mode” al contenedor principal, éste aplica las siguientes modificaciones a los selectores hijo.

.main-container {
  &.battle-mode {
    background-color: #ee5757;
    .form-wrapper {
      .input-monster,
      label,
      h1,
      p {
        z-index: 0;
        filter: blur(5px);
        opacity: 0.2;
        &.active {
          z-index: 1;
          filter: blur(0px);
          opacity: 1;
          box-shadow: 0px 5px 0px rgb(185, 59, 59);
          .mouth-contianer,
          .eye-contianer,
          .right-arm-container,
          .left-arm-container,
          .life-bar {
            opacity: 1;
          }
          input,
          textarea {
            border: solid 1px #a32424;
          }
        }
      }
    }
  }
}

Estas son las alteraciones en los distintos sub-elementos.

  • Se cambia el color de fondo del propio contenedor.
  • Se aplica un desenfoque y una reducción de la opacidad a todos los elementos de dentro de “form-wrapper”.
  • Si alguno de estos elementos tiene la clase “active” agregada, se deshace el efecto de desenfoque y se resetea la opacidad. Además se aplica una sombra y se muestran todos los elementos que componen al monstruo oculto.

Antes de finalizar el archivo main-container, también incluimos un media-querie para ajustar tamaños de tipografia y scroll en dispositivos con anchos inferiores a 600px.

@media screen and (max-width: 600px) {
  .main-container {
    overflow: hidden;
    .form-wrapper {
      h1 {
        font-size: 30px;
        display: block;
        img {
          display: none;
        }
      }
    }
  }
}

Sigamos ahora con la definición de los estilos CSS para cada monstruo. Los selectores y sus clases CSS se encuentran detallados en el archivo “syles/input-monster.scss”.

Nuevamente separaré el código según distintos estados y selectores.

El selector “.input-monster” hace referencia al contenedor que engloba cada campo del formulario susceptible de convertirse en monstruo.

Estos son los estilos generales, de momento obviaré aquellos selectores que afectan a las partes del cuerpo del monstruo.

.input-monster {
  transform: translateZ(0px) rotateY(0deg);
  will-change: transform, opacity, box-shadow;
  transition: transform 0.3s ease, box-shadow 0.3s ease;
  display: block;
  position: relative;
  box-sizing: border-box;
  opacity: 1;
  border-radius: 5px;
  margin-bottom: 15px;
  width: 100%;
  box-shadow: 10px 10px 15px rgba(245, 245, 220, 0);
  input,
  textarea {
    display: block;
    box-sizing: border-box;
    width: 100%;
    border: none;
    border-radius: 5px;
    border: solid 1px var(--mainColorDarker);
    padding: 10px 10px;
    background-color: #fff;
    resize: none;
    font-family: "Lora", serif;
    cursor: pointer;
    &[type="color"] {
      padding: 0px;
    }
    &:focus {
      outline: none;
    }
  }
  &.no-shadow {
    box-shadow: none;
  }
  input[type="submit"] {
    background-color: var(--secondaryColor);
    color: #fff;
    &[disabled="true"] {
      opacity: 0.4;
      cursor: not-allowed;
    }
  }
}

Cada “input-monster” puede tener un estado “.dead” que veremos más adelante, por ahora veamos las líneas de CSS para dar estilos genéricos.

  • Se prepara la propiedad “transform” para ser transicionada.
  • Se incluye una sombra, sin opacidad, también para ser modificada más adelante.
  • Se le dan estilos varios como un borde redondeado y una posición relativa, entre otros.
  • Los inputs de texto y las áreas de texto también tienen sus propios estilos CSS que definen su borde, espaciado o tipo de letra.
  • Destacar también que el input de envío contempla el estado de deshabilitado.
  • He incluido un sub-selector opcional llamado “no-shadow” para eliminar la sombra en caso de que sea necesario.

Cuando marcamos un monstruo como derrotado le asignaremos la clase “.dead”. Esta clase se compone de las siguientes líneas de código.

.input-monster {
  &.dead {
    .mouth-contianer,
    .eye-contianer,
    .right-arm-container,
    .left-arm-container {
      animation: none;
    }
    .right-arm-container {
      transform: rotateZ(0deg);
    }
    .left-arm-container {
      transform: rotateZ(0deg);
    }
    .life-bar {
      display: none;
    }
  }
}

En esencia se encargará de rotar los brazos del enemigo, ocultar su barra de vida y deshabilitar la animación asociada a las partes de su cuerpo.

Entre las otras características del input-monstruo está la barra de vida, veamos pues como dotar de estilos ese selector.

.input-monster {
  .life-bar {
    position: absolute;
    top: -50px;
    left: 0;
    width: 100%;
    height: 4px;
    background-color: #fff;
    opacity: 0;
    pointer-events: none;
    padding: 1px;
    border-radius: 20px;
    overflow: hidden;
    .inner-bar {
      content: "";
      display: block;
      width: 100%;
      height: 100%;
      transition: transform 0.2s ease;
      background-color: #ac2b2a;
      transform: scale(1);
      transform-origin: 0% 50%;
    }
  }
}

En definitiva, se trata de un contenedor con un hijo con la clase “inner-bar” con posiciones absolutas y relativas respectivamente. Aplicando colores de fondo y los tamaños correctos será suficiente para cumplir su función.

El bloque de código que sigue se encarga de mostrar y posicionar los recursos gráficos que representarán las partes del cuerpo de cada monstruo.

.input-monster {
  .mouth-contianer,
  .eye-contianer,
  .right-arm-container,
  .left-arm-container {
    background-image: url("$cssAssets/imgs/mouth.svg");
    position: absolute;
    left: 50%;
    transform: translate3d(-50%, 0px, 1px);
    bottom: -30px;
    width: 50px;
    height: 50px;
    // border: solid 1px red;
    opacity: 0;
    background-size: contain;
    background-position: center;
    background-repeat: no-repeat;
    pointer-events: none;
    transition: opacity 0.3s ease;
  }
  .mouth-contianer {
    animation-name: floatmove;
    animation-duration: 2s;
    animation-iteration-count: infinite;
    &[data-mouth="2"] {
      background-image: url("$cssAssets/imgs/mouth-2.svg");
    }
    &[data-mouth="3"] {
      background-image: url("$cssAssets/imgs/mouth-3.svg");
    }
    &.with-pain {
      background-image: url("$cssAssets/imgs/mouth-pain.svg");
    }
  }
  .eye-contianer {
    animation-name: floatmove;
    animation-duration: 2s;
    animation-iteration-count: infinite;
    animation-delay: 0.5s;
    background-image: url("$cssAssets/imgs/eye.svg");
    transform: translate3d(-50%, 0px, 1px);
    top: -30px;
    bottom: auto;
    width: 50px;
    height: 50px;
    &[data-eye="2"] {
      background-image: url("$cssAssets/imgs/eye-2.svg");
    }
    &[data-eye="3"] {
      background-image: url("$cssAssets/imgs/eye-3.svg");
    }
    &[data-eye="pain"] {
      background-image: url("$cssAssets/imgs/eye-pain.svg");
    }
    &.with-pain {
      background-image: url("$cssAssets/imgs/eye-pain.svg");
    }
  }
  .right-arm-container {
    background-image: url("$cssAssets/imgs/right-arm.svg");
    left: auto;
    right: -20px;
    animation-name: rightarmmove;
    animation-duration: 2s;
    animation-iteration-count: infinite;
    transform-origin: 50% 0%;
    &[data-r-arm="2"] {
      background-image: url("$cssAssets/imgs/right-arm-2.svg");
    }
    &[data-r-arm="3"] {
      background-image: url("$cssAssets/imgs/right-arm-3.svg");
    }
    &.with-pain {
      animation: none;
      transform: rotateZ(-90deg);
    }
  }
  .left-arm-container {
    background-image: url("$cssAssets/imgs/left-arm.svg");
    left: -20px;
    animation-name: leftarmmove;
    animation-duration: 2s;
    animation-iteration-count: infinite;
    transform-origin: 50% 0%;
    &[data-l-arm="2"] {
      background-image: url("$cssAssets/imgs/left-arm-2.svg");
    }
    &[data-l-arm="3"] {
      background-image: url("$cssAssets/imgs/left-arm-3.svg");
    }
    &.with-pain {
      animation: none;
      transform: rotateZ(90deg);
    }
  }
}

Como ves, se trata de recuperar la imagen adecuada y colocarla como fondo en distintos elementos con posiciones absolutas. 

Fíjate también, que algunas de estas partes tienen una animación asignada, en seguida llegamos a ella.

@keyframes floatmove {
  0% {
    transform: translateY(-3px) translateX(-50%);
  }
  50% {
    transform: translateY(-8px) translateX(-50%);
  }
  100% {
    transform: translateY(-3px) translateX(-50%);
  }
}
@keyframes leftarmmove {
  0% {
    transform: rotateZ(0deg);
  }
  50% {
    transform: rotateZ(45deg);
  }
  100% {
    transform: rotateZ(0deg);
  }
}
@keyframes rightarmmove {
  0% {
    transform: rotateZ(0deg);
  }
  50% {
    transform: rotateZ(-45deg);
  }
  100% {
    transform: rotateZ(0deg);
  }
}

Tres animaciones para mover los brazos y el ojo de los bichos.

Cerramos añadiendo una última instrucción para garantizar la correcta visualización en dispositivos móvil.

@media screen and (max-width: 600px) {
  .main-container {
    &.battle-mode {
      .form-wrapper {
        .input-monster {
          &.active {
            max-width: 80%;
            margin-left: 10%;
          }
        }
      }
    }
  }
}

No me detendré mucho a explicar los estilos que sirven para posicionar el “tootip” ya que es un recurso opcional y tampoco es muy complejo

[data-tooltip] {
  position: relative;
  border-bottom: 1px dashed #000;
  cursor: help;
  &::after {
    position: absolute;
    opacity: 0;
    pointer-events: none;
    content: attr(data-tooltip);
    left: 0;
    top: calc(100% + 10px);
    border-radius: 3px;
    box-shadow: 0 0 5px 2px rgba(100, 100, 100, 0.6);
    background-color: white;
    z-index: 10;
    padding: 8px;
    width: 300px;
    transform: translateY(-20px);
    color: var(--mainColorDarker);
    transition: all 150ms cubic-bezier(0.25, 0.8, 0.25, 1);
  }
  &:hover::after {
    opacity: 1;
    transform: translateY(0);
    transition-duration: 300ms;
  }
}

Listo!, con esto tenemos nuestro “layout” a punto para cumplir con su propósito, ya solo queda darle vida a todo esto con JavaScript.

Para ello vamos a crear el archivo “main.js” e incluir algunas líneas de código JS.

Iniciamos el archivo importando la librería canvas-confeti y los estilos antes mencionados.

import "./style.scss";
import confetti from "canvas-confetti";

Guardamos una referencia al contenedor principal del DOM y aprovechamos para declarar una variable “let” que controlará el estado de la interfaz en modo batalla o normal.

const mainContainer = document.querySelector(".main-container");
let battleMode = false;

Escuchamos todos los eventos de tipo “pointerdown” que sucedan en el contenedor. 

Si alguno de estos se aplica sobre un elemento de tipo “INPUT” o “TEXTAREA” llamamos a la función enterModeBattle pasando los elementos sobre los que se han activado dicho evento.

Por defecto, saldremos del modo batalla llamando a la función “leaveModeBattle”

mainContainer.addEventListener("pointerdown", (e) => {
  switch (e.target.tagName) {
    case "INPUT":
    case "TEXTAREA":
      enterModeBattle(e.target, e.currentTarget);
      break;
    default:
      leaveModeBattle();
      break;
  }
});

Seguidamente declaramos la función “enterModeBattle”.

const enterModeBattle = (target, currentTarget) => {
  if (target.type === "submit") {
    return;
  }
  const inputMonster = target.closest(".input-monster");
  if (battleMode) {
    if (!inputMonster.classList.contains("active")) {
      leaveModeBattle();
    }
    return;
  }
  currentTarget.classList.add("battle-mode");
  battleMode = true;
  target
    .closest(".main-container")
    .querySelectorAll(".input-monster")
    .forEach((el) => {
      el.classList.remove("active");
    });
  const sizeContiner = currentTarget.getBoundingClientRect();
  const sizeInput = inputMonster.getBoundingClientRect();
  const posY = sizeContiner.height / 2 - sizeInput.y;
  if (!inputMonster.classList.contains("dead")) {
    inputMonster.style = `transform: 
    translateZ(0px)
    translateY(${posY}px)
    scale(1.2)
    rotateY(0deg);
  `;
  }
  inputMonster.classList.add("active");
};

Esta función sólo se completará si el elemento dom recibido no es un input de envío.

Si se trata de un elemento monstruo ya está activo, saldremos del modo batalla. 

En caso contrario, agregamos el estilo “battle-mode” al contenedor.

Luego desactivamos los otros inputs activos y posicionamos el enemigo en primer plano con las funciones «getBoundingClientRect”, la propiedad “style” y la clase “active”.

Por contra, la función leaveModeBattle eliminará la clase “battle-mode” y quitará todas las clases “active” de los inputs.

const leaveModeBattle = () => {
  if (battleMode) {
    document.querySelector(".main-container").classList.remove("battle-mode");
    document.querySelectorAll(".input-monster").forEach((el) => {
      el.classList.remove("active");
      el.style = ``;
    });
    battleMode = false;
    return;
  }
};

Ahora toca programar la funcionalidad de dañar a los monstruos tras cada interacción con el formulario.

Para ello, preparamos la función “painEffect”

const painEffect = (e) => {
  const _currentTarget = e.currentTarget;
  const inputMonster = _currentTarget.closest(".input-monster");
  if (!inputMonster.classList.contains("active")) {
    _currentTarget.value = "";
    e.preventDefault();
    return;
  }
  e.currentTarget.removeEventListener("input", painEffect);
  const prevStyleTransform = inputMonster.style.transform;
  const posY = /translateY\((.+)px\)/.exec(prevStyleTransform)[1];
  inputMonster.style.transform = `scale(1) translateY(${posY}px)`;
  const mouthTarget = inputMonster.querySelector(".mouth-contianer");
  const eyeTarget = inputMonster.querySelector(".eye-contianer");
  const lArmTarget = inputMonster.querySelector(".left-arm-container");
  const rArmTarget = inputMonster.querySelector(".right-arm-container");
  const lifeBar = inputMonster.querySelector(".inner-bar");
  lifeBar.dataset.life = lifeBar.dataset.life || 1;
  lifeBar.dataset.life = lifeBar.dataset.life - 0.2;
  lifeBar.dataset.life = lifeBar.dataset.life < 0 ? 0 : lifeBar.dataset.life;
  lifeBar.style.transform = `scaleX(${lifeBar.dataset.life})`;
  lArmTarget.classList.add("with-pain");
  rArmTarget.classList.add("with-pain");
  mouthTarget.classList.add("with-pain");
  eyeTarget.classList.add("with-pain");
  if (lifeBar.dataset.life <= 0) {
    inputMonster.style = ``;
    inputMonster.classList.add("dead");
    // leaveModeBattle();
    checkBattle();
    return;
  }
  window.setTimeout(() => {
    rArmTarget.classList.remove("with-pain");
    inputMonster.style.transform = prevStyleTransform;
    mouthTarget.classList.remove("with-pain");
    eyeTarget.classList.remove("with-pain");
    lArmTarget.classList.remove("with-pain");
    _currentTarget.addEventListener("input", painEffect);
  }, 150);
};

PainEffect se encarga de los siguientes procesos:

  • Modificar los estilos de las partes del cuerpo para que muestren otras imágenes como el ojo cerrado o la boca torcida.
  • Aplica la clase “with-pain” a cada elemento del cuerpo.
  • Calcular la “vida”del monstruo y actualizar su barra.
  • Si la vida es inferior o igual a cero, aplica el estilo “dead”.
  • Llamar a checkBattle tras cada interacció.
  • Controlar la escucha de eventos de tipo “input” para evitar que un usuario pueda romper la animación

Por último, declaramos la función checkBattle.

const checkBattle = () => {
  const monstersLifes = document.querySelectorAll(".inner-bar");
  for (let i = 0, j = monstersLifes.length; i < j; i++) {
    const lifeBar = monstersLifes[i].dataset.life;
    if (!lifeBar || Number(lifeBar) > 0) {
      return;
    }
  }
  const submitBtn = document.querySelector('input[type="submit"]');
  submitBtn.removeAttribute("disabled");
  submitBtn.closest("span").removeAttribute("data-tooltip");
  submitBtn.addEventListener("click", () => {
    console.log("SEND FORM");
    confetti({
      particleCount: 100,
      spread: 70,
      origin: { y: 1 },
      colors: ["FFFFFF", "04d9c4", "16103c"],
    });
  });
};
mainContainer.querySelectorAll("input, textarea").forEach((el) => {
  el.addEventListener("input", painEffect);
});

En esta se comprueba si todos los monstruos han sido vencidos y se activa el boton de envio encargado de lanzar confetti a través de la librería importada.

También agregamos un “listener” para la escucha del primer input sobre cualquiera de los elementos interactivos.

Listo para asustar a los visitantes de tu web

Esto es todo! Enhorabuena por haber llegado hasta aquí, espero que te haya gustado el tutorial.

Puedes dejar en los comentarios tus observaciones o propuestas, estaré encantado de leerlo.

Gracias por leerme y hasta la próxima.

Deja un comentario