내보내기(0) 인쇄
모두 확장

확대/축소 상자 구현

확대/축소 상자 구현 간소화에서 제공한 정보를 기반으로 여기서는 가능한 Mandelbrot 확대/축소 상자 구현에 대해 설명합니다.

다음은 이 항목에서 제공하는 Mandelbrot 3 예제 인스턴스입니다.

Mandelbrot 3 스크린샷

이 이미지는 Mandelbrot 집합의 특정 영역을 확대하여 생성되었습니다. 원래대로 단추는 작동하지만 밝게저장의 다른 두 단추는 이후 예제에서 구현됩니다.

다음은 이 예제에 대한 코드입니다.

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>

이 예제(Mandelbrot 3)는 다음과 같은 논리 파티션을 사용하여 설명합니다.

태그

다음과 같이 form 요소는 처음에 숨겨져 있으며 사용자가 저장 단추를 클릭하는 경우에만 표시됩니다.


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

다음과 같은 경우도 있습니다.


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

초기화

초기화 섹션은 전역 네임스페이스handleLoad 함수로 구성됩니다.

전역 네임스페이스

전역 네임스페이스는 간단합니다.


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

대문자 전역 변수를 사용하여 상수를 시뮬레이션합니다. 페이지가 로드되면 handleLoad가 발생합니다.

handleLoad

handleLoad 콜백은 대부분의 초기화를 처리합니다.


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

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

adjusted_RE_MAX()는 복소 평면에 대해 선택한 치수(RE_MAX, RE_MIN, IM_MAX, IM_MIN)가 캔버스의 치수에 비례하도록 합니다.

setExtrema는 전역 버전을 주어진 극값으로 설정하고 다음에 설명된 대로 drawMandelbrot는 Mandelbrot 집합을 그립니다.

Mandelbrot 집합 그리기

캔버스 이미지 data array 사용을 제외한 나머지 핵심적인 그리기 알고리즘은 동일합니다.


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

ctx.fillRect(x, y, 1, 1)를 사용하여 검정색 픽셀을 그리는 대신 drawMandelbrot는 캔버스 image object data array (캔버스에서 픽셀 조작 참조)을 설정합니다. 이 배열은 캔버스 이미지를 나타냅니다. 이 배열의 4개 요소는 모두 이미지 픽셀을 나타냅니다. 4개 요소는 0에서 255 사이의 배율(각각 0%에서 100%를 나타냄)을 사용하여 연관된 픽셀의 빨간색, 녹색, 파란색 및 알파 값을 지정합니다. 알파 값 0은 연관된 픽셀의 불투명도가 0%(완전히 투명)이고 알파 값 255는 픽셀의 불투명도가 100%(전혀 투명하지 않음)임을 의미합니다. 따라서 다음에서는 주어진 픽셀을 검정색(색 없음)이나 흰색(모든 색)으로 설정합니다.


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

이미지 데이터 배열을 설정했으면 배열의 이미지를 다음과 같이 캔버스에 그립니다.


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

이전 코드 예제에서 imageDataObjectDatactx.imageDataObject.data에 대한 참조(포인터)이므로 imageDataObjectData를 변경하면 ctx.imageDataObject.data가 변경되어 상위 ctx.imageDataObject도 변경됩니다.

이 섹션을 요약하자면 drawMandelbrot()는 4개의 전역 변수 globals.ReMax, globals.ReMin, globals.ImMaxglobals.ImMin에 지정된 대로 Mandelbrot의 영역을 그립니다. 다음 단계에서는 다음에 설명된 대로 이러한 4개의 전역 변수를 확대/축소 상자에 지정된 값으로 설정합니다.

이벤트 처리

첫 번째 단계에서는 handleLoad 함수 내에 이벤트 처리기를 등록합니다.


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


이제 캔버스에서 마우스 이벤트가 발생하면 다음 코드가 호출됩니다.


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

확대/축소 상자를 만드는 이벤트 흐름은 다음과 같습니다.

mousedown

mouse down 이벤트의 위치는 확대/축소 상자의 왼쪽 위 모서리(또는 한 번 클릭하는 경우 고정 크기 확대/축소 상자의 중심)를 나타내므로 나중에 사용할 수 있도록 이 정보를 기록합니다.


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

mousemove

이 경우 사용자는 클릭하여 끌기 작업을 통해 확대/축소 상자를 지정하는 것입니다. ctx.fillRect를 사용하여 반투명 확대/축소 상자를 무중단 방식으로 그리려면 확대/축소 상자의 치수가 변경될 때마다 ctx.putImageData를 사용하여 기본 Mandelbrot 이미지를 다시 그려야 합니다.


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;

좌표 변환 방정식은 비례를 가정하므로(화면 좌표를 복소 평면에 매핑 참조) 다음과 같이 확대/축소 상자의 치수가 캔버스 치수에 비례하여 유지되도록 합니다.


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

즉, 사용자가 지정한 확대/축소 상자의 높이를 먼저 계산한 후 캔버스의 너비/높이 비율을 곱합니다. 이렇게 하면 비례가 보장됩니다.

mouseup

mouse down 처리기는 확대/축소 상자의 왼쪽 위 모서리 좌표를 기록합니다. mouse move 처리기는 반투명 확대/축소 상자를 그립니다. mouse up 처리기는 확대/축소 상자의 오른쪽 아래 모서리(또는 중심) 좌표를 기록한 다음 Mandelbrot 집합의 지정된 영역을 다음과 같이 그립니다.


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;

먼저 확대/축소 상자의 (비례) 크기를 계산합니다.


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

zoomBoxHeight가 0이면 사용자가 한 번 클릭 작업을 수행한 것입니다. 즉, 사용자가 확대/축소 상자를 지정하지 않았습니다. 이 경우 mouse up 위치를 기준으로 중심에 있는 고정 크기의 확대/축소 상자를 그릴 수 있습니다.


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


예제를 통해 알 수 있듯이 xToReyToIm은 지정된 캔버스 화면 좌표를 복소 평면의 연관된 점으로 변환합니다(화면 좌표를 복소 평면에 매핑 참조).

그렇지 않으면 확대/축소 상자의 왼쪽 위 모서리와 오른쪽 아래 모서리를 유사한 복소 평면 좌표로 변환합니다.


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

JavaScript의 쿼크 중 하나는 포함하는 코드 블록이 종료된 후까지 요소의 내용이 변경되지 않는다는 것입니다. 따라서 화면에 바로 "계산 중..."이 표시되도록 하려면 포함하는 drawMandelbrot 블록이 바로 종료되도록 해야 합니다. 이 작업에는 setTimeout(또는 사용 가능한 경우 setImmediate)을 사용합니다:


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

즉, drawMandelbrot는 포함하는 블록(handlePointer)이 종료되는 즉시 실행됩니다. 그런 다음 완료되면 drawMandelbrot에서 "계산 중..."을 "확대/축소하려면 클릭하거나 클릭하여 끄십시오."로 다시 변경합니다.

참고  drawMandelbrotglobals.ImMin을 통해 globals.ReMax를 사용하므로 drawMandelbrot를 호출하기 전에 setExtrema를 사용하여 이러한 값을 설정해야 합니다. setExtrema 사용은 간단합니다.


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

하지만 이렇게 하면 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

요약하지만 Mandelbrot 집합의 특정 영역은 확대/축소 상자를 사용하여 지정합니다. 확대/축소 상자의 왼쪽 위 및 오른쪽 아래 캔버스 화면 좌표는 유사한 복소 평면 좌표로 변환됩니다. 그런 다음 setExtremadrawMandelbrot에서 이러한 유사한 좌표를 사용하여 Mandelbrot 집합의 지정된 영역을 캔버스로 렌더링합니다. 그러나 렌더링된 이미지는 완전히 흑백입니다. 렌더링된 이미지를 개선하기 위해 Mandelbrot 이미지에 회색조 추가에 설명된 대로 255개의 회색 음영을 추가합니다.

관련 항목

Mandelbrot 이미지에 회색조 추가

 

 

표시:
© 2014 Microsoft