The mandelbrot set

This DCP app demonstrates how to create a mandelbrot set zoom GIF in a distributed manner using the Distributive Compute Protocol (DCP). If you have a good understanding of how DCP works and the terminology already please feel free to skip the refresher and jump straight to the background.

A refresher

Click to see the refresher

DCP is a distributed computing framework made from web-based technology.

Computers and devices in classrooms, computer labs, households, and enterprises become computing clusters with a single click with DCP. The entire workload, a job, is sub-divided into smaller parts, called slices. Slices execute in parallel using DCP. Each slice of the job corresponds to a frame in the final mandelbrot zoom. Slices deploy to different Workers (for example, laptops, desktops, cell phones, etc.) which contain sandboxes. A sandbox is a clean environment on your Worker that runs your work function.

DCP is a powerful parallel computing framework that allows users to express a computational job as effortlessly as:

job = compute.for(inputSet, workFunction);

resultSet = await job.exec();

A workFunction is mapped onto each element in an inputSet. Each mapping represents a slice. Slices are distributed across DCP networks for computation. Results are returned to the user from DCP networks coherently.

Remember, the work function must contain progress();. Progress is a call made to tell the scheduler that the job is still alive and running. Progress is considered the heart beat of the job, without it the job would die and no results would be returned.

  1. Create compute nodes: go to https://dcp.work on as many devices as you want and click Start to join the public Compute Group, or to dcp.work/joinKey if you have a private Compute Group joinKey and joinSecret.

  2. Configure dev environment: load dcp-client and any required packages.

  3. Specify the inputSet: an arbitrary, but enumerable input dataset (parameters, mp3 files, images, blender project file, etc)

  4. Specify the workFunction: an arbitrary work function (physics simulation, inference model, rendering process, etc)

  5. Express the job: Map the workFunction onto the inputSet with

    job = compute.for(inputSet, workFunction);
    
  6. (optional) Specify a Compute Group with

    job.computeGroups = [{ joinKey: 'name', joinSecret: 'passphrase' }];
    
  7. Await the resultSet: Execute the job in parallel on DCP via

    resultSet = await job.exec();
    

Background

The mandelbrot set is a set of complex numbers that gained popularity because of the beautiful fractal images it produces. You can find videos online of infinite mandelbrot zooms with appealing colours. This tutorial walks you through creating your own mandelbrot zoom.

To begin, the mandelbrot set is the set of complex numbers c for which the following function, \(f_c (z) = z^2 + c\) doesn’t diverge when iterated from z = 0.

There exist two cases with this function,

  1. The value gets arbitrarily large

  2. The value gets bounded by 2

Here are examples of both of these cases.

  • c = 1

\(f_1 (0) = 0^2 + 1 = 1\)

\(f_1 (1) = 1^2 + 1 = 2\)

\(f_1 (2) = 2^2 + 1 = 5\)

\(f_1 (5) = 5^2 + 1 = 26\)

Here the values blow up past 2 and so c = 1 isn’t in the mandelbrot set.

  • c = -1

\(f_{-1} (0) = 0^2 + (-1) = -1\)

\(f_{-1} (-1) = -1^2 + (-1) = 0\)

\(f_{-1} (0) = 0^2 + (-1) = -1\)

\(f_{-1} (-1) = -1^2 + (-1) = 0\)

C =-1 continues to repeat and never blow up past 2, hence c =-1 is in the mandelbrot set.

Now that you understand what the mandelbrot set is, it’s time to start creating your own mandelbrot set zoom on DCP. Establish a canvas size and run each point in that canvas through the function \(f_c(z)\). Each point iterates through that function until it goes toward infinity or it reaches a set max number of iterations without the points value going passed 2. Then the colour of each point depends on what category it falls into, going up to infinity or staying below 2. This process creates one frame. To create a zoom these steps repeat for any number of canvases as desired.

Prep installation

Before you begin, set up your workspace.

  1. Download VS Code

  2. Install npm

  3. Install node npm and node In terminal

  4. npm init

  5. npm install sharp

  6. npm install png-file-stream

  7. npm install gifencoder

  8. npm install fs

  9. Set up DCP

Setting up files

Start by creating a new folder called “mandelbrot-set.” Then create three files, “main.js,” “image-animation.js” and “mandelbrot-set.js.” Lastly, make another folder called “mandelbrot_images.”

main.js

You’ll start by setting some variables that you’ll need.

First, boundary points for the axes you’ll be plotting on. The code here uses the traditional boundary points, but feel free to play around with them.

Open your main.js file and copy the following code.

async function main() {
    let complex_coordinates = {
        x1: -2.5,
        y1: -1,
        x2: 1,
        y2: 1
    }

Next you’ll set up the size of your canvas. You can change this, but the bigger the canvas size the longer to compute.

const canvas_size = {
  height: 800,
  width: 1200,
};

Next, set up the point to zoom to, the speed of zoom, the max number of iterations through the function .. math:: f_c(z), the number of frames you want to produce and the number of frames each slice creates.

const z0 = { re: 0.42884, im: -0.231345 };
const speed = 0.4;
const max_iterations = 1000;
const frames_num = 25;
const frames_per_worker = 3;
}

mandelbrot-set.js

Next, open the mandelbrot-set.js file.

Calculate escape time

The following calculates which points are in the mandelbrot set and which aren’t.

/**
 * Determines how many iterations of the function fc(z) before the point escapes
 * @param {int} z
 * @param {int} max_iterations
 * @returns k
 */
function calculate_escape_time(z, max_iterations) {
  c = z;
  var k = 0;
  while (k < max_iterations) {
    real = z.re * z.re - z.im * z.im + c.re;
    imaginary = 2 * z.re * z.im + c.im;
    z = { re: real, im: imaginary };

    if (z.re * z.re + z.im * z.im >= 4) {
      break;
    }
    k++;
  }
  return k;
}

Complex coordinates to feature

Next, create a function called complex_coordinates_to_features().

/**
 * Calculates the deltas needed to convert a point on the canvas to a point on the real and imaginary axes being plotted on.
 * @param {int} complex_coordinates
 * @param {int} canvas_size
 * @returns x1
 * @returns y1
 * @returns delta_x
 * @returns delta_y
 */
function complex_coordinates_to_features(complex_coordinates, canvas_size) {
  let delta_x =
    (complex_coordinates.x2 - complex_coordinates.x1) / canvas_size.width;
  let delta_y =
    (complex_coordinates.y2 - complex_coordinates.y1) / canvas_size.height;

  let x1 = complex_coordinates.x1;
  let y1 = complex_coordinates.y1;

  return {
    x1: x1,
    y1: y1,
    delta_x: delta_x,
    delta_y: delta_y,
  };
}

Coordinates to complex

Next, create a function coordinates_to_complex(), and copy the following code to your mandelbrot-set.js file.

/**
 * Translates any point on the canvas to a point on the real and imaginary axes. Takes in a point x, y on the canvas and the lower left deltas that were calcualted. It will then return the real and imaginary components of the point.
 * @param {int} x
 * @param {int} y
 * @param {int} lower_left_deltas
 * @returns re
 * @returns im
 */
function coordinates_to_complex(x, y, lower_left_deltas) {
  let re = lower_left_deltas.x1 + x * lower_left_deltas.delta_x;
  let im = lower_left_deltas.y1 + y * lower_left_deltas.delta_y;

  return {
    re: re,
    im: im,
  };
}

Make frame

The next function makes a matrix for every frame that contains the escape time for points in that frame.

/**
 * Creates a matrix for every frame which will contain the escape time of every point in the frame. That current escape time will then be replaced with the colour of the specific point. This is also where progress will be called which is needed to run on DCP.
 * @param {int} canvas_size
 * @param {int} complex_coordinates
 * @param {int} max_iterations
 * @returns frame
 */
function make_frame(canvas_size, complex_coordinates, max_iterations) {
  const rows = canvas_size.height;
  const cols = canvas_size.width;
  let frame = Array.from({ length: rows }, () =>
    Array.from({ length: cols }, () => [0, 0, 0]),
  );

  let lower_left_deltas = complex_coordinates_to_features(
    complex_coordinates,
    canvas_size,
  );

  for (let y = 0; y < rows; y++) {
    for (let x = 0; x < cols; x++) {
      let z = coordinates_to_complex(x, y, lower_left_deltas);
      let current_escape_time =
        calculate_escape_time(z, max_iterations) / max_iterations;
      frame[y][x] = escape_time_to_colour(current_escape_time, max_iterations);
    }
    progress();
  }
  return frame;
}

Make frames

Copy the following code.

/**
 * For every frame each worker will calculate the complex coordinates for that frame and then make that frame by calling the make_frame function. Frames will be an array of matrices where each entry of the matrix is the color of the point.
 * @param {int} canvas_size
 * @param {int} max_iterations
 * @param {int} first_complex_coordinates
 * @param {int} last_complex_coordinates
 * @param {int} total_frames
 * @returns frames
 */
function make_frames(
  canvas_size,
  max_iterations,
  first_complex_coordinates,
  last_complex_coordinates,
  total_frames,
) {
  frames = [];

  for (let k = 0; k < total_frames; k++) {
    let current_complex_coordinates = kth_complex_coordinate(
      first_complex_coordinates,
      last_complex_coordinates,
      total_frames,
      k,
    );
    let current_frame = make_frame(
      canvas_size,
      current_complex_coordinates,
      max_iterations,
    );
    frames.push(current_frame);
  }
  return frames;
}

K complex coordinate

Copy the following code.

/**
 * Calculates the new complex coordinates for each frame per slice.
 * @param {int} first_complex_coordinates
 * @param {int} last_complex_coordiantes
 * @param {int} total_frames
 * @param {int} k
 * @returns x1
 * @returns x2
 * @returns y1
 * @returns y2
 */
function kth_complex_coordinate(
  first_complex_coordinates,
  last_complex_coordinates,
  total_frames,
  k,
) {
  let alpha = k / total_frames;
  let x1 =
    (1 - alpha) * first_complex_coordinates.x1 +
    alpha * last_complex_coordinates.x1;
  let x2 =
    (1 - alpha) * first_complex_coordinates.x2 +
    alpha * last_complex_coordinates.x2;
  let y1 =
    (1 - alpha) * first_complex_coordinates.y1 +
    alpha * last_complex_coordinates.y1;
  let y2 =
    (1 - alpha) * first_complex_coordinates.y2 +
    alpha * last_complex_coordinates.y2;
  return {
    x1: x1,
    x2: x2,
    y1: y1,
    y2: y2,
  };
}

Escape time to colour

Copy the following code.

/**
 * Translates an escape time to an array of three values that will determine the colour of that point. The colour of the points will be determined by how many iterations it took before the point escaped.
 * @param {*} current_escape_time
 * @returns colour
 */
function escape_time_to_colour(current_escape_time) {
  let R = Math.floor(255 * Math.log10(current_escape_time));
  let B = Math.floor(255 * Math.pow(current_escape_time, 0.2));
  let G = Math.floor(
    255 *
      3 *
      (current_escape_time * (1 - current_escape_time) +
        0.25 * current_escape_time),
  );

  return [R, G, B];
}

Zoom in

Lastly, create the zoom in function. Copy the following code.

/**
 * Creates new axes boundaries based off of the speed and the point it's zooming towards.
 * @param {int} complex_coordinates
 * @param {int} z0
 * @param {int} speed
 * @returns x1
 * @returns x2
 * @returns y1
 * @returns y2
 */

function zoom_in(complex_coordinates, z0, speed) {
  let x1 = complex_coordinates.x1 * (1 - speed) + z0.re * speed;
  let x2 = complex_coordinates.x2 * (1 - speed) + z0.re * speed;
  let y1 = complex_coordinates.y1 * (1 - speed) + z0.im * speed;
  let y2 = complex_coordinates.y2 * (1 - speed) + z0.im * speed;

  return {
    x1: x1,
    x2: x2,
    y1: y1,
    y2: y2,
  };
}

Module export mandelbrot-set

In the last line of this file, you’ll export the functions you’ll need in the main.js file.

module.exports = { make_frames, zoom_in };

Now your mandelbrot-set.js file is complete.

image-animation.js

Next, open the image-animation.js file.

Image animation requirements

Copy the following requirements needed for saving the images and GIF.

const sharp = require('sharp');
const pngFileStream = require('png-file-stream');
const GIFEncoder = require('gifencoder');
const fs = require('fs');

Save image

Copy the following code to save the images.

/**
 * take in the frame which is a Uint8 array and saves each frame as a png.
 * @param {int} frame
 * @param {int} width
 * @param {int} height
 * @param {string} folder
 * @param {string} name
 * @returns image
 */
async function save_image(frame, width, height, folder, name) {
  image = sharp(frame, {
    raw: {
      width: width,
      height: height,
      channels: 3,
    },
  });
  await image.toFile(folder + name);
  return image;
}

Create GIF

Copy the following code to create the GIF.

/**
 * Take the pngs created and turns them into a gif.
 * @param {int} width
 * @param {int} height
 * @param {string} folder
 * @param {string} name
 */
async function create_gif(width, height, folder, name) {
  const encoder = new GIFEncoder(width, height);
  const image_names = folder + name + '*.png';
  const stream = pngFileStream(folder + name + '*.png')
    .pipe(encoder.createWriteStream({ repeat: 0, delay: 500, quality: 10 }))
    .pipe(fs.createWriteStream(folder + name + '.gif'));

  await new Promise((resolve, reject) => {
    stream.on('finish', resolve);
    stream.on('error', reject);
  });
}

Module export image animation

The last line exports both of these functions so that you can use them in main.js.

module.exports = { save_image, create_gif };

Returning to main.js

Lastly, go back to main.js file and tie everything together.

Requirements

Add the requirements for the files you just made, mandelbrot-set.js and image-animation.js. Also add the DCP scheduler.

const mandelbrot = require('./mandelbrot-set.js');
const create_image = require('./image-animation.js');

const SCHEDULER_URL = new URL('https://scheduler.distributed.computer');

Main function

Then, alter what you added to main.js earlier by adding to your main function.

In your main function, create an array the same size as the number of frames you have. Each entry in the array is the plots boundary points for each new frame after zooming.

let complex_coordinates_new;
let pre_frames = new Array(frames_num);
for (let i = 0; i < frames_num; i++) {
  complex_coordinates_new = mandelbrot.zoom_in(complex_coordinates, z0, speed);
  pre_frames[i] = [complex_coordinates, complex_coordinates_new];
  complex_coordinates = complex_coordinates_new;
}

Require the DCP compute and wallet. The following code allows you to run your program on DCP. You have the option to switch between running on DCP or your local computer by changing the value of the variable do_it_on_dcp. If set to true the program runs on DCP, otherwise it just runs locally. You’ll need to remember to un-comment that line progress() in the mandelbrot-set.js file in the function make_frame() if you wish to run this program without DCP.

    const compute = require('dcp/compute');
    const wallet = require('dcp/wallet');
    let startTime;

    let do_it_on_dcp = true;
    let array_results = Array();
    if (do_it_on_dcp) {

        const job = compute.for(
            pre_frames,
            (pre_frame, canvas_size, max_iterations, frames_per_worker) => {
                const mandelbrot = require("./mandelbrot-set")
                const frames = mandelbrot.make_frames(canvas_size, max_iterations, pre_frame[0], pre_frame[1], frames_per_worker);
                return frames;
            }, [canvas_size, max_iterations, frames_per_worker]
        );


        job.requires("./mandelbrot-set")

        job.on('accepted', () => {
            console.log(` - Job accepted by scheduler, waiting for results`);
            console.log(` - Job has id ${job.id}`);
            startTime = Date.now();
        });

        job.on('readystatechange', (arg) => {
            console.log(`new ready state: ${arg}`);
        });

        job.on('result', (ev) => {
            console.log(
                ` - Received result for slice ${ev.sliceNumber} at ${Math.round((Date.now() - startTime) / 100) / 10
                }s`,
            );
        });
        job.on('status', (ev) => {
            console.log('Got status update: ', ev)
        })

        job.public.name = 'mandelbrot set, nodejs';

        // SKIP IF: you don't need a Compute Group
        //job.computeGroups = [{ joinKey: "", joinSecret: "" }]

        const ks = await wallet.get(); /* usually loads ~/.dcp/default.keystore */
        job.setPaymentAccountKeystore(ks);
        const results = await job.exec(); //compute.marketValue
        //console.log('results=', Array.from(results));
        array_results = Array.from(results);
        debugger;
    } else {

        for (let idx = 0; idx < pre_frames.length - 1; idx++) {
            const frames = mandelbrot.make_frames(canvas_size, max_iterations, pre_frames[idx][0], pre_frames[idx][1], frames_per_worker);
            array_results.push(frames);
        }
    }

    for (let i = 0; i < array_results.length; i++) {
        for (let j = 0; j < array_results[i].length; j++) {
            let idx = i * array_results.length + j
            let image = Uint8Array.from(array_results[i][j].flat(3))
            create_image.save_image(image, canvas_size.width, canvas_size.height, "./mandelbrot_images", "/mandelbrot" + String(idx).padStart(3, "0") + '.png')
        }
    }

    create_image.create_gif(canvas_size.width, canvas_size.height, './mandelbrot_images', '/mandelbrot')

}

You can see in the last for loop it’s saying that for each result, which is the array of colours for each point, turn that into a Uint8Array. Then create PNG files in the mandelbrot image folder with the filename mandelbrot[x].png. Then, create a GIF and send it to the mandelbrot image folder with the name mandelbrot.gif.

Initialize DCP

/* Initialize DCP Client and run main() */
require('dcp-client').initSync(SCHEDULER_URL);
main();

Now wait for your slices and then look in the mandelbrot image folder for your GIF.

Next steps

Thank you for following along and if you’d like to continue to play with the mandelbrot set you can try zooming towards different points. In this tutorial you zoomed towards (0.42884, -0.231345) but there are various interesting points to zoom to. You can also try changing the colouring of the image.

Final code

Click to see the full code.

main.js

#!/usr/bin/env node

const mandelbrot = require('./mandelbrot-set.js');
//const set_up = require("./set-up.js");
const create_image = require('./image-animation.js');

const SCHEDULER_URL = new URL('https://scheduler.distributed.computer');

/** Main program entry point */
function set_up() {
  let complex_coordinates = {
    x1: -2.5,
    y1: -1,
    x2: 1,
    y2: 1,
  };

  const canvas_size = {
    height: 400,
    width: 600,
  };

  const z0 = { re: 0.42884, im: -0.231345 };
  const speed = 0.4;
  const max_iterations = 1000;
  const frames_num = 10;
  const frames_per_worker = 2;

  let complex_coordinates_new;
  let pre_frames = new Array(frames_num);
  for (let i = 0; i < frames_num; i++) {
    complex_coordinates_new = mandelbrot.zoom_in(
      complex_coordinates,
      z0,
      speed,
    );
    pre_frames[i] = [complex_coordinates, complex_coordinates_new];
    complex_coordinates = complex_coordinates_new;
  }
  return {
    canvas_size: canvas_size,
    max_iterations: max_iterations,
    frames_per_worker: frames_per_worker,
    pre_frames: pre_frames,
  };
}

async function main() {
  let canvas_size,
    max_iterations,
    frames_per_worker,
    pre_frames = set_up();
  console.log(canvas_size);
  let do_it_on_dcp = true;
  let array_results = Array();

  if (do_it_on_dcp) {
    let startTime;

    const compute = require('dcp/compute');
    const wallet = require('dcp/wallet');

    const job = compute.for(
      pre_frames,
      (pre_frame, canvas_size, max_iterations, frames_per_worker) => {
        const mandelbrot = require('./mandelbrot-set');
        progress();
        const frames = mandelbrot.make_frames(
          canvas_size,
          max_iterations,
          pre_frame[0],
          pre_frame[1],
          frames_per_worker,
        );
        return frames;
      },
      [canvas_size, max_iterations, frames_per_worker],
    );

    job.requires('./mandelbrot-set');
    job.requirements.discrete = true;

    job.on('accepted', () => {
      console.log(` - Job accepted by scheduler, waiting for results`);
      console.log(` - Job has id ${job.id}`);
      startTime = Date.now();
    });

    job.on('readystatechange', (arg) => {
      console.log(`new ready state: ${arg}`);
    });

    job.on('result', (ev) => {
      console.log(
        ` - Received result for slice ${ev.sliceNumber} at ${
          Math.round((Date.now() - startTime) / 100) / 10
        }s`,
      );
    });
    job.on('status', (ev) => {
      console.log('Got status update: ', ev);
    });

    job.public.name = 'mandelbrot Set';

    // SKIP IF: you don't need a Compute Group
    //job.computeGroups = [{ joinKey: '', joinSecret: '' }];

    const ks = await wallet.get(); /* usually loads ~/.dcp/default.keystore */
    job.setPaymentAccountKeystore(ks);
    const results = await job.exec(); //compute.marketValue
    //console.log('results=', Array.from(results));
    array_results = Array.from(results);
    debugger;
  } else {
    for (let idx = 0; idx < pre_frames.length - 1; idx++) {
      const frames = mandelbrot.make_frames(
        canvas_size,
        max_iterations,
        pre_frames[idx][0],
        pre_frames[idx][1],
        frames_per_worker,
      );
      array_results.push(frames);
    }
  }
  image_gif_creation(array_results);
}

function image_gif_creation(array_results) {
  let canvas_size,
    max_iterations,
    frames_per_worker,
    pre_frames = set_up();
  let frame_size = canvas_size.height * canvas_size.width * 3;

  for (let i = 0; i < array_results.length; i++) {
    for (let j = 0; j < frames_per_worker; j++) {
      let idx = i * frames_per_worker + j;
      let image = array_results[i].slice(j * frame_size, (j + 1) * frame_size);
      create_image.save_image(
        image,
        canvas_size.width,
        canvas_size.height,
        '/mandelbrot' + String(idx).padStart(3, '0') + '.png',
      );
    }
  }

  create_image.create_gif(canvas_size.width, canvas_size.height, '/mandelbrot');
}

/* Initialize DCP Client and run main() */
require('dcp-client').initSync(SCHEDULER_URL);
main();
//exports.main = main;
// .catch(console.error)
// .finally(() => {console.log("came here")})

image-animation.js

const sharp = require('sharp');
const pngFileStream = require('png-file-stream');
const GIFEncoder = require('gifencoder');
const fs = require('fs');

/**
 * take in the frame which is a Uint8 array and saves each frame as a png.
 * @param {int} frame
 * @param {int} width
 * @param {int} height
 * @param {string} folder
 * @param {string} name
 * @returns image
 */
async function save_image(frame, width, height, name) {
  image = sharp(frame, {
    raw: {
      width: width,
      height: height,
      channels: 3,
    },
  });
  await image.toFile(name);
  return image;
}

/**
 * Take the pngs created and turns them into a gif.
 * @param {int} width
 * @param {int} height
 * @param {string} folder
 * @param {string} name
 */
async function create_gif(width, height, name) {
  const encoder = new GIFEncoder(width, height);
  const image_names = folder + name + '*.png';
  const stream = pngFileStream(folder + name + '*.png')
    .pipe(encoder.createWriteStream({ repeat: 0, delay: 500, quality: 10 }))
    .pipe(fs.createWriteStream(name + '.gif'));

  await new Promise((resolve, reject) => {
    stream.on('finish', resolve);
    stream.on('error', reject);
  });
}

module.exports = { save_image, create_gif };

mandelbrot-set.js

/**
 * Determines how many iterations of the function fc(z) before the point escapes
 * @param {int} z
 * @param {int} max_iterations
 * @returns k
 */
function calculate_escape_time(z, max_iterations) {
  c = z;
  var k = 0;
  while (k < max_iterations) {
    real = z.re * z.re - z.im * z.im + c.re;
    imaginary = 2 * z.re * z.im + c.im;
    z = { re: real, im: imaginary };

    if (z.re * z.re + z.im * z.im >= 4) {
      break;
    }
    k++;
  }
  return k;
}

/**
 * Calculates the deltas needed to convert a point on the canvas to a point on the real and imaginary axes being plotted on.
 * @param {int} complex_coordinates
 * @param {int} canvas_size
 * @returns x1
 * @returns y1
 * @returns delta_x
 * @returns delta_y
 */
function complex_coordinates_to_features(complex_coordinates, canvas_size) {
  let delta_x =
    (complex_coordinates.x2 - complex_coordinates.x1) / canvas_size.width;
  let delta_y =
    (complex_coordinates.y2 - complex_coordinates.y1) / canvas_size.height;

  let x1 = complex_coordinates.x1;
  let y1 = complex_coordinates.y1;

  return {
    x1: x1,
    y1: y1,
    delta_x: delta_x,
    delta_y: delta_y,
  };
}

/**
 * Translates any point on the canvas to a point on the real and imaginary axes. Takes in a point x, y on the canvas and the lower left deltas that were calcualted. It will then return the real and imaginary components of the point.
 * @param {int} x
 * @param {int} y
 * @param {int} lower_left_deltas
 * @returns re
 * @returns im
 */
function coordinates_to_complex(x, y, lower_left_deltas) {
  let re = lower_left_deltas.x1 + x * lower_left_deltas.delta_x;
  let im = lower_left_deltas.y1 + y * lower_left_deltas.delta_y;

  return {
    re: re,
    im: im,
  };
}

/**
 * Creates a matrix for every frame which will contain the escape time of every point in the frame. That current escape time will then be replaced with the colour of the specific point. This is also where progress will be called which is needed to run on DCP.
 * @param {int} canvas_size
 * @param {int} complex_coordinates
 * @param {int} max_iterations
 * @returns frame
 */
function make_frame(canvas_size, complex_coordinates, max_iterations) {
  const rows = canvas_size.height;
  const cols = canvas_size.width;
  const frame_size = rows * cols * 3;
  let frame = new Uint8Array(frame_size);

  let lower_left_deltas = complex_coordinates_to_features(
    complex_coordinates,
    canvas_size,
  );

  for (let y = 0; y < rows; y++) {
    for (let x = 0; x < cols; x++) {
      let z = coordinates_to_complex(x, y, lower_left_deltas);
      let current_escape_time =
        calculate_escape_time(z, max_iterations) / max_iterations;
      let colours = escape_time_to_colour(current_escape_time, max_iterations);
      frame.set(colours, 3 * (y * cols + x));
    }
    //if (y % rows/4 == 0){
    //  progress();
    //}
    progress();
  }

  return frame;
}

/**
 * For every frame each worker will calculate the complex coordinates for that frame and then make that frame by calling the make_frame function. Frames will be an array of matrices where each entry of the matrix is the color of the point.
 * @param {int} canvas_size
 * @param {int} max_iterations
 * @param {int} first_complex_coordinates
 * @param {int} last_complex_coordinates
 * @param {int} total_frames
 * @returns frames
 */
function make_frames(
  canvas_size,
  max_iterations,
  first_complex_coordinates,
  last_complex_coordinates,
  total_frames,
) {
  const rows = canvas_size.height;
  const cols = canvas_size.width;
  const frame_size = rows * cols * 3;
  let frames = new Uint8Array(total_frames * frame_size);

  for (let k = 0; k < total_frames; k++) {
    let current_complex_coordinates = kth_complex_coordinate(
      first_complex_coordinates,
      last_complex_coordinates,
      total_frames,
      k,
    );
    let current_frame = make_frame(
      canvas_size,
      current_complex_coordinates,
      max_iterations,
    );
    frames.set(current_frame, k * frame_size);
    //progress();
  }

  return frames;
}

/**
 * Calculates the new complex coordinates for each frame per slice.
 * @param {int} first_complex_coordinates
 * @param {int} last_complex_coordiantes
 * @param {int} total_frames
 * @param {int} k
 * @returns x1
 * @returns x2
 * @returns y1
 * @returns y2
 */
function kth_complex_coordinate(
  first_complex_coordinates,
  last_complex_coordinates,
  total_frames,
  k,
) {
  let alpha = k / total_frames;
  let x1 =
    (1 - alpha) * first_complex_coordinates.x1 +
    alpha * last_complex_coordinates.x1;
  let x2 =
    (1 - alpha) * first_complex_coordinates.x2 +
    alpha * last_complex_coordinates.x2;
  let y1 =
    (1 - alpha) * first_complex_coordinates.y1 +
    alpha * last_complex_coordinates.y1;
  let y2 =
    (1 - alpha) * first_complex_coordinates.y2 +
    alpha * last_complex_coordinates.y2;
  return {
    x1: x1,
    x2: x2,
    y1: y1,
    y2: y2,
  };
}

/**
 * Translates an escape time to an array of three values that will determine the colour of that point. The colour of the points will be determined by how many iterations it took before the point escaped.
 * @param {*} current_escape_time
 * @returns colour
 */
function escape_time_to_colour(current_escape_time) {
  let R = Math.floor(255 * Math.log10(current_escape_time));
  let B = Math.floor(255 * Math.pow(current_escape_time, 0.2));
  let G = Math.floor(
    255 *
      3 *
      (current_escape_time * (1 - current_escape_time) +
        0.25 * current_escape_time),
  );

  let colours = new Uint8Array([R, G, B]);
  return colours;
}

/**
 * Creates new axes boundaries based off of the speed and the point it's zooming towards.
 * @param {int} complex_coordinates
 * @param {int} z0
 * @param {int} speed
 * @returns x1
 * @returns x2
 * @returns y1
 * @returns y2
 */

function zoom_in(complex_coordinates, z0, speed) {
  let x1 = complex_coordinates.x1 * (1 - speed) + z0.re * speed;
  let x2 = complex_coordinates.x2 * (1 - speed) + z0.re * speed;
  let y1 = complex_coordinates.y1 * (1 - speed) + z0.im * speed;
  let y2 = complex_coordinates.y2 * (1 - speed) + z0.im * speed;

  return {
    x1: x1,
    x2: x2,
    y1: y1,
    y2: y2,
  };
}
module.exports = { make_frames, zoom_in };