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.

## Additional resources

Jony Hayama 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/

## Top comments (11)

I remember now why I subscribe to Dev.to years ago; to read such great and original piece of content.

π

Hope you don't mind, but I liked this stuff so much I ended up adding some UI around it so it's a bit easier to play around with the variables π

jony.dev/traveling-through-space/

That is awesome! I updated the article with your simulation :)

No idea what the math is doing even though I have a math degree, but this is mesmerizing stuff. Thank you for sharing.

this is super cool !!! need more like this

this is the coolest stuff I've seen in a while!

Can you let us know how do you make the .gif animation to demonstrate the cos and sin relationship?

You will find more information here: 1ucasvb.tumblr.com/faq.

Wikipedia user profile: en.wikipedia.org/wiki/User:LucasVB....

Hey, that was a nice read, you got my follow, keep writing π

That's impressive! Thanks a lot!

You got my follow :) Marvelous!!