Implementing a zoom box

Building on information presented in Simplifying zoom box implementation, we discuss here a possible Mandelbrot zoom box implementation.

The following is an instance of the example, Mandelbrot 3, presented in this topic:

This image was generated by zooming into a particular region of the Mandelbrot set. Be aware that the Reset button is functional but the other two are not - the Lighten and Save buttons will be implemented in later examples.

Here's the code for this example:

Mandelbrot 3

<!DOCTYPE html>
<html>

<head>
  <meta http-equiv="X-UA-Compatible" content="IE=10" />
  <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
  <title>Mandelbrot 3</title>
  <style>
    html, body {
      margin: 0;
      padding: 0;
      text-align: center;
    }
    
    canvas {
      border: 2px solid black;
    }    
    
    table {
      margin: 0 auto; /* Center the table. */
    }
    
    #messageBox {
      text-align: left;
    }
        
    #elapsedTime {
      width: 23em;
      text-align: right;
    }    

    button {
      width: 5em;
    }
    
    #filenameForm {
      visibility: hidden; /* As opposed to "display: none", keep room for this hidden element in the layout. */
    }
}
  </style>      
</head>

<body>
  <h1>Mandelbrot 3</h1>
  <p>This example demonstrates a Mandelbrot zoom box implementation.</p>    
  <table>
    <tr>
      <td id="messageBox"></td>
      <td id="elapsedTime"></td>
    </tr>
  </table>
  <canvas width="600" height="400"> 
    Canvas not supported - upgrade your browser (after checking that your browser is in the correct mode).
  </canvas><br>
  <button type="button" id="resetButton">Reset</button>  
  <button type="button" id="lightenButton">Lighten</button>    
  <button type="button" id="saveButton">Save</button>
  <form id="filenameForm"> 
    Extensionless filename: <input id="filename" type="text"> <input type="submit" value="Submit">
  </form>  
  <script>
    var RE_MAX = 1.1; // This value will be adjusted as necessary to ensure that the rendered Mandelbrot set is never skewed (that is, true to it's actual shape).
    var RE_MIN = -2.5;
    var IM_MAX = 1.2;
    var IM_MIN = -1.2;
    var MAX_ITERATIONS = 1200; // Increase this value to improve detection of complex c values that belong to the Mandelbrot set.
    var STATIC_ZOOM_BOX_FACTOR = 0.25; // Increase to make the double-click and hold gesture zoom box bigger.
    var DEFAULT_MESSAGE = "Click or click-and-drag to zoom."  
    
    var globals = {}; // See the handleLoad function.
         
    window.addEventListener('load', handleLoad, false);
                    
    /************************************************************************************************************************************************************/
    
    Number.prototype.format = function() {
    /* 
      Formats this integer so that it has commas in the expected places.
    */
        var numberString = Math.round(this).toString(); // An integer value is assumed, so we ensure that it is indeed an integer.
        var precompiledRegularExpression = /(\d+)(\d{3})/;
        
        while ( precompiledRegularExpression.test(numberString) ) {
            numberString = numberString.replace(precompiledRegularExpression, '$1' + ',' + '$2'); // For this integer, inject ","'s at the appropriate locations.
        } // while
        
        return numberString;
    } // Number.prototype.format

    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/    

    function handleLoad() {      
      var canvas = document.getElementsByTagName('canvas')[0];
      var canvasWidth = canvas.width;
      var canvasHeight = canvas.height;      
      var ctx = canvas.getContext('2d');
      
      document.getElementsByTagName('table')[0].width = canvasWidth; // Make the table's width the same as the canvas's width.  
      document.getElementById('messageBox').innerHTML = DEFAULT_MESSAGE;
      
      globals.canvas = canvas;
      globals.canvas.context = ctx;
      globals.canvas.context.imageDataObject = ctx.createImageData(canvasWidth, canvasHeight); // Create an appropriately sized but empty canvas image data object.
            
      globals.staticZoomBoxWidth = STATIC_ZOOM_BOX_FACTOR * canvasWidth; // Maintains the original canvas width/height ratio.
      globals.staticZoomBoxHeight = STATIC_ZOOM_BOX_FACTOR * canvasHeight; // Maintains the original canvas width/height ratio.      
      
      globals.pointer = {};
      globals.pointer.down = false;        
             
      canvas.addEventListener('mousedown', handlePointer, false);
      canvas.addEventListener('mousemove', handlePointer, false);
      canvas.addEventListener('mouseup', handlePointer, false);    
          
      document.getElementById('resetButton').addEventListener('click', handleResetButton, false);
      document.getElementById('lightenButton').addEventListener('click', handleLightenButton, false);    
      document.getElementById('saveButton').addEventListener('click', handleSaveButton, false);        
      document.getElementById('filenameForm').addEventListener('submit', handleFormSubmit, false);    
    
      ctx.fillStyle = "rgba(255, 0, 0, 0.3)"; // The color and opacity of the zoom box. This is what gets saved when calling ctx.save().      

      handleResetButton();
    } // handleLoad
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/    
    
    function adjusted_RE_MAX() {    
      var ReMax = globals.canvas.width * ( (IM_MAX - IM_MIN) / globals.canvas.height ) + RE_MIN;
      
      if (RE_MAX != ReMax) {
        alert("RE_MAX has been adjusted to: " + ReMax); // The user should never see this if RE_MAX is set correctly above.
      } // if

      return ReMax;
    } // adjusted_RE_MAX    
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/    
    
    function drawMandelbrot() {     
      var startTime = new Date(); // Report how long it takes to render this particular region of the Mandelbrot set.             
      var canvas = globals.canvas;
      var canvasWidth = canvas.width;
      var canvasHeight = canvas.height;
      var ctx = canvas.context;
      var imageDataObjectData = ctx.imageDataObject.data; // imageDataObjectData is a reference to, not a copy of, ctx.imageDataObject.data
      
      var ReMax = globals.ReMax; // These 4 lines general require that setExtrema be called prior to calling drawMandelbrot.
      var ReMin = globals.ReMin;
      var ImMax = globals.ImMax;
      var ImMin = globals.ImMin;
      
      var x_coefficient = (ReMax - ReMin) / canvasWidth; // Keep the below loops as computation-free as possible.
      var y_coefficient = (ImMin - ImMax) / canvasHeight; // Keep the below loops as computation-free as possible.

      var iterationSum = 0;
      var currentPixel = 0;          
      for (var y = 0; y < canvasHeight; y++) {
        var c_Im = (y * y_coefficient) + ImMax; // Note that c = c_Re + c_Im*i
        
        for (var x = 0; x < canvasWidth; x++) {
          var c_Re = (x * x_coefficient) + ReMin // Convert the canvas x-coordinate to a complex plane Re-coordinate. c_Re represents the real part of a c value.
          
          var z_Re = 0; // The first z value (Zo) must be 0.
          var z_Im = 0; // The first z value (Zo) must be 0. Note that z = z_Re + z_Im*i
          
          var c_belongsToMandelbrotSet = true; // Assume that the c associated with Zn belongs to the Mandelbrot set (i.e., Zn remains bounded under iteration of Zn+1 = (Zn)^2 + c).
          for (var iterationCount = 1; iterationCount <= MAX_ITERATIONS; iterationCount++) {
            iterationSum++; // Keep track of how many iterations were performed in total so we can report this to the user.
          
            var z_Re_squared = z_Re * z_Re; // A small speed optimization.
            var z_Im_squared = z_Im * z_Im; // A small speed optimization.
    
            if (z_Re_squared + z_Im_squared > 4) { // Checks if |z^2| is greater than 2. This approach avoids the expensive square root operation.
              c_belongsToMandelbrotSet = false; // This complex c value is not part of the Mandelbrot set (because it will always tend towards infinity under iteration).
              break; // Immediately check the next c value to see if it belongs to the Mandelbrot set or not.
            } // if
            
            // The next two lines perform Zn+1 = (Zn)^2 + c (recall that (x + yi)^2 = x^2 - y^2 + 2xyi, thus the real part is x^2 - y^2 and the imaginary part is 2xyi).
            z_Im = (2 * z_Re * z_Im) + c_Im; // We must calculate the next value of z_Im first because it depends on the current value of z_Re (not the next value of z_Re).
            z_Re = z_Re_squared - z_Im_squared + c_Re; // Calculate the next value of z_Re.
          } // for   
          
          if (c_belongsToMandelbrotSet) { // This complex c value is probably part of the Mandelbrot set because Zn did not tend toward infinity within MAX_ITERATIONS iterations.
            imageDataObjectData[currentPixel++] = 0; // Red. Note that there are 255 possible shades of red, green, blue, and alpha (i.e., opacity).
            imageDataObjectData[currentPixel++] = 0; // Green.
            imageDataObjectData[currentPixel++] = 0; // Blue.
            imageDataObjectData[currentPixel++] = 255; // Alpha (i.e., 0% transparency).
          } 
          else { // This complex c valus is definitely not part of the Mandelbrot set because Zn would tend toward infinity under iteration (i.e., |Zn| > 2).
            imageDataObjectData[currentPixel++] = 255; 
            imageDataObjectData[currentPixel++] = 255; 
            imageDataObjectData[currentPixel++] = 255; 
            imageDataObjectData[currentPixel++] = 255;
          } // if-else
        } // for
      } // for        
      
      ctx.putImageData(ctx.imageDataObject, 0, 0); // Render our carefully constructed canvas image data array to the canvas.
          
      var elapsedMilliseconds = (new Date()) - startTime;
      document.getElementById('elapsedTime').innerHTML = iterationSum.format() + " iterations in " + (elapsedMilliseconds / 1000).toFixed(2) + " seconds"; // Note that the UI element is not updated until after this block terminates (which is the desired behavior).            
      document.getElementById('messageBox').innerHTML = DEFAULT_MESSAGE; // Erase the "Calculating..." message and replace it with the default message.
    } // drawMandelbrot
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/
    
    function xToRe(x) {
      var x_coefficient = (globals.ReMax - globals.ReMin) / globals.canvas.width; 
      
      return (x * x_coefficient) + globals.ReMin; // Converts a canvas x-coordinate value to the associated complex plane Re-coordinate.
    } // xToRe
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/    

    function yToIm(y) {
      var y_coefficient = (globals.ImMin - globals.ImMax) / globals.canvas.height; 
      
      return (y * y_coefficient) + globals.ImMax; // Converts a canvas y-coordinate value to the associated complex plane Im-coordinate.
    } // yToIm
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/

    function handlePointer(evt) {
      var canvasWidthHeightRatio = globals.canvas.width / globals.canvas.height;
      var ctx = globals.canvas.context;
      
      var canvasX;
      var canvasY;      
      
      if (evt.offsetX && evt.offsetY) {
        canvasX = evt.offsetX; // Not supported in Firefox.
        canvasY = evt.offsetY; // Does not assume that the canvas element is a direct descendent of the body element.
      } else {
        canvasX = evt.clientX - evt.target.offsetLeft; // Supported in Firefox.
        canvasY = evt.clientY - evt.target.offsetTop; // Assumes that the canvas element is a direct descendent of the body element.
      } // if-else
      
      var zoomBoxWidth;
      var zoomBoxHeight;
      
      var ReMax;
      var ReMin;
      var ImMax;
      var ImMin;
            
      switch (evt.type) {
        case 'mousedown':
          globals.pointer.x1 = canvasX;
          globals.pointer.y1 = canvasY;
          globals.pointer.down = true;
          break;
        case 'mousemove':
          if (globals.pointer.down) {
            zoomBoxHeight = Math.abs(canvasY - globals.pointer.y1);  
            zoomBoxWidth = zoomBoxHeight * canvasWidthHeightRatio; // We must keep the zoom box dimensions proportional to the canvas dimensions in order to ensure that the resulting zoomed Mandelbrot image does not become skewed.
            ctx.putImageData(globals.canvas.context.imageDataObject, 0, 0); // Assumes that an initial image of the Mandelbrot set is drawn before we get to this point in the code. The purpose of this line is to erase the prior zoom box rectangle before drawing the next zoom box rectangle.
            ctx.fillRect(globals.pointer.x1, globals.pointer.y1, zoomBoxWidth, zoomBoxHeight); // With a freshly painted image of the current Mandelbrot set in place (see prior line), draw a new zoom box rectangle.
          } // if
          break;
        case 'mouseup':
          globals.pointer.down = false;          
          
          zoomBoxHeight = Math.abs(canvasY - globals.pointer.y1); // Only allow the zoom box to be drawn from an upper-left corner position down to a lower-right corner position.
          zoomBoxWidth = zoomBoxHeight * canvasWidthHeightRatio; // Again, ensure that the width/height ratio of the zoom box is proportional to the canvas's (this simplifies the algorithm).          

          if (zoomBoxHeight == 0) { // No zoom box has been drawn, so honor the fixed sized zoom box.
            var staticZoomBoxWidth = globals.staticZoomBoxWidth;
            var staticZoomBoxHeight = globals.staticZoomBoxHeight;
            var halfStaticZoomBoxWidth = staticZoomBoxWidth / 2;
            var halfStaticZoomBoxHeight = staticZoomBoxHeight / 2;
          
            ctx.fillRect(canvasX - halfStaticZoomBoxWidth, canvasY - halfStaticZoomBoxHeight, staticZoomBoxWidth, staticZoomBoxHeight); // Just leave this on the screen.
                         
            ReMin = xToRe(canvasX - halfStaticZoomBoxWidth); // Center the static zoom box about the point (evt.offsetX, evt.offsetY).
            ImMax = yToIm(canvasY - halfStaticZoomBoxHeight); 
            
            ReMax = xToRe(canvasX + halfStaticZoomBoxWidth);
            ImMin = yToIm(canvasY + halfStaticZoomBoxHeight);
          } 
          else { // A (possibly tiny) zoom box has been drawn, so honor it.
            ReMin = xToRe(globals.pointer.x1); // Convert the mouse's x-coordinate value (on the canvas) to the associated Re-coordinate value in the complex plane.
            ImMax = yToIm(globals.pointer.y1); // Convert the mouse's y-coordinate value (on the canvas) to the associated Im-coordinate value in the complex plane.
                                      
            ReMax = xToRe(zoomBoxWidth + globals.pointer.x1); // Convert the zoom box's final x-coordinate value to the associated Re-coordinate value in the complex plane.  
            ImMin = yToIm(zoomBoxHeight + globals.pointer.y1);  // Convert the zoom box's final y-coordinate value to the associated Re-coordinate value in the complex plane.            
          } // if-else
          
          document.getElementById('messageBox').innerHTML = "Calculating..."; // This isn't displayed until its containing code block exits, hence the need for setImmediate/setTimeout calls next. 
          document.getElementById('elapsedTime').innerHTML = ""; // Erase the prior run's statistics.     

          setExtrema(ReMax, ReMin, ImMax, ImMin); // Must set these globals prior to calling drawMandelbort because drawMandelbort accesses them.
          if (window.setImmediate) {
            window.setImmediate(drawMandelbrot); // Allow "Calculating..." to be immediately displayed on the screen.
          }
          else {
            window.setTimeout(drawMandelbrot, 0); // Allow "Calculating..." to be immediately displayed on the screen.
          }     
          break; 
        default:
          alert("Error in switch statement."); // Although unnecessary, defensive programming techniques such as this are highly recommended.
      } // switch              
    } // handlePointer    
        
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/
    
    function setExtrema(ReMax, ReMin, ImMax, ImMin) {
    /* 
      This generally must be called prior to calling drawMandelbrot in that drawMandelbrot accesses the following 4 global variables.
    */
      globals.ReMax = ReMax;
      globals.ReMin = ReMin;
      globals.ImMax = ImMax;
      globals.ImMin = ImMin;          
    } // setExtrema
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/
    
    function handleResetButton() {
      var reMax = adjusted_RE_MAX(); // If the constant RE_MAX is set correctly, the user will never see the alert that the adjusted_RE_MAX function can throw.
      
      setExtrema(reMax, RE_MIN, IM_MAX, IM_MIN);
      drawMandelbrot();          
    } // handleResetButton
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/
    
    function handleLightenButton() {
      alert("handleLightenButton fired.");
    } // handleResetButton
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/

    function handleSaveButton() {
      document.getElementById('filenameForm').style.visibility = "visible";
      document.getElementById('filename').focus(); // Place the cursor in the filename text input box.
    } // handleResetButton 
 
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/

    function handleFormSubmit(evt) {
      alert("handleFormSubmit fired.");    
      evt.preventDefault(); // Do not refresh the page when the submit button is clicked.
      document.getElementById('filenameForm').style.visibility = "hidden";
    } // handleFormSubmit
  </script>
</body>

</html>

This example (Mandelbrot 3) is described using the following logical partitions:

  • Markup
  • Initialization
    • Global namespace
    • handleLoad
  • Drawing the Mandelbrot set
  • Event handling

Markup

The form element is initially hidden and only becomes visible when the user clicks the Save button, in that:

#filenameForm {
  visibility: hidden; /* As opposed to "display: none", keep room for this hidden element in the layout. */
}

and:

function handleSaveButton() {
  document.getElementById('filenameForm').style.visibility = "visible";
  document.getElementById('filename').focus(); // Place the cursor in the filename text input box.
} // handleResetButton 

Clicking the form's Submit button re-hides it:

function handleFormSubmit(evt) {
  alert("handleFormSubmit fired.");    
  evt.preventDefault(); // Do not refresh the page when the submit button is clicked.
  document.getElementById('filenameForm').style.visibility = "hidden";
} // handleFormSubmit

Initialization

The initialization section is comprised of the global namespace and the handleLoad function:

Global namespace

The global namespace is simple:

var RE_MAX = 1.1; // This value will be adjusted as necessary to ensure that the rendered Mandelbrot set is never skewed (that is, true to it's actual shape).
var RE_MIN = -2.5;
var IM_MAX = 1.2;
var IM_MIN = -1.2;
var MAX_ITERATIONS = 1200; // Increase this value to improve detection of complex c values that belong to the Mandelbrot set.
var STATIC_ZOOM_BOX_FACTOR = 0.25; // Increase to make the double-click and hold gesture zoom box bigger.


var globals = {}; // See the handleLoad function.
     
window.addEventListener('load', handleLoad, false);

Constants are simulated by using uppercase global variables. After the page loads, handleLoad fires.

handleLoad

The handleLoad callback handles the majority of the initialization:

function handleLoad() {      
  var canvas = document.getElementsByTagName('canvas')[0];
  var canvasWidth = canvas.width;
  var canvasHeight = canvas.height;      
  var ctx = canvas.getContext('2d');
  
  document.getElementsByTagName('table')[0].width = canvasWidth; // Make the table's width the same as the canvas's width.  
  document.getElementById('messageBox').innerHTML = DEFAULT_MESSAGE;
  
  globals.canvas = canvas;
  globals.canvas.context = ctx;
  globals.canvas.context.imageDataObject = ctx.createImageData(canvasWidth, canvasHeight); // Create an appropriately sized but empty canvas image data object.
        
  globals.staticZoomBoxWidth = STATIC_ZOOM_BOX_FACTOR * canvasWidth; // Maintains the original canvas width/height ratio.
  globals.staticZoomBoxHeight = STATIC_ZOOM_BOX_FACTOR * canvasHeight; // Maintains the original canvas width/height ratio.      
  
  globals.pointer = {};
  globals.pointer.down = false;        
         
  canvas.addEventListener('mousedown', handlePointer, false);
  canvas.addEventListener('mousemove', handlePointer, false);
  canvas.addEventListener('mouseup', handlePointer, false);    
      
  document.getElementById('resetButton').addEventListener('click', handleResetButton, false);
  document.getElementById('lightenButton').addEventListener('click', handleLightenButton, false);    
  document.getElementById('saveButton').addEventListener('click', handleSaveButton, false);        
  document.getElementById('filenameForm').addEventListener('submit', handleFormSubmit, false);    

  ctx.fillStyle = "rgba(255, 0, 0, 0.3)"; // The color and opacity of the zoom box. This is what gets saved when calling ctx.save().      

  handleResetButton();
} // handleLoad

The handleResetButton function is straightforward:

function handleResetButton() {
  var reMax = adjusted_RE_MAX(); // If the constant RE_MAX is set correctly, the user will never see the alert that the adjusted_RE_MAX function can throw.
  
  setExtrema(reMax, RE_MIN, IM_MAX, IM_MIN);
  drawMandelbrot();          
} // handleResetButton

adjusted_RE_MAX() ensures that the chosen dimensions (RE_MAX, RE_MIN, IM_MAX, IM_MIN) for the complex plane are proportional to those of the canvas.

setExtrema sets the global versions to the given extrema and, as discussed next, drawMandelbrot draws the Mandelbrot set.

Drawing the Mandelbrot set

Other than the utilization of the canvas image data array, the core drawing algorithm is the same:

function drawMandelbrot() {     
  var startTime = new Date(); // Report how long it takes to render this particular region of the Mandelbrot set.             
  var canvas = globals.canvas;
  var canvasWidth = canvas.width;
  var canvasHeight = canvas.height;
  var ctx = canvas.context;
  var imageDataObjectData = ctx.imageDataObject.data; // imageDataObjectData is a reference to, not a copy of, ctx.imageDataObject.data
  
  var ReMax = globals.ReMax; // These 4 lines generally require that setExtrema be called prior to calling drawMandelbrot.
  var ReMin = globals.ReMin;
  var ImMax = globals.ImMax;
  var ImMin = globals.ImMin;
  
  var x_coefficient = (ReMax - ReMin) / canvasWidth; // Keep the below loops as computation-free as possible.
  var y_coefficient = (ImMin - ImMax) / canvasHeight; // Keep the below loops as computation-free as possible.

  var iterationSum = 0;
  var currentPixel = 0;          
  for (var y = 0; y < canvasHeight; y++) {
    var c_Im = (y * y_coefficient) + ImMax; // Note that c = c_Re + c_Im*i
    
    for (var x = 0; x < canvasWidth; x++) {
      var c_Re = (x * x_coefficient) + ReMin // Convert the canvas x-coordinate to a complex plane Re-coordinate. c_Re represents the real part of a c value.
      
      var z_Re = 0; // The first z value (Zo) must be 0.
      var z_Im = 0; // The first z value (Zo) must be 0. Note that z = z_Re + z_Im*i
      
      var c_belongsToMandelbrotSet = true; // Assume that the c associated with Zn belongs to the Mandelbrot set (i.e., Zn remains bounded under iteration of Zn+1 = (Zn)^2 + c).
      for (var iterationCount = 1; iterationCount <= MAX_ITERATIONS; iterationCount++) {
        iterationSum++; // Keep track of how many iterations were performed in total so we can report this to the user.
      
        var z_Re_squared = z_Re * z_Re; // A small speed optimization.
        var z_Im_squared = z_Im * z_Im; // A small speed optimization.

        if (z_Re_squared + z_Im_squared > 4) { // Checks if |z^2| is greater than 2. This approach avoids the expensive square root operation.
          c_belongsToMandelbrotSet = false; // This complex c value is not part of the Mandelbrot set (because it will always tend towards infinity under iteration).
          break; // Immediately check the next c value to see if it belongs to the Mandelbrot set or not.
        } // if
        
        // The next two lines perform Zn+1 = (Zn)^2 + c (recall that (x + yi)^2 = x^2 - y^2 + 2xyi, thus the real part is x^2 - y^2 and the imaginary part is 2xyi).
        z_Im = (2 * z_Re * z_Im) + c_Im; // We must calculate the next value of z_Im first because it depends on the current value of z_Re (not the next value of z_Re).
        z_Re = z_Re_squared - z_Im_squared + c_Re; // Calculate the next value of z_Re.
      } // for   
      
      if (c_belongsToMandelbrotSet) { // This complex c value is probably part of the Mandelbrot set because Zn did not tend toward infinity within MAX_ITERATIONS iterations.
        imageDataObjectData[currentPixel++] = 0; // Red. Note that there are 255 possible shades of red, green, blue, and alpha (i.e., opacity).
        imageDataObjectData[currentPixel++] = 0; // Green.
        imageDataObjectData[currentPixel++] = 0; // Blue.
        imageDataObjectData[currentPixel++] = 255; // Alpha (i.e., 0% transparency).
      } 
      else { // This complex c value is definitely not part of the Mandelbrot set because Zn would tend toward infinity under iteration (i.e., |Zn| > 2).
        imageDataObjectData[currentPixel++] = 255; 
        imageDataObjectData[currentPixel++] = 255; 
        imageDataObjectData[currentPixel++] = 255; 
        imageDataObjectData[currentPixel++] = 255;
      } // if-else
    } // for
  } // for        
  
  ctx.putImageData(ctx.imageDataObject, 0, 0); // Render our carefully constructed canvas image data array to the canvas.
      
  var elapsedMilliseconds = (new Date()) - startTime;
  document.getElementById('elapsedTime').innerHTML = iterationSum.format() + " iterations in " + (elapsedMilliseconds / 1000).toFixed(2) + " seconds"; // Note that the UI element is not updated until after this block terminates (which is the desired behavior).            
  document.getElementById('messageBox').innerHTML = DEFAULT_MESSAGE; // Erase the "Calculating..." message and replace it with the default message.
} // drawMandelbrot

Instead of drawing a black pixel using ctx.fillRect(x, y, 1, 1), drawMandelbrot sets the canvas image objectdata array (see Pixel manipulation with canvas). This array represents a canvas image. Every 4 elements of this array represents an image pixel. The 4 elements specify the red, green, blue and alpha values for the associated pixel, using a 0 to 255 scale (0% to 100%, respectively). Be aware that an alpha value of 0 means that the associated pixel will be 0% opaque (completely transparent) and an alpha value of 255 means the pixel will be 100% opaque (not transparent at all). Thus, the following sets the given pixel to either black (no colors) or white (all colors):

if (c_belongsToMandelbrotSet) { // This complex c value is probably part of the Mandelbrot set because Zn did not tend toward infinity within MAX_ITERATIONS iterations.
  imageDataObjectData[currentPixel++] = 0; // Red. Note that there are 255 possible shades of red, green, blue, and alpha (i.e., opacity).
  imageDataObjectData[currentPixel++] = 0; // Green.
  imageDataObjectData[currentPixel++] = 0; // Blue.
  imageDataObjectData[currentPixel++] = 255; // Alpha (i.e., 0% transparency).
} 
else { // This complex c value is definitely not part of the Mandelbrot set because Zn would tend toward infinity under iteration (i.e., |Zn| > 2).
  imageDataObjectData[currentPixel++] = 255; 
  imageDataObjectData[currentPixel++] = 255; 
  imageDataObjectData[currentPixel++] = 255; 
  imageDataObjectData[currentPixel++] = 255; 
} // if-else

After the image data array has been set, the array's image is drawn to the canvas as follows:

ctx.putImageData(ctx.imageDataObject, 0, 0);

In the previous code example, imageDataObjectData is a reference (pointer) to ctx.imageDataObject.data, so changing imageDataObjectData changes ctx.imageDataObject.data, which changes its parent ctx.imageDataObject.

To recap this section, drawMandelbrot() draws the region of the Mandelbrot as specified by the 4 global variables globals.ReMax, globals.ReMin, globals.ImMax, and globals.ImMin. The next step is setting these 4 globals to the values specified by a zoom box, as discussed next.

Event handling

The first step is registering the event handler within the handleLoad function:

canvas.addEventListener('mousedown', handlePointer, false);
canvas.addEventListener('mousemove', handlePointer, false);
canvas.addEventListener('mouseup', handlePointer, false);    

Now when a mouse event occurs over the canvas, the following code is invoked:

function handlePointer(evt) {
  var canvasWidthHeightRatio = globals.canvas.width / globals.canvas.height;
  var ctx = globals.canvas.context;
  
  var canvasX;
  var canvasY;      
  
  if (evt.offsetX && evt.offsetY) {
    canvasX = evt.offsetX; // Not supported in Firefox.
    canvasY = evt.offsetY; // Does not assume that the canvas element is a direct descendent of the body element.
  } else {
    canvasX = evt.clientX - evt.target.offsetLeft; // Supported in Firefox.
    canvasY = evt.clientY - evt.target.offsetTop; // Assumes that the canvas element is a direct descendent of the body element.
  } // if-else
  
  var zoomBoxWidth;
  var zoomBoxHeight;
  
  var ReMax;
  var ReMin;
  var ImMax;
  var ImMin;
        
  switch (evt.type) {
    case 'mousedown':
      globals.pointer.x1 = canvasX;
      globals.pointer.y1 = canvasY;
      globals.pointer.down = true;
      break;
    case 'mousemove':
      if (globals.pointer.down) {
        zoomBoxHeight = Math.abs(canvasY - globals.pointer.y1);  
        zoomBoxWidth = zoomBoxHeight * canvasWidthHeightRatio; // We must keep the zoom box dimensions proportional to the canvas dimensions in order to ensure that the resulting zoomed Mandelbrot image does not become skewed.
        ctx.putImageData(globals.canvas.context.imageDataObject, 0, 0); // Assumes that an initial image of the Mandelbrot set is drawn before we get to this point in the code. The purpose of this line is to erase the prior zoom box rectangle before drawing the next zoom box rectangle.
        ctx.fillRect(globals.pointer.x1, globals.pointer.y1, zoomBoxWidth, zoomBoxHeight); // With a freshly painted image of the current Mandelbrot set in place (see prior line), draw a new zoom box rectangle.
      } // if
      break;
    case 'mouseup':
      globals.pointer.down = false;          
      
      zoomBoxHeight = Math.abs(canvasY - globals.pointer.y1); // Only allow the zoom box to be drawn from an upper-left corner position down to a lower-right corner position.
      zoomBoxWidth = zoomBoxHeight * canvasWidthHeightRatio; // Again, ensure that the width/height ratio of the zoom box is proportional to the canvas's (this simplifies the algorithm).          

      if (zoomBoxHeight == 0) { // No zoom box has been drawn, so honor the fixed sized zoom box.
        var staticZoomBoxWidth = globals.staticZoomBoxWidth;
        var staticZoomBoxHeight = globals.staticZoomBoxHeight;
        var halfStaticZoomBoxWidth = staticZoomBoxWidth / 2;
        var halfStaticZoomBoxHeight = staticZoomBoxHeight / 2;
      
        ctx.fillRect(canvasX - halfStaticZoomBoxWidth, canvasY - halfStaticZoomBoxHeight, staticZoomBoxWidth, staticZoomBoxHeight); // Just leave this on the screen.
                     
        ReMin = xToRe(canvasX - halfStaticZoomBoxWidth); // Center the static zoom box about the point (evt.offsetX, evt.offsetY).
        ImMax = yToIm(canvasY - halfStaticZoomBoxHeight); 
        
        ReMax = xToRe(canvasX + halfStaticZoomBoxWidth);
        ImMin = yToIm(canvasY + halfStaticZoomBoxHeight);
      } 
      else { // A (possibly tiny) zoom box has been drawn, so honor it.
        ReMin = xToRe(globals.pointer.x1); // Convert the mouse's x-coordinate value (on the canvas) to the associated Re-coordinate value in the complex plane.
        ImMax = yToIm(globals.pointer.y1); // Convert the mouse's y-coordinate value (on the canvas) to the associated Im-coordinate value in the complex plane.
                                  
        ReMax = xToRe(zoomBoxWidth + globals.pointer.x1); // Convert the zoom box's final x-coordinate value to the associated Re-coordinate value in the complex plane.  
        ImMin = yToIm(zoomBoxHeight + globals.pointer.y1);  // Convert the zoom box's final y-coordinate value to the associated Re-coordinate value in the complex plane.            
      } // if-else
      
      document.getElementById('messageBox').innerHTML = "Calculating..."; // This isn't displayed until its containing code block exits, hence the need for setImmediate/setTimeout calls next. 
      document.getElementById('elapsedTime').innerHTML = ""; // Erase the prior run's statistics.     

      setExtrema(ReMax, ReMin, ImMax, ImMin); // Must set these globals prior to calling drawMandelbort because drawMandelbort accesses them.
      if (window.setImmediate) {
        window.setImmediate(drawMandelbrot); // Allow "Calculating..." to be immediately displayed on the screen.
      }
      else {
        window.setTimeout(drawMandelbrot, 0); // Allow "Calculating..." to be immediately displayed on the screen.
      }     
      break; 
    default:
      alert("Error in switch statement."); // Although unnecessary, defensive programming techniques such as this are highly recommended.
  } // switch              
} // handlePointer

The event flow for creating a zoom box is as follows:

  • mousedown
  • mousemove
  • mouseup

mousedown

The position of the mouse down event represents the upper- left corner of the zoom box (or, for a single-click, the center of a zoom box of fixed size), so we record this information for future use:

case 'mousedown':
  globals.pointer.x1 = canvasX;
  globals.pointer.y1 = canvasY;
  globals.pointer.down = true;
  break;

mousemove

In this case, the user is specifying a zoom box through a click-and-drag operation. In order to draw the semi-transparent zoom box (using ctx.fillRect) non-destructively, we must redraw the underlying Mandelbrot image (using ctx.putImageData) each time the dimensions of the zoom box change:

case 'mousemove':
  if (globals.pointer.down) {
    zoomBoxHeight = Math.abs(canvasY - globals.pointer.y1);  
    zoomBoxWidth = zoomBoxHeight * canvasWidthHeightRatio; // We must keep the zoom box dimensions proportional to the canvas dimensions in order to ensure that the resulting zoomed Mandelbrot image does not become skewed.
    ctx.putImageData(globals.canvas.context.imageDataObject, 0, 0); // Assumes that an initial image of the Mandelbrot set is drawn before we get to this point in the code. The purpose of this line is to erase the prior zoom box rectangle before drawing the next zoom box rectangle.
    ctx.fillRect(globals.pointer.x1, globals.pointer.y1, zoomBoxWidth, zoomBoxHeight); // With a freshly painted image of the current Mandelbrot set in place (see prior line), draw a new zoom box rectangle.
  } // if
  break;

Because our coordinate transformation equations assume proportionality (see Mapping screen coordinates to the complex plane), we ensure that the dimensions of the zoom box remain proportional to the canvas dimensions as follows:

zoomBoxHeight = Math.abs(canvasY - globals.pointer.y1);  
zoomBoxWidth = zoomBoxHeight * canvasWidthHeightRatio;

That is, we first calculated the height of the user-specified zoom box and multiply it by the canvas's width/height ratio. This guarantees proportionality.

mouseup

The mouse down handler records the upper-left corner coordinate of the zoom box. The mouse move handler draws the (semi-transparent) zoom box. The mouse up handler records the lower-right corner (or center) coordinate of the zoom box and then draws the specified region of the Mandelbrot set as follows:

case 'mouseup':
  globals.pointer.down = false;          
  
  zoomBoxHeight = Math.abs(canvasY - globals.pointer.y1); // Only allow the zoom box to be drawn from an upper-left corner position down to a lower-right corner position.
  zoomBoxWidth = zoomBoxHeight * canvasWidthHeightRatio; // Again, ensure that the width/height ratio of the zoom box is proportional to the canvas's (this simplifies the algorithm).          

  if (zoomBoxHeight == 0) { // No zoom box has been drawn, so honor the fixed sized zoom box.
    var staticZoomBoxWidth = globals.staticZoomBoxWidth;
    var staticZoomBoxHeight = globals.staticZoomBoxHeight;
    var halfStaticZoomBoxWidth = staticZoomBoxWidth / 2;
    var halfStaticZoomBoxHeight = staticZoomBoxHeight / 2;
  
    ctx.fillRect(canvasX - halfStaticZoomBoxWidth, canvasY - halfStaticZoomBoxHeight, staticZoomBoxWidth, staticZoomBoxHeight); // Just leave this on the screen.
                 
    ReMin = xToRe(canvasX - halfStaticZoomBoxWidth); // Center the static zoom box about the point (evt.offsetX, evt.offsetY).
    ImMax = yToIm(canvasY - halfStaticZoomBoxHeight); 
    
    ReMax = xToRe(canvasX + halfStaticZoomBoxWidth);
    ImMin = yToIm(canvasY + halfStaticZoomBoxHeight);
  } 
  else { // A (possibly tiny) zoom box has been drawn, so honor it.
    ReMin = xToRe(globals.pointer.x1); // Convert the mouse's x-coordinate value (on the canvas) to the associated Re-coordinate value in the complex plane.
    ImMax = yToIm(globals.pointer.y1); // Convert the mouse's y-coordinate value (on the canvas) to the associated Im-coordinate value in the complex plane.
                              
    ReMax = xToRe(zoomBoxWidth + globals.pointer.x1); // Convert the zoom box's final x-coordinate value to the associated Re-coordinate value in the complex plane.  
    ImMin = yToIm(zoomBoxHeight + globals.pointer.y1);  // Convert the zoom box's final y-coordinate value to the associated Re-coordinate value in the complex plane.            
  } // if-else
  
  document.getElementById('messageBox').innerHTML = "Calculating..."; // This isn't display until its containing code block exits, hence the need for setImmediate/setTimeout calls next. 
  document.getElementById('elapsedTime').innerHTML = ""; // Erase the prior run's statistics.     

  setExtrema(ReMax, ReMin, ImMax, ImMin); // Must set these globals prior to calling drawMandelbort because drawMandelbort accesses them.
  if (window.setImmediate) {
    window.setImmediate(drawMandelbrot); // Allow "Calculating..." to be immediately displayed on the screen.
  }
  else {
    window.setTimeout(drawMandelbrot, 0); // Allow "Calculating..." to be immediately displayed on the screen.
  }     
  break;

We first calculate the (proportional) size of the zoom box:

zoomBoxHeight = Math.abs(canvasY - globals.pointer.y1);  
zoomBoxWidth = zoomBoxHeight * canvasWidthHeightRatio;

If zoomBoxHeight is 0, then the user performed a single-click operation (that is, the user did not specify a zoom box). If this is the case, we prepare to draw a zoom box of fixed size that is centered about the mouse up position:

if (zoomBoxHeight == 0) { // No zoom box has been drawn, so honor the fixed sized zoom box.
  var staticZoomBoxWidth = globals.staticZoomBoxWidth;
  var staticZoomBoxHeight = globals.staticZoomBoxHeight;
  var halfStaticZoomBoxWidth = staticZoomBoxWidth / 2;
  var halfStaticZoomBoxHeight = staticZoomBoxHeight / 2;
  
  ctx.fillRect(canvasX - halfStaticZoomBoxWidth, canvasY - halfStaticZoomBoxHeight, staticZoomBoxWidth, staticZoomBoxHeight); // Just leave this on the screen.
               
  ReMin = xToRe(canvasX - halfStaticZoomBoxWidth); // Center the static zoom box about the point (evt.offsetX, evt.offsetY).
  ImMax = yToIm(canvasY - halfStaticZoomBoxHeight); 
    
  ReMax = xToRe(canvasX + halfStaticZoomBoxWidth);
  ImMin = yToIm(canvasY + halfStaticZoomBoxHeight);
} 

As you can see, xToRe and yToIm convert a given canvas screen coordinate to the associated point in the complex plane (see Mapping screen coordinates to the complex plane).

Otherwise, we convert the upper-left and lower-right corners of the zoom box to their analogous complex plane coordinates:

else { // A (possibly tiny) zoom box has been drawn, so honor it.
  ReMin = xToRe(globals.pointer.x1); // Convert the mouse's x-coordinate value (on the canvas) to the associated Re-coordinate value in the complex plane.
  ImMax = yToIm(globals.pointer.y1); // Convert the mouse's y-coordinate value (on the canvas) to the associated Im-coordinate value in the complex plane.
                            
  ReMax = xToRe(zoomBoxWidth + globals.pointer.x1); // Convert the zoom box's final x-coordinate value to the associated Re-coordinate value in the complex plane.  
  ImMin = yToIm(zoomBoxHeight + globals.pointer.y1);  // Convert the zoom box's final y-coordinate value to the associated Re-coordinate value in the complex plane.            
} // if-else

One of the quirks of JavaScript is that changing the content of an element doesn't occur until after the containing code block terminates. Thus, in order to get "Calculating..." immediately to the screen, we must allow its containing block, drawMandelbrot, to immediately terminate. This is accomplished by using setTimeout (or setImmediate, if available):

setExtrema(ReMax, ReMin, ImMax, ImMin); // Must set these globals prior to calling drawMandelbort because drawMandelbort accesses them.
if (window.setImmediate) {
  window.setImmediate(drawMandelbrot); // Allow "Calculating..." to be immediately displayed on the screen.
}
else {
  window.setTimeout(drawMandelbrot, 0); // Allow "Calculating..." to be immediately displayed on the screen.
}

That is, drawMandelbrot is executed as soon as its containing block (handlePointer) terminates. It's then the responsibility of drawMandelbrot to change "Calculating..." back to "Click or click-and-drag to zoom." when complete.

Note  Because drawMandelbrot relies on globals.ReMax through globals.ImMin, we must set these values, using setExtrema, before calling drawMandelbrot. Using setExtrema is straightforward:

function setExtrema(ReMax, ReMin, ImMax, ImMin) {
/* 
  This generally must be called prior to calling drawMandelbrot in that drawMandelbrot accesses the following 4 global variables.
*/
  globals.ReMax = ReMax;
  globals.ReMin = ReMin;
  globals.ImMax = ImMax;
  globals.ImMin = ImMin;          
} // setExtrema

 

This, by the way, simplifies the handleResetButton event handler:

function handleResetButton() {
  var reMax = adjusted_RE_MAX(); // If the constant RE_MAX is set correctly, the user will never see the alert that the adjusted_RE_MAX function can throw.
  
  setExtrema(reMax, RE_MIN, IM_MAX, IM_MIN);
  drawMandelbrot();          
} // handleResetButton

To recap, a particular region of the Mandelbrot set is specified by using a zoom box. The upper-left and lower-right canvas screen coordinates of the zoom box are converted to their complex plane analogs. These analogs are then used by setExtrema and drawMandelbrot to render the specified region of the Mandelbrot set to the canvas. Unfortunately, the rendered image is strictly black and white. To improve the rendered image, we next add 255 shades of gray, as discussed in Adding Mandelbrot image grayscale.

Adding Mandelbrot image grayscale