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

Utilisation de traitements Web

S’appuyant sur les informations présentées dans Activation des fonctionnalités tactiles, cette rubrique explique comment améliorer les performances à l’aide de traitements Web.

Remarque  Une page Web qui utilise des traitements Web doit être servie via le protocole HTTP ou HTTPS.

Comme décrit dans Traitements Web, l’API Web Worker permet aux auteurs d’applications Web de générer des scripts en arrière-plan qui s’exécutent parallèlement (de manière simultanée) à la page principale. Vous pouvez générer plusieurs threads à la fois destinés à des tâches longues. Un nouvel objet thread de travail nécessite un fichier .js, qui est inclus via une demande asynchrone envoyée au serveur Web.


var myWorker = new Worker('worker.js');

Toutes les communications vers et depuis le thread de travail sont gérées via des messages. Le thread de travail hôte et le script de thread de travail peuvent envoyer des messages à l’aide de postMessage et écouter une réponse à l’aide de l’événement onmessage. Le contenu du message est envoyé en tant que propriété data de l’objet d’événement.

Ceci étant dit, l’algorithme est le suivant :

  1. Dans le thread de la page principale (interface utilisateur), placez les informations requises pour générer une image de Mandelbrot (sous la forme d’un canvas data array) dans un littéral d’objet.
  2. À l’aide de postMessage, envoyez le littéral d’objet au thread de travail afin de le traiter.
  3. Une fois que le thread de travail a terminé son traitement, le tableau de données d’image de zone de dessin achevé est renvoyé au thread d’interface utilisateur, et il est affiché sur la zone de dessin.

Pour gérer cet algorithme, drawMandelbrot est modifié comme suit :


function drawMandelbrot(ReMax, ReMin, ImMax, ImMin, grayscaleFactor) {      
  var startTime = new Date(); // Report how long it takes to render this particular region of the Mandelbrot set.             
  var messageBox = document.getElementById('messageBox');     
  var elapsedTime =  document.getElementById('elapsedTime');     
  var canvas = globals.canvas; // A small speed optimization - accessing local variables tends to be faster than accessing global variables.
  var canvasWidth = canvas.width;
  var canvasHeight = canvas.height;
  var ctx = canvas.context;  
  var imageDataObject = ctx.imageDataObject; // imageDataObject ends up receiving an altered copy of ctx.imageDataObject, so imageDataObject is not a pointer to (reference to) ctx.imageDataObject.
  var maxPixelGrayscaleValue = 0; // This will contain the lightest shade of gray in the drawn Mandelbrot image.
  var fineDetailMandelbrotReceived = false; // Just in case the fine detail Web Worker callback finishes before the coarse detail Web Worker callback.

  messageBox.innerHTML = "Calculating..."; // This isn't displayed until the drawMandelbrot function block exits.
  elapsedTime.innerHTML = ""; // Erase the prior run's statistics.
          
  var workerMessage = {
    workerID: "",
    MAX_ITERATIONS: MAX_ITERATIONS,
    ReMax: ReMax,
    ReMin: ReMin,
    ImMax: ImMax,
    ImMin: ImMin,
    grayscaleFactor: grayscaleFactor,
    canvasWidth: canvasWidth,
    canvasHeight: canvasHeight,
    imageDataObject: imageDataObject
  };
  
  function workerCallback(evt) { // Receive the required data from the Web Worker to draw the Mandelbrot set to the canvas (plus a few other items).          
    if (fineDetailMandelbrotReceived) {
      return; // For some reason, the fine detail callback finished before the coarse detail callback - do not display the coarse Mandelbrot image.
    }
    
    ctx.putImageData(evt.data.imageDataObject, 0, 0); // Render our carefully constructed canvas image data array to the canvas.
    globals.canvas.context.imageDataObject = evt.data.imageDataObject; 
    globals.maxPixelGrayscaleValue = evt.data.maxPixelGrayscaleValue; // Store this information in case the user clicks the Lighten button.          
  
    var elapsedMilliseconds = (new Date()) - startTime;
    elapsedTime.innerHTML = evt.data.workerID + ": " + evt.data.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).            
    
    if (evt.data.workerID == "Fine detail") {
      fineDetailMandelbrotReceived = true;
      messageBox.innerHTML = DEFAULT_MESSAGE; // Erase the "Calculating..." message and replace it with the default message.
    } // if
  } // workerCallback
  
  globals.coarseDetailWorker.onmessage = workerCallback; // I unnecessarily set this callback each time drawMandelbrot is called - this is fine in that there's no significant performance hit.
  globals.fineDetailWorker.onmessage = workerCallback;

  workerMessage.MAX_ITERATIONS = Math.round(MAX_ITERATIONS / 2); // MAX_ITERATIONS must always been a (positive) integer.
  workerMessage.workerID = "Coarse detail";
  globals.coarseDetailWorker.postMessage(workerMessage); // postMessage to the coarse detail Web Worker. 

  workerMessage.MAX_ITERATIONS = MAX_ITERATIONS;
  workerMessage.workerID = "Fine detail";                
  globals.fineDetailWorker.postMessage(workerMessage); // postMessage to the fine detail Web Worker.
} // drawMandelbrot

Un thread de travail n’ayant pas accès au modèle DOM (et ne pouvant pas, en général, recevoir d’objets qui font référence au modèle DOM), nous plaçons les informations nécessaires pour générer une image de Mandelbrot dans un littéral d’objet. Ce littéral d’objet est envoyé du thread d’interface utilisateur vers le thread de travail :


var workerMessage = {
  workerID: "",
  MAX_ITERATIONS: MAX_ITERATIONS,
  ReMax: ReMax,
  ReMin: ReMin,
  ImMax: ImMax,
  ImMin: ImMin,
  grayscaleFactor: grayscaleFactor,
  canvasWidth: canvasWidth,
  canvasHeight: canvasHeight,
  imageDataObject: imageDataObject
};

L’objet imageDataObject fournit le tableau de données d’image de zone de dessin qui doit être rempli par le thread de travail.

Ceci étant dit, l’exemple Mandelbrot 8 est présenté ci-dessous :

Mandelbrot 8


<!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 8</title>
  <style>
    html, body {
      margin: 0;
      padding: 0;
      text-align: center;
    }
    
    canvas {
      border: 2px solid black;
      -ms-touch-action: none;
      touch-action: none; /* Use the standard, if available. */
    }    
    
    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 8</h1>
  <table>
    <tr>
      <td id="messageBox"></td>
      <td id="elapsedTime"></td>
    </tr>
  </table>
  <canvas width="600" height="400" oncontextmenu="return false;"> <!-- Because the hold gesture event can fire more than once, the 'oncontextmenu="return false;"' attribute is used to stop the right-click context menu from appearing inappropriately. -->
    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>
    if (!window.Worker) { // Check for the availability of the Worker() constructor.
      document.getElementsByTagName('body')[0].innerHTML = "<h2>Web Workers not supported - upgrade your browser<br>(after checking that your browser is in the correct mode)</h2>";      
    }
    else {
      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);
    } // if-else
                
    /************************************************************************************************************************************************************/
    
    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;  
                 
      window.addEventListener('hashchange', handleHashChange, false); // This event handler executes whenever the URL hash string changes.
      
      if (window.navigator.pointerEnabled || window.navigator.msPointerEnabled) { // Future proofing.
        // It's either-or with MS pointer events - they cannot be registered concurrently.
        window.gesture = window.gesture || window.MSGesture; // Future proofing.
        globals.gesture = new gesture();
        globals.gesture.target = canvas; 
        canvas.addEventListener('MSPointerDown', function(evt) { globals.gesture.addPointer(evt.pointerId); }, false); 
          
        canvas.addEventListener('MSGestureStart', handlePointer, false); 
        canvas.addEventListener('mousedown', handlePointer, false); // Required for the case when the mouse is clicked but not moved.
        
        canvas.addEventListener('MSGestureChange', handlePointer, false);
        
        canvas.addEventListener('MSGestureEnd', handlePointer, false);
        canvas.addEventListener('mouseup', handlePointer, false); // Required for the case when the mouse is clicked but not moved.

        canvas.addEventListener('MSGestureHold', handlePointer, false);
      }    
      else {
        canvas.addEventListener('mousedown', handlePointer, false);
        canvas.addEventListener('mousemove', handlePointer, false);
        canvas.addEventListener('mouseup', handlePointer, false);    
      } // if-else
            
      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().          
 
      handleHashChange(); // On page load, simulate a page URL change to draw the initial Mandelbrot set.
    } // handleLoad
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/    
    
    function handleHashChange() {
      var hashValues = getHashValues(); // This function examines window.location.hash but doesn't change it.
      
      if (hashValues) {
        globals.ReMax = hashValues.ReMax;
        globals.ReMin = hashValues.ReMin;
        globals.ImMax = hashValues.ImMax;
        globals.ImMin = hashValues.ImMin;
        globals.grayscaleFactor = hashValues.grayscaleFactor;
      }
      else {
        globals.ReMax = adjusted_RE_MAX();
        globals.ReMin = RE_MIN;
        globals.ImMax = IM_MAX;
        globals.ImMin = IM_MIN;     
        globals.grayscaleFactor = 1; // Multiplying any value by 1 has no effect.
      } // if-else
      
      initializeWebWorkers('mandelbrotWebWorker.js'); // Halt any in-process Web Workers so that the back/forward buttons behave as expected (i.e., deal with the asynchronous nature of the Web Workers).
      drawMandelbrot(globals.ReMax, globals.ReMin, globals.ImMax, globals.ImMin, globals.grayscaleFactor);
    } // handelHashChange    

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

    function getHashValues() {
      var dirtyComplexPlaneExtremaString = (window.location.hash).replace('#', ''); // Remove the leading "#" character from the string.
      var complexPlaneExtremaString = dirtyComplexPlaneExtremaString.split(','); // Returns an array. Assumes the following string form: "ReMax,ReMin,ImMax,ImMin,grayscaleFactor" (note that if grayscaleFactor is 1, the image's grayscale is not affected).
      
      var ReMax = parseFloat( complexPlaneExtremaString[0] ); 
      var ReMin = parseFloat( complexPlaneExtremaString[1] ); 
      var ImMax = parseFloat( complexPlaneExtremaString[2] ); 
      var ImMin = parseFloat( complexPlaneExtremaString[3] );
      var grayscaleFactor = parseFloat( complexPlaneExtremaString[4] );
      
      if ( isNaN(ReMax) || isNaN(ReMin) || isNaN(ImMax) || isNaN(ImMin) || isNaN(grayscaleFactor) ) { 
        return null;
      } // if 
      
      return {ReMax: ReMax, ReMin: ReMin, ImMax: ImMax, ImMin: ImMin, grayscaleFactor: grayscaleFactor};
    } // getHashValues
        
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/        

    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(ReMax, ReMin, ImMax, ImMin, grayscaleFactor) {      
      var startTime = new Date(); // Report how long it takes to render this particular region of the Mandelbrot set.             
      var messageBox = document.getElementById('messageBox');     
      var elapsedTime =  document.getElementById('elapsedTime');     
      var canvas = globals.canvas; // A small speed optimization - accessing local variables tends to be faster than accessing global variables.
      var canvasWidth = canvas.width;
      var canvasHeight = canvas.height;
      var ctx = canvas.context;  
      var imageDataObject = ctx.imageDataObject; // imageDataObject ends up receiving an altered copy of ctx.imageDataObject, so imageDataObject is not a pointer to (reference to) ctx.imageDataObject.
      var maxPixelGrayscaleValue = 0; // This will contain the lightest shade of gray in the drawn Mandelbrot image.
      var fineDetailMandelbrotReceived = false; // Just in case the fine detail Web Worker callback finishes before the coarse detail Web Worker callback.

      messageBox.innerHTML = "Calculating..."; // This isn't displayed until the drawMandelbrot function block exits.
      elapsedTime.innerHTML = ""; // Erase the prior run's statistics.
              
      var workerMessage = {
        workerID: "",
        MAX_ITERATIONS: MAX_ITERATIONS,
        ReMax: ReMax,
        ReMin: ReMin,
        ImMax: ImMax,
        ImMin: ImMin,
        grayscaleFactor: grayscaleFactor,
        canvasWidth: canvasWidth,
        canvasHeight: canvasHeight,
        imageDataObject: imageDataObject
      };
      
      function workerCallback(evt) { // Receive the required data from the Web Worker to draw the Mandelbrot set to the canvas (plus a few other items).          
        if (fineDetailMandelbrotReceived) {
          return; // For some reason, the fine detail callback finished before the coarse detail callback - do not display the coarse Mandelbrot image.
        }
        
        ctx.putImageData(evt.data.imageDataObject, 0, 0); // Render our carefully constructed canvas image data array to the canvas.
        globals.canvas.context.imageDataObject = evt.data.imageDataObject; 
        globals.maxPixelGrayscaleValue = evt.data.maxPixelGrayscaleValue; // Store this information in case the user clicks the Lighten button.          
      
        var elapsedMilliseconds = (new Date()) - startTime;
        elapsedTime.innerHTML = evt.data.workerID + ": " + evt.data.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).            
        
        if (evt.data.workerID == "Fine detail") {
          fineDetailMandelbrotReceived = true;
          messageBox.innerHTML = DEFAULT_MESSAGE; // Erase the "Calculating..." message and replace it with the default message.
        } // if
      } // workerCallback
      
      globals.coarseDetailWorker.onmessage = workerCallback; // I unnecessarily set this callback each time drawMandelbrot is called - this is fine in that there's no significant performance hit.
      globals.fineDetailWorker.onmessage = workerCallback;

      workerMessage.MAX_ITERATIONS = Math.round(MAX_ITERATIONS / 2); // MAX_ITERATIONS must always been a (positive) integer.
      workerMessage.workerID = "Coarse detail";
      globals.coarseDetailWorker.postMessage(workerMessage); // postMessage to the coarse detail Web Worker. 

      workerMessage.MAX_ITERATIONS = MAX_ITERATIONS;
      workerMessage.workerID = "Fine detail";                
      globals.fineDetailWorker.postMessage(workerMessage); // postMessage to the fine detail Web Worker.
    } // 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;
      
      var staticZoomBoxWidth = globals.staticZoomBoxWidth;
      var staticZoomBoxHeight = globals.staticZoomBoxHeight;
      var halfStaticZoomBoxWidth = staticZoomBoxWidth / 2;
      var halfStaticZoomBoxHeight = staticZoomBoxHeight / 2;
      
      switch (evt.type) {
        case 'MSGestureStart':              
        case 'mousedown':
          globals.pointer.down = true;      
          globals.pointer.x1 = canvasX;
          globals.pointer.y1 = canvasY;
          break;
        case 'MSGestureChange':                  
        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(ctx.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.
          }
          break;
        case 'MSGestureEnd':
        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.  
            ctx.putImageData(ctx.imageDataObject, 0, 0); // For the MSGestureHold case, erase the previously drawn zoom box so we don't draw two or more on top of each other.
            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        
        
          window.location.hash = ReMax + "," + ReMin + "," + ImMax + "," + ImMin + "," + globals.grayscaleFactor; // This triggers the handleHashChange event handler which, among other things, is responsible for drawing the Mandelbrot set.
          break; 
        case 'MSGestureHold':
          if (evt.detail & evt.MSGESTURE_FLAG_BEGIN) {
            ctx.fillRect(canvasX - halfStaticZoomBoxWidth, canvasY - halfStaticZoomBoxHeight, staticZoomBoxWidth, staticZoomBoxHeight); // At the first sign of a hold gesture, get the zoom box up on the screen immediately.                 
          }  
          
          // The evt.MSGESTURE_FLAG_END component of the hold gesture is handled by the "if (zoomBoxHeight == 0)" clause of the MSGestureEnd clause above.
          
          break;
        default:
          alert("Error in switch statement."); // Although unnecessary, defensive programming techniques such as this are highly recommended.
      } // switch              
    } // handlePointer    
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/
    
    function handleResetButton() {
      window.location.hash = adjusted_RE_MAX() + "," + RE_MIN + "," + IM_MAX + "," + IM_MIN + "," + 1; // // This triggers the handleHashChange event handler which, among other things, is responsible for drawing the Mandelbrot set.
    } // handleResetButton
    
    /*----------------------------------------------------------------------------------------------------------------------------------------------------------*/
    
    function handleLightenButton() {
    /* 
      This creates a value (factor) such that black (0) stays black and the lightest gray value in the image becomes white (255). Thus, clicking the 
      Lighten button can remove mathematical meaning of the (proper) grayscale but can make dark images more visible.
    */
      var grayscaleFactor = 255 / globals.maxPixelGrayscaleValue; // For the canvas element, 255 is white, 0 is black.

      window.location.hash = globals.ReMax + "," + globals.ReMin + "," + globals.ImMax + "," + globals.ImMin + "," + grayscaleFactor; // This invokes handleHashChange which, among other things, is responsibile for drawing the Mandelbrot set.
    } // 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) {
    /* 
      As of 8/2012, the following code only works starting with Internet Explorer 10.
    */
      evt.preventDefault(); // Do not refresh the page when the Submit button is clicked.
      
      window.BlobBuilder = window.BlobBuilder || window.MSBlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder;
      globals.canvas.toBlob = globals.canvas.toBlob || globals.canvas.msToBlob;      
      window.navigator.saveBlob = window.navigator.saveBlob || window.navigator.msSaveBlob;
      
      if (window.BlobBuilder && globals.canvas.toBlob && window.navigator.saveBlob) {
        var extensionlessFilename = evt.target[0].value;
        var filename = extensionlessFilename + ".png";
        var blobBuilderObject = new BlobBuilder(); // Create a blob builder object so that we can append content to it.
                
        blobBuilderObject.append( globals.canvas.toBlob() ); // Append the user's drawing in PNG format to the builder object.
        window.navigator.saveBlob(blobBuilderObject.getBlob(), filename); // Move the builder object content to a blob and save it to a file.      
      }
      else {
        document.getElementById('messageBox').innerHTML = "<strong style='color: red;'>CANNOT SAVE FILE</strong>";
      } // if-else
      
      document.getElementById('filenameForm').style.visibility = "hidden";
    } // handleFormSubmit

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

    function initializeWebWorkers(webWorkerJsPath) {
      if (globals.coarseDetailWorker) {
        globals.coarseDetailWorker.terminate();      
      }
      
      if (globals.fineDetailWorker) {
        globals.fineDetailWorker.terminate();      
      }
      
      globals.coarseDetailWorker = new Worker(webWorkerJsPath);
      globals.fineDetailWorker = new Worker(webWorkerJsPath);
    } // initializeWebWorkers
  </script>
</body>

</html>

Pour commencer, si les traitements Web ne sont pas disponibles, nous le disons et nous arrêtons. Autrement, nous initialisons comme précédemment :


if (!window.Worker) { // Check for the availability of the Worker() constructor.
  document.getElementsByTagName('body')[0].innerHTML = "<h2>Web Workers not supported - upgrade your browser<br>(after checking that your browser is in the correct mode)</h2>";      
}
else {
  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);
} // if-else

Dans handleHashChange, nous appelons initializeWebWorkers('mandelbrotWebWorker.js'). Le fichier mandelbrotWebWorker.js définit le code devant être exécuté par les threads de travail. La fonction initializeWebWorkers proprement dite est illustré ci-dessous :


function initializeWebWorkers(webWorkerJsPath) {
  if (globals.coarseDetailWorker) {
    globals.coarseDetailWorker.terminate();      
  }
  
  if (globals.fineDetailWorker) {
    globals.fineDetailWorker.terminate();      
  }
  
  globals.coarseDetailWorker = new Worker(webWorkerJsPath);
  globals.fineDetailWorker = new Worker(webWorkerJsPath);
} // initializeWebWorkers

Les deux instructions if servent à mettre fin à un thread de travail intraprocessus possible, ce qui est nécessaire si l’utilisateur clique sur le bouton Précédent ou Suivant avant que l’image de Mandelbrot actuelle soit entièrement affichée.

Les deux dernières lignes créent deux variables globales contenant deux objets de travail identiques (à savoir, ils exécutent tous deux le même code résidant dans webWorkerJsPath).

Au chargement de la page, handleLoad est exécuté, ce qui appelle handleHashChange. handleHashChange appelle initializeWebWorkers('mandelbrotWebWorker.js') (comme décrit plus haut), suivi de drawMandelbrot, qui a été modifié de façon à appeler deux threads de travail simultanés comme suit :


function drawMandelbrot(ReMax, ReMin, ImMax, ImMin, grayscaleFactor) {      
  var startTime = new Date(); // Report how long it takes to render this particular region of the Mandelbrot set.             
  var messageBox = document.getElementById('messageBox');     
  var elapsedTime =  document.getElementById('elapsedTime');     
  var canvas = globals.canvas; // A small speed optimization - accessing local variables tends to be faster than accessing global variables.
  var canvasWidth = canvas.width;
  var canvasHeight = canvas.height;
  var ctx = canvas.context;  
  var imageDataObject = ctx.imageDataObject; // imageDataObject ends up receiving an altered copy of ctx.imageDataObject, so imageDataObject is not a pointer to (reference to) ctx.imageDataObject.
  var maxPixelGrayscaleValue = 0; // This will contain the lightest shade of gray in the drawn Mandelbrot image.
  var fineDetailMandelbrotReceived = false; // Just in case the fine detail Web Worker callback finishes before the coarse detail Web Worker callback.

  messageBox.innerHTML = "Calculating..."; // This isn't displayed until the drawMandelbrot function block exits.
  elapsedTime.innerHTML = ""; // Erase the prior run's statistics.
          
  var workerMessage = {
    workerID: "",
    MAX_ITERATIONS: MAX_ITERATIONS,
    ReMax: ReMax,
    ReMin: ReMin,
    ImMax: ImMax,
    ImMin: ImMin,
    grayscaleFactor: grayscaleFactor,
    canvasWidth: canvasWidth,
    canvasHeight: canvasHeight,
    imageDataObject: imageDataObject
  };
  
  function workerCallback(evt) { // Receive the required data from the Web Worker to draw the Mandelbrot set to the canvas (plus a few other items).          
    if (fineDetailMandelbrotReceived) {
      return; // For some reason, the fine detail callback finished before the coarse detail callback. Don't display the coarse Mandelbrot image.
    }
    
    ctx.putImageData(evt.data.imageDataObject, 0, 0); // Render our carefully constructed canvas image data array to the canvas.
    globals.canvas.context.imageDataObject = evt.data.imageDataObject; 
    globals.maxPixelGrayscaleValue = evt.data.maxPixelGrayscaleValue; // Store this information in case the user clicks the Lighten button.          
  
    var elapsedMilliseconds = (new Date()) - startTime;
    elapsedTime.innerHTML = evt.data.workerID + ": " + evt.data.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).            
    
    if (evt.data.workerID == "Fine detail") {
      fineDetailMandelbrotReceived = true;
      messageBox.innerHTML = DEFAULT_MESSAGE; // Erase the "Calculating..." message and replace it with the default message.
    } // if
  } // workerCallback
  
  globals.coarseDetailWorker.onmessage = workerCallback; // I unnecessarily set this callback each time drawMandelbrot is called - this is fine in that there's no significant performance hit.
  globals.fineDetailWorker.onmessage = workerCallback;

  workerMessage.MAX_ITERATIONS = Math.round(MAX_ITERATIONS / 2); // MAX_ITERATIONS must always been a (positive) integer.
  workerMessage.workerID = "Coarse detail";
  globals.coarseDetailWorker.postMessage(workerMessage); // postMessage to the coarse detail Web Worker. 

  workerMessage.MAX_ITERATIONS = MAX_ITERATIONS;
  workerMessage.workerID = "Fine detail";                
  globals.fineDetailWorker.postMessage(workerMessage); // postMessage to the fine detail Web Worker.
} // drawMandelbrot

Si l’on ignore pour le moment la fonction de rappel workerCallback, la fonction drawMandelbrot exécute ensuite les lignes suivantes :


globals.coarseDetailWorker.onmessage = workerCallback; // I unnecessarily set this callback each time drawMandelbrot is called - this is fine in that there's no significant performance hit.
globals.fineDetailWorker.onmessage = workerCallback;

workerMessage.MAX_ITERATIONS = Math.round(MAX_ITERATIONS / 2); // MAX_ITERATIONS must always be a (positive) integer.
workerMessage.workerID = "Coarse detail";
globals.coarseDetailWorker.postMessage(workerMessage); // postMessage to the coarse detail Web Worker. 

workerMessage.MAX_ITERATIONS = MAX_ITERATIONS;
workerMessage.workerID = "Fine detail";                
globals.fineDetailWorker.postMessage(workerMessage); // postMessage to the fine detail Web Worker.

Autrement dit, et pour les deux objets de travail, nous spécifions la fonction de rappel (workerCallback) à exécuter quand un thread de travail renvoie un message, sous la forme d’un littéral d’objet, au thread d’interface utilisateur.

Ensuite, nous définissons la valeur workerID vide afin d’identifier ultérieurement le littéral d’objet qui provenait de chaque thread de travail.

Pour finir, nous envoyons les messages de littéral d’objet workerMessage construits précédemment aux threads de travail afin qu’ils soient traités. Ces deux messages sont reçus par le code de thread de travail (non compatible avec le modèle DOM) :

mandelbrotWebWorker.js


/* 
  Recall that a Web Worker JavaScript file has no access to the DOM and, in general, cannot receive DOM related objects.  
*/

self.onmessage = function(evt) {
  var MAX_ITERATIONS = evt.data.MAX_ITERATIONS;
  var ReMax = evt.data.ReMax;
  var ReMin = evt.data.ReMin;
  var ImMax = evt.data.ImMax;
  var ImMin = evt.data.ImMin;
  var grayscaleFactor = evt.data.grayscaleFactor;
  var canvasWidth = evt.data.canvasWidth;
  var canvasHeight = evt.data.canvasHeight;
  var imageDataObject = evt.data.imageDataObject; // This is a copy of, not a reference to, globals.canvas.context.imageDataObject.
  
  var imageDataObjectData = imageDataObject.data; // As a performance optimization, we cache this data array.
  var maxPixelGrayscaleValue = 0; // This will contain the lightest shade of gray in the drawn Mandelbrot image.
  
  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).
      var exponentialSmoothingSum = 0; // Used to color the c-value pixels that are not part of the Mandelbrot set (i.e., tend toward infinity 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.
                      
        exponentialSmoothingSum += Math.exp( -(z_Re_squared + z_Im_squared) ); // Technically, this should be e^(-|z|). However, avoiding the expensive square root operation does not significantly effect the resulting image.              
        if (exponentialSmoothingSum >= 255) { // Don't cycle through the (gray) colors.
          exponentialSmoothingSum = 255;
        } // if

        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).
        var pixelGrayscaleValue = 255 - exponentialSmoothingSum % 256; // Force the value of exponentialSmoothingSum to be between 0 and 255 inclusively. Note that all values for red, green, and blue are identical when using a grayscale.
        var adjustedPixelGrayscaleValue = pixelGrayscaleValue * grayscaleFactor;
        
        imageDataObjectData[currentPixel++] = adjustedPixelGrayscaleValue; // Because we mod by 256, the value of exponentialSmoothingSum will always be between 0 and 255.
        imageDataObjectData[currentPixel++] = adjustedPixelGrayscaleValue; // If exponentialSmoothingSum is 255 (it's maximum possible value), then 255 % 256 = 255.
        imageDataObjectData[currentPixel++] = adjustedPixelGrayscaleValue; // When exponentialSmoothingSum is 255, we have 255 - 255 = 0, so the shade values for RGB are all set to 0 (that is, the c-value pixel is rendered black - indicating that this particular c-value very slowly tends towards infinity).
        imageDataObjectData[currentPixel++] = 255; // Always draw the c-value pixels with no transparency.
        
        if (pixelGrayscaleValue > maxPixelGrayscaleValue) {
          maxPixelGrayscaleValue = pixelGrayscaleValue; // Determine the lightest shade of gray in case the user clicks the Lighten button.
        } // if
      } // if-else
    } // for
  } // for  
  
  self.postMessage({
    imageDataObject: imageDataObject,  
    maxPixelGrayscaleValue: maxPixelGrayscaleValue,
    workerID: evt.data.workerID,
    iterationSum: iterationSum
  });
} // self.onmessage

Le code de thread de travail est contenu dans une seule fonction anonyme :


self.onmessage = function(evt) { ... }

Cette fonction anonyme est appelée lorsqu’un message lui est envoyé à partir du thread d’interface utilisateur. Le message envoyé est contenu dans evt.data. Pour améliorer les performances, les données contenues dans le message sont transférées à des variables locales de threads de travail :


var MAX_ITERATIONS = evt.data.MAX_ITERATIONS;
var ReMax = evt.data.ReMax;
var ReMin = evt.data.ReMin;
var ImMax = evt.data.ImMax;
var ImMin = evt.data.ImMin;
var grayscaleFactor = evt.data.grayscaleFactor;
var canvasWidth = evt.data.canvasWidth;
var canvasHeight = evt.data.canvasHeight;
var imageDataObject = evt.data.imageDataObject; // This is a copy of, not a reference to, globals.canvas.context.imageDataObject.

Le code de calcul de l’image de Mandelbrot est rigoureusement identique au précédent, hormis le fait qu’au lieu de dessiner immédiatement le tableau de données d’image sur la zone de dessin, nous le repassons au thread d’interface utilisateur appelant (avec quelques autres éléments devant être gérés par le thread d’interface utilisateur) :


self.postMessage({
  imageDataObject: imageDataObject,  
  maxPixelGrayscaleValue: maxPixelGrayscaleValue,
  workerID: evt.data.workerID,
  iterationSum: iterationSum
});

Cet appel self.postMessage appelle (et passe le littéral d’objet) le rappel suivant dans le thread d’interface utilisateur :


function workerCallback(evt) { // Receive the required data from the Web Worker to draw the Mandelbrot set to the canvas (plus a few other items).          
  if (fineDetailMandelbrotReceived) {
    return; // For some reason, the fine detail callback finished before the coarse detail callback. Don't display the coarse Mandelbrot image.
  }
  
  ctx.putImageData(evt.data.imageDataObject, 0, 0); // Render our carefully constructed canvas image data array to the canvas.
  globals.canvas.context.imageDataObject = evt.data.imageDataObject; 
  globals.maxPixelGrayscaleValue = evt.data.maxPixelGrayscaleValue; // Store this information in case the user clicks the Lighten button.          

  var elapsedMilliseconds = (new Date()) - startTime;
  elapsedTime.innerHTML = evt.data.workerID + ": " + evt.data.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).            
  
  if (evt.data.workerID == "Fine detail") {
    fineDetailMandelbrotReceived = true;
    messageBox.innerHTML = DEFAULT_MESSAGE; // Erase the "Calculating..." message and replace it with the default message.
  } // if
} // workerCallback


Autrement dit, la fonction workerCallback est appelée par le thread de travail une fois sa tâche terminée. Le littéral d’objet du thread de travail est contenu dans l’objet d’événement associé (evt.data). Les quatre informations passées du thread de travail au thread d’interface utilisateur sont les suivantes :

  • evt.data.imageDataObject - contient l’image de Mandelbrot achevée sous forme de tableau de données de zone de dessin. ctx.putImageData(evt.data.imageDataObject, 0, 0) dessine ensuite l’image sur la zone de dessin (dans le thread d’interface utilisateur).
  • evt.data.maxPixelGrayscaleValue - contient la nuance de gris la plus claire dans l’image de Mandelbrot, qui est généralement nécessaire si l’utilisateur clique sur le bouton Lighten.
  • workerID - indique de quel thread de travail provenait le littéral d’objet. Ceci est nécessaire pour savoir si, par chance, le thread de travail fin (basse résolution) s’est terminé avant le thread de travail grossier (haute résolution) :
    
    if (fineDetailMandelbrotReceived) {
      return; // For some reason, the fine detail callback finished before the coarse detail callback. Don't display the coarse Mandelbrot image.
    }
    
    
    
  • iterationSum - fournit le nombre d’itérations requises pour générer l’image de Mandelbrot demandée. Cette valeur, ainsi que le nombre de secondes nécessaires pour calculer l’image de Mandelbrot (dans le thread de travail Fine detail), fournit une métrique de performance pour l’application Mandelbrot 8.

La disponibilité des threads de travail donne accès à de nombreux algorithmes possibles pour l’amélioration des performances. Par exemple, au lieu de calculer simultanément une image avec basse résolution et une autre avec haute résolution (dans le but d’éviter la présence prolongée d’un écran noir), deux threads de travail pourraient être utilisés pour calculer simultanément la moitié supérieure et la moitié inférieure d’une image de Mandelbrot haute résolution. Cette approche, sans discussion, est illustrée dans Mandelbrot 9 (cliquez avec le bouton droit sur la page pour afficher le code source) - les fichiers JavaScript associés sont mandelbrotWebWorkerManager.js et mandelbrotWebWorker.js.

Dans la prochaine et dernière section, Mandelbrot Explorer, nous allons améliorer le code de la zone d’agrandissement afin de pouvoir dessiner une zone d’agrandissement à partir de n’importe quel coin.

Rubriques connexes

Mandelbrot Explorer

 

 

Afficher:
© 2014 Microsoft