Home

Pixel Patterns

14 December 2021

A while ago I saw an interesting pattern generated by a pure mathematical rule, which inspired me to implement a function to recreate the pattern with minimal syntax.

The beauty lies in its rule. Simple yet the results are appealing.

I've then found several other patterns but soon the limitation of grid size 32x32 bothered me. Creating so many DOM elements to represent cells in the grid is also a bit slow.

@grid: 21 / 9em;
@match(tan.cos.sin(x*y) > 1) {
  background: #000;
}

Later one night, I came across another pattern from the book TAOCP Vol.4. It uses a similar rule in a 256x256 grid. I really wanted to draw this out myself, so I decided to look for other implementations.

pattern

Pattern function

I tried Canvas2D but using shader is much faster for large size grids. The only thing to deal with is to map each pixel to a defined grid cell.

background: @shaders(
  void main() {
    vec2 uv = gl_FragCoord.xy/u_resolution.xy;
    vec3 color = vec3(1.0);
    vec2 grid = vec2(256);
    vec2 p = 1.0/grid;
    float x = ceil(uv.x/p.x);
    float y = ceil(grid.y - uv.y/p.y);
    if (((int(y*y*x)>>11)&1) == 1) {
      color = vec3(0.0);
    }
    FragColor = vec4(color, 1.0);
  }
);

In order to do explorations more quickly I added another function that uses shader programs as the bottom layer. Now the above code can be simplified as:

background: @pattern(
  grid: 256;
  match(((int(y*y*x)>>11)&1) == 1) {
    fill: #000;
  }
);

Color

In shader programs, colors are in RGBA format. How to transform CSS color from all other formats into RGBA?

/* How to recongize the color "purple" ? */
fill: purple;

/* or HSL() ? */
fill: hsl(70, 80%, 85%);

A color library might help but I noticed that the built-in getComputedStyle() function will always normalize colors into RGBA format. Life is much easier this way.

function rgba(el, color) {
  el.style.color = color;
  let [r, g, b, a = 1] = getComputedStyle(el).color
    .replace(/rgba?\((.+)\)/, (_, v) => v)
    .split(/,\s*/);
  return {r, g, b, a};
}

Usage

The grid property defines the grid dimension. There's no gap property yet.

background: @pattern(
  grid: 10;
  /* different columns and rows  */
  grid: 100x50;
);

The fill property fills the grid cell with the given color value. I didn't use color because other properties may also need colors.

background: @pattern(
  grid: 11;
  fill: #000;
);

The third keyword is the match selector. It accepts an expression that will be passed to the underneath shader program.

background: @pattern(
  grid: 10;
  match((int(x+y)%2) == 0) {
    fill: #000;
  }
);

Examples

background: @pattern(
  grid: 63;
  fill: #143d59;
  match(((int(x*x*y*y)>>5)%2) == 1) {
    fill: #f4b51c;
  }
);
background: @pattern(
  grid: 71;
  fill: #143d59;
  match(((int(x*y*x*y*7.)>>4)&2) == 2) {
    fill: #f4b51c;
  }
);

Combining with trigonometric functions.

background: @pattern(
  grid: 43;
  fill: #f4b51c;
  match(((int(sin(y)*sin(x)*6.)>>2)&1) == 1) {
    fill: #143d59;
  }
);
background: @pattern(
  grid: 43;
  fill: #f4b51c;
  match(((int(cos(x)*cos(y)*5.)>>2)&1) == 1) {
    fill: #143d59;
  }
);

One more.

background: @pattern(
  grid: 80;
  fill: #f4b51c;
  match((int(x*x + y*y)%80) > 17) {
    fill: #143d59;
  }
);

I'm not satisfied with the syntax inside the match selector, as you can see the value types in the shader program are being strictly defined. Anyway this is a good start.

I'm happy and excited when the idea comes true.

CodePen: https://codepen.io/yuanchuan/pen/eYGgEaK