Hyperspace travel in JavaScript Canvas
Tags: javascript, beginners, tutorial
Published: 2022-05-22 17:29:41

We have seen cool hyperspace visuals in Starwars. Here we are going mimic the same using Javascript canvas.

So, first we create a `canvas` element in our html file and also a `script` tag to hold our javascript.

```html
<body>
    <canvas></canvas>
    <script></script>
</body>
```

then in our `script` tag, we get the canvas element.

```js
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
```

## Stars

![Stars](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ilan9qafxe9uwm8gq1r.png)

Now lets define a blueprint for a `Star`. Each star has a location `(x,y)`, `radius`, `color` and a `draw` function that draws the star on the canvas.

```js
function Star(x,y,radius,color){
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.color = color;
    this.draw = function (){
        ctx.strokeStyle = color;
        ctx.fillStyle = color;
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
        ctx.stroke();
        ctx.fill();
    }
}
```
lets create an array to hold the star objects and `const` to specify how many stars we create.

```js
const NUMBER_OF_STARS = 1000;
let stars = [];
```

Lets write a function to create the stars, we use random locations so lets use a simple random number generator.

```js

function getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min) + min);
}

function createStars() {
    for (let counter = 0; counter < NUMBER_OF_STARS; counter++) {
        let radius = getRandomInt(1, 3);
        let x = getRandomInt(radius, (canvas.width - radius));
        let y = getRandomInt(radius, (canvas.height - radius));
        let color = "lightblue";
        stars.push(
            new Star(x, y, radius, color)
        );
    }
}
```
Another quick loop to draw them on the screen.

```js
createStars();
stars.forEach(star => star.draw());
```
Creating stars with different radius makes it feel like few are far and few are near.

Now to the important stuff, How do we jump to hyperspace.


## Movement

By movie scenes we can see that when we travel at lightspeed, each star moves from it's location to the edge of the screen, but we need to calculate the `angle` or `linepath`.

To simplify this, we can assume a center point and if we draw a line from center to the star, that essentially is the path that star will follow until it reaches the edge of the screen. like below

![StarPath](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e2ar1sybhyv0agcb5vm7.png)

Now, we have two point for each line, 1st is the center of the screen and second point is the Star's location. Using the following formula, we can calculate the `slope` of the line, which we can use to move the star later.

{% katex inline %}
m = \frac {(y_2-y_1)} {(x_2-x_1)}
{% endkatex %}

so, lets add `slope` attribute to `Star`

```js
function Star(x,y,radius,color, slope){
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.color = color;
    this.slope = slope;
    this.draw = function (){
        ctx.strokeStyle = color;
        ctx.fillStyle = color;
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
        ctx.stroke();
        ctx.fill();
    }
}
```

 and define a function as follows:

```js
function slopeFromCenter(x, y) {
    let x1 = x;
    let y1 = y;
    let x2 = centerX;
    let y2 = centerY;
    return (y2 - y1) / (x2 - x1);
}
```

Now change the Star Object creation as follows.

```js
stars.push(new Star(x, y, radius, color, slopeFromCenter(x, y)));
```

## Animation Loop

For the animation, we use `requestAnimationFrame` method which will be called once every frame, and to start & stop the travel using spacebar key, we will create a variable `travel` as follows.

```js
let travel = null;
```

lets create a basic `jump` method too, will make changes to it later.

```js
function jump() {

    for (let i = 0; i < stars.length; i++) {
        const star = stars[i];
        ctx.beginPath();
        ctx.moveTo(star.x, star.y);
        // we will work these in a minute
        const nextXY = [star.x + 1, star.y + 1];
        const nextX = nextXY[0];
        const nextY = nextXY[1];
        ctx.lineTo(nextX, nextY);
        ctx.strokeStyle = star.color;
        ctx.lineWidth = star.radius;
        ctx.stroke();
        ctx.closePath();
        star.x = nextX;
        star.y = nextY;
    }
}
```

All we are doing is simply adding `1` to star's `xy` coordinates.

```js
let travel = null;
function animate() {
    travel = requestAnimationFrame(animate);
    jump();
}

window.addEventListener("keypress", function (event) {
    if (event.key === " ") {
        if (travel == null) {
            travel = window.requestAnimationFrame(animate);
        } else {
            window.cancelAnimationFrame(travel);
            travel = null;
        }
    }
});
```


![Star Movement](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/45561apu62fnjgu54elk.gif)

We can see that stars are going to right bottom cornor as we are incrementing `xy` values by 1. Now lets do the actual movement code by defining a `nextStop` function as follows.

```js
function nextStop(currentX, currentY, m, velocity) {
    let s = velocity / Math.sqrt(1 + m * m);
    let nextX = currentX + s;
    let nextY = currentY + m * s;
    return [Math.ceil(nextX), Math.ceil(nextY)];
}
```

this function simply takes `(x,y)`, `slope` and `velocity` and returns a new coordinates, by adjusting the velocity we can adjust the travel speed too.

lets update the `jump` function to use `nextStop` function as follows. 

```js
function jump() {

    for (let i = 0; i < stars.length; i++) {
        const star = stars[i];
        ctx.beginPath();
        ctx.moveTo(star.x, star.y);
        // update the following line
        const nextXY = nextStop(star.x, star.y, star.slope, 20);
        const nextX = nextXY[0];
        const nextY = nextXY[1];
        ctx.lineTo(nextX, nextY);
        ctx.strokeStyle = star.color;
        ctx.lineWidth = star.radius;
        ctx.stroke();
        ctx.closePath();
        star.x = nextX;
        star.y = nextY;
    }
}
```

![Star Movement 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jkcgdlsk2j9n3a9daycr.gif)

## but... 

We can see, we got right half right but left half of the screen is in reverse... This is because the slope we calculated. To fix that update the `slopeFromCenter` function as follows.

```js
function slopeFromCenter(x, y) {

    let x1 = x;
    let y1 = y;
    let x2 = centerX;
    let y2 = centerY;
    let q = null; // Quadrant
    let direction = 1;

    if (x > centerX && y <= centerY) {
        q = "Q1";
    } else if (x <= centerX && y <= centerY) {
        q = "Q2";
    } else if (x <= centerX && y > centerY) {
        q = "Q3";
    } else if (x > centerX && y > centerY) {
        q = "Q4";
    }

    if (q == "Q2" || q == "Q3") {
        direction = -1;
    }

    return [(y2 - y1) / (x2 - x1), direction];
}
```

And update the `createStars` method as follows

```js
function createStars() {
    for (let counter = 0; counter < NUMBER_OF_STARS; counter++) {
        let radius = getRandomInt(1, 4);
        let x = getRandomInt(radius, (canvas.width - radius));
        let y = getRandomInt(radius, (canvas.height - radius));
        let color = "white";
        let slopeAndDirection = slopeFromCenter(x, y);
        stars.push(
            new Star(x, y, radius, slopeAndDirection[0], slopeAndDirection[1], color)
        );
    }
}
```

Also update the `jump` function to use the direction as well re-creating the stars at random location if they reach the edge of the screen. Instead of creating the new object we are simply changing it's location so it would feel like it's new star. It's better for memory & performance.

```js
function jump() {
    for (let i = 0; i < stars.length; i++) {
        const star = stars[i];
        if (
            star.x <= 0 ||
            star.x > canvas.width ||
            star.y <= 0 ||
            star.y > canvas.height
        ) {
            star.x = Math.ceil(
                Math.random() * (window.innerWidth - star.radius * 2 + star.radius)
            );
            star.y = Math.ceil(
                Math.random() * (window.innerHeight - star.radius * 2 + star.radius)
            );
            const slopeAndDirection = slopeFromCenter(star.x, star.y);
            star.slope = slopeAndDirection[0];
            star.direction = slopeAndDirection[1];
        }
        ctx.beginPath();
        ctx.moveTo(star.x, star.y);
        const nextXY = nextStop(
            star.x * star.direction,
            star.y * star.direction,
            star.slope,
            20
        );
        const nextX = nextXY[0];
        const nextY = nextXY[1];
        ctx.lineTo(nextX * star.direction, nextY * star.direction);
        ctx.strokeStyle = star.color;
        ctx.lineWidth = star.radius;
        ctx.stroke();
        ctx.closePath();
        star.x = nextX * star.direction;
        star.y = nextY * star.direction;
    }
}
```

Now, it will be working fine... but over the time screen will be filled complete... to have nice animation, we modify the `animate` function. Here using `ctx.fillRect` we are filling the screen with black transparent color, which prevents the screen from getting completely filled.

```js
function animate() {
    travel = requestAnimationFrame(animate);
    ctx.fillStyle = "rgba(0,0,0,0.1)";
    ctx.fillRect(0, 0, window.innerWidth, innerHeight);
    jump();
}
```

The final result can be accessed [here](https://dillir07.github.io/canvasart/space.html).
Hit `Spacebar` to start or stop,
`+` key to increase speed,
`-` key to slowdown and enjoy the journey :)

The complete code with few other improvements are linked [here](https://github.com/dillir07/canvasart/blob/master/space.html)



Thank you...