@pattern
A small language compiled to a GPU shader, as a background image or as a live
<canvas> with @content.
Each cell of a virtual grid gets a fill chosen by match rules.
Syntax
- @pattern(<body>), @pattern<width>x<height>(<body>)
- grid
- Columns, or
n x m. Omit it and the pattern is one cell, souvandposaddress every pixel. - fill
- The color of the cell: a CSS color, a number for gray, or a
vec3/vec4. The lastfillthat runs wins. - shape, size
-
Mask the cell.
shapeissquare,circle,diamond,none, or a distance induanddv.sizeis 0 to 1; asizewith noshapemasks with a square. - match
-
match(cond) { … }runs where the condition holds. Several heads on one block are one test. Follow withelse match(…)orelse. - repeat
-
repeat(n as k) { … }runs the bodyntimes;kis the zero-based index. Extra arguments stop the loop when they all hold.fill,shape,sizeandgridare not allowed inside. - width, height
- Digits glued to the name set the pixel size of the image. One number is square.
Any other name is a variable. Statements run in source order.
Expressions are GLSL. Each cell sees x, y, i,
the distances from dx … db,
the in-cell offsets du, dv, the pixel positions uv and pos,
and time t, which animates the pattern.
A number next to a name multiplies (2t).
and, or and not may stand for &&, || and !.
GLSL functions work, plus these:
-
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 -
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)
Examples
@grid: 1 / 60vmin;
background: @pattern(
grid: 119;
match(x*x*y*y % 120 < 24) {
fill: #000
}
); @grid: 1 / 60vmin;
background: @pattern(
grid: 64;
fill: match((x - 1) & (y - 1), #000, #fff);
); Animated cells. A size with no shape masks with a square:
@grid: 1 / 60vmin;
@content: @pattern800(
grid: 32;
size: sin(4*log(dr) - 2t - da);
fill: #000;
);
Without a grid, it targets every pixel.
@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)
); repeat iterates a Julia set; extra arguments stop the loop:
@grid: 1 / 60vmin;
background: @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));
); escape(c) starts from 0, so the Mandelbrot set is the same loop in one call:
@grid: 1 / 60vmin;
background: @pattern(
n: escape(3pos - vec2(.7, 0));
fill: -cos(2π(min(3n, 1) + vec3(0, .1, .2)));
);