Make the Web Brighter with the

CSS Paint API

Vitalii Bobrov

Β  Β  Β  Β @bobrov1989

https://vitaliy-bobrov.github.io/

  • 5 years of front-end
  • build enterprise web apps
  • CSS experimenter
  • mentor
  • open-source contributor

Agenda

  • What is the Houdini Project?
  • CSS Paint API
  • Examples

Houdini

🌟

CSS Paint API

Houdini

😲 🧐

πŸ€“ πŸ‘½

We want to use the feature after browser implementation....

We will implement the feature when more developers will use it...

😲 🧐

πŸ€“ πŸ‘½

CSS Engine Internals

Parser

CCSOM

Cascade

Layout

Paint

Composite

JS Polyfills

JavaScript Polyfilling

  • Too Big
  • Too Slow
  • Too Incorrect

Compile Polyfills

CSS Paint API

πŸ–ŒοΈ

Used for ANY Property that expects an Image

  • background-image
  • border-image
  • list-style-image
  • custom <image> property

Usage

some-element {
  background-image: paint(my-custom-paint);
}

CSS Fallback

some-element {
  background-image: url('./assets/fallback.jpg');
  background-image: paint(my-custom-paint);
}

PostCSS Fallback

some-element {
  background-image: paint(my-custom-paint);
}
some-element {
  background-image: url('./assets/fallback.jpg');
  background-image: paint(my-custom-paint);
}

Processed on build

const postcss = require('postcss');

module.exports = postcss.plugin('postcss-fallback-my-paint', (options) => {
    return css => {
        css.walkRules(rule => {
            rule.walkDecls(decl => {
                const value = decl.value;

                if (value.includes('my-custom-paint')) {
                    decl.cloneBefore({value: options.fallbackValue});
                }
            });
        });
    };
});

PostCSS Plugin

How to create Custom Paint?

  1. Declare a custom paint class
  2. Register paint
  3. Load worklet

1. Declare a custom paint class

class MyCustomPainter {
    paint(ctx, geom, props, args) {
        /// paint implementation.
    }
}

2. Register paint

registerPaint('my-custom-paint', MyCustomPainter);

3. Load worklet

if ('paintWorklet' in CSS) {
    CSS.paintWorklet.addModule('custom-paint.js');
}

Examples

🎨

Don't Try this at Home

β˜’οΈβ€‹

CSS Paint Hello World

class CirclesPainter {
  paint(ctx, geom) {
    const offset = 10;
    const size = Math.min(geom.width, geom.height);
    const radius = (size / 4) - offset;
    const point = radius + offset;

    ...
  }
}

registerPaint('circles', CirclesPainter);

https://vitaliy-bobrov.github.io/css-paint-demos/hello-world/

CSS Paint Hello World

paint(ctx, geom) {
  ...

  for (let i = 0; i < 2; i++) {
    for (let j = 0; j < 2; j++) {
      ctx.fillStyle = `rgb(0, 
        ${Math.floor(255 - 42.5 * i)}, 
        ${Math.floor(255 - 42.5 * j)})`;

      ctx.beginPath();
      ctx.arc(
        point + (i * (point * 2)), 
        point + (j * (point * 2)), radius, 0, 2 * Math.PI);
      ctx.fill();
    }
  }
}

https://vitaliy-bobrov.github.io/css-paint-demos/hello-world/

CSS Paint Hello World

.circles {
  background: #000;
  background: paint(circles);
}

https://vitaliy-bobrov.github.io/css-paint-demos/hello-world/

CSS Paint + variables

static get inputProperties() {
  return [
    '--circles-offset',
    '--circles-count',
    '--circles-opacity'
  ];
}

paint(ctx, geom, props) {
  const offset = parseInt(props.get('--circles-offset'), 10) || 0;
  const count = parseInt(props.get('--circles-count'), 10) || 2;
  const opacity = parseFloat(props.get('--circles-opacity')) || 1;

  ...
}

https://vitaliy-bobrov.github.io/css-paint-demos/circles-with-params/

CSS Paint + variables

.circles {
  --circles-count: 2;
  --circles-offset: 10;
  --circles-opacity: 1;
  background: #000;
  background: paint(circles);
}

https://vitaliy-bobrov.github.io/css-paint-demos/circles-with-params/

Experiment

Think OUT of the BOX

🀯

CSS Paint Chart

paint(ctx, geom, props) {
  const outerR = parseInt(props.get('--chart-outer-radius'), 10) || 100;
  const innerR = parseInt(props.get('--chart-inner-radius'), 10) || 0;
  const value = parseFloat(props.get('--chart-value')) || 0;
  const background = props.get('--chart-background') || 'red';
  const foreground = props.get('--chart-foreground') || 'blue';
  
  ...
}

https://vitaliy-bobrov.github.io/css-paint-demos/circle-chart/

CSS Paint Chart

paint(ctx, geom, props) {
  ...

  ctx.fillStyle = background;

  ctx.beginPath();
  ctx.arc(x, y, outerRadius, 0, Math.PI * 2, false);
  ctx.arc(x, y, innerRadius, 0, Math.PI * 2, true);
  ctx.fill();

  ctx.fillStyle = foreground;

  const sRadian = Math.PI * 1.5;
  const eRadian = Math.PI * 2 * (0.75 + value / 100);
  ctx.beginPath();
  ctx.arc(x, y, outerRadius, sRadian, eRadian, false);
  ctx.arc(x, y, innerRadius, eRadian, sRadian, true);
  ctx.fill();
}

https://vitaliy-bobrov.github.io/css-paint-demos/circle-chart/

CSS Paint Chart

.chart {
  --chart-inner-radius: 0;
  --chart-outer-radius: 100;
  --chart-value: 10;
  --chart-background: #795548;
  --chart-foreground: #FF5722;
  background: #999 paint(circle-chart);
}

https://vitaliy-bobrov.github.io/css-paint-demos/circle-chart/

https://vitaliy-bobrov.github.io/css-paint-demos/qr-code/

CSS Arguments

.gradient {
  background: linear-gradient(to top, white, red);
}

chrome://flags/

Chart + arguments

static get inputArguments() {
    return ['<color>', '<color>'];
}

paint(ctx, geom, props, args) {
    ...    

    const [background, foreground] = args;

    ctx.fillStyle = background.toString();

    ...

    ctx.fillStyle = foreground.toString();

    ...
}

https://vitaliy-bobrov.github.io/css-paint-demos/circle-chart-args/

Chart + arguments

.chart {
  --chart-inner-radius: 0;
  --chart-outer-radius: 100;
  --chart-value: 10;
  background: paint(circle-chart, #795548, #FF5722);
}

https://vitaliy-bobrov.github.io/css-paint-demos/circle-chart-args/

Animations

https://lab.iamvdo.me/houdini/ripple

requestAnimationFrame(function raf(now) {
    const count = Math.floor(now - start);
    button.style.cssText = `
        --ripple-x: ${x}; 
        --ripple-y: ${y}; 
        --animation-tick: ${count};
    `;

    if (count > 1000) {
      button.classList.remove('animating');
      button.style.cssText = `--animation-tick: 0`;
      return;
    }

    requestAnimationFrame(raf);
});

RAF? What?!

πŸ€”

.my-elem {
    --my-color: #fff;
    background: var(--my-color);
}

String interpolation

.my-elem {
    --my-color: #fff;
    background: --my-color;
    transition: --my-color .3s;
}

.my-elem:hover {
    --my-color: red;
}

Custom Properties

CSS.registerProperty({
  name: '--my-color',
  syntax: '<color>',
  inherits: false,
  initialValue: 'white'
});

https://lab.iamvdo.me/houdini/animating-gradient

Resources

  • https://developers.google.com/web/updates/2018/01/paintapi
  • https://drafts.css-houdini.org/css-paint-api/​
  • https://www.w3.org/TR/css-paint-api-1/​
  • https://github.com/w3c/css-houdini-drafts​
  • https://lab.iamvdo.me/houdini/
  • https://vitaliy-bobrov.github.io/css-paint-demos/​

Thank YOU!

Β  Β  Β  Β @bobrov1989

https://vitaliy-bobrov.github.io/