Informations
Le sujet que vous avez demandé est indiqué ci-dessous. Toutefois, ce sujet ne figure pas dans la bibliothèque.

Mise en œuvre d’une zone d’agrandissement

S’appuyant sur les informations présentées dans Simplification de la mise en œuvre de la zone d’agrandissement, cette rubrique traite d’une mise en œuvre possible de la zone d’agrandissement Mandelbrot.

Voici une instance de l’exemple Mandelbrot 3 :

Capture d’écran de Mandelbrot 3

Cette image a été générée en agrandissant une région particulière de l’ensemble de Mandelbrot. Sachez que le bouton Reset est fonctionnel, mais que les deux autres ne le sont pas. Les boutons Lighten et Save seront implémentés dans des exemples ultérieurs.

Voici le code de cet exemple :

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>

Cet exemple (Mandelbrot 3) est décrit à l’aide des partitions logiques suivantes :

Balisage

L’élément form est initialement masqué et devient visible uniquement lorsque l’utilisateur clique sur le bouton Save, comme suit :


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

et :


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

Un clic sur le bouton Submit du formulaire permet de le masquer de nouveau :


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

Initialisation

La section d’initialisation est constituée de l’espace de noms global et de la fonction handleLoad :

Espace de noms global

L’espace de noms global est 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);

Les constantes sont simulées en utilisant des variables globales en majuscules. Une fois la page chargée, handleLoad se déclenche.

handleLoad

Le rappel handleLoad gère la plus grande partie de l’initialisation :


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

La fonction handleResetButton est simple :


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() garantit que les dimensions choisies (RE_MAX, RE_MIN, IM_MAX, IM_MIN) pour le plan complexe sont proportionnelles à celles de la zone de dessin.

setExtrema définit les versions globales aux extrêmes données et, comme discuté plus loin, drawMandelbrot dessine l’ensemble de Mandelbrot.

Dessin de l’ensemble de Mandelbrot

En dehors de l’utilisation du data array de l’image de la zone de dessin, l’algorithme de dessin principal est identique :


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

Au lieu de dessiner un pixel noir avec ctx.fillRect(x, y, 1, 1), drawMandelbrot définit la zone de dessin image object data array (voir Manipulation de pixels avec l’élément canvas). Ce tableau représente une image de zone de dessin. Les quatre éléments de ce tableau représentent un pixel de l’image. Ces quatre éléments spécifient les valeurs rouge, verte, bleue et alpha du pixel associé, à l’aide d’une échelle allant de 0 à 255 (0 % à 100 %, respectivement). Notez qu’une valeur alpha de 0 signifie que le pixel associé sera opaque à 0 % (complètement transparent) et qu’une valeur alpha de 255 signifie que le pixel sera opaque à 100 % (pas du tout transparent). Ainsi, ce qui suit définit le pixel donné en noir (aucune couleur) ou en blanc (toutes les couleurs).


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

Une fois le tableau de données d’image défini, l’image du tableau est dessinée sur la zone de dessin comme suit :


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

Dans l’exemple de code précédent, imageDataObjectData est une référence (pointeur) à ctx.imageDataObject.data. La modification de imageDataObjectData entraîne donc la modification de ctx.imageDataObject.data, qui modifie son ctx.imageDataObject parent.

Récapitulons : drawMandelbrot() dessine la région de l’ensemble de Mandelbrot spécifiée par les quatre variables globales globals.ReMax, globals.ReMin, globals.ImMax et globals.ImMin. L’étape suivante consiste à affecter à ces quatre variables globales les valeurs spécifiées par une zone d’agrandissement, comme discuté plus loin.

Gestion des événements

La première étape consiste à enregistrer le gestionnaire d’événements dans la fonction handleLoad :


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


Quand un événement de souris se produit sur la zone de dessin, le code suivant est appelé :


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

Le flux d’événements pour la création d’une zone d’agrandissement est le suivant :

mousedown

La position de l’événement de souris appuyée représente le coin supérieur gauche de la zone d’agrandissement (ou, pour un clic unique, le centre d’une zone d’agrandissement de taille fixe). Nous notons donc ces informations pour utilisation ultérieure :


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

mousemove

Dans ce cas, l’utilisateur spécifie une zone d’agrandissement par le biais d’une opération cliquer-déplacer. Pour dessiner la zone d’agrandissement semi-transparente (à l’aide de ctx.fillRect) de manière non destructive, nous devons redessiner l’image de Mandelbrot sous-jacente (à l’aide de ctx.putImageData) chaque fois que les dimensions de la zone d’agrandissement changent :


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;

Étant donné que nos équations de transformation de coordonnées supposent la proportionnalité (voir Mappage de coordonnées d’écran au plan complexe), nous nous assurons que les dimensions de la zone d’agrandissement demeurent proportionnelles à celles de la zone de dessin, comme suit :


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

Autrement dit, nous avons d’abord calculé la hauteur de la zone d’agrandissement spécifiée par l’utilisateur et nous l’avons multipliée par le rapport largeur/hauteur de la zone de dessin. Ceci permet de garantir la proportionnalité.

mouseup

Le gestionnaire d’événements de souris appuyée enregistre la coordonnée de coin supérieur gauche de la zone d’agrandissement. Le gestionnaire d’événements de souris déplacée dessine la zone d’agrandissement (semi-transparente). Le gestionnaire d’événements de souris relâchée enregistre la coordonnée de coin inférieur droit (ou centre) de la zone d’agrandissement, puis dessine la région spécifiée de l’ensemble de Mandelbrot comme suit :


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;

Nous calculons d’abord la taille (proportionnelle) de la zone d’agrandissement :


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

Si zoomBoxHeight est égal à 0, l’utilisateur a effectué une opération de clic unique (autrement dit, il n’a pas spécifié de zone d’agrandissement). Si c’est le cas, nous nous préparons à dessiner une zone d’agrandissement de taille fixe centrée autour de la position du relâchement de souris :


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);
} 


Comme vous pouvez le voir, xToRe et yToIm convertissent une coordonnée d’écran de zone de dessin donnée en un point associé dans le plan complexe (voir Mappage de coordonnées d’écran au plan complexe).

Autrement, nous convertissons les coins supérieur gauche et inférieur droit de la zone d’agrandissement en leurs coordonnées de plan complexe analogues :


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

L’une des particularités de JavaScript est que la modification du contenu d’un élément ne se produit qu’après la fin du bloc de code contenant. Ainsi, pour afficher immédiatement le message « Calculating... » à l’écran, nous devons faire en sorte que son bloc contenant, drawMandelbrot, se termine immédiatement. Ceci est possible grâce à setTimeout (ou setImmediate, si disponible) :


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.
}

Autrement dit, drawMandelbrot est exécuté dès que son bloc contenant (handlePointer) se termine. Il incombe ensuite à drawMandelbrot de remplacer « Calculating... » par « Click or click-and-drag to zoom ». une fois terminé.

Remarque  Étant donné que drawMandelbrot repose sur globals.ReMax par le biais de globals.ImMin, nous devons définir ces valeurs, à l’aide de setExtrema, avant d’appeler drawMandelbrot. L’utilisation de setExtrema est relativement simple :


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

Soit dit en passant, ceci simplifie le gestionnaire d’événements handleResetButton :


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

Récapitulons : une région particulière de l’ensemble de Mandelbrot est spécifiée en utilisant une zone d’agrandissement. Les coordonnées d’écran de zone de dessin supérieur gauche et inférieur droit de la zone de dessin sont converties en leurs analogues dans le plan complexe. Ces analogues sont ensuite utilisés par setExtrema et drawMandelbrot pour afficher la région spécifiée de l’ensemble de Mandelbrot sur la zone de dessin. Malheureusement, l’image affichée est entièrement en noir et blanc. Pour l’améliorer, nous allons ajouter 255 nuances de gris, comme discuté dans Ajout de nuances de gris à une image de Mandelbrot.

Rubriques connexes

Ajout de nuances de gris à une image de Mandelbrot

 

 

Afficher:
© 2014 Microsoft