mandelbrot-js/index.js

167 lines
6.4 KiB
JavaScript

"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
const distance = x2 * x2 + y2 * y2;
// If distance from origin is greater than 2, will always expand to
// infinity. In that case, end iteration.
if (distance > 4) {
// 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);
}
}