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

## 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:

```
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:

```
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:

```
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:

```
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:

```
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:

## 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:

```
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:

```
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:

```
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; therefore they can be used to represent points in three-dimensional space.

Thus we get the following code:

```
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:

```
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:

```
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:

```
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:

```
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:

```
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:

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:

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:

```
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:

```
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:

```
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:

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:

```
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:

```
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:

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

## Conclusion

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

Source: https://dev.to/eyudinkov/creating-the-effect-of-traveling-through-space-mfg