/* * Copyright (c) Joshua John Lawrence 2012-2014, all rights reserved. */ /* * A class for rendering Mandelbrot sets. */ function Mandelbrot( maxIterations ) { /* * Private helper. Translate the number of iterations performed on a * point in the complex plane into a color. An optimisation would be * to store the colors in an array and reuse them, rather than create * a new one each time. An extension would be to present an API to * allow the color mapping to be changed. * * Argument should be numeric. Returns a Color. */ function lookupColor( iterations ) { if( iterations == maxIterations ) return Color.BLACK; return new Color( 210 - iterations % 200, 220 - iterations % 200, 255 - ( iterations % 200 ) / 3 ); } /* * Private helper. Iterate a point in the complex plane using Mandelbrot's formula. * * Argument should be numeric. Returns the number of iterations. */ function countIterations( c ) { var iterations = 1; var z = c.copy(); // Seeing as we are using mutators below, ensure z and c are not aliases. while( z.modulusSquared() < 4 && iterations < maxIterations ) { // This optimisation gains an order of magnitude speed increase. // z = z.squared().plus( c ); z.inlineSquaredPlus( c ); ++iterations; } return iterations; } /* * Private helper. Find the color a point in the complex plane should be drawn. * * Argument is Complex. Returns a Color. */ function findColor( c ) { return lookupColor( countIterations( c ) ); } /* * Set the maximum number of iterations to attempt. Argument should be numeric. */ this.setMaxIterations = function( newMaxIterations ) { maxIterations = newMaxIterations; }; /* * Draw a Mandelbrot set filling the PixelCanvas and using the Transform to * map pixels to points in the complex plane. I'm using my own Transform * class instead of the built-in canvas transform operations because I need * to apply the transform manually when working out how to zoom. */ this.draw = function( transform, pixelCanvas ) { var x = 0; // var t0 = new Date().getTime(); // Like a for loop over x, but with a break in between each column of // pixels to allow the screen to update. Doing it this way provides visual // feedback of progress, but does create some overhead. An extension // would be to do blocks of columns at a time, adapting the block width // depending on image complexity. var interval = setInterval( function() { for( var y = 0; y < pixelCanvas.getHeight(); ++y ) { var screenPos = new Vector( x, y ); var worldPos = transform.screenToWorld( screenPos ); pixelCanvas.drawPixel( screenPos, findColor( new Complex( worldPos.getX(), worldPos.getY() ) ) ); } pixelCanvas.paint(); ++x; if( x >= pixelCanvas.getWidth() ) { clearInterval( interval ); // alert( "Done in " + ( new Date().getTime() - t0 ) ); } }, 0 ); } } /* * The user interface and starting mandelbrot set. */ try { // The canvases. var canvas = document.getElementById( "theCanvas" ); var uiOverlay = document.getElementById( "theUiOverlay" ); // Sanity checking. if( !canvas ) { throw "Could not find the canvas"; } if( !canvas.getContext ) { throw "Could not find getContext in canvas"; } if( !uiOverlay ) { throw "Could not find the UI overlay"; } if( !uiOverlay.getContext ) { throw "Could not find getContext in the UI overlay"; } // Find 2D contexts. var context = canvas.getContext( "2d" ); if( !context ) { throw "Could not get a 2d context for the canvas"; } var overlayContext = uiOverlay.getContext( "2d" ); if( !overlayContext ) { throw "Could not get a 2d context for the UI overlay"; } /* * The UI. */ // UI variables. var highlight = false; overlayContext.strokeStyle = "#505050"; overlayContext.lineWidth = 0; /* * Helper for mouse event handlers. Get the current mouse position in pixels * from the corner of the UI canvas. The argument is a mouse event. */ function getMousePosition( event ) { if( event.offsetX || event.offsetX == 0 ) { return new Vector( Math.floor( event.offsetX ), Math.floor( event.offsetY ) ); } else if( event.layerX || event.layerX == 0 ) { return new Vector( Math.floor( event.layerX ), Math.floor( event.layerY ) ); } } /* * Handle mouse down. Start dragging a rectangle, storing the size and position * in highlight. The argument is a mouse event. */ function mouseDown( event ) { var mousePosition = getMousePosition( event ); highlight = { x: mousePosition.getX(), y: mousePosition.getY(), w: 0, h: 0 }; // Mouse events. uiOverlay.removeEventListener( "mousedown", mouseDown, true ); uiOverlay.addEventListener( "mousemove", mouseMove, true ); uiOverlay.addEventListener( "mouseup", mouseUp, true ); // Touch events. uiOverlay.removeEventListener( "touchstart", mouseDown, true ); uiOverlay.addEventListener( "touchmove", mouseMove, true ); uiOverlay.addEventListener( "touchend", mouseUp, true ); // Prevent scrolling on mobile browsers. event.preventDefault(); } /* * Handle mouse move. Update highlight and the display. */ function mouseMove( event ) { var mousePosition = getMousePosition( event ); if( highlight ) { highlight.w = mousePosition.getX() - highlight.x; highlight.h = mousePosition.getY() - highlight.y; } updateOverlay(); // Prevent scrolling on mobile browsers. event.preventDefault(); } /* * Handle mouse up. Clear highlight, update the trandform and redraw the Mandelbrot set. */ function mouseUp( event ) { var a = transform.screenToWorld( new Vector( highlight.x, highlight.y ) ); var b = transform.screenToWorld( new Vector( highlight.x + highlight.w, highlight.y + highlight.h ) ); transform = new Transform( new Vector( Math.min( a.getX(), b.getX() ), Math.min( a.getY(), b.getY() ) ), new Vector( Math.max( a.getX(), b.getX() ), Math.max( a.getY(), b.getY() ) ), new Vector( 0, pixelCanvas.getHeight() ), new Vector( pixelCanvas.getWidth(), 0 ) ); mandelbrot.draw( transform, pixelCanvas ); highlight = false; updateOverlay(); // Mouse events. uiOverlay.addEventListener( "mousedown", mouseDown, true ); uiOverlay.removeEventListener( "mousemove", mouseMove, true ); uiOverlay.removeEventListener( "mouseup", mouseUp, true ); // Touch events. uiOverlay.addEventListener( "touchstart", mouseDown, true ); uiOverlay.removeEventListener( "touchmove", mouseMove, true ); uiOverlay.removeEventListener( "touchend", mouseUp, true ); // Prevent scrolling on mobile browsers. event.preventDefault(); } // Initialize the UI event handlers. uiOverlay.addEventListener( "mousedown", mouseDown, true ); uiOverlay.addEventListener( "touchstart", mouseDown, true ); /* * Redraw the dragged rectangle. */ function updateOverlay() { overlayContext.clearRect( 0, 0, uiOverlay.width, uiOverlay.height ); if( highlight ) { overlayContext.strokeRect( highlight.x + 0.5, highlight.y + 0.5, highlight.w, highlight.h ); } } /* * Update the maximum number of iterations to attempt for a pixel and redraw the * Mandelbrot set. Argument should be numeric. */ function changeMaxIterations( newMaxNumIterations ) { mandelbrot.setMaxIterations( Number( newMaxNumIterations ) ); mandelbrot.draw( transform, pixelCanvas ); } /* * Reset the state of and redraw the Mandelbrot set. */ function reset() { var mandelbrotIterations = document.getElementById( "mandelbrotIterations" ); if( !pixelCanvas ) { // The UI code probably hit an exception, which means we are probably in Internet Explorer, and // nothing can save us. return; } transform = new Transform( new Vector( -2.2, -1.2 ), new Vector( 1.0, 1.2 ), new Vector( 0, pixelCanvas.getHeight() ), new Vector( pixelCanvas.getWidth(), 0 ) ); mandelbrotIterations.value = DEFAULT_ITERATIONS; // This doesn't actually call the onchange handler. changeMaxIterations( mandelbrotIterations.value ); } // Initialise and draw a starting mandelbrot set. var DEFAULT_ITERATIONS = 100; var pixelCanvas = new PixelCanvas( context ); var transform; var mandelbrot = new Mandelbrot( DEFAULT_ITERATIONS ); } catch( e ) { // We are probably in Internet Explorer, and nothing can save us. }