Function

@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, so uv and pos address every pixel.
fill
The color of the cell: a CSS color, a number for gray, or a vec3 / vec4. The last fill that runs wins.
shape, size
Mask the cell. shape is square, circle, diamond, none, or a distance in du and dv. size is 0 to 1; a size with no shape masks with a square.
match
match(cond) { … } runs where the condition holds. Several heads on one block are one test. Follow with else match(…) or else.
repeat
repeat(n as k) { … } runs the body n times; k is the zero-based index. Extra arguments stop the loop when they all hold. fill, shape, size and grid are 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 dxdb, 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((min(3n, 1) + vec3(0, .1, .2)));
);

See also