Cómo recrear el efecto dominó de los botones de diseño de materiales »Wiki Ùtil Programar Plus

Cuando descubrí Material Design, me inspiré particularmente en su componente de botón. Utiliza un efecto dominó para dar retroalimentación a los usuarios de una manera simple y elegante.

¿Cómo funciona este efecto? Los botones de Material Design no solo tienen una animación ondulada ordenada, sino que la animación también cambia de posición dependiendo de dónde se haga clic en cada botón.

Podemos lograr el mismo resultado. Comenzaremos con una solución concisa utilizando ES6 + JavaScript, antes de analizar algunos enfoques alternativos.

HTML

Nuestro objetivo es evitar cualquier marcado HTML extraño. Así que iremos con lo mínimo:

<button>Find out more</button>

Estilo del botón

Necesitaremos diseñar algunos elementos de nuestro rizo dinámicamente, usando JavaScript. Pero todo lo demás se puede hacer en CSS. Para nuestros botones, solo es necesario incluir dos propiedades.

button {
  position: relative;
  overflow: hidden;
}

Utilizando position: relative nos permite usar position: absolute en nuestro elemento de onda, que necesitamos para controlar su posición. Mientras tanto, overflow: hidden evita que la ondulación exceda los bordes del botón. Todo lo demás es opcional. Pero ahora mismo, nuestro botón parece un poco anticuado. Aquí hay un punto de partida más moderno:

/* Roboto is Material's default font */
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');

button {
  position: relative;
  overflow: hidden;
  transition: background 400ms;
  color: #fff;
  background-color: #6200ee;
  padding: 1rem 2rem;
  font-family: 'Roboto', sans-serif;
  font-size: 1.5rem;
  outline: 0;
  border: 0;
  border-radius: 0.25rem;
  box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.3);
  cursor: pointer;
}

Peinando las ondas

Más adelante, usaremos JavaScript para inyectar ondas en nuestro HTML como intervalos con un .ripple clase. Pero antes de pasar a JavaScript, definamos un estilo para esas ondas en CSS para tenerlas listas:

span.ripple {
  position: absolute; /* The absolute position we mentioned earlier */
  border-radius: 50%;
  transform: scale(0);
  animation: ripple 600ms linear;
  background-color: rgba(255, 255, 255, 0.7);
}

Para hacer que nuestras ondas sean circulares, hemos establecido el border-radius al 50%. Y para asegurarnos de que cada rizo surja de la nada, hemos establecido la escala predeterminada en 0. En este momento, no podremos ver nada porque aún no tenemos un valor para el top, left, width, o height propiedades; pronto inyectaremos estas propiedades con JavaScript.

En cuanto a nuestro CSS, lo último que necesitamos agregar es un estado final para la animación:

@keyframes ripple {
  to {
    transform: scale(4);
    opacity: 0;
  }
}

Observe que no estamos definiendo un estado inicial con el from palabra clave en los fotogramas clave? Podemos omitir from y CSS construirá los valores faltantes basándose en los que se aplican al elemento animado. Esto ocurre si los valores relevantes se indican explícitamente, como en transform: scale(0) – o si son los predeterminados, como opacity: 1.

Ahora para JavaScript

Finalmente, necesitamos JavaScript para establecer dinámicamente la posición y el tamaño de nuestras ondas. El tamaño debe basarse en el tamaño del botón, mientras que la posición debe basarse tanto en la posición del botón como del cursor.

Comenzaremos con una función vacía que toma un evento de clic como argumento:

function createRipple(event) {
  //
}

Accederemos a nuestro botón encontrando el currentTarget del evento.

const button = event.currentTarget;

A continuación, crearemos una instancia de nuestro elemento span y calcularemos su diámetro y radio en función del ancho y alto del botón.

const circle = document.createElement("span");
const diameter = Math.max(button.clientWidth, button.clientHeight);
const radius = diameter / 2;

Ahora podemos definir las propiedades restantes que necesitamos para nuestras ondas: el left, top, width y height.

circle.style.width = circle.style.height = `${diameter}px`;
circle.style.left = `${event.clientX - (button.offsetLeft + radius)}px`;
circle.style.top = `${event.clientY - (button.offsetTop + radius)}px`;
circle.classList.add("ripple"); 

Antes de agregar nuestro elemento span al DOM, es una buena práctica verificar si hay ondas existentes que puedan quedar de los clics anteriores y eliminarlas antes de ejecutar el siguiente.

const ripple = button.getElementsByClassName("ripple")[0];

if (ripple) {
  ripple.remove();
}

Como paso final, agregamos el intervalo como hijo al elemento del botón para que se inyecte dentro del botón.

button.appendChild(circle);

Con nuestra función completa, todo lo que queda es llamarla. Esto se puede hacer de varias formas. Si queremos agregar la ondulación a cada botón de nuestra página, podemos usar algo como esto:

const buttons = document.getElementsByTagName("button");
for (const button of buttons) {
  button.addEventListener("click", createRipple);
}

¡Ahora tenemos un efecto dominó que funciona!

Llevándolo más lejos

¿Qué pasa si queremos ir más allá y combinar este efecto con otros cambios en la posición o el tamaño de nuestro botón? La capacidad de personalizar es, después de todo, una de las principales ventajas que tenemos al elegir recrear el efecto nosotros mismos. Para probar lo fácil que es extender nuestra función, decidí agregar un efecto de “imán”, que hace que nuestro botón se mueva hacia nuestro cursor cuando el cursor está dentro de un área determinada.

Necesitamos confiar en algunas de las mismas variables definidas en la función de rizado. En lugar de repetir el código innecesariamente, deberíamos almacenarlos en algún lugar donde sea accesible para ambos métodos. Pero también debemos mantener las variables compartidas dentro del alcance de cada botón individual. Una forma de lograr esto es mediante el uso de clases, como en el siguiente ejemplo:

Dado que el efecto de imán necesita realizar un seguimiento del cursor cada vez que se mueve, ya no necesitamos calcular la posición del cursor para crear una ondulación. En cambio, podemos confiar en cursorX y cursorY.

Dos nuevas variables importantes son magneticPullX y magneticPullY. Controlan la fuerza con la que nuestro método de imán tira del botón después del cursor. Entonces, cuando definimos el centro de nuestra ondulación, necesitamos ajustar tanto la posición del nuevo botón (x y y) y el tirón magnético.

const offsetLeft = this.left + this.x * this.magneticPullX;
const offsetTop = this.top + this.y * this.magneticPullY;

Para aplicar estos efectos combinados a todos nuestros botones, necesitamos crear una nueva instancia de la clase para cada uno:

const buttons = document.getElementsByTagName("button");
for (const button of buttons) {
  new Button(button);
}

Otras tecnicas

Por supuesto, esta es solo una forma de lograr un efecto dominó. En CodePen, hay muchos ejemplos que muestran diferentes implementaciones. A continuación se muestran algunos de mis favoritos.

Solo CSS

Si un usuario ha desactivado JavaScript, nuestro efecto dominó no tiene ninguna alternativa. Pero es posible acercarse al efecto original con solo CSS, usando la pseudoclase: active para responder a los clics. La principal limitación es que la onda solo puede surgir de un lugar, generalmente el centro del botón, en lugar de responder a la posición de nuestros clics. Este ejemplo de Ben Szabo es particularmente conciso:

JavaScript anterior a ES6

La demostración de Leandro Parice es similar a nuestra implementación pero es compatible con versiones anteriores de JavaScript:

jQuery

Este ejemplo usa jQuery para lograr el efecto dominó. Si ya tiene jQuery como dependencia, podría ayudarlo a ahorrar algunas líneas de código.

Reaccionar

Finalmente, un último ejemplo mío. Aunque es posible usar funciones de React como el estado y las referencias para ayudar a crear el efecto dominó, estas no son estrictamente necesarias. La posición y el tamaño de la ondulación deben calcularse para cada clic, por lo que no hay ninguna ventaja en mantener esa información en estado. Además, podemos acceder a nuestro elemento de botón desde el evento de clic, por lo que tampoco necesitamos referencias.

Este ejemplo de React usa un createRipple función idéntica a la de la primera implementación de este artículo. La principal diferencia es que, como método de Button componente: nuestra función tiene como alcance ese componente. También el onClick El detector de eventos ahora es parte de nuestro JSX: