API CSS Paint

Novas possibilidades no Chrome 65

A API CSS Paint, também conhecida como "CSS Custom Paint" ou "worklet de pintura da Hudini", é ativada por padrão a partir do Chrome 65. O que é? O que você pode fazer com ele? E como funciona? Continue lendo: está...

A API CSS Paint permite que você gere 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 de CSS, como linear-gradient(). Em vez de usá-los, agora você pode usar paint(myPainter) para referenciar um worklet de pintura.

Como escrever uma worklet de tinta

Para definir uma worklet de pintura chamada myPainter, precisamos carregar um arquivo de worklet 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 usamos um CanvasRenderingContext2D como o conhecemos em <canvas>. Se você sabe como desenhar em uma <canvas>, pode usar uma worklet de pintura. geometry informa a largura e a altura da tela que está à nossa disposição. properties Explicaremos isso posteriormente neste artigo.

Como exemplo introdutório, vamos escrever uma worklet de tinta quadriculada e usá-la como uma 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 <canvas>, esse código parecerá familiar. Veja a demonstração ao vivo aqui.

Área de texto com um 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 do uso de 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 tem sempre o 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 tamanho diferente? A resposta é não.

Como parametrizar o worklet

Felizmente, o worklet de pintura pode acessar outras propriedades CSS, que é quando o parâmetro adicional properties entra em jogo. Ao atribuir à classe um atributo inputProperties estático, você pode 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 tabuleiro. Melhor ainda, podemos acessar o DevTools e trabalhar com os valores até encontrar o visual certo.

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

No momento em que este artigo foi escrito, apenas o Chrome tinha worklet de pintura implementada. Embora haja sinais positivos de todos os outros fornecedores de navegador, não há muito progresso. Para ficar por dentro das novidades, consulte Is Houdini Ready ainda? 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 conforme esperado, é preciso ajustar seu código em dois lugares: CSS e JS.

Para detectar o suporte a worklet de pintura em JS, verifique o objeto CSS: js if ('paintWorklet' in CSS) { CSS.paintWorklet.addModule('mystuff.js'); } Para o CSS, há duas opções. Você pode usar @supports:

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

Uma dica mais compacta é usar o fato de que o CSS invalida e, em seguida, 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 que oferecem suporte ao worklet de pintura, a segunda declaração de background-image substituirá a primeira. Em navegadores sem suporte a worklet de pintura, a segunda declaração será inválida e será descartada, deixando a primeira declaração em vigor.

Polifill de pintura CSS

Para muitos usos, também é possível usar o Polyfill de pintura de CSS (em inglês), que adiciona compatibilidade com os Worklets de pintura personalizada e os Worklets de CSS aos navegadores mais recentes.

Casos de uso

Há muitos casos de uso para worklets de pintura, alguns deles mais óbvios do que outros. Uma das mais óbvias é usar a worklet de pintura para reduzir o tamanho do DOM. Muitas vezes, os elementos são adicionados puramente para criar enfeites usando CSS. Por exemplo, no Material Design Lite, o botão com o efeito de ondulação contém dois elementos <span> extras para implementar a ondulação em si. Se você tiver muitos botões, isso poderá gerar uma grande quantidade de elementos DOM e prejudicar o desempenho em dispositivos móveis. Se você implementar o efeito de ondulação usando a worklet de pintura, vai acabar sem elementos adicionais e apenas uma worklet de pintura. Além disso, você tem algo muito mais fácil de personalizar e parametrizar.

Outra vantagem de usar a worklet de pintura é que, na maioria dos cenários, uma solução que usa a worklet de pintura é pequena em termos de bytes. É claro que há uma desvantagem: o 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 trabalhos de pintura mais longos não afetem a capacidade de resposta da linha de execução principal.

Para mim, a perspectiva mais interessante é que a worklet de pintura permite um polyfill eficiente de recursos CSS que um navegador ainda não tem. Um exemplo seria fazer o polyfill de gradientes de cónic até que cheguem ao Chrome de maneira nativa. Outro exemplo: em uma reunião do CSS, decidimos que agora você pode ter várias cores de borda. Durante a reunião, meu colega Ian Kilpatrick escreveu um polyfill para esse novo comportamento do CSS usando a 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 a worklet de pintura. Um caso de uso menos intuitivo para a worklet de pintura é mask-image para fazer com que elementos DOM tenham formas arbitrárias. Por exemplo, um diamond:

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

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

Agora no Chrome

A worklet de pintura está no Chrome Canary há algum tempo. No Chrome 65, ele é ativado por padrão. Teste as novas possibilidades da Worklet de pintura e mostre o que você criou. Para mais inspiração, dê uma olhada na coleção de Vincent De Oliveira.