CSS Paint is an API that allows developers to programmatically generate and draw graphics where CSS expects graphics.

It is part of THE CSS Houdini, an umbrella name for seven new low-level apis that expose different parts of the CSS engine and allow developers to extend CSS by hooking up with the browser rendering engine’s styling and layout processes.

It enables developers to write code that the browser can parse into CSS to create new CSS features without having to wait for their native implementation in the browser.

Today we’ll explore two specific apis that are part of the CSS Houdini umbrella.

  1. CSS Paint, as of this writing, is fully implemented in Chrome, Opera, and Edge, and is available in Firefox and Safari with a polyfill.
  2. CSS properties and Values API, which will allow us to explicitly define our CSS variables, their initial values, what types of values they support, and whether these variables can be inherited.

CSS drawing gives us the ability to render graphics using CSS apis. [PaintWorklet] (https://developer.mozilla.org/en-US/docs/Web/API/PaintWorklet), a simplified version. [CanvasRenderingContext2D](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D). The main difference is.

  • Text rendering is not supported
  • No direct pixel access/operations

With these two omissions in mind, whenever you can use Canvas2D, you can use the CSS Paint API to draw on ordinary DOM elements. For those of you who have done any graphics with Canvas2D, you should be at home.

In addition, as developers, we have the ability to pass CSS variables as input to our PaintWorklet and control its performance with custom predefined properties.

This allows for a high degree of customization, even for designers who are not necessarily familiar with Javascript.

You can see more examples here and here. With that said, let’s start coding!

Simplest example: two diagonals

Let’s create a CSS paintlet that, once loaded, will draw two diagonal lines on the surface of the DOM element to which we apply it. The size of the surface painted by the Painttlet will match the width and height of the DOM element, and we will be able to control the thickness of the diagonal by passing a CSS variable.

Create our PaintWorklet

To load a PaintWorklet, we will need to create it as a separate Javascript file (gut-lines.js).

Const PAINTLET_NAME = 'gut-lines' class CSSPaintlet {// 👉 Define the names of the input CSS variables we will support static get inputProperties() { return [ `--${PAINTLET_NAME}-line-width`, ]} // 👉 Define names for input CSS arguments supported in paint() // ⚠ This part of the API is still experimental and Hidden // Behind a flag. static get inputArguments () {return []} // 👉 paint() will be executed every time: // - any input property changes // - the DOM element we apply our paintlet to changes its dimensions paint(ctx, paintSize, {// 👉 Obtain the numeric value of our line width that is passed // as a CSS variable const lineWidth = Number(' --${PAINTLET_NAME}-line-width ')) ctx.lineWidth = lineWidth // 🎨 Draw line #1 ctx.beginPath()  ctx.moveTo(0, 0) ctx.lineTo(paintSize.width, Draw diagonal line #2 ctx.beginPath() ctx.moveto (0, 0) paintsize.height) ctx.stroke() // 🎨 Draw line #2 ctx.beginPath() ctx.moveto (0, 0) paintSize.height) ctx.lineTo(paintSize.width, 0) ctx.stroke()}} // 👉 Register our CSS Paintlet with the correct name // so we can reference it from our CSS registerPaint(PAINTLET_NAME, CSSPaintlet)Copy the code

We define our CSS Paintlet as a separate class. This class only needs one method to work -paint(), which will draw graphics on the surface of the CSS paintlet we specify. It will be executed when any CSS variables that our painttlet depends on change or the size of our DOM element changes.

Another static method, inputProperties(), is optional. It tells CSS Paintlet exactly which input CSS variables it supports. In our case, this is going to be — gut-lines-line-width. We declare it as an input property and use it in our paint() method. It is important that we put it in Number to ensure cross-browser support.

There’s also an optional static method support: inputArguments. It exposes the parameters to our paint() method like this.

#myImage {
  background-image: paint(myWorklet, 30px, red, 10deg);
}
Copy the code

However, this part of the CSS Paintlet API is still hidden behind a logo and is considered experimental. For ease of use and compatibility, we won’t cover it in this article, but I encourage you to read it for yourself. Instead, we’ll use CSS variables, using the inputProperties() method to control all input to our Painttlet.

Sign up for our CSS PaintWorklet

After that, we have to reference our CSS Paintlet and register it on our home page. It is important that we should conditionally loaded awesome [CSS – paint – polyfill] (https://github.com/GoogleChromeLabs/css-paint-polyfill), This will ensure that our painttlets work in Firefox and Safari.

It should be noted that along with our CSS paintlet, we can use the new CSS properties and values API (also part of the Houdini umbrella) to explicitly define our CSS variable inputs via css.registerProperty (). We can control our CSS variables like this.

  • Their types and syntax
  • Whether the CSS variable inherits from any parent element
  • If the user does not specify it, what is its initial value?

The API is also not supported in Firefox and Safari, but we can still use it in the Chromium browser. This way our demo is future-oriented, and browsers that don’t support it will simply ignore it.

; (async function() {// ⚠ Handle Firefox and Safari by importing a polyfill for CSS Pain if (CSS['paintWorklet'] === Undefined) {await import('https://unpkg.com/css-paint-polyfill')} // 👉 Explicitly define our custom CSS variable This is not supported in Safari and Firefox, so they will // ignore it, but we can optionally use it in browsers that // support it. // This way we will future-proof our applications so once Safari // and Firefox support it, they will benefit from these // definitions too. // // Make sure that the browser treats it as a number // It does not inherit it's value // It's initial value defaults to 1 if ('registerProperty' in CSS) { CSS.registerProperty({ name: '--diagonal-lines-line-width', syntax: '<number>', inherits: false, initialValue: 1})} // 👉 Include our separate paintlet file CSS.paintWorklet.addModule('path/to/our/external/worklet/diagonal-files.js') })()Copy the code

Refer to our painttlet as the CSS background

Once we have our painttlet as a JS file, it is very simple to use it. We select the target DOM element we want to style in CSS and apply our painttlet with the paint() CSS command.

#myElement {// 👉 Reference our CSS paintlet background-image: paint('-- gut-lines '); // 👉 Pass in custom CSS variable to be used in our CSS paintlet -- gut-lines-line-width: 10; // 👉 Remember - the browser treats this as a regular image // referenced in CSS. We can control it's repeat, size, position // and any other background related property available background-repeat: no-repeat; background-size: cover; background-position: 50% 50%; // Some more styles to make sure we can see our element on the page border: 1px solid red; width: 200px; height: 200px; margin: 0 auto; }Copy the code

With this code completed, we have the following result.

See Georgi Nikoloff’s (@gbnikolov) example of the PenCSS Worklet introduction on CodePen.

Remember, we can use this CSS gadget as a background for DOM elements of any size. Let’s enlarge our DOM element to full screen, lower its background-size X and y values, and set its background-repeat to repeat. So that’s the example of our update.

See Georgi Nikoloff (@gbnikolov) at CodePen for an example of the PenCSS Worklet introduction.

We used the CSS Paintlet from the previous example, but now we have extended it to the entire demo page.

So, now that we’ve covered our basic example and seen how to organize our code, let’s write some more elegant demonstrations

Particle connection

See Georgi Nikoloff (@gbnikolov) PenCSS Worklet Particles on CodePen.

This painttlet was inspired by a great demonstration by @NucliWeb.

Again, this will be very simple for those of you who have used the Canvas2D API to draw graphs in the past.

We control how many points we want to render with the CSS variable ‘-dots-connections-count’. Once we have its value in our painttlet, we create an array of the appropriate size and fill it with objects with random X, Y, and RADIUS attributes.

Then we loop through each item in the array, draw a sphere on its coordinates, find its nearest neighbor (the minimum distance is controlled by the ‘-dots-connections-connection-min-dist’ CSS variable), and connect them with a line.

We will also control the fill color of the sphere and stroke color of the line with the ‘-dots-connections-fill-color’ and –dots-connections-stroke-color CSS variables, respectively.

Below is the complete working code.

Const PAINTLET_NAME = 'dot-connections' class CSSPaintlet {// 👉 Define names for input CSS variables we will support static get inputProperties() { return [ `--${PAINTLET_NAME}-line-width`, `--${PAINTLET_NAME}-stroke-color`, `--${PAINTLET_NAME}-fill-color`, `--${PAINTLET_NAME}-connection-min-dist`, `--${PAINTLET_NAME}-count`, ]} // 👉 Our paint method to be executed when CSS vars change paint(CTX, paintSize, props, args) { const lineWidth = Number(props.get(`--${PAINTLET_NAME}-line-width`)) const minDist = Number(props.get(`--${PAINTLET_NAME}-connection-min-dist`)) const strokeColor = props.get(`--${PAINTLET_NAME}-stroke-color`) const fillColor = props.get(`--${PAINTLET_NAME}-fill-color`) const NumParticles = Number(' --${PAINTLET_NAME}-count ') // 👉 Generate particles at random positions // across our DOM element surface const particles = new Array(numParticles).fill(null).map(_ => ({ x: Math.random() * paintSize.width, y: Math.random() * paintSize.height, radius: 2 + Math.random() * 2, })) // 👉 Assign lineWidth coming from CSS variables and make sure // lineCap and lineWidth are round ctx.lineWidth = LineWidth ctx.lineCap = 'round' // 👉 Loop over the particles with nested -o (n^2) for (let  i = 0; i < numParticles; I ++) {const particle = particles[I] // 👉 Loop time for (let n = 0; n < numParticles; N ++) {if (I === n) {continue} const nextParticle = Particles [n] // 👉 Calculate distance between the current particle  // and the particle from the previous loop iteration const dx = nextParticle.x - particle.x const dy = nextParticle.y - SQRT (dx * dx + dy * dy) 👉 If the dist is smaller then the minDist specified via //  variable, then we will connect them with a line if (dist < minDist) { ctx.strokeStyle = strokeColor ctx.beginPath() ctx.moveTo(nextParticle.x, nextParticle.y) ctx.lineTo(particle.x, Y) // 👉 Draw the connecting line ctx.stroke()}} // Finally Draw the particle at the right position ctx.fillStyle = fillColor ctx.beginPath() ctx.arc(particle.x, particle.y, particle.radius, 0, Math.pi * 2) ctx.closepath () ctx.fill()}}} // 👉 Register our CSS paintlet with a unique name // so we can reference it from our CSS registerPaint(PAINTLET_NAME, CSSPaintlet)Copy the code

Line cycle

Here’s our next example. It expects the following CSS variables as input to our painttlet.

--loop-line-width

--loop-stroke-color

--loop-sides

--loop-scale

--loop-rotation

We loop around a full circle (PI * 2) and position it along the circumference according to the CSS variable –loop-sides. For each location, we loop around our full circle again and connect it to all the other locations with the ctx.lineto () command.

See the PenCSS Worklet Line Loop written by Georgi Nikoloff (@gbnikolov) on CodePen.

Const PAINTLET_NAME = 'loop' class CSSPaintlet {// 👉 Define names for input CSS variables we will support static get inputProperties() { return [ `--${PAINTLET_NAME}-line-width`, `--${PAINTLET_NAME}-stroke-color`, `--${PAINTLET_NAME}-sides`, `--${PAINTLET_NAME}-scale`, `--${PAINTLET_NAME}-rotation`, ]} // 👉 Our paint method to be executed when CSS vars change paint(CTX, paintSize, props, args) { const lineWidth = Number(props.get(`--${PAINTLET_NAME}-line-width`)) const strokeColor = props.get(`--${PAINTLET_NAME}-stroke-color`) const numSides = Number(props.get(`--${PAINTLET_NAME}-sides`)) const scale = Number(props.get(`--${PAINTLET_NAME}-scale`)) const rotation = Number(props.get(`--${PAINTLET_NAME}-rotation`)) const angle = Math.PI * 2 / numSides const radius = paintSize.width / 2 ctx.save() ctx.lineWidth = lineWidth ctx.lineJoin = 'round' ctx.lineCap = 'round' ctx.strokeStyle = strokeColor ctx.translate(paintSize.width / 2, paintSize.height / 2) ctx.rotate(rotation * (Math.PI / 180)) ctx.scale(scale / 100, scale / 100) ctx.moveTo(0, Radius) // 👉 Loop over the numsides twice in nested loop-o (n^2) // Connect each corner with all other corners for (let i = 0; i < numSides; i++) { const x = Math.sin(i * angle) * radius const y = Math.cos(i * angle) * radius for (let n = i; n < numSides; n++) { const x2 = Math.sin(n * angle) * radius const y2 = Math.cos(n * angle) * radius ctx.lineTo(x, y) ctx.lineTo(x2, y2); ClosePath () ctx.stroke() ctx.restore()}} // 👉 Register our CSS paintlet with a unique name // so we can reference it from our CSS registerPaint(PAINTLET_NAME, CSSPaintlet)Copy the code

Noise button

See Georgi Nikoloff’s (@gbnikolov) PenCSS Worklet Noise Button on CodePen.

So here’s our next example. It was inspired by another great CSS Paintlet by Jhey Tompkins. It expects the following CSS variables as input to our painttlet.

--grid-size

--grid-color

--grid-noise-scale

The Paintlet itself uses Perlin noise (code provided by Joeiddon) to control the opacity of each cell.

Const PAINTLET_NAME = 'grid' class CSSPaintlet {// 👉 Define names for input CSS variables we will support static get inputProperties() { return [ `--${PAINTLET_NAME}-size`, `--${PAINTLET_NAME}-color`, '--${PAINTLET_NAME}-noise-scale']} // 👉 Our paint method to be executed when CSS vars change paint(CTX, paintSize, props, args) { const gridSize = Number(props.get(`--${PAINTLET_NAME}-size`)) const color = props.get(`--${PAINTLET_NAME}-color`) const noiseScale = Number(props.get(`--${PAINTLET_NAME}-noise-scale`)) ctx.fillStyle = color for (let x = 0; x < paintSize.width; x += gridSize) { for (let y = 0; y < paintSize.height; Y += gridSize) {// 👉 Use perlin noise to determine the cell opacity ctx.globalAlpha = mapRange(perlin.get(x *) NoiseScale, y * noiseScale), -1, 1, 0.5, 1) ctx.fillrect (x, y, gridSize, GridSize)}}}} // 👉 Register our CSS paintlet with a unique name // so we can reference it from our CSS registerPaint(PAINTLET_NAME, CSSPaintlet)Copy the code

Curved dividing lines

As a final example, let’s do something that might be more useful. We will draw delimiters programmatically to separate the text content of our page.

See the PenCSS Worklet Curvy Dividers written by Georgi Nikoloff (@gbnikolov) on CodePen.

As usual, here is the code for CSS Paintlet.

Const PAINTLET_NAME = 'curvy-Dividor' class CSSPaintlet {// 👉 Define names for input CSS variables we will support static get inputProperties() { return [ `--${PAINTLET_NAME}-points-count`, `--${PAINTLET_NAME}-line-width`, '--${PAINTLET_NAME}-stroke-color']} // 👉 Our paint method to be executed when CSS vars change paint(CTX, paintSize, props, args) { const pointsCount = Number(props.get(`--${PAINTLET_NAME}-points-count`)) const lineWidth = Number(props.get(`--${PAINTLET_NAME}-line-width`)) const strokeColor = props.get(`--${PAINTLET_NAME}-stroke-color`) const stepX = paintSize.width / pointsCount ctx.lineWidth = lineWidth ctx.lineJoin = 'round' ctx.lineCap = 'round' ctx.strokeStyle = strokeColor const offsetUpBound = -paintSize.height / 2 const offsetDownBound = paintSize.height / 2 // 👉 Draw quadratic Bezier curves across the horizontal axies // of our Dividers: ctx.moveTo(-stepX / 2, paintSize.height / 2) for (let i = 0; i < pointsCount; i++) { const x = (i + 1) * stepX - stepX / 2 const y = paintSize.height / 2 + (i % 2 === 0 ? offsetDownBound : offsetUpBound) const nextx = (i + 2) * stepX - stepX / 2 const nexty = paintSize.height / 2 + (i % 2 === 0 ? offsetUpBound : offsetDownBound) const ctrlx = (x + nextx) / 2 const ctrly = (y + nexty) / 2 ctx.quadraticCurveTo(x, y, ctrlx, Ctrly)} ctx.stroke()}} // 👉 Register our CSS paintlet with a unique name // so we can reference it from our CSS registerPaint(PAINTLET_NAME, CSSPaintlet)Copy the code

conclusion

In this article, we introduced all the key components and methods of the CSS Paint API. It’s easy to set up and useful if we want to draw more advanced graphics that CSS doesn’t support.

We can easily create a library with these CSS painting tools and reuse them over and over in our projects with minimal setup.

As a matter of good practice, I encourage you to find cool Canvas2D demos and port them to the new CSS Paint API.

The post Drawing Graphicswith the CSS Paint APIappeared first onCodrops.