API CSS Paint

Novas possibilidades no Chrome 65

A CSS Paint API (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 é possível fazer com ele? E como funciona? Então continue lendo…

A CSS Paint API permite gerar uma imagem programaticamente sempre que uma propriedade CSS espera uma imagem. Propriedades como background-image ou border-image geralmente são usadas com url() para carregar um arquivo de imagem ou com funções integradas do CSS, como linear-gradient(). Em vez disso, use paint(myPainter) para fazer referência a um worklet de pintura.

Como escrever um worklet de pintura

Para definir um worklet de pintura chamado 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);

No callback paint(), podemos usar ctx da mesma forma que um CanvasRenderingContext2D, como sabemos em <canvas>. Se você sabe desenhar em um <canvas>, pode desenhar em um worklet de pintura. geometry informa a largura e a altura da tela disponível. properties Vou explicar mais adiante neste artigo.

Como exemplo introdutório, vamos escrever um worklet de pintura de tabuleiro de xadrez e usá-lo como uma imagem de plano de fundo de um <textarea>. Estou usando um textarea porque ele é 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 vai parecer familiar. Confira a demonstração ao vivo.

Textarea com um padrão de xadrez como imagem de plano de fundo
Textarea com um padrão de tabuleiro de xadrez 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 o textarea. Isso significa que a imagem de plano de fundo é sempre exatamente do tamanho necessário, incluindo a compensação para telas de alta densidade.

Isso é muito legal, mas também é bastante estático. Seria necessário 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 do CSS, que é onde o parâmetro adicional properties entra em ação. Ao atribuir à classe um atributo inputProperties estático, você pode se inscrever para receber notificações de mudanças em qualquer propriedade CSS, incluindo propriedades personalizadas. Os valores serão fornecidos 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 tipos de tabuleiro de xadrez. Mas ainda melhor, agora podemos acessar o DevTools e mexer nos valores até encontrar a aparência certa.

Navegadores que não oferecem suporte ao worklet de pintura

No momento da escrita, apenas o Chrome tem o worklet de pintura implementado. Embora existam sinais positivos de todos os outros fornecedores de navegadores, não há muito progresso. Para se manter atualizado, confira regularmente a página Is Houdini Ready Yet?. 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 as coisas funcionem conforme o esperado, ajuste o código em dois lugares: CSS e JS.

Para detectar o suporte a worklets de pintura no JS, verifique o objeto CSS: js if ('paintWorklet' in CSS) { CSS.paintWorklet.addModule('mystuff.js'); } No lado do CSS, você tem duas opções. É possível usar @supports:

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

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

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

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

CSS Paint Polyfill

Para muitos usos, também é possível usar o polyfill do CSS Paint, que adiciona suporte ao CSS Custom Paint e aos worklets do Paint a navegadores modernos.

Casos de uso

Há muitos casos de uso para worklets de pintura, alguns deles mais óbvios do que outros. Uma das mais óbvias é usar o worklet de pintura para reduzir o tamanho do DOM. Muitas vezes, os elementos são adicionados apenas 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> adicionais para implementar o efeito. Se você tiver muitos botões, isso pode resultar em um grande número de elementos DOM e levar a uma degradação no desempenho em dispositivos móveis. Se você implementar o efeito de ondulação usando a worklet de pintura, você terá zero elementos adicionais e apenas uma worklet de pintura. Além disso, você tem algo muito mais fácil de personalizar e parametrizar.

Outra vantagem do uso de worklets de pintura é que, na maioria dos casos, uma solução que usa worklets de pintura é pequena em termos de bytes. É claro que há um trade-off: 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á causar jank. O Chrome está trabalhando para mover 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, a perspectiva mais interessante é que o worklet de pintura permite um polyfill eficiente de recursos CSS que um navegador ainda não tem. Um exemplo seria usar o polifill de gradientes cônicos até que eles sejam lançados no Chrome de forma nativa. Outro exemplo: em uma reunião do CSS, foi decidido que agora é possível ter várias cores de borda. Enquanto essa reunião ainda estava acontecendo, meu colega Ian Kilpatrick criou um polyfill para esse novo comportamento do CSS usando o 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 com que os elementos do DOM tenham formas arbitrárias. Por exemplo, um diamante:

Um elemento DOM em forma de losango.
Um elemento DOM em forma de losango.

mask-image usa uma imagem do tamanho do elemento. As áreas em que a imagem da máscara é transparente, o elemento é transparente. Áreas em que a imagem da máscara é opaca, 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. Teste as novas possibilidades que o worklet de pintura oferece e mostre o que você criou. Para mais inspiração, confira a coleção de Vincent De Oliveira.