2022-07-04 00:17:36 +00:00
"use strict" ;
//
// This program graphs the Mandelbrot set and the burning ship fractal.
//
let width , height = 0 ;
// Set default viewing point
let view = [ - 0.75 , 0 ] ;
let canvas , context , imagedata , viewWidth ;
// Event called every time the page loads or reloads
window . onload = function ( ) {
canvas = document . getElementById ( "viewport" ) ;
width = canvas . width ;
height = canvas . height ;
// Obtain the 2d graphics context from the canvas
context = canvas . getContext ( "2d" ) ;
// create an image data object that will be set with pixels later
imagedata = context . createImageData ( width , height ) ;
render ( ) ;
// Register event to trigger on click of the canvas
canvas . addEventListener ( "mousedown" , function ( e )
{
clickCanvas ( e ) ;
} ) ;
} ;
function clickCanvas ( event ) {
// Get mouse position on canvas
const rect = canvas . getBoundingClientRect ( ) ;
const x = event . clientX - rect . left ;
const y = event . clientY - rect . top ;
// Convert canvas coordinates to usable coordinates within the coordinate system
const yu = ( viewWidth / height * ( 2 * ( height - y ) - height ) ) + view [ 1 ] ;
const xu = ( viewWidth / width * ( 2 * x - width ) ) + view [ 0 ] ;
// Update the viewing area
view = [ xu , yu ] ;
// Re-render the canvas
render ( ) ;
}
function render ( ) {
// Store the starting time of render so that it can be subtracted from the end time to find the render time.
const startTime = performance . now ( ) ;
// Read the value of the zoom slider, then scale it down.
const zoomv = document . getElementById ( "zoom" ) . value / 100 ;
// Read the value of the iterations slider
const maxIterations = document . getElementById ( "iterations" ) . value ;
// Get value of radio button to see which mode will be used for coloring
const colorMode = document . getElementById ( "mode" ) . options [ document . getElementById ( "mode" ) . selectedIndex ] . value ;
// Determine whether or not mandelbrot or burning ship
const fractalType = document . getElementById ( "type" ) . options [ document . getElementById ( "type" ) . selectedIndex ] . value ;
// Adjust the viewWidth the be : viewWidth = 1 / 10^zoom
viewWidth = Math . pow ( 10 , - zoomv ) ;
// Loop through every pixel on the canvas representing points on the complex plane.
// X values, (Real)
for ( let x = 0 ; x <= width ; x ++ ) {
// Y values, (Imaginary)
for ( let y = 0 ; y <= height ; y ++ ) {
// Reset the colors for each individual pixel after a loop
let red , green , blue = 0 ;
// xu and yu represent the coordinate system adjusted values of x and y (canvas values)
// This is needed as the top left of the canvas is considered (0,0). Without this, only the 4th quadrant would be rendered, and there would be no zoom.
const yu = ( viewWidth / height * ( 2 * ( height - y ) - height ) ) + view [ 1 ] ;
const xu = ( viewWidth / width * ( 2 * x - width ) ) + view [ 0 ] ;
let x2 , y2 = 0 ;
// Set up the first iteration of the fractal before looping through the rest.
// this may not be necesary, but I found that I needed to do this to get it to work right.
// There may be another way to do this though.
let x1 = ( xu * xu ) + xu - ( yu * yu ) ;
let y1 = ( 2 * xu * yu ) + yu ;
// Itterate
for ( let i = 0 ; i < maxIterations ; i ++ ) {
// Update x and y following z = z^2 + c
x2 = ( x1 * x1 + xu - y1 * y1 ) ;
y2 = ( 2 * x1 * y1 + yu ) ;
// Set Inital values to be the ones calculated. If the mode is set to 1,
// render the burning ship fractal. (A close cousin to the Mandelbrot set)
if ( fractalType == 1 ) {
x1 = x2 ;
y1 = y2 ;
} else {
x1 = Math . abs ( x2 ) ;
y1 = - Math . abs ( y2 ) ;
}
// Find the distance between the point on last iteration and origin
2022-07-05 05:31:56 +00:00
const distance = x2 * x2 + y2 * y2 ;
2022-07-04 00:17:36 +00:00
// If distance from origin is greater than 2, will always expand to
// infinity. In that case, end iteration.
2022-07-05 05:31:56 +00:00
if ( distance > 4 ) {
2022-07-04 00:17:36 +00:00
// This section colors in the point based entirely on the i value.
// There is not a science to this, I just found some combinations
// of trig functions and other nonsense that looks acceptable.
// Coloring Mode 1
if ( colorMode == 2 ) {
i = ( i / maxIterations ) * 255 ;
if ( i < ( 2 / 3 * maxIterations ) ) {
i = i + ( Math . pow ( ( i - ( 2 / 3 * maxIterations ) ) , 2 ) / ( maxIterations * 0.8 ) ) ;
}
red = i * ( Math . sin ( i ) ) ;
blue = i * ( 0 - Math . sin ( i ) ) ;
green = i * ( Math . sin ( i * 2 ) ) ;
}
// Coloring Mode 2
if ( colorMode == 1 ) {
i = i / maxIterations ;
red = i * 20 + Math . abs ( Math . cos ( 2 * i ) * 230 ) ;
blue = Math . sqrt ( i * 10 ) + Math . abs ( Math . sin ( 2 * i + 1 / 4 ) * 230 ) ;
green = Math . sqrt ( i * 10 ) + Math . abs ( Math . sin ( 10 * i ) * 105 ) ;
}
// End looping after coloring.
break ;
}
}
// Store pixel data into imagedata
const pixelindex = ( y * width + x ) * 4 ;
imagedata . data [ pixelindex + 2 ] = blue ;
imagedata . data [ pixelindex + 1 ] = green ;
imagedata . data [ pixelindex ] = red ;
imagedata . data [ pixelindex + 3 ] = 255 ;
}
}
// Render the image to the canvas
// For some reason chrome did not like placing the imagedata directly onto the canvas.
// Offsetting it by one pixel fixed it somehow
context . putImageData ( imagedata , 0 , 1 ) ;
// Calculate time taken to render image
const renderTime = performance . now ( ) - startTime ;
// Add additional info as text at the top left
context . font = "14px serif" ;
context . strokeStyle = "white" ;
context . fillStyle = "white" ;
context . fillText ( "Re = " + view [ 0 ] , 5 , 19 ) ;
context . fillText ( "Im = " + view [ 1 ] , 5 , 2 * 19 ) ;
context . fillText ( "width = " + viewWidth , 5 , 3 * 19 ) ;
context . fillText ( "Iterations:" + maxIterations , 5 , 4 * 19 ) ;
if ( renderTime > 1000 ) {
context . fillText ( "time = " + Math . round ( renderTime / 100 ) / 10 + "s" , 5 , 5 * 19 ) ;
} else {
context . fillText ( "time = " + Math . round ( renderTime ) + "ms" , 5 , 5 * 19 ) ;
}
}