## DEV Community

Eduard Iudinkov

Posted on • Updated on

# Creating the effect of traveling through space

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;
}

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 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]))
}
``````

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);
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]))

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]))

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.

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/

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

π

Jony Hayama

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/

Eduard Iudinkov

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

z2lai

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

Arun

this is super cool !!! need more like this

Jony Hayama

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

samyip9000

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

MickGe

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

Al - Naubit

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

Viet Hoang

That's impressive! Thanks a lot!

Lucas Fieri

You got my follow :) Marvelous!!

DEV Community

## 11 Tips That Make You a Better Typescript Programmer

### 1 Think in {Set}

Type is an everyday concept to programmers, but itβs surprisingly difficult to define it succinctly. I find it helpful to use Set as a conceptual model instead.

### #2 Understand declared type and narrowed type

One extremely powerful typescript feature is automatic type narrowing based on control flow. This means a variable has two types associated with it at any specific point of code location: a declaration type and a narrowed type.

...