"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); } }