Search code examples
colorsp5.jsmandelbrot

Issue with Mandelbrot smooth coloring


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.


Solution

  • There were two deviations from the linked algorithm that were causing issues:

    1. You were setting the escape radius to a low value (~2) in some cases
    2. You were dividing by the 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>