@pattern
A small language for drawing on a grid of cells, compiled to a GPU shader.
Every cell runs the same rules, and each picks its own color with fill.
Syntax
- @pattern(<body>)
- @pattern<width>x<height>(<body>)
The body is a list of name: value; statements, run in source order.
Digits after the name set the image size in pixels (@pattern800 is square).
Without them the image takes the size of the element and redraws when it resizes.
Put it in @content to draw a live <canvas>.
It also works as a background, but then every frame is encoded into a new image,
which is much slower when the pattern animates.
Grid and fill
grid sets the columns, or n x m. Each cell knows its column x and row y,
both counted from 1 at the top-left. fill gives the cell its color:
@grid: 1 / 60vmin;
@content: @pattern(
grid: 64;
fill: #fff;
match((x - 1) & (y - 1)) {
fill: #000;
}
);
A fill is a CSS color, a number for gray, a vec3 / vec4, or a list of 3 or 4 channels.
The last fill that runs wins, and a pixel with no fill is transparent.
Choosing cells with match
The rules inside match(condition) { … } only run in the cells where the condition holds:
@grid: 1 / 60vmin;
@content: @pattern(
grid: 119;
match(x*x*y*y % 120 < 24) {
fill: #000
}
); match(a), match(b) { … } runs where either holds.
For an else chain, list the tests in one block; the first test that holds wins:
@grid: 1 / 60vmin;
@content: @pattern(
grid: 32;
match {
x = y or x + y = 33 { fill: #fff }
dc % 3 < 1 { fill: #000 }
else { fill: #fff }
}
);
Inside a value, match(test, a, b) picks a or b.
There is no ?:.
@grid: 1 / 60vmin;
@content: @pattern(
grid: 32;
fill: match(y > 16 + 6sin(x / 3 + t), #000, #fff);
); Shape and size
shape masks each cell: square, circle, diamond, none,
or a distance written with du and dv, the offsets inside the cell.
size scales the mask from 0 to 1; on its own it masks with a square.
With t, the time in seconds, the pattern animates:
@grid: 1 / 60vmin;
@content: @pattern800(
grid: 32;
size: sin(4*log(dr) - 2t - da);
fill: #000;
); Pixels and variables
Without a grid the whole image is one cell, so uv and pos address each pixel.
Any name that is not a keyword is a variable:
@grid: 1 / 60vmin;
@content: @pattern(
n: fbm(uv.x*6, uv.y*6 - t*4);
h: clamp(2n * pow(1 - uv.y, 1.2), 0, 1);
fill: vec3(1.9h, 1.5h*h, pow(h, 2)*.8)
); ramp(t, stop, …) maps a value onto stops written as in a CSS gradient, colors or numbers,
each with an optional position. A repeated position is a hard edge:
@grid: 1 / 60vmin;
@content: @pattern(
fill: ramp(length(pos), #f99a53 .28, #006b50 .28, #00055f .5)
); $name inserts the text of the cell's custom property --name when the pattern is made,
so each cell can draw something different. A name that is not defined skips the pattern:
@grid: 3 / 60vmin _2px;
--k: @i;
background: @pattern(
shape: circle;
size: $k / 9;
fill: #000;
); Loops with repeat
repeat(n as k) { … } runs its body n times, with k counting from 0.
Extra arguments stop the loop early when they all hold. Here it iterates a Julia set:
@grid: 1 / 60vmin;
@content: @pattern(
z: 2.8pos;
c: vec2(-.8, .156);
n: 0;
repeat(96, dot(z, z) > 256) {
z: vec2(z.x * z.x - z.y * z.y, 2 * z.x * z.y) + c;
n: n + 1;
}
fill: match(n = 96, #000, hsl(n / 96, .75, .45));
); n is at most 1024, and nested loops 65536 steps in all.
fill, shape, size and grid are not allowed inside a loop;
collect a value in a variable and fill after it.
escape(c) is the same loop starting from 0, so the Mandelbrot set is one call:
@grid: 1 / 60vmin;
@content: @pattern(
n: escape(3pos - vec2(.7, 0));
fill: -cos(2π(min(3n, 1) + vec3(0, .1, .2)));
); Drawing with distances
box and segment give the distance to a shape, and shape(d, w) turns
a distance into ink. shape(abs(d), w) is a stroke of width w along the edge:
@grid: 1 / 60vmin;
@content: @pattern(
a: 0;
repeat(12 as k) {
d: box(rot(pos, k * .12sin(t)), .04 + .036k);
a: max(a, shape(abs(d), .012));
}
fill: mix(#fff, #000, a);
); Lines with segment. Point k on a circle joins point k × m, and m grows over time:
@grid: 1 / 60vmin;
@content: @pattern(
m: 2 + .2t;
a: 0;
repeat(90 as k) {
p: .45 * vec2(cos(k * π / 45), sin(k * π / 45));
q: .45 * vec2(cos(k * m * π / 45), sin(k * m * π / 45));
a: max(a, shape(segment(pos, p, q), .008));
}
fill: mix(#fff, #000, a);
); Doodle textures
A section named texture… holds doodle code. It is rendered to an image,
and texture(texture0, uv) reads it. Here fract() tiles it 4 × 4:
@grid: 1 / 60vmin;
@content: @pattern(
texture0 {
@shape: heart;
background: #000;
}
fill: texture(texture0, fract(4uv));
); Reference
Variables
| Name | Value |
|---|---|
x, y, i | column, row and index of the cell, from 1, row 1 at the top |
X, Y, I | number of columns, rows and cells |
dx … db | the cell's offset and distance from the center, as in @dx |
du, dv | offset inside the cell, −0.5 to 0.5 |
uv | position in the image, 0 to 1 from the bottom-left corner |
pos | centered position, the shorter side from −0.5 to 0.5 |
t | time in seconds; using it animates the pattern |
Operators
Expressions are GLSL, with a few shortcuts.
A number next to a name multiplies (2t, 2π), and ** is a power.
a < b < c tests both sides.
and, or, not, =, ≤, ≥ and ≠ may stand for
&&, ||, !, ==, <=, >= and !=.
Hex colors and color names can be used as values.
Functions
GLSL functions work, plus these:
-
ramp(t, stop, …)t mapped onto gradient stops, colors or numbers -
hsl(h, s, l)color, channels 0 to 1 -
hsv(h, s, v)color, channels 0 to 1 -
noise(a, b)value noise, 0 to 1 -
fbm(a, b)six octaves of noise -
voronoi(a, b)distance to the nearest point -
rand(n)seeded random, 0 to 1 -
rot(p, a)rotate p by a radians -
box(p, b)signed distance to a box of half-size b -
segment(p, a, b)distance to the segment from a to b -
shape(d, size)1 inside the distance size / 2, 0 outside -
smin(a, b, k)smooth minimum of a and b -
ngon(p, n)distance with an n-gon as the unit -
spiral(dx, dy)index on a square spiral from the center -
dither(x, y)4×4 Bayer threshold, 0 to 1 -
match(test, value, …)the value after the first true test -
escape(c)0 where z → z² + c stays bounded, otherwise how soon it leaves (96 steps)