Mandelbrot Explorer
Building on information presented in Leveraging web workers, here we learn how to improve Mandelbrot 8 to produce the final version - Mandelbrot Explorer.
We made the following improvements to Mandelbrot 8 to produce Mandelbrot Explorer:
- Drawing status: The unobtrusive
calculating...
message is replaced with a more visible animation positioned in the center of the canvas. - Zoom box drawing: You can now draw a zoom box starting from any corner and its animation performance is improved.
- Web Workers fallback: Although supporting Web Workers provides for faster performance, the browser need not support Web Workers for Mandelbrot Explorer to work.
Drawing status
To help confirm for you that your Mandelbrot image is indeed being drawn, the upper-left calculating...
message is replaced with an animation centered in the middle of the canvas by using the <progress>
element. To position the progress
element in the center of the canvas, use the following HTML:
<div id="canvasWrapper">
<progress id="progressIndicator"></progress>
<canvas width="600" height="400" oncontextmenu="return false;">
Canvas not supported - upgrade your browser (after checking that your browser is in the correct mode).
</canvas>
</div>
The <div id="canvasWrapper">
wrapper is used to easily find the center of the canvas and position the progress
element relative to it, using the following CSS (the comments explain each CSS property):
##canvasWrapper {
position: relative; /* Allows any absolutely positioned child element to be positioned relative to this (parent) element. */
width: 604px; /* Account for the two 2px wide borders of the 600px wide canvas element. */
margin: 0 auto; /* Center the canvas wrapper on the page. */
}
progress {
display: block; /* For browsers that do not recognize <progress> tags, this makes them work as expected (i.e., as a generic DIV element would). */
}
#progressIndicator {
color: red; /* Make the progress bar highly visible. */
display: none; /* Only display the <progress> tag when the Mandelbrot set is being calculated. */
position: absolute; /* This is positioned relative to the #canvasWrapper DIV element. */
width: 400px; /* The width of the progress bar. */
height: 20px; /* The height of the progress bar. */
left: 102px; /* The canvas is 600px wide, the progress bar is 400px wide, so to center the progress bar, the progress bar must start 100px from the left of the canvas plus a 2px border. */
top: 192px; /* The canvas is 400px high, the progress bar is 20px tall, so to center the progress bar, the progress bar must start 190px from the top of the canvas plus a 2px border. */
}
Now, to display the progress indicator, we programmatically toggle its display
CSS property from progressIndicator.style.display = "none"
to progressIndicator.style.display = "block"
.
Zoom box drawing
One of the problems with the existing zoom box implementation is that you must draw the zoom box from the upper-left corner down towards the lower-right. Here we describe how to remove this limitation so that the user can draw a zoom box starting from any corner:
As you can see, a zoom box can be defined by starting from any corner (x₁, y₁) and ending at the opposite corner (x₂, y₂).
Because zoom box processing is handled by the handlePointer
event handler, handlePointer
is modified as indicated by the red font in the following before-and-after table:
The first difference is the line var point = {x: 0, y: 0}
. This object ends up containing the upper-left corner point of the zoom box regardless of how the zoom box was drawn.
Next, we wrap the down event clause in an if
statement to ensure that the down event code only executes one time:
if (!globals.pointer.down) {
globals.pointer.down = true;
globals.pointer.x1 = canvasX;
globals.pointer.y1 = canvasY;
}
In the move event clause, we use requestAnimationFrame
to improve the animation performance of the zoom box by only drawing a zoom box (and the underlying Mandelbrot image) at appropriate intervals:
case 'MSGestureChange':
case 'mousemove':
if (globals.pointer.down) {
zoomBoxHeight = Math.abs(canvasY - globals.pointer.y1);
zoomBoxWidth = zoomBoxHeight * canvasWidthHeightRatio;
if (window.requestAnimationFrame.id) {
window.cancelAnimationFrame(window.requestAnimationFrame.id);
}
window.requestAnimationFrame.id = window.requestAnimationFrame(function() {
ctx.putImageData(ctx.imageDataObject, 0, 0);
point = getTopLeftZoomBoxPoint(globals.pointer.x1, globals.pointer.y1, canvasX, canvasY);
ctx.fillRect(point.x, point.y, zoomBoxWidth, zoomBoxHeight);
});
}
break;
In handleLoad
, if requestAnimationFrame
is not supported, setTimeout
is used instead:
if (!window.requestAnimationFrame) {
window.cancelAnimationFrame = function(ID) {
window.clearInterval(ID);
}
window.requestAnimationFrame = function(callback) {
return window.setTimeout(callback, 16.7);
}
}
As you can see in the move event clause, during a move event, getTopLeftZoomBoxPoint
is called, returning the upper-left corner of the zoom box so the zoom box can be drawn using fillRect
:
point = getTopLeftZoomBoxPoint(globals.pointer.x1, globals.pointer.y1, canvasX, canvasY);
ctx.fillRect(point.x, point.y, zoomBoxWidth, zoomBoxHeight);
This function is defined as follows:
function getTopLeftZoomBoxPoint(x1, y1, x2, y2) {
/*
(x1, y1) is where the down pointer event occurred. (x2, y2) is where the move or up pointer event occurred.
*/
if (x1 <= x2) {
if (y1 <= y2) {
return {x: x1, y: y1}; // User has drawn (or is drawing) a zoom box from the upper-left toward the lower-right.
} else { // y1 > y2
return {x: x1, y: y1 - zoomBoxHeight}; // User has drawn (or is drawing) a zoom box from the lower-left toward the upper-right.
} // if-else
} else { // x1 > x2
if (y1 <= y2) {
return {x: x1 - zoomBoxWidth, y: y1}; // User has drawn (or is drawing) a zoom box from the upper-right toward the lower-left.
} else { // y1 > y2
return {x: x1 - zoomBoxWidth, y: y1 - zoomBoxHeight}; // User has drawn (or is drawing) a zoom box from the lower-right toward the upper-left.
} // if-else
} // if-else
} // getTopLeftZoomBoxPoint
You can understand the if-else
clauses by reviewing this image, which shows the four possible ways to draw a zoom box from corner-to-corner:
Consider the case where the user starts from the lower-left and ends at the upper-right (that is, x₁ < x₂ and y₁ > y₂). In this case, getTopLeftZoomBoxPoint
returns the point {x: x1, y: y1 - zoomBoxHeight}
because x₁ is the x-coordinate for the upper-left corner of the zoom box and y1 - zoomBoxHeight
is the y-coordinate for the upper-left corner of the zoom box. The three remaining possibilities for drawing a zoom box are similarly calculated.
Returning to handlePointer
, the primary change to the up event clause is the globals.holdGesture
check:
if (zoomBoxHeight == 0 || globals.holdGesture)
If during a press-and-hold gesture an up event occurs, we execute the same code as when no zoom box was drawn (that is, zoomBoxHeight == 0
). Note that globals.holdGesture
is initialized to false
in the handleLoad
event (the MSGestureHold
event clause, sets globals.holdGesture
to true
).
Web Workers
Mandelbrot 8 simply announces that Web Workers is required and terminates - not a great user experience. To accommodate browsers that don't support Web Workers, we borrow the non-worker Mandelbrot code from Mandelbrot 7 (renaming it drawMandelbrotWithoutWebWorkers
) and call it, if necessary, in handleHashChange
:
if (window.Worker) {
initializeWebWorkers('mandelbrotWebWorker.js');
drawMandelbrotWithWebWorkers(globals.ReMax, globals.ReMin, globals.ImMax, globals.ImMin, globals.grayscaleFactor);
}
else {
drawMandelbrotWithoutWebWorkers(globals.ReMax, globals.ReMin, globals.ImMax, globals.ImMin, globals.grayscaleFactor);
}
In summary, the How to explore the Mandelbrot set using HTML5 tutorial has demonstrated the following key HTML5 features:
These features, along with a few others, were jointly used to create a performant web app capable of exploring one of the most complex and beautiful mathematical objects known - the Mandelbrot set. The following table provides links to some particularly interesting/unusual regions of the Mandelbrot set to help motivate your own exploration of this remarkable set: