API de Paint de CSS

Nuevas posibilidades en Chrome 65

La API de CSS Paint (también conocida como "CSS Custom Paint" o "Houdini’s Paint Paintlet") está habilitada de forma predeterminada a partir de Chrome 65. ¿Qué es? ¿Qué puedes hacer con él? ¿Cómo funciona? Bueno, sigue leyendo, ¿verdad?

La API de CSS Paint te permite generar una imagen de manera programática cada vez que una propiedad de CSS espera una imagen. Por lo general, las propiedades como background-image o border-image se usan con url() para cargar un archivo de imagen o con funciones integradas de CSS, como linear-gradient(). En lugar de usarlos, ahora puedes usar paint(myPainter) para hacer referencia a un worklet de pintura.

Cómo escribir un worklet de pintura

Para definir un worklet de pintura llamado myPainter, debemos cargar un archivo de worklet de pintura de CSS con CSS.paintWorklet.addModule('my-paint-worklet.js'). En ese archivo, podemos usar la función registerPaint para registrar una clase de worklet de pintura:

class MyPainter {
  paint(ctx, geometry, properties) {
    // ...
  }
}

registerPaint('myPainter', MyPainter);

Dentro de la devolución de llamada paint(), podemos usar ctx de la misma manera en que lo haríamos con una CanvasRenderingContext2D, tal como la conocemos en <canvas>. Si sabes dibujar en un <canvas>, puedes hacerlo en un worklet de pintura. geometry nos indica el ancho y el alto del lienzo que está a nuestra disposición. properties La explicaré más adelante en este artículo.

Como ejemplo introductorio, escribamos un worklet de pintura de tablero de ajedrez y usémoslo como imagen de fondo de un <textarea>. (Estoy usando un área de texto porque se puede cambiar de tamaño de forma predeterminada):

<!-- index.html -->
<!doctype html>
<style>
  textarea {
    background-image: paint(checkerboard);
  }
</style>
<textarea></textarea>
<script>
  CSS.paintWorklet.addModule('checkerboard.js');
</script>
// checkerboard.js
class CheckerboardPainter {
  paint(ctx, geom, properties) {
    // Use `ctx` as if it was a normal canvas
    const colors = ['red', 'green', 'blue'];
    const size = 32;
    for(let y = 0; y < geom.height/size; y++) {
      for(let x = 0; x < geom.width/size; x++) {
        const color = colors[(x + y) % colors.length];
        ctx.beginPath();
        ctx.fillStyle = color;
        ctx.rect(x * size, y * size, size, size);
        ctx.fill();
      }
    }
  }
}

// Register our class under a specific name
registerPaint('checkerboard', CheckerboardPainter);

Si usaste <canvas> en el pasado, este código debería resultarte familiar. Mira la demostración en vivo aquí.

Área de texto con un patrón de tablero de ajedrez como imagen de fondo
Área de texto con un patrón de ajedrez como imagen de fondo.

La diferencia con respecto al uso de una imagen de fondo común en este caso es que el patrón se volverá a dibujar a pedido, cada vez que el usuario cambie el tamaño del área de texto. Esto significa que la imagen de fondo siempre es exactamente tan grande como debe ser, incluida la compensación por las pantallas de alta densidad.

Eso es genial, pero también bastante estático. ¿Querríamos escribir un nuevo worklet cada vez que quisiéramos el mismo patrón, pero con cuadrados de diferentes tamaños? La respuesta es no.

Parametriza tu worklet

Afortunadamente, el worklet de pintura puede acceder a otras propiedades de CSS, y es cuando el parámetro adicional properties entra en juego. Si le otorgas a la clase un atributo inputProperties estático, puedes suscribirte a los cambios de cualquier propiedad de CSS, incluidas las propiedades personalizadas. Los valores se te proporcionarán a través del parámetro properties.

<!-- index.html -->
<!doctype html>
<style>
  textarea {
    /* The paint worklet subscribes to changes of these custom properties. */
    --checkerboard-spacing: 10;
    --checkerboard-size: 32;
    background-image: paint(checkerboard);
  }
</style>
<textarea></textarea>
<script>
  CSS.paintWorklet.addModule('checkerboard.js');
</script>
// checkerboard.js
class CheckerboardPainter {
  // inputProperties returns a list of CSS properties that this paint function gets access to
  static get inputProperties() { return ['--checkerboard-spacing', '--checkerboard-size']; }

  paint(ctx, geom, properties) {
    // Paint worklet uses CSS Typed OM to model the input values.
    // As of now, they are mostly wrappers around strings,
    // but will be augmented to hold more accessible data over time.
    const size = parseInt(properties.get('--checkerboard-size').toString());
    const spacing = parseInt(properties.get('--checkerboard-spacing').toString());
    const colors = ['red', 'green', 'blue'];
    for(let y = 0; y < geom.height/size; y++) {
      for(let x = 0; x < geom.width/size; x++) {
        ctx.fillStyle = colors[(x + y) % colors.length];
        ctx.beginPath();
        ctx.rect(x*(size + spacing), y*(size + spacing), size, size);
        ctx.fill();
      }
    }
  }
}

registerPaint('checkerboard', CheckerboardPainter);

Ahora podemos usar el mismo código para todo tipo de tabla de ajedrez. Pero lo que es aún mejor, ahora podemos ir a Herramientas para desarrolladores y controlar los valores hasta que encontremos el aspecto correcto.

Navegadores que no admiten el proceso de pintura

Al momento de la redacción, solo Chrome tiene el worklet de pintura implementado. Si bien hay señales positivas de todos los demás proveedores de navegadores, no hay mucho progreso. Para mantenerte al tanto, consulta periódicamente la página Is Houdini Ready Yet?. Mientras tanto, asegúrate de usar la mejora progresiva para mantener tu código en ejecución, incluso si no es compatible con el worklet de pintura. Para asegurarte de que todo funcione como se espera, debes ajustar tu código en dos lugares: CSS y JS.

Para detectar la compatibilidad con el worklet de pintura en JS, puedes verificar el objeto CSS: js if ('paintWorklet' in CSS) { CSS.paintWorklet.addModule('mystuff.js'); } En el lado del CSS, tienes dos opciones. Puedes usar @supports:

@supports (background: paint(id)) {
  /* ... */
}

Un truco más compacto es usar el hecho de que CSS invalida una declaración de propiedad completa y, por lo tanto, ignora esa declaración si contiene una función desconocida. Si especificas una propiedad dos veces (la primera sin el worklet de pintura y otra sin el worklet de pintura), obtendrás una mejora progresiva:

textarea {
  background-image: linear-gradient(0, red, blue);
  background-image: paint(myGradient, red, blue);
}

En navegadores compatibles con el worklet de pintura, la segunda declaración de background-image reemplazará la primera. En navegadores sin compatibilidad con el worklet de pintura, la segunda declaración no es válida y se descartará, lo que dejará en efecto la primera.

Polyfill de pintura de CSS

Para muchos usos, también es posible utilizar el Polyfill de pintura de CSS, que agrega compatibilidad con los Worklets de pintura y pintura personalizados de CSS en los navegadores modernos.

Casos de uso

Hay muchos casos de uso de los Worklets de pintura, algunos de ellos más obvios que otros. Uno de los más obvios es usar el worklet de pintura para reducir el tamaño de tu DOM. A menudo, los elementos se agregan solo para crear adornos mediante CSS. Por ejemplo, en Material Design Lite, el botón con el efecto de ondas contiene 2 elementos <span> adicionales para implementar el efecto ondas. Si tienes muchos botones, es posible que sumen varios elementos del DOM y que disminuya el rendimiento en dispositivos móviles. Si, en cambio, implementas el efecto de ondas con el worklet de pintura, obtendrás 0 elementos adicionales y solo un worklet de pintura. Además, hay algo que es mucho más fácil de personalizar y parametrizar.

Otra ventaja de usar un worklet de pintura es que, en la mayoría de los casos, una solución que usa un worklet de pintura es pequeña en términos de bytes. Por supuesto, hay una desventaja: tu código de pintura se ejecutará cada vez que cambie el tamaño del lienzo o cualquiera de los parámetros. Por lo tanto, si tu código es complejo y tarda mucho tiempo, es posible que se generen bloqueos. Chrome está trabajando para mover los trabajos de pintura fuera del subproceso principal para que incluso los trabajos de pintura de larga duración no afecten la capacidad de respuesta del subproceso principal.

Para mí, el cliente potencial más interesante es que el worklet de pintura permite un polyfille eficiente de las funciones de CSS que un navegador aún no tiene. Un ejemplo sería polyfill gradientes cónicos hasta que llegan a Chrome de forma nativa. Otro ejemplo: en una reunión de CSS, se decidió que ahora puedes tener varios colores de borde. Durante la reunión, mi colega Ian Kilpatrick escribió un polyfill para este nuevo comportamiento de CSS con el worklet de pintura.

Pensar fuera de lo común

La mayoría de las personas comienzan a pensar en las imágenes de fondo y las imágenes de borde cuando aprenden sobre el worklet de pintura. Un caso de uso menos intuitivo para el worklet de pintura es mask-image, que permite que los elementos del DOM tengan formas arbitrarias. Por ejemplo, un diamante:

Un elemento del DOM en forma de diamante.
Un elemento del DOM con forma de diamante.

mask-image toma una imagen que tiene el tamaño del elemento. Áreas en las que la imagen de máscara es transparente y el elemento es transparente. Áreas en las que la imagen de la máscara es opaca, el elemento opaco.

Ahora disponible en Chrome

El worklet de pintura está en Chrome Canary desde hace un tiempo. En Chrome 65, esta función está habilitada de forma predeterminada. Avanza y prueba las nuevas posibilidades que se abren con el worklet de pintura y muéstranos lo que construiste. Para obtener más inspiración, consulta la colección de Vincent De Oliveira.