정보
요청한 주제가 아래에 표시됩니다. 그러나 이 주제는 이 라이브러리에 포함되지 않습니다.

Mandelbrot 이미지에 회색조 추가

확대/축소 상자 구현에서 제공한 정보를 기반으로 여기서는 255개의 가능한 회색 음영을 Mandelbrot 집합의 흑백 이미지에 추가하는 방법에 대해 설명합니다.

Mandelbrot 집합의 순수한 흑백 이미지는 그 자체로는 아름답지만 중요한 수학적 정보가 부족합니다. 특히, 특정 반복계수가 무한을 향해 나선 이동하는 속도와 같은 정보가 부족합니다.

흑백 이미지에서 검은색은 점이 Mandelbrot 집합의 구성원임을 나타내고 흰색은 구성원이 아님을 나타냅니다. 다음 이미지에서도 그대로 적용되지만 점이 어두운 회색 음영일 경우 점(점의 계수)은 비교적 느린 속도로 무한으로 기울어집니다. 그러나 점에 밝은 회색 음역이 있을 경우 점(점의 계수)은 비교적 빠른 속도로 무한으로 기울어집니다. 이러한 추가 회색조 정보를 사용하면 약간 더 아름다운 이미지가 생성될 수 있습니다.

Mandelbrot 4 스크린샷

문제는 특정 점(점의 계수)이 무한을 향해 나선 이동하는 속도를 확인하는 방법입니다. 간단하게 확인하는 방법은 각 반복에서 점 계수의 합계를 구한 후 다음을 수행하는 것입니다.

  • 합계가 비교적 큰 경우 연결된 픽셀을 어두운 회색 음영으로 칠합니다.
  • 합계가 비교적 작을 경우 연결된 픽셀을 밝은 회색 음영으로 칠합니다.

그러나 이 방법을 사용하면 매력 없는 회색 띠가 생성될 수 있습니다. 이러한 띠가 생성되지 않도록 하려면 지수 평활법이라는 비교적 간단한 방법을 사용할 수 있습니다. 이상적으로 Mandelbrot 집합의 지정된 z 값을 반복하여 집합의 구성원이지 여부를 확인할 때 다음 값의 합계를 구합니다.

지수 평활 식

이 식에는 z의 비교적 큰 절대값이 총 합계에 거의 영향을 주지 않는 속성이 있습니다. 다음 코드 조각을 사용하여 이를 확인할 수 있습니다.

지수 평활법


function drawMandelbrot() {     
  .
  .
  .

  var iterationSum = 0;
  var currentPixel = 0;          
  for (var y = 0; y < canvasHeight; y++) {
    var c_Im = (y * y_coefficient) + ImMax;
    
    for (var x = 0; x < canvasWidth; x++) {
      var c_Re = (x * x_coefficient) + ReMin 
      
      var z_Re = 0;
      var z_Im = 0;
      
      var c_belongsToMandelbrotSet = true;
      var exponentialSmoothingSum = 0;
      for (var iterationCount = 1; iterationCount <= MAX_ITERATIONS; iterationCount++) {
        iterationSum++; 
      
        var z_Re_squared = z_Re * z_Re; 
        var z_Im_squared = z_Im * z_Im; 
        
        exponentialSmoothingSum += Math.exp( -(z_Re_squared + z_Im_squared) ); // Technically, this should be e^(-|z|). However, avoiding the expensive square root operation doesn't really effect the resulting image.              
        if (exponentialSmoothingSum >= 255) { // Don't cycle through the gray colors.
          exponentialSmoothingSum = 255;
        }

        if (z_Re_squared + z_Im_squared > 4) { 
          c_belongsToMandelbrotSet = false;
          break;
        }
        
        z_Im = (2 * z_Re * z_Im) + c_Im;
        z_Re = z_Re_squared - z_Im_squared + c_Re;
      }
      
      if (c_belongsToMandelbrotSet) {
        // DRAW A BLACK PIXEL HERE.
      } 
      else { 
        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.
        
        // DRAW A PIXEL USING A SHADE VALUE OF pixelGrayscaleValue HERE.
      }
    }
  }
  
  .
  .
  .
} // drawMandelbrot

z 값에 대해 exponentialSmoothingSum을 0으로 설정합니다. 그런 다음 z를 반복할 때 다음을 수행합니다.


exponentialSmoothingSum += Math.exp( -(z_Re_squared + z_Im_squared) );

기술적으로는 Math.exp( -Math.sqrt(z_Re_squared + z_Im_squared) )가 되어야 하지만 비용이 많이 드는 제곱근 연산을 제거해도 결과 이미지에 거의 영향을 주지 않으므로 성능상의 이유로 그대로 유지합니다.

많은 Mandelbrot 색 지정 구성표에서는 z의 계수가 증가하면 색이 순환됩니다. 다음을 사용하여 이 동작을 방지합니다.


if (exponentialSmoothingSum >= 255) { 
  exponentialSmoothingSum = 255;
}

캔버스는 0에서 255 사이의 색 값만 지원하므로 지수 합계가 다음 정수 값 중 하나가 되도록 합니다.


var pixelGrayscaleValue = 255 - exponentialSmoothingSum % 256;

위의 회색조 음영을 제외하면 Mandelbrot 3은 다음과 같이 Mandelbrot 4와 동일합니다.

Mandelbrot 4


<!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 4</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 4</h1>
  <p>This example demonstrates how to create grayscale Mandelbrot images.</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 its 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 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).
          var exponentialSmoothingSum = 0;
          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 doesn't really effect the resulting image.              
            if (exponentialSmoothingSum >= 255) { // Don't cycle through the gray colors.
              exponentialSmoothingSum = 255;
            }
    
            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 always tends 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).
            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.
            
            imageDataObjectData[currentPixel++] = pixelGrayscaleValue; // Red
            imageDataObjectData[currentPixel++] = pixelGrayscaleValue; // Green
            imageDataObjectData[currentPixel++] = pixelGrayscaleValue; // Blue
            imageDataObjectData[currentPixel++] = 255; // Alpha
          } // 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>

현재까지의 모든 작업 예제에서 보다 성가신 문제 중 하나는 브라우저의 뒤로 및 앞으로 단추가 아무 작업도 수행하지 않는다는 점입니다. 이 문제는 다음에 나오는 뒤로 및 앞으로 단추 사용에서 해결합니다.

관련 항목

뒤로 및 앞으로 단추 사용

 

 

표시:
© 2014 Microsoft