Houdini: CSS Paint API

Houdini Paint CSS

Houdini: CSS Paint API

Gracias al elemento <canvas> de HTML5 podemos dibujar una imagen, mediante código JavaScript. Pero, ¿qué sucede si queremos hacer lo mismo con una imagen que queremos usar en CSS? Pues bien, en este artículo vamos a ver como con la nueva CSS Paint API podemos hacer justamente eso; dibujar mediante código JavaScript una imagen que después podemos usar en CSS.

¿Que es Houdini CSS?

Hoidini CSS es un nuevo conjunto de APIs para mejorar y expandir la funcionalidad de CSS desde JavaScript. Sobretodo, lo que nos ofrece Houdini es acceso al CSSOM. El CSSOM es el equivalente al DOM (Document Object Model) de HTML, pero para CSS (es decir, el CSS Object Model).

Houdini aún está bastante verde y deberías consultar su evolución en los diferentes navegadores para decidir cuando es buena idea empezar a usarlo. El resumen, en el día que escribo este artículo, es que algunas cosas ya funcionan en Chrome (y navegadores con el mismo motor, como el Edge), mientras que en otros navegadores como Firefox el soporte es básicamente nulo.

Pero bueno, igualmente en este artículo veremos que medidas podemos tomar para evitar problemas en navegadores donde estos APIs no funcionan todavía.

¿Como funciona CSS Paint API?

Este API nos permite generar una imagen, mediante JavaScript, que posteriormente podemos usar en cualquier propiedad CSS que admita una imagen. Como, por ejemplo, background-image, border-image, mask-image, etc.

Normalmente en una propiedad CSS que acepta una imagen, como background-image, usamos la función url() para indicar la ruta y nombre de archivo de la imagen que queremos usar. En el caso de CSS Paint API, debemos de usar la función paint() en lugar de la función url(), y como parámetro debemos de indicarle el nombre de nuestro paint worklet.

¿Que es un Paint Worklet?

Un Worklet es una especie de Web Worker simplificado, que nos va a dar un acceso más directo al «dibujado» de los elementos.

A efectos prácticos esto se traduce en una clase que debemos de escribir en un módulo JavaScript. Dicha clase debe de implementar un método paint(), y debemos de registrarla en el sistema para que pueda ser usada desde CSS.

Esto que suena tan complicado se reduce a crear un archivo .js con un código similar a este:

class MiWorklet {
  // debemos implementar este método con estos parámetros
  paint(ctx, geometry, properties) {
    // ctx - es similar (con algunas limitaciones) al "context" que 
    //       usamos para pintar en 2D con el API del Canvas
    // geometry - en este objeto tenemos información como por ejemplo
    //            el ancho y alto del "canvas" donde vamos a dibujar
    // properties - sirve para acceder a ciertas propiedades CSS que 
    //              nos pueden servir para parametrizar nuestro worklet

    // y aquí pondríamos nuestro código para pintar la imagen
  }
}

// IMPORTANTE: debemos de registrar nuestro worklet para que sea accesible desde CSS
registerPaint('miSuperWorklet', MiWorklet); // nombre en CSS, clase del worklet

y después debemos de cargar dicho archivo, como un módulo, del siguiente modo:

<script>CSS.paintWorklet.addModule('mi-worklet.js');</script>

Y ya está, ya podemos usar nuestro worklet desde CSS del siguiente modo:

h1 { background-image: paint(miSuperWorklet); }

Cuando se vaya a dibujar la imagen, se generará una imagen del tamaño adecuado mediante el worklet escrito en JavaScript y se usará como cualquier otra imagen. Ojo, porque si cambiamos el tamaño del elemento HTML la imagen se va a tener que volver a generar. Esto último es importante tenerlo en cuenta para no escribir un código demasiado pesado de ejecutar para el dispositivo donde se visualice la página Web.

Veamos un ejemplo de como podría ser nuestro método paint():

class MiWorklet {
  // nuestro método paint para dibujar la imagen
  paint(ctx, geometry, properties) {
    // vamos a dibujar una especie de curva sinusoide
    ctx.strokeStyle = 'pink';    
    let mitad_altura = geometry.height * 0.5;
    for (let x=0;x<geometry.width;x++) {
      ctx.beginPath();
      ctx.moveTo(x, mitad_altura);
      ctx.lineTo(x, mitad_altura + Math.sin(x * 0.1) * mitad_altura);
      ctx.closePath();
      ctx.stroke();
    }
  }
}
registerPaint('miSuperWorklet', MiWorklet); // nombre en CSS, clase del worklet

El resultado de nuestro worklet es el siguiente:

Ejemplo CSS Paint

Tal y como se puede ver, la forma de trabajar es muy similar a la que usamos para dibujar en un elemento canvas. Pero hay que tener en cuenta que existen ciertas limitaciones, por lo que si algo que nos funciona en un canvas y no nos funciona aquí, ya sabemos a que puede deberse.

Muy bien, hemos logrado generar una imagen mediante código JavaScript y usarla de fondo en CSS. Ahora bien, ¿Qué pasa si queremos darle más flexibilidad a nuestro generador de imágenes? Para ello es necesario añadir algún tipo de parámetros que nos permitan controlar mejor como se genera nuestra imagen.

Parametrizar un Paint Worklet

Para poder parametrizar nuestro worklet podemos usar variables CSS. y después podemos acceder al valor de dichas variables a través del parámetro properties del método paint(). Veamos de nuevo nuestro ejemplo, pero esta vez con algunos parámetros.

Código CSS:

h1 { 
    --mi-amplitud: 0.5; 
    --mi-frecuencia: 0.05;
    --mi-color: lightgreen;
    background-image: paint(miSuperWorklet);
}

Código para cargar el módulo JS:

<script>CSS.paintWorklet.addModule('mi-worklet.js');</script>

Código del módulo JS (mi-worklet.js):

class MiWorklet {
  // esto es necesario para tener acceso a las propiedades CSS que necesitamos
  static get inputProperties() { return ['--mi-amplitud', '--mi-frecuencia', '--mi-color']; }
  // nuestro método paint
  paint(ctx, geometry, properties) {
    // accedemos a las propiedades CSS
    const ampl = parseFloat(properties.get('--mi-amplitud').toString());
    const freq = parseFloat(properties.get('--mi-frecuencia').toString());
    const color = properties.get('--mi-color').toString();
    
    // vamos a dibujar una especie de curva sinusoide
    ctx.strokeStyle = color;    
    let mitad_altura = geometry.height * 0.5;
    for (let x=0;x<geometry.width;x++) {
      ctx.beginPath();
      ctx.moveTo(x, mitad_altura);
      ctx.lineTo(x, mitad_altura + Math.sin(x * freq) * mitad_altura * ampl);
      ctx.closePath();
      ctx.stroke();
    }
  }
}
registerPaint('miSuperWorklet', MiWorklet); // nombre en CSS, clase del worklet

Y este es el resultado con los nuevos parámetros a partir de las variables CSS:

Ejemplo CSS Paint con parámetros

Tal y como se puede observar, con estos parámetros podemos cambiar el color, amplitud y frecuencia de la onda, lo cual nos permite una mayor flexibilidad en la imagen generada.

Evitar problemas de compatibilidad

¿Y que sucede con los navegadores que no soportan CSS Paint API?

Pues bien, la parte de CSS es fácil de solucionar. Tan solo debemos de indicar la propiedad 2 veces, una con un valor aceptable por navegadores «viejos», y otra con la función paint(). Ojo, el orden es importante. Primero con el valor «viejo» y después con la función paint().

h1 { 
    --mi-amplitud: 0.5; 
    --mi-frecuencia: 0.05;
    --mi-color: lightgreen;
    background-image: linear-gradient(transparent, var(--mi-color), transparent);
    background-image: paint(miSuperWorklet);
}

En este ejemplo, en navegadores que no soporten CSS Paint API, se usará un degradado en lugar de nuestra imagen con la onda sinusoide.

Respecto a la parte de código JavaScript, podemos hacerlo de este modo:

if ('paintWorklet' in CSS) {
    // si el navegador soporta CSS Paint API, cargamos nuestro módulo
    CSS.paintWorklet.addModule('mi-worklet.js');
} else {
    // si hay que tomar alguna medida si el navegador no soporta CSS Paint API,
    // podemos hacerlo aquí
}

Conclusión

Y nada más. Espero que os haya resultado interesante este nuevo API. Si queréis inspiraros viendo otros ejemplos de worklets podéis visitar el siguiente enlace.

Escribe un comentario