Search code examples
javascripthtml5-canvascomplex-numbers

Javascript canvas color not smooth change in brightness


Full demo of what I'm talking about is here: https://ggodfrey.github.io/ComplexColor/index.html

I'm trying to make a website that can attempt to plot a complex-valued function using color. The hue is determined using the angle of the complex answer, and the brightness is determined by taking the log of the magnitude of the complex answer and then finding the fractional part. From there, I use a function to convert HSL to RGB, then putting that into an Image object that I draw onto the canvas, allowing me to draw on each pixel.

As seen on the page above, the brightness "levels" have "rough" edges between where the logarithm changes from one integer to another. It should look something more like this. Is this issue having to do with how I actually calculate the brightness or using the javascript canvas?

  window.onload = function(){

    var EQUATION = '';
    var canvas = document.getElementById("canvas");
    var ctx = canvas.getContext('2d');
    var x_min = -3;
    var x_max = 3;
    var y_min = -3;
    var y_max = 3;
    var image = ctx.createImageData(canvas.width, canvas.height);

    Complex = function(re, im){
      this.re = re;
      this.im = im;
    }

    Complex.prototype.add = function(other){
      return new Complex(this.re+other.re, this.im+other.im);
    }

    Complex.prototype.multiply = function(other){
      return new Complex(this.re*other.re-other.im*this.im, this.re*other.im+this.im*other.re);
    }

    Complex.prototype.power = function(num){
      var r = this.magnitude();
      var theta = this.angle();
      var a = Math.pow(r, num)*Math.cos(num*theta);
      var b = Math.pow(r, num)*Math.sin(num*theta);
      return new Complex(a, b);
    }

    Complex.prototype.magnitude = function(){
      return Math.pow(Math.pow(this.re, 2) + Math.pow(this.im, 2), 0.5);
    }

    Complex.prototype.angle = function(){
      return Math.atan2(this.im, this.re);
    }

    Complex.prototype.divide = function(other){
      x = new Complex(this.re, this.im);
      y = new Complex(other.re, other.im);
      x = x.multiply(new Complex(other.re, -other.im));
      y = y.multiply(new Complex(other.re, -other.im));
      x = new Complex(x.re/y.re, x.im/y.re);
      return x;
    }

    function hslToRgb(h, s, l){ //NOT MY CODE
      var r, g, b;

      if(s == 0){
        r = g = b = l; // achromatic
      } else {
        function hue2rgb(p, q, t){
          if(t < 0) t += 1;
          if(t > 1) t -= 1;
          if(t < 1/6) return p + (q - p) * 6 * t;
          if(t < 1/2) return q;
          if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
          return p;
        }
        var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        var p = 2 * l - q;
        r = hue2rgb(p, q, h + 1/3.0);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1/3.0);
      }
      return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
    }

    function evaluate(i, j){
      var z = new Complex(x_min+j*(x_max-x_min)/canvas.width, y_min+i*(y_max-y_min)/canvas.height);
      var num = z.power(2).add(new Complex(-1, 0)).multiply(z.add(new Complex(-2, -1)).power(2));
      var den = z.power(2).add(new Complex(2, 4));
      var end = num.divide(den);
      var color = end.angle()/(2*Math.PI);
      var brightness = end.magnitude();
      brightness = Math.log(brightness)/Math.log(2) % 1;
      return [color, brightness];
    }

    function main(){
      var data = image.data;
      if(EQUATION !== null){
        var count = 0;
        for(var i=0;i<canvas.height;i++){
          for(var j=0;j<canvas.width;j++){

            var c = evaluate(i, j);
            rgb = hslToRgb(c[0], 1, 0.4+c[1]/5);
            var r = rgb[0];
            var g = rgb[1];
            var b = rgb[2];
            var c = count*4;
            data[c] = r;
            data[c+1] = g;
            data[c+2] = b;
            data[c+3] = 255;
            count++;
          }
        }
        image.data = data;
        ctx.putImageData(image, 0, 0);
    }
  }

  main();

  function getMousePos(canvas, evt){
    var rect = canvas.getBoundingClientRect();
    return {x: (evt.clientX-rect.left)/(rect.right-rect.left)*canvas.width,
      y: (evt.clientY-rect.top)/(rect.bottom-rect.top)*canvas.height};
  }

  document.getElementById("submit").addEventListener("mousedown", function(event){
    EQUATION = document.getElementById("equation").innerHTML;
    var x = main();
  })

  document.getElementById("canvas").addEventListener("mousemove", function(event){
    var loc = getMousePos(canvas, event);
    document.getElementById('x').innerHTML = Math.round(loc.x*100)/100;
    document.getElementById('y').innerHTML = Math.round(loc.y*100)/100;
    document.getElementById('brightness').innerHTML = evaluate(loc.y, loc.x)[1];
  })

}
<head>
  <title>Complex Color</title>
  <meta charset="utf-8"></head>
<body>
  <input id="equation" type="text">Type Equation</input><button id="submit">Submit</button><br>
  <canvas id="canvas" style="width:500px;height:500px"></canvas><p> <span id="x"></span>, <span id="y"></span>, <span id="brightness"></span></p>
</body>


Solution

  • Assuming the formulas are correct:

    • Increase the bitmap resolution of the canvas and use a smaller CSS size to introduce smoothing - or - implement a manual anti-aliasing. This is because you write on a pixel by pixel basis which bypasses anti-aliasing.

    • Decrease saturation to about 80%: rgb = hslToRgb(c[0], 0.8, 0.4 + c[1] / 5);. 100% will typically produce an over-saturated looking image on screen. For print though use 100%.

    var EQUATION = '';
    var canvas = document.getElementById("canvas");
    var ctx = canvas.getContext('2d');
    var x_min = -3;
    var x_max = 3;
    var y_min = -3;
    var y_max = 3;
    var image = ctx.createImageData(canvas.width, canvas.height);
    
    Complex = function(re, im) {
      this.re = re;
      this.im = im;
    }
    
    Complex.prototype.add = function(other) {
      return new Complex(this.re + other.re, this.im + other.im);
    }
    
    Complex.prototype.multiply = function(other) {
      return new Complex(this.re * other.re - other.im * this.im, this.re * other.im + this.im * other.re);
    }
    
    Complex.prototype.power = function(num) {
      var r = this.magnitude();
      var theta = this.angle();
      var a = Math.pow(r, num) * Math.cos(num * theta);
      var b = Math.pow(r, num) * Math.sin(num * theta);
      return new Complex(a, b);
    }
    
    Complex.prototype.magnitude = function() {
      return Math.pow(Math.pow(this.re, 2) + Math.pow(this.im, 2), 0.5);
    }
    
    Complex.prototype.angle = function() {
      return Math.atan2(this.im, this.re);
    }
    
    Complex.prototype.divide = function(other) {
      x = new Complex(this.re, this.im);
      y = new Complex(other.re, other.im);
      x = x.multiply(new Complex(other.re, -other.im));
      y = y.multiply(new Complex(other.re, -other.im));
      x = new Complex(x.re / y.re, x.im / y.re);
      return x;
    }
    
    function hslToRgb(h, s, l) { //NOT MY CODE
      var r, g, b;
    
      if (s == 0) {
        r = g = b = l; // achromatic
      } else {
        function hue2rgb(p, q, t) {
          if (t < 0) t += 1;
          if (t > 1) t -= 1;
          if (t < 1 / 6) return p + (q - p) * 6 * t;
          if (t < 1 / 2) return q;
          if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
          return p;
        }
        var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
        var p = 2 * l - q;
        r = hue2rgb(p, q, h + 1 / 3.0);
        g = hue2rgb(p, q, h);
        b = hue2rgb(p, q, h - 1 / 3.0);
      }
      return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
    }
    
    function evaluate(i, j) {
      var z = new Complex(x_min + j * (x_max - x_min) / canvas.width, y_min + i * (y_max - y_min) / canvas.height);
      var num = z.power(2).add(new Complex(-1, 0)).multiply(z.add(new Complex(-2, -1)).power(2));
      var den = z.power(2).add(new Complex(2, 4));
      var end = num.divide(den);
      var color = end.angle() / (2 * Math.PI);
      var brightness = end.magnitude();
      brightness = Math.log(brightness) / Math.log(2) % 1;
      return [color, brightness];
    }
    
    function main() {
      var data = image.data;
      if (EQUATION !== null) {
        var count = 0;
        for (var i = 0; i < canvas.height; i++) {
          for (var j = 0; j < canvas.width; j++) {
    
            var c = evaluate(i, j);
            rgb = hslToRgb(c[0], 0.8, 0.4 + c[1] / 5);
            var r = rgb[0];
            var g = rgb[1];
            var b = rgb[2];
            var c = count * 4;
            data[c] = r;
            data[c + 1] = g;
            data[c + 2] = b;
            data[c + 3] = 255;
            count++;
          }
        }
        image.data = data;
        ctx.putImageData(image, 0, 0);
      }
    }
    
    main();
    #canvas {width:500px;height:500px}
    <canvas id="canvas" width=1000 height=1000></canvas>