Creating the effect of traveling through space
Tags: javascript, algorithms, canvas
Published: 2023-01-29 16:16:45

Hello everyone! Today we're going to create the effect of traveling through space using javascript and canvas. Let's get started!

## Theory

This effect is based on the simplest way of obtaining a perspective projection of a point from three-dimensional space onto a plane. For our case, we need to divide the value of the x and y coordinates of a three-dimensional point by their distance from the origin:

P'X = Px / Pz
P'Y = Py / Pz


![The projection of a point from three-dimensional space onto a plane](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jfec666wndcgry3ugpbf.png)

## Environment setup

Let's define the `Star` class that will store the states of the star and have three main methods: updating the state of the star, drawing the star on the screen, and getting its position in 3D space:

```javascript
class Star {
  constructor() {}

  getPosition() {}

  update() {}

  draw(ctx) {}
}
```
Next, we need a class that will be used to create and manage the instances of the `Star` class. Let's call it `Space` and create an array of `Star` objects in its constructor, each one representing a star:

```javascript
class Space {
  constructor() {
    this.stars = new Array(STARS).fill(null).map(() => new Star());
  }
}
```
It will also have three methods: update, draw, and run. The run method will iterate through the star instances by first calling the update method, and then drawing them with the draw method:

```javascript
class Space {
  constructor() {
    this.stars = new Array(STARS).fill(null).map(() => new Star());
  }

  update() {
    this.stars.forEach((star) => star.update());
  }

  draw(ctx) {
    this.stars.forEach((star) => star.draw(ctx));
  }

  run(ctx) {
    this.update();
    this.draw(ctx);
  }
}
```
Next, we should define a new class called `Canvas` that will create the canvas element and call the run method of the Space class:

```javascript
class Canvas {
  constructor(id) {
    this.canvas = document.createElement("canvas");

    this.canvas.id = id;
    this.canvas.width = window.innerWidth;
    this.canvas.height = window.innerHeight;

    document.body.appendChild(this.canvas);
    this.ctx = this.canvas.getContext("2d");
  }


  draw() {
    const space = new Space();
    const draw = () => {
      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
      space.run(this.ctx);
      requestAnimationFrame(draw);
    };
    draw();
  }
}
```
Thus, the preparatory part of the project has been completed and we can begin to implement its main functionality.

## Main functionality

The first step we need to take is to define a uniform function that generates random numbers in a given range of numbers. To do this, we will create a random object and implement the function in it using the Math.random() method:

```javascript
const random = {
  uniform: (min, max) => Math.random() * (max - min) + min,
};
```
Once we need a class to implement the space vectors `Vec`, since javascript does not support working with vectors. What is a vector? A vector is a mathematical object that describes directions in space. Vectors are built from the numbers that form their components. In the picture below you can see a 2D vector with two components:

![2D vector](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m3kfmpi6iesuctvtixw2.png)

## Vector operations

Consider two vectors. The following basic operations are defined for these vectors:

Addition: **V + W = (Vx + Wx, Vy + Wy)**

Subtraction: **V - W = (Vx - Wx, Vy - Wy)**

Division: **V / W = (Vx / Wx, Vy / Wy)**

Scaling: **aV = (aVx, aVy)**

Multiplication: **V * W = (Vx * Wx, Vy * Wy)**

Based on this information, we will implement the main methods of working with vectors that we will need in future:

```javascript
class Vec {
  constructor(...components) {
    this.components = components;
  }

  add(vec) {
    this.components = this.components.map((c, i) => c + vec.components[i]);
    return this;
  }

  sub(vec) {
    this.components = this.components.map((c, i) => c - vec.components[i]);
    return this;
  }

  div(vec) {
    this.components = this.components.map((c, i) => c / vec.components[i]);
    return this;
  }

  scale(scalar) {
    this.components = this.components.map((c) => c * scalar);
    return this;
  }

  multiply(vec) {
    this.components = this.components.map((c, i) => c * vec.components[i]);
    return this;
  }
}
```

## Implementation

First, let's define the center of the screen as a two-dimensional vector and make a set of several colors for our stars:

```javascript
const CENTER = new Vec(window.innerWidth / 2, window.innerHeight / 2);
const COLORS = ["#FF7900", "#F94E5D", "#CA4B8C"];
```
and also introduce the constant Z, which will be used to indicate the distance along the z axis from which stars will start moving:

```javascript
const Z = 35;
```
Next, we will assign the position of each star in three-dimensional space to the attributes. We will do this by implementing the `getPosition` method of our `Star` class. This method uses a unit circle with a random radius to generate coordinates using sin and cos. These functions are [mathematically related to unit circles](https://en.wikipedia.org/wiki/Sine_and_cosine#Relation_to_the_unit_circle); therefore they can be used to represent points in three-dimensional space.


![Circle cos sin](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wblmorm6aphospm1tb90.gif)

Thus we get the following code:

```javascript
getPosition() {
  const angle = random.uniform(0, 2 * Math.PI);
  const radius = random.uniform(0, window.innerHeight);

  const x = Math.cos(angle) * radius;
  const y = Math.sin(angle) * radius;

  return new Vec(x, y, Z);
}
```
Now let's call it in the class constructor:

```javascript
class Star {
  constructor() {
    this.pos = this.getPosition();
  }
}
```

Next, in the constructor we set the speed of the star, its color and position on the screen in terms of a two-dimensional vector and its size:

```javascript
class Star {
  constructor() {
    this.size = 10;
    this.pos = this.getPosition();
    this.screenPos = new Vec(0, 0);
    this.vel = random.uniform(0.05, 0.25);
    this.color = COLORS[Math.floor(Math.random() * COLORS.length)];
  }
}
```
Next, we will move the star along the Z axis at a set speed and when it reaches its minimum value, we will call a getPosition method to randomly set its new position:

```javascript
update() {
  this.pos.components[2] -= this.vel;
  this.pos = this.pos.components[2] < 1 ? this.getPosition() : this.pos;
}
```
The coordinates of a star on the screen can be calculated by dividing the X and Y coordinates by the value of the Z component, taking the center of the screen into account:

```javascript
update() {
  this.pos.components[2] -= this.vel;
  this.pos = this.pos.components[2] < 1 ? this.getPosition() : this.pos;
  this.screenPos = new Vec(this.pos.components[0], this.pos.components[1])
      .div(new Vec(this.pos.components[2], this.pos.components[2]))
      .add(CENTER);
}
```
Next, we will display the star on the screen by using the draw method. To do this, we use rect method:

```javascript
draw(ctx) {
  ctx.fillStyle = this.color;

  ctx.beginPath();
  ctx.rect(this.screenPos.components[0], this.screenPos.components[1], this.size, this.size);
  ctx.closePath();
  ctx.fill();
}
```
Let's see how the stars move in real time. As you can see, the stars move as expected, but their size does not change:

{% codepen https://codepen.io/anstertum/pen/YzjjQjM %}

To solve this problem, we divide the value of the Z constant by the current value of the star along the axis Z. The result is as follows:

{% codepen https://codepen.io/anstertum/pen/XWBBarB %}

If you look closely, you'll see that the stars that are farther away are drawn on top of the nearby stars. To solve this problem, we will use the so-called Z Buffer and sort the stars by distance until they are drawn. Let's do this sorting in the run method of the `Space` class:

```javascript
run(ctx) {
  this.update();
  this.stars.sort((a, b) => b.pos.components[2] - a.pos.components[2]);
  this.draw(ctx);
  }
```

In addition, we will introduce a scale factor in the getPosition method of the `Star` class to scale our visualization by increasing the random radius to create larger stars:

```javascript
getPosition(scale = 35) {
  const angle = random.uniform(0, 2 * Math.PI);
  const radius =
      random.uniform(window.innerHeight / scale, window.innerHeight) * scale;

  const x = Math.cos(angle) * radius;
  const y = Math.sin(angle) * radius;

  return new Vec(x, y, Z);
}
```
and also slightly change the function for the value of the projection of the star to a more suitable one:

```javascript
update() {
  this.pos.components[2] -= this.vel;
  this.pos = this.pos.components[2] < 1 ? this.getPosition() : this.pos;
  this.screenPos = new Vec(this.pos.components[0], this.pos.components[1])
      .div(new Vec(this.pos.components[2], this.pos.components[2]))
      .add(CENTER);

  this.size = (Z - this.pos.components[2]) / (this.pos.components[2] * 0.2);
}
```

As a result, we get a complete space picture:

{% codepen https://codepen.io/anstertum/pen/WNKKExE %}

In addition we can rotate the XY plane by a small angle. To do this, we calculate the new values of x and y using sin and cos:

```javascript
rotateXY(angle) {
  const x = this.components[0] * Math.cos(angle) - this.components[1] * Math.sin(angle);
  const y = this.components[0] * Math.sin(angle) + this.components[1] * Math.cos(angle);
  this.components[0] = x;
  this.components[1] = y;
}
```

and call this method in the update method of the `Star` class:

```javascript
update() {
  this.pos.components[2] -= this.vel;
  this.pos = this.pos.components[2] < 1 ? this.getPosition() : this.pos;
  this.screenPos = new Vec(this.pos.components[0], this.pos.components[1])
      .div(new Vec(this.pos.components[2], this.pos.components[2]))
      .add(CENTER);

  this.size = (Z - this.pos.components[2]) / (this.pos.components[2] * 0.2);
  this.pos.rotateXY(0.003);
}
```
As a result, we get the following picture:

{% codepen https://codepen.io/anstertum/pen/MWBBvoB %}

Moreover, if we slightly change the initial parameters and calculate the random radius differently, we can get the effect of traveling through a tunnel:

{% codepen https://codepen.io/anstertum/pen/abjjyVr %}

## Conclusion

We created a visualization of movement through space and learned how to do this kind of visualization.

## Additional resources

[Jony Hayama](https://dev.to/jonyhayama) has created UI for the simulation, so if you want to play with variables more conveniently check this link out - [https://jony.dev/traveling-through-space/](https://jony.dev/traveling-through-space/)