I implemented the regular algorithm to display the Mandelbrot set and colour it, and now I'm working on the smooth colouring features using a 255 colourmap.
This part is already well documented online (and even on this forum) and I choose the most popular way with the renormalization of the escape:
function setup() {
createCanvas(500, 500);
background(0);
pixelDensity(1);
noLoop();
}
function draw() {
// Mandelbrot
const A = [
[-2, 1],
[-1.5, 1.5]
];
mandelbrot(A, 18, colormap);
}
/* ################## */
/* ### Mandelbrot ### */
/* ################## */
function mandelbrot(A, K, colormap) {
loadPixels();
// Loop through all area point
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
// Get point coordinates
const c = new Complex(
map(x, 0, width, A[0][0], A[0][1]),
map(y, 0, height, A[1][0], A[1][1])
);
let R = max(c.abs(), 2);
let z = new Complex(0, 0);
let ic = color(0);
for (let i = 0; i < K; i++) {
// Next sequence term
z.pow2();
z.add(c);
if (z.abs() > R) {
// Anti-aliasing (smooth) color index from the map
let mu = i - log(log(z.abs())) / log(R);
let mapIndex = int(colormap.length * mu / K);
// Retieve color from map
ic = colormap[mapIndex];
break;
}
}
// Color pixel
const index = (x + y * width) * 4;
pixels[index] = red(ic);
pixels[index + 1] = green(ic);
pixels[index + 2] = blue(ic);
pixels[index + 3] = 255;
}
}
updatePixels();
}
/* ############### */
/* ### Complex ### */
/* ############### */
class Complex {
constructor(r, im) {
this.r = r;
this.im = im;
}
add(c) {
this.r += c.r;
this.im += c.im;
}
pow2() {
let r = this.r;
let im = this.im;
this.r = r ** 2 - im ** 2;
this.im = 2 * r * im;
}
abs() {
return sqrt(this.r ** 2 + this.im ** 2)
}
}
/* ################ */
/* ### ColorMap ### */
/* ################ */
const colormap = [
[0, 0, 0],
[96, 148, 140],
[92, 140, 136],
[92, 132, 128],
[88, 124, 124],
[88, 116, 116],
[84, 112, 112],
[84, 104, 104],
[80, 96, 100],
[80, 88, 92],
[76, 80, 88],
[76, 72, 80],
[72, 64, 76],
[68, 56, 68],
[88, 60, 80],
[112, 64, 96],
[136, 68, 108],
[160, 72, 124],
[184, 76, 136],
[208, 80, 152],
[232, 88, 168],
[216, 104, 176],
[196, 124, 188],
[180, 140, 196],
[160, 160, 208],
[140, 176, 220],
[124, 196, 228],
[104, 212, 240],
[84, 232, 252],
[88, 232, 248],
[96, 232, 240],
[104, 232, 232],
[112, 232, 224],
[120, 232, 216],
[128, 232, 208],
[136, 232, 200],
[140, 232, 192],
[148, 232, 184],
[156, 232, 176],
[164, 232, 168],
[172, 232, 160],
[180, 232, 152],
[188, 232, 144],
[176, 232, 152],
[160, 232, 160],
[144, 232, 168],
[128, 232, 180],
[124, 228, 176],
[120, 220, 168],
[112, 212, 160],
[108, 204, 152],
[104, 196, 144],
[92, 168, 124],
[76, 140, 104],
[60, 112, 84],
[48, 84, 64],
[32, 56, 44],
[16, 28, 24],
[0, 0, 0],
[0, 0, 0],
[4, 4, 4],
[4, 4, 4],
[8, 8, 8],
[8, 8, 8],
[12, 12, 12],
[12, 12, 12],
[16, 16, 16],
[16, 16, 16],
[20, 20, 20],
[20, 20, 20],
[24, 24, 24],
[24, 24, 24],
[28, 28, 28],
[28, 28, 28],
[32, 32, 32],
[32, 32, 32],
[36, 36, 36],
[36, 36, 36],
[40, 40, 40],
[40, 40, 40],
[44, 44, 44],
[44, 44, 44],
[48, 48, 48],
[48, 48, 48],
[52, 52, 52],
[52, 52, 52],
[56, 56, 56],
[56, 56, 56],
[60, 60, 60],
[60, 60, 60],
[64, 64, 64],
[64, 64, 64],
[68, 68, 68],
[68, 68, 68],
[72, 72, 72],
[72, 72, 72],
[76, 76, 76],
[76, 76, 76],
[80, 80, 80],
[84, 84, 84],
[84, 84, 84],
[88, 88, 88],
[88, 88, 88],
[92, 92, 92],
[92, 92, 92],
[96, 96, 96],
[96, 96, 96],
[100, 100, 100],
[100, 100, 100],
[104, 104, 104],
[104, 104, 104],
[108, 108, 108],
[108, 108, 108],
[112, 112, 112],
[112, 112, 112],
[116, 116, 116],
[116, 116, 116],
[120, 120, 120],
[120, 120, 120],
[124, 124, 124],
[124, 124, 124],
[128, 128, 128],
[128, 128, 128],
[132, 132, 132],
[132, 132, 132],
[136, 136, 136],
[136, 136, 136],
[140, 140, 140],
[140, 140, 140],
[144, 144, 144],
[144, 144, 144],
[148, 148, 148],
[148, 148, 148],
[152, 152, 152],
[152, 152, 152],
[156, 156, 156],
[156, 156, 156],
[160, 160, 160],
[160, 160, 160],
[164, 164, 164],
[168, 168, 168],
[168, 168, 168],
[172, 172, 172],
[172, 172, 172],
[176, 176, 176],
[176, 176, 176],
[180, 180, 180],
[180, 180, 180],
[184, 184, 184],
[184, 184, 184],
[188, 188, 188],
[188, 188, 188],
[192, 192, 192],
[192, 192, 192],
[196, 196, 196],
[196, 196, 196],
[200, 200, 200],
[200, 200, 200],
[204, 204, 204],
[204, 204, 204],
[208, 208, 208],
[208, 208, 208],
[212, 212, 212],
[212, 212, 212],
[216, 216, 216],
[216, 216, 216],
[220, 220, 220],
[220, 220, 220],
[224, 224, 224],
[224, 224, 224],
[228, 228, 228],
[228, 228, 228],
[232, 232, 232],
[232, 232, 232],
[236, 236, 236],
[236, 236, 236],
[240, 240, 240],
[240, 240, 240],
[244, 244, 244],
[244, 244, 244],
[248, 248, 248],
[252, 252, 252],
[252, 252, 228],
[252, 252, 204],
[252, 252, 180],
[252, 252, 152],
[252, 252, 128],
[252, 252, 104],
[252, 252, 80],
[252, 252, 52],
[244, 240, 56],
[236, 228, 60],
[228, 212, 64],
[220, 200, 72],
[212, 184, 76],
[204, 172, 80],
[196, 160, 88],
[188, 144, 92],
[176, 132, 96],
[168, 116, 100],
[160, 104, 108],
[152, 92, 112],
[144, 76, 116],
[136, 64, 124],
[128, 48, 128],
[120, 36, 132],
[108, 20, 140],
[196, 184, 220],
[188, 176, 212],
[180, 168, 204],
[172, 160, 192],
[160, 148, 184],
[152, 140, 172],
[144, 132, 164],
[132, 120, 152],
[124, 112, 144],
[116, 104, 136],
[104, 92, 124],
[96, 84, 116],
[88, 76, 104],
[76, 64, 96],
[68, 56, 84],
[60, 48, 76],
[48, 36, 64],
[52, 36, 72],
[60, 32, 84],
[68, 32, 92],
[76, 28, 104],
[80, 28, 112],
[88, 24, 124],
[96, 20, 136],
[104, 20, 144],
[108, 16, 156],
[116, 16, 164],
[124, 12, 176],
[132, 8, 188],
[136, 8, 168],
[140, 8, 144],
[144, 8, 120],
[148, 8, 96],
[152, 8, 72],
[156, 8, 48],
[160, 4, 24],
[156, 16, 36],
[152, 32, 48],
[144, 48, 60],
[140, 60, 72],
[136, 76, 84],
[128, 92, 96],
[124, 108, 108],
[120, 120, 120],
[112, 136, 132],
[108, 152, 144],
[100, 168, 156],
[100, 164, 152],
[96, 156, 148]
];
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
Despite this formula, I can still see the bands/gap of colours in the visualization and can't figure out where my issue come from.
P.S: I removed the "+ 1" of the log formula the avoid out-of-bound when looking for the colour in the colourmap.
There were two deviations from the linked algorithm that were causing issues:
log(R)
which was not correct. You should divide by log(2)
Additionally some of the banding was because you were still using a discrete color map. You could use a continuous mapping of value -> hue, but if you want a particular cycle of colors, you can use lerpColor()
to smoothly transition from one color and the next based on the difference between the continuous/smooth value and the current index.
function setup() {
createCanvas(500, 500);
background(0);
pixelDensity(1);
noLoop();
}
function draw() {
// Mandelbrot
const A = [
[-2, 1],
[-1.5, 1.5]
];
mandelbrot(A, 18, colormap);
}
/* ################## */
/* ### Mandelbrot ### */
/* ################## */
function mandelbrot(A, K, colormap) {
loadPixels();
// Allowing the escape radius to be too small (i.e. 2) leads to bad results
const R = 4;
// Loop through all area point
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
// Get point coordinates
const c = new Complex(
map(x, 0, width, A[0][0], A[0][1]),
map(y, 0, height, A[1][0], A[1][1])
);
// let R = max(c.abs(), 2);
let z = new Complex(0, 0);
let ic = color(0);
for (let i = 0; i < K; i++) {
// Next sequence term
z.pow2();
z.add(c);
if (z.abs() > R) {
// Anti-aliasing (smooth) color index from the map
// / log(R) is incorrect
let mu = i - log(log(z.abs())) / log(2);
let mapIndex = floor((colormap.length - 1) * mu / K);
let interval = (colormap.length - 1) * mu / K - mapIndex;
if (mapIndex < colormap.length - 1) {
// lerp between the current color and the next color by interval
ic = lerpColor(color(...colormap[mapIndex]), color(...colormap[mapIndex + 1]), interval);
} else {
// Retieve color from map
ic = colormap[mapIndex];
}
break;
}
}
// Color pixel
const index = (x + y * width) * 4;
pixels[index] = red(ic);
pixels[index + 1] = green(ic);
pixels[index + 2] = blue(ic);
pixels[index + 3] = 255;
}
}
updatePixels();
}
/* ############### */
/* ### Complex ### */
/* ############### */
class Complex {
constructor(r, im) {
this.r = r;
this.im = im;
}
add(c) {
this.r += c.r;
this.im += c.im;
}
pow2() {
let r = this.r;
let im = this.im;
this.r = r ** 2 - im ** 2;
this.im = 2 * r * im;
}
abs() {
return sqrt(this.r ** 2 + this.im ** 2)
}
}
/* ################ */
/* ### ColorMap ### */
/* ################ */
const colormap = [
[96, 148, 140],
[68, 56, 68],
[232, 88, 168],
[84, 232, 252],
[0, 0, 0],
[252, 252, 252],
[252, 252, 52],
[196, 184, 220],
[48, 36, 64],
[160, 4, 24],
[96, 156, 148]
];
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>