Pattern making with CSS Paint
Exposing most of the Canvas API save for text and image rendering, Paint Worklets allow for putting together a wide range of interesting graphics. Coupled potentially with Web Components for isolating and CSS custom properties for editing or animating parameters, they make it easy to create portable pattern generators.
Pick a unit trace of some sort to get started. In the simplest case that might be a line or a curve. In the example above the unit is a rectangle. It's dimensions are linked to the HTML element the paint()
is applied to later on. If the element is a square, the trace will be square. CSS variables are used for choosing fill, stroke style and line width,
// Sample worklet.js class QuadPainterMaybe { static get inputProperties() { return [ '--x-fill-style', '--x-line-width', '--x-stroke-style' ] } // Conventiently automatically called when geometry changes paint(context, geometry, properties) { // Use the longest side for the diameter const { width: w, height: h } = geometry const radius = Math.max(w, h) / 2 // Ninety degrees const Q = Math.PI * 0.5 // For converting degrees to radians const D = Math.PI / 180 // Set options for (const [k,v] of properties) { // Convert from e.g. '--x-fill-style' to 'fill-style' for configuring context const s = k.replace('--x-', '') context[s] = v.toString() } // Move origin to geometry center, rotate and start drawing context.translate(w / 2, h / 2) context.beginPath() // Lay out vertices, anticlockwise from the top right quadrant Array.from({ length: 2 * 2 }) .map((_, i) => { // Get polar position for each vertex const t = ((i % 2 ? Q : 0) + (i * Q)) * D // Pol2car const x = radius * Math.cos(t) const y = radius * Math.sin(t) context.lineTo(x, y) }) context.closePath() context.stroke() context.fill() } } registerPaint('quad', QuadPainterMaybe)
Once installed, the worklet is available for calling via CSS on properties that accept an image: background
, border
etc. To achieve tiling it might be useful to set up a custom element to automatically create a range of empty <div>
tags to then apply some paint()
to,
/* style.css */ div[is="grid-maybe"] { /* Do a 4x4 grid out of the 16 cells in the shadow DOM below */ display: grid; grid-template-columns: repeat(4, 1fr); }
// script.js class GridMaybe extends HTMLDivElement { constructor() { super() this.attachShadow({ mode: 'open' }) } connectedCallback() { if (this.isConnected) { // Create a `data-size` total of empty `<div>` cells Array.from({ length: this.dataset.size }) .map(() => document.createElement('div')) .forEach((x) => { this.shadowRoot.appendChild(x) }) } } } customElements.define('grid-maybe', GridMaybe, { extends: 'div' })
Alright, the worklet takes care of drawing the master cell, the custom element lays out the grid and to recurse, translate, rotate, color, or scale simply use CSS,
/* style.css */ @supports (background: paint(_)) { div { --x-fill-style: transparent; --x-line-width: 1px; --x-stroke-style: black; /* Draw five rectangles of succesively smaller height on each shadow DOM <div> cell */ background-image: paint(quad), paint(quad), paint(quad), paint(quad), paint(quad); background-position: center; background-repeat: no-repeat; background-size: 100%, 80%, 60%, 40%, 20%; } div:nth-of-type(2n + 1) { /* Give odd cells gaps */ background-size: 100%, 0%, 50%, 0%, 25%; } div:nth-of-type(7) { /* Do something exceptionally fancy */ transform: translate(-1px, -1px) rotate(1deg); }
And to install in a way that keeps the styling scoped to decendants of the host element in order to support multiple grid instances on a given page,
<!-- client.html --> <div is="simple-grid" size="16"> <!-- insert shadow DOM scoped styles --> </div>
// script.js customElements.whenDefined('simple-grid') .then(() => { if ('paintWorklet' in CSS) { CSS.paintWorklet.addModule('worklet.js') .then(() => { const owner = document.querySelector('div[is="simple-grid"]') const style = document.createElement('style') style.textContent = `@import 'style.css';` // Keep styles scoped owner.shadowRoot.appendChild(style) }) .catch(console.log) } })
Theoretically, the above should work on Safari when the CSS Paint option is enabled in the Develop menu. It sort of does when the script is read in as a string. The supposed property Map
-type argument in the paint()
method is also buggy on Safari. Only Chrome and Opera seem fairly caught up with most of the spec. Module home is @
thewhodidthis/cell ›