DEV Community

Cover image for Hyperspace travel in JavaScript Canvas
Dilli Babu R
Dilli Babu R

Posted on

Hyperspace travel in JavaScript Canvas

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.

<body>
    <canvas></canvas>
    <script></script>
</body>
Enter fullscreen mode Exit fullscreen mode

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

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
Enter fullscreen mode Exit fullscreen mode

Stars

Stars

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.

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

lets create an array to hold the star objects and const to specify how many stars we create.

const NUMBER_OF_STARS = 1000;
let stars = [];
Enter fullscreen mode Exit fullscreen mode

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


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)
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Another quick loop to draw them on the screen.

createStars();
stars.forEach(star => star.draw());
Enter fullscreen mode Exit fullscreen mode

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

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.

m=(y2y1)(x2x1) m = \frac {(y_2-y_1)} {(x_2-x_1)}

so, lets add slope attribute to Star

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

and define a function as follows:

function slopeFromCenter(x, y) {
    let x1 = x;
    let y1 = y;
    let x2 = centerX;
    let y2 = centerY;
    return (y2 - y1) / (x2 - x1);
}
Enter fullscreen mode Exit fullscreen mode

Now change the Star Object creation as follows.

stars.push(new Star(x, y, radius, color, slopeFromCenter(x, y)));
Enter fullscreen mode Exit fullscreen mode

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.

let travel = null;
Enter fullscreen mode Exit fullscreen mode

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

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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;
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

Star Movement

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.

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)];
}
Enter fullscreen mode Exit fullscreen mode

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.

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Star Movement 2

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.

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];
}
Enter fullscreen mode Exit fullscreen mode

And update the createStars method as follows

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)
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

function animate() {
    travel = requestAnimationFrame(animate);
    ctx.fillStyle = "rgba(0,0,0,0.1)";
    ctx.fillRect(0, 0, window.innerWidth, innerHeight);
    jump();
}
Enter fullscreen mode Exit fullscreen mode

The final result can be accessed here.
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

Thank you...

Top comments (0)