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.