API CSS Paint

Novas possibilidades no Chrome 65

A API CSS Paint (também conhecida como "CSS Custom Paint" ou "worklet de pintura do Houdini") é ativada por padrão a partir do Chrome 65. O que é? O que você pode fazer com ele? E como funciona? Bom, continue lendo, ok...

A API CSS Paint permite gerar uma imagem de forma programática sempre que uma propriedade CSS espera uma imagem. Propriedades como background-image ou border-image costumam ser usadas com url() para carregar um arquivo de imagem ou com funções integradas do CSS, como linear-gradient(). Em vez disso, agora você pode usar paint(myPainter) para referenciar um worklet de pintura.

Como escrever uma worklet de pintura

Para definir um worklet de pintura chamado myPainter, precisamos carregar um arquivo de pintura CSS usando CSS.paintWorklet.addModule('my-paint-worklet.js'). Nesse arquivo, podemos usar a função registerPaint para registrar uma classe de worklet de pintura:

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

registerPaint('myPainter', MyPainter);

Dentro do callback paint(), podemos usar ctx da mesma forma que faríamos uma CanvasRenderingContext2D, como conhecemos de <canvas>. Se você souber desenhar em um <canvas>, poderá desenhar em um worklet de pintura. geometry informa a largura e a altura da tela que está à nossa disposição. properties Explicarei mais adiante neste artigo.

Como exemplo introdutório, vamos criar uma worklet de tinta quadriculada e usá-la como a imagem de plano de fundo de uma <textarea>. Estou usando uma área de texto porque ela é redimensionável por padrão:

<!-- 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);

Se você já usou o <canvas> antes, esse código deve parecer familiar. Confira a demonstração ao vivo aqui.

Área de texto com padrão quadriculado como imagem de plano de fundo
Área de texto com um padrão quadriculado como imagem de plano de fundo.

A diferença de usar uma imagem de plano de fundo comum aqui é que o padrão será redesenhado sob demanda sempre que o usuário redimensionar a área de texto. Isso significa que a imagem de plano de fundo é sempre exatamente do tamanho necessário, incluindo a compensação por telas de alta densidade.

Isso é muito legal, mas também é bastante estático. Queríamos escrever um novo worklet sempre que quiséssemos o mesmo padrão, mas com quadrados de tamanhos diferentes? A resposta é não.

Como parametrizar seu worklet

Felizmente, o worklet de pintura pode acessar outras propriedades CSS. É aí que o parâmetro extra properties entra em jogo. Ao atribuir um atributo inputProperties estático à classe, é possível se inscrever para receber mudanças em qualquer propriedade CSS, incluindo propriedades personalizadas. Os valores serão fornecidos a você pelo 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);

Agora podemos usar o mesmo código para todos os diferentes tipos de tabuleiros. Melhor ainda, agora podemos acessar o DevTools e mudar os valores até encontrar o visual certo.

Navegadores que não são compatíveis com o worklet de pintura

Atualmente, apenas o Chrome tem um worklet de pintura implementado. Embora existam sinais positivos de todos os outros fornecedores de navegador, não há muito progresso. Para ficar por dentro das novidades, consulte Is Houdini Ready Yet? regularmente. Enquanto isso, use o aprimoramento progressivo para manter o código em execução, mesmo que não haja suporte para o worklet de pintura. Para garantir que tudo funcione como esperado, ajuste o código em dois lugares: no CSS e no JS.

A detecção do suporte ao worklet de pintura no JS pode ser feita verificando o objeto CSS: js if ('paintWorklet' in CSS) { CSS.paintWorklet.addModule('mystuff.js'); } No lado do CSS, você tem duas opções. Você pode usar @supports:

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

Um truque mais compacto é usar o fato de que o CSS invalida e, subsequentemente, ignora uma declaração de propriedade inteira se houver uma função desconhecida. Se você especificar uma propriedade duas vezes, primeiro sem o worklet de pintura e depois com o worklet de pintura, terá o aprimoramento progressivo:

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

Em navegadores com suporte ao worklet de pintura, a segunda declaração de background-image vai substituir a primeira. Em navegadores sem suporte ao worklet de pintura, a segunda declaração é inválida e será descartada, deixando a primeira declaração em vigor.

Polyfill de pintura CSS

Para muitos usos, também é possível usar o Polyfill de Paint do CSS, que adiciona suporte a CSS Custom Paint e Paint Worklets para navegadores modernos.

Casos de uso

Há muitos casos de uso para worklets de pintura, alguns deles mais óbvios que outros. Um dos mais óbvios é o uso de worklet de pintura para reduzir o tamanho do seu DOM. Muitas vezes, os elementos são adicionados apenas para criar ornamentos usando CSS. Por exemplo, no Material Design Lite (link em inglês), o botão com o efeito de ondulação contém dois outros elementos <span> para implementar a própria ondulação. Se você tiver muitos botões, isso poderá resultar em um grande número de elementos DOM e prejudicar o desempenho em dispositivos móveis. Se você implementar o efeito de ondulação usando um worklet de pintura, o resultado será 0 elementos extras e apenas um worklet de pintura. Além disso, você tem algo que é muito mais fácil de personalizar e parametrizar.

Outra vantagem de usar o worklet de pintura é que, na maioria dos cenários, uma solução que usa o worklet de pintura é pequena em termos de bytes. Obviamente, há uma compensação: seu código de pintura será executado sempre que o tamanho da tela ou qualquer um dos parâmetros mudar. Portanto, se o código for complexo e demorar muito, ele poderá introduzir instabilidade. O Chrome está trabalhando para remover os worklets de pintura da linha de execução principal para que mesmo os worklets de pintura de longa duração não afetem a capacidade de resposta da linha de execução principal.

Para mim, o cliente em potencial mais interessante é que o worklet de pintura permite um polyfilling eficiente de recursos CSS que um navegador ainda não tem. Um exemplo seria adicionar polyfills de gradientes cônicos até eles chegarem ao Chrome de forma nativa. Outro exemplo: em uma reunião do CSS, decidimos que é possível ter várias cores de borda. Enquanto a reunião ainda estava em andamento, meu colega Ian Kilpatrick escreveu um polyfill para esse novo comportamento de CSS usando um worklet de pintura.

Pensar fora da caixa

A maioria das pessoas começa a pensar em imagens de plano de fundo e de borda quando aprendem sobre o worklet de pintura. Um caso de uso menos intuitivo para o worklet de pintura é mask-image para fazer os elementos do DOM terem formas arbitrárias. Por exemplo, um diamond:

Um elemento DOM na forma de um losango.
Um elemento DOM na forma de um losango.

mask-image usa uma imagem que tem o tamanho do elemento. Áreas em que a imagem da máscara é transparente, o elemento é transparente. Áreas em que a imagem da máscara é opaca, ou seja, o elemento opaco.

Agora no Chrome

O worklet de pintura está no Chrome Canary há algum tempo. No Chrome 65, ele é ativado por padrão. Vá em frente e teste as novas possibilidades que a worklet de pintura se abre e nos mostre o que você construiu! Para se inspirar ainda mais, confira a coleção de Vincent de Oliveira.