Exploración de la API de pintura de CSS: efecto de fragmentación de imágenes | Programar Plus

En mi artículo anterior, creé un efecto de fragmentación usando una máscara CSS y propiedades personalizadas. Fue un buen efecto pero tiene un inconveniente: usa mucho código CSS (generado usando Sass). Esta vez voy a rehacer el mismo efecto pero confiaré en la nueva API de Paint. Esto reduce drásticamente la cantidad de CSS y elimina por completo la necesidad de Sass.

Explorando la serie CSS Paint API:

  • Parte 1: Efecto de fragmentación de imagen (¡estás aquí!)
  • Parte 2: Animación de blob
  • Parte 3: Borde de polígono
  • Parte 4: Formas de redondeo

Esto es lo que estamos haciendo. Como en el artículo anterior, solo Chrome y Edge admiten esto por ahora.

¿Mira eso? No más de cinco declaraciones CSS y, sin embargo, obtenemos una animación flotante bastante buena.

¿Qué es la API de Paint?

Paint API es parte del proyecto Houdini. Sí, “Houdini”, el extraño término del que todo el mundo habla. Muchos artículos ya cubren el aspecto teórico, así que no los molestaré con más. Si tengo que resumirlo en pocas palabras, simplemente diría: es el futuro de CSS. La API de Paint (y las otras API que caen bajo el paraguas de Houdini) nos permiten extender CSS con nuestras propias funcionalidades. ¡Ya no necesitamos esperar el lanzamiento de nuevas funciones porque podemos hacerlo nosotros mismos!

De la especificación:

Una API para permitir a los desarrolladores web definir un CSS personalizado <image> con javascript [sic], que responderá a los cambios de estilo y tamaño.

Y del explicador:

La API de pintura de CSS se está desarrollando para mejorar la extensibilidad de CSS. Específicamente, esto permite a los desarrolladores escribir una función de pintura que nos permite dibujar directamente en un elemento. [sic] fondo, borde o contenido.

Creo que la idea es bastante clara. Podemos dibujar lo que queramos. Comencemos con una demostración muy básica de coloración de fondo:

  1. Agregamos el worklet de pintura usando CSS.paintWorklet.addModule('your_js_file').
  2. Registramos un nuevo método de pintura llamado draw.
  3. Dentro de eso, creamos un paint() función donde hacemos todo el trabajo. ¿Y adivina qué? Todo es como trabajar con <canvas>. Ese ctx es el contexto 2D, y simplemente utilicé algunas funciones conocidas para dibujar un rectángulo rojo que cubra toda el área.

Esto puede parecer poco intuitivo a primera vista, pero observe que la estructura principal es siempre la misma: los tres pasos anteriores son la parte de “copiar / pegar” que repite para cada proyecto. El verdadero trabajo es el código que escribimos dentro del paint() función.

Agreguemos una variable:

Como puede ver, la lógica es bastante simple. Definimos el getter inputProperties con nuestras variables como una matriz. Añadimos properties como tercer parámetro para paint() y luego obtenemos nuestra variable usando properties.get().

¡Eso es! Ahora tenemos todo lo que necesitamos para construir nuestro complejo efecto de fragmentación.

Construyendo la máscara

Quizás se pregunte por qué la API de pintura crea un efecto de fragmentación. Dijimos que es una herramienta para dibujar imágenes, ¿cómo nos permitirá fragmentar una imagen?

En el artículo anterior hice el efecto usando una capa de máscara diferente donde cada una es un cuadrado definido con un degradado (recuerda que un degradado es una imagen) así que obtuvimos una especie de matriz y el truco fue ajustar el canal alfa de cada uno. uno individualmente.

Esta vez, en lugar de usar muchos degradados, definiremos solo una imagen personalizada para nuestra máscara y esa imagen personalizada será manejada por nuestra API de pintura.

¡Un ejemplo por favor!

En lo anterior, he creado una imagen que tiene un color opaco que cubre la parte izquierda y una semitransparente que cubre la parte derecha. Aplicar esta imagen como máscara nos da el resultado lógico de una imagen semitransparente.

Ahora todo lo que tenemos que hacer es dividir nuestra imagen en más partes. Definamos dos variables y actualicemos nuestro código:

La parte relevante del código es la siguiente:

const n = properties.get('--f-n');
const m = properties.get('--f-m');

const w = size.width/n;
const h = size.height/m;

for(var i=0;i<n;i++) {
  for(var j=0;j<m;j++) {
    ctx.fillStyle="rgba(0,0,0,"+(Math.random())+')';    
    ctx.fillRect(i*w, j*h, w, h);
}
}

N y M define la dimensión de nuestra matriz de rectángulos. W y H son el tamaño de cada rectángulo. Entonces tenemos un básico FOR bucle para llenar cada rectángulo con un color transparente aleatorio.

Con un poco de JavaScript, obtenemos una máscara personalizada que podemos controlar fácilmente ajustando las variables CSS:

Ahora, necesitamos controlar el canal alfa para crear el efecto de desvanecimiento de cada rectángulo y construir el efecto de fragmentación.

Introduzcamos una tercera variable que usamos para el canal alfa que también cambiamos al pasar el mouse.

Definimos una propiedad personalizada de CSS como <number> que hacemos la transición de 1 a 0, y esa misma propiedad se usa para definir el canal alfa de nuestros rectángulos. No sucederá nada elegante al pasar el mouse porque todos los rectángulos se desvanecerán de la misma manera.

Necesitamos un truco para evitar el desvanecimiento de todos los rectángulos al mismo tiempo, en lugar de crear un retraso entre ellos. Aquí hay una ilustración para explicar la idea que voy a utilizar:

Lo anterior muestra la animación alfa de dos rectángulos. Primero definimos una variable L que debería ser mayor o igual a 1, luego para cada rectángulo de nuestra matriz (es decir, para cada canal alfa) realizamos una transición entre X y Y donde X - Y = L por lo que tenemos la misma duración general para todo el canal alfa. X debe ser mayor o igual a 1 e Y menor o igual a 0.

Espera, el valor alfa no debería estar en el rango [1 0], derecho ?

¡Sí, debería! Y todos los trucos en los que estamos trabajando se basan en eso. Arriba, el alfa se anima de 8 a -2, lo que significa que tenemos un color opaco en el [8 1] rango, uno transparente en el [0 -2] rango y una animación dentro [1 0]. En otras palabras, cualquier valor mayor que 1 tendrá el mismo efecto que 1, y cualquier valor menor que 0 tendrá el mismo efecto que 0.

Animación dentro [1 0] no sucederá al mismo tiempo para nuestros dos rectángulos. El rectángulo 2 alcanzará [1 0] antes que el Rectángulo 1. Aplicamos esto a todos los canales alfa para obtener nuestras animaciones retrasadas.

En nuestro código actualizaremos esto:

rgba(0,0,0,'+(o)+')

…a esto:

rgba(0,0,0,'+((Math.random()*(l-1) + 1) - (1-o)*l)+')

L es la variable ilustrada anteriormente, y O es el valor de nuestra variable CSS que pasa de 1 a 0

Cuándo O=1, tenemos (Math.random()*(l-1) + 1). Considerando el hecho de que el random() La función nos da un valor dentro de la [0 1] rango, el valor final estará en el [L 1]rango.

Cuándo O=0, tenemos (Math.random()*(l-1) + 1 - l) y un valor con el [0 1-L] rango.

L es nuestra variable para controlar el retraso.

Veamos esto en acción:

Nos estamos acercando. Tenemos un efecto de fragmentación genial, pero no el que vimos al principio del artículo. Este no es tan sencillo.

El problema está relacionado con random() función. Dijimos que cada canal alfa necesita animarse entre X y Y, por lo que, lógicamente, esos valores deben seguir siendo los mismos. Pero el paint() La función se llama un grupo durante la transición, por lo que cada vez, el random() la función nos da diferente X y Y valores para cada canal alfa; de ahí el efecto “aleatorio” que estamos obteniendo.

Para solucionar esto, necesitamos encontrar una manera de almacenar el valor generado para que sean siempre los mismos para cada llamada del paint() función. Consideremos una función pseudoaleatoria, una función que siempre genera la misma secuencia de valores. En otras palabras, queremos controlar la semilla.

Desafortunadamente, no podemos hacer esto con el JavaScript incorporado. random() , así que como cualquier buen desarrollador, escojamos uno de Stack Overflow:

const mask = 0xffffffff;
const seed = 30; /* update this to change the generated sequence */
let m_w  = (123456789 + seed) & mask;
let m_z  = (987654321 - seed) & mask;

let random =  function() {
  m_z = (36969 * (m_z & 65535) + (m_z >>> 16)) & mask;
  m_w = (18000 * (m_w & 65535) + (m_w >>> 16)) & mask;
  var result = ((m_z << 16) + (m_w & 65535)) >>> 0;
  result /= 4294967296;
  return result;
}

Y el resultado se convierte en:

Tenemos nuestro efecto de fragmentación sin código complejo:

  • un bucle anidado básico para crear rectángulos NxM
  • una fórmula inteligente para el canal alfa para crear el retardo de transición
  • un listo random() función tomada de la red

¡Eso es! Todo lo que tienes que hacer es aplicar el mask propiedad a cualquier elemento y ajuste las variables CSS.

¡Luchando contra las brechas!

Si juegas con las demostraciones anteriores notarás, en algún caso particular, extraños huecos entre los rectángulos

Para evitar esto, podemos extender el área de cada rectángulo con un pequeño desplazamiento.

Actualizamos esto:

ctx.fillRect(i*w, j*h, w, h);

…con este:

ctx.fillRect(i*w-.5, j*h-.5, w+.5, h+.5);

Crea una pequeña superposición entre los rectángulos que compensa los espacios entre ellos. No hay una lógica particular con el valor 0.5 Solía. Puede ir más grande o más pequeño según su caso de uso.

¿Quieres más formas?

¿Se puede ampliar lo anterior para considerar algo más que una forma rectangular? ¡Seguro que puede! No olvidemos que podemos usar Canvas para dibujar cualquier tipo de forma, a diferencia de las formas CSS puras donde a veces necesitamos algún código hacky. Intentemos construir ese efecto de fragmentación triangular.

Después de buscar en la web, encontré algo llamado triangulación de Delaunay. No entraré en la teoría profunda detrás de esto, pero es un algoritmo para un conjunto de puntos para dibujar triángulos conectados con propiedades específicas. Hay muchas implementaciones listas para usar, pero optaremos por Delaunator porque se supone que es la más rápida de todas.

Primero definimos un conjunto de puntos (usaremos random() aquí) luego ejecute Delauntor para generar los triángulos para nosotros. En este caso, solo necesitamos una variable que defina el número de puntos.

const n = properties.get('--f-n');
const o = properties.get('--f-o');
const w = size.width;
const h = size.height;
const l = 7; 

var dots = [[0,0],[0,w],[h,0],[w,h]]; /* we always include the corners */
/* we generate N random points within the area of the element */
for (var i = 0; i < n; i++) {
  dots.push([random() * w, random() * h]);
}
/**/
/* We call Delaunator to generate the triangles*/
var delaunay = Delaunator.from(dots);
var triangles = delaunay.triangles;
/**/
for (var i = 0; i < triangles.length; i += 3) { /* we loop the triangles points */
  /* we draw the path of the triangles */
  ctx.beginPath();
  ctx.moveTo(dots[triangles[i]][0]    , dots[triangles[i]][1]);
  ctx.lineTo(dots[triangles[i + 1]][0], dots[triangles[i + 1]][1]);
  ctx.lineTo(dots[triangles[i + 2]][0], dots[triangles[i + 2]][1]);  
  ctx.closePath();
  /**/
  var alpha = (random()*(l-1) + 1) - (1-o)*l; /* the alpha value */
  /* we fill the area of triangle with the semi-transparent color */
  ctx.fillStyle="rgba(0,0,0,"+alpha+')';
  /* we consider stroke to fight the gaps */
  ctx.strokeStyle="rgba(0,0,0,"+alpha+')';
  ctx.stroke();
  ctx.fill();
} 

No tengo nada más que agregar a los comentarios en el código anterior. Simplemente utilicé algunas cosas básicas de JavaScript y Canvas y, sin embargo, tenemos un efecto bastante bueno.

¡Podemos hacer aún más formas! Todo lo que tenemos que hacer es encontrar un algoritmo para ello.

¡No puedo seguir adelante sin hacer el hexágono!

Tomé el código de este artículo escrito por Izan Pérez Cosano. Nuestra variable es ahora R que definirá la dimensión de un hexágono.

¿Que sigue?

Ahora que hemos creado nuestro efecto de fragmentación, centrémonos en el CSS. Observe que el efecto es tan simple como cambiar el opacity valor (o el valor de cualquier propiedad con la que esté trabajando) de un elemento en su estado de desplazamiento.

Animación de opacidad

img {
  opacity:1;
  transition:opacity 1s;
}

img:hover {
  opacity:0;
}

Efecto de fragmentación

img {
  -webkit-mask: paint(fragmentation);
  --f-o:1;
  transition:--f-o 1s;
}

img:hover {
  --f-o:0;
}

Esto significa que podemos integrar fácilmente este tipo de efecto para crear animaciones más complejas. ¡Aquí tienes un montón de ideas!

Control deslizante de imagen receptiva

Otra versión del mismo control deslizante:

Efecto de ruido

Cargando pantalla

Efecto de desplazamiento de tarjeta

Eso es un envoltorio

Y todo esto es solo la punta del iceberg de lo que se puede lograr con Paint API. Terminaré con dos puntos importantes:

  • La API de pintura es del 90% <canvas>, así que cuanto más sepa sobre <canvas>, las cosas más sofisticadas que puedas hacer. Canvas se usa ampliamente, lo que significa que hay mucha documentación y escritura al respecto para ponerte al día. ¡Oye, aquí tienes uno aquí en CSS-Tricks!
  • Paint API elimina toda la complejidad del lado CSS de las cosas. No hay que lidiar con código complejo y hacky para dibujar cosas interesantes. Esto hace que el código CSS sea mucho más fácil de mantener, sin mencionar que es menos propenso a errores.

Explorando la serie CSS Paint API:

  • Parte 1: Efecto de fragmentación de la imagen (¡estás aquí!)
  • Parte 2: Animación de blob
  • Parte 3: Borde de polígono
  • Parte 4: Formas de redondeo