Reinterpreting Gerhard Richter's "4900 Colors" with p5.js and Processing
Gerhard Richter is a titan of modern art. In this post, I explore the logic behind his "Color Chart" series and attempt to recreate its essence using p5.js and Processing.
"4900 Colors" by Gerhard Richter
Richter’s process involves arranging color chips—randomly selected from a fixed palette—into a matrix. But does this simple randomization actually yield interesting results?
ハルゲルト・リタヒー 32x32#processing #creativecoding pic.twitter.com/XqlX8EjKQI
— deconbatch (@deconbatch) December 4, 2022
I wrote a basic script to test this, and honestly, the initial results were underwhelming.
I realized that the secret lies in the palette: "How does one select the perfect 25 colors?" Without a curated palette, a random grid often feels meaningless.
How can I make a beautiful color palette? It's so difficult to answer this question.
/**
* Color chips with 25 random colors.
* ref. Gerhard Richter '4900 Farben'
*
* @author @deconbatch
* @version 0.1
* @license CC0
* p5.js 1.5.0
* created 2022.12.08
*/
const margin = 20;
const chipNum = 32;
const chipSiz = 20;
const chipGap = 0;
const colorNum = 25;
const canvasW = chipSiz * chipNum + margin * 2;
function setup() {
createCanvas(canvasW, canvasW);
colorMode(HSB, 360, 100, 100, 100);
// set random color palette
const palette = new Array(colorNum);
for (let i = 0; i < colorNum; i++) {
palette[i] = color(random(360), random(40, 60), random(60, 80));
}
// draw
background(0, 0, 90, 100);
translate(margin, margin);
stroke(0, 0, 90, 100);
strokeWeight(chipGap);
for (let chipY = 0; chipY < chipNum; chipY++) {
for (let chipX = 0; chipX < chipNum; chipX++) {
fill(palette[floor(random(colorNum))]);
rect(
chipX * chipSiz,
chipY * chipSiz,
chipSiz,
chipSiz
);
}
}
}
Introducing Structural Rules
When observing Richter's original work, the eye naturally seeks patterns, tracing similar hues and tonal structures even in a random sequence. This led me to a new idea: What if I inject a predefined structure into the randomness?
My approach uses a recursive-like hierarchy: a "large structure" defined by a pattern, which is then populated by "smaller structures" following that same pattern.
I made a color pattern with the randomly selected 25 colors, like the artwork of Gerhard Richter.
I placed the smaller version of the same pattern here, but if it were placed normally, the meaning of the larger structure would be lost.
So, I placed the semi-transparent smaller patterns.
Implementation: Recursive Grids without Recursion
While the structure is conceptually recursive, I implemented it using a more intuitive nested loop.
To add depth, I applied a BLUR filter to the larger background tiles, allowing the sharper foreground chips to pop while maintaining the overall color structure.
/**
* Color patterns with 25 random colors.
* ref. Gerhard Richter '4900 Farben'
*
* @author @deconbatch
* @version 0.1
* @license GPL3
* p5.js 1.5.0
* created 2022.12.12
*/
const margin = 20;
const chipNum = 5;
const chipSiz = 24;
const chipGap = 2;
const colorNum = 25;
const palette = new Array(colorNum);
const pattern = new Array(chipNum * chipNum);
const canvasW = chipSiz * chipNum * chipNum + margin * 2;
function setup() {
createCanvas(canvasW, canvasW);
colorMode(HSB, 360, 100, 100, 100);
smooth();
noLoop();
// palette
for (let i = 0; i < colorNum; i++) {
let h = random(360);
let s = random(40, 60);
let b = random(60, 80);
palette[i] = color(h, s, b, 100);
}
// make pattern
for (let y = 0; y < chipNum; y++) {
for (let x = 0; x < chipNum; x++) {
pattern[x + y * chipNum] = palette[floor(random(colorNum))];
}
}
// draw
background(0, 0, 90, 100);
translate(margin, margin);
// large pattern
noStroke();
for (let y = 0; y < chipNum; y++) {
for (let x = 0; x < chipNum; x++) {
fill(pattern[x + y * chipNum]); // no transparent
rect(
x * chipSiz * chipNum,
y * chipSiz * chipNum,
chipSiz * chipNum,
chipSiz * chipNum
);
}
}
filter(BLUR, 6);
// small patterns
strokeWeight(chipGap);
stroke(0, 0, 90, 100);
for (let largeY = 0; largeY < chipNum; largeY++) {
for (let largeX = 0; largeX < chipNum; largeX++) {
for (let y = 0; y < chipNum; y++) {
for (let x = 0; x < chipNum; x++) {
pattern[x + y * chipNum].setAlpha(50); // transparent
fill(pattern[x + y * chipNum]);
rect(
(largeX * chipNum + x) * chipSiz,
(largeY * chipNum + y) * chipSiz,
chipSiz,
chipSiz
);
}
}
}
}
}
/**
* Color patterns with 25 random colors.
* ref. Gerhard Richter '4900 Farben'
*
* @author @deconbatch
* @version 0.1
* @license GPL3
* Processing 3.5.3
* created 2022.12.12
*/
public void setup() {
size(760, 760);
colorMode(HSB, 360.0, 100.0, 100.0, 100.0);
smooth();
noLoop();
int margin = 20;
int chipNum = 6;
int chipGap = 2;
int chipSiz = floor((width - margin * 2) / chipNum / chipNum);
// palette
int colorNum = 25;
color palette[] = new color[colorNum];
for (int i = 0; i < colorNum; i++) {
float h = random(360.0);
float s = random(40.0, 60.0);
float b = random(60.0, 80.0);
palette[i] = color(h, s, b);
}
// make pattern
color pattern[][] = new color[chipNum][chipNum];
for (int y = 0; y < chipNum; y++) {
for (int x = 0; x < chipNum; x++) {
pattern[x][y] = palette[floor(random(colorNum))];
}
}
// draw
translate(margin, margin);
background(0.0, 0.0, 90.0, 100.0);
// large pattern
noStroke();
for (int y = 0; y < chipNum; y++) {
for (int x = 0; x < chipNum; x++) {
fill(pattern[x][y], 100.0); // no transparent
rect(
x * chipSiz * chipNum,
y * chipSiz * chipNum,
chipSiz * chipNum,
chipSiz * chipNum
);
}
}
filter(BLUR, 6);
// small patterns
strokeWeight(chipGap);
stroke(0.0, 0.0, 90.0, 100.0);
for (int largeY = 0; largeY < chipNum; largeY++) {
for (int largeX = 0; largeX < chipNum; largeX++) {
for (int y = 0; y < chipNum; y++) {
for (int x = 0; x < chipNum; x++) {
fill(pattern[x][y], 50.0); // transparent
rect(
(largeX * chipNum + x) * chipSiz,
(largeY * chipNum + y) * chipSiz,
chipSiz,
chipSiz
);
}
}
}
}
}
Conclusion
Recreating famous masterpieces is more than an exercise; it's a source of inspiration. Deconstructing a master's logic through code often sparks unexpected creative breakthroughs.
Richter once said that randomness is what makes a painting 'objective.' By adding my own rules to his logic, am I creating more objectivity, or less?






