We create a mini-game with a drip effect and moving circles. Part 1

Final demo of the first part of the lesson:

Let’s begin!

Project structure

First we need to create a basic project structure. These will be 3 empty files.

index.html
assets
  ├── style.css
  └── main.js

Project preparation

In index.html we will create a basic HTML5 structure by connecting the style.css file and the JavaScript main.js file.

<!doctype html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <title>Spore</title>
    <link rel="stylesheet" href="https://habr.com/ru/articles/761608/assets/style.css">
</head>
<body>

<script src="https://habr.com/ru/articles/761608/assets/main.js"></script>
</body>
</html>

In style.css we will write styles to change the default indents.

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

HTML and CSS

Now you can start the fun part – development!

Let’s add the following structure to the tag in index.html.

<div id="board">
    <div id="zone"></div>
</div>

Here we use two blocks: “board” and “zone”, which we will need in order to further create a “drip effect” of merging objects.

Let’s add the following styles to the style.css file.

html, body {
    width: 100%;
    height: 100%;
    overflow: hidden;
}


#board {
    width: 100%;
    height: 100%;
    background: #fff;
    filter: contrast(10);
}


#zone {
    width: 100%;
    height: 100%;
    background: #fff;
    filter: blur(10px);
}


.spore {
    width: 200px;
    height: 200px;
    border-radius: 50%;
    background: cyan;
    position: absolute;
    top: 50%;
    left: 50%;
}

Let’s check the result we got and add two div blocks with the spore class for testing.

<div id="board">
    <div id="zone">
        <div class="spore"></div>
        <div class="spore"></div>
    </div>
</div>

Let’s open our index.html page in the browser. If everything was done correctly, we will see the result in the form of one circle.

Then in the developer panel we will find the second circle, which is currently superimposed on the first, and change its positioning. Let’s add positioning styles to the left (left property) for the second circle. You can select the required value yourself to see this effect!

CSS drip effect in action!

CSS drip effect in action!

This effect will look beautiful when we have dozens of such elements, and even in different colors!

After testing, we will remove the div blocks with the spore class, and also edit the style.css file.

.spore {
    width: 200px;
    height: 200px;
    border-radius: 50%;
    background: cyan;
    position: absolute;
    transform: translate(-50%, -50%);
}

These styles are final, and we will no longer edit them, just like the HTML file.

JavaScript

Finally, we’ll start populating our main.js file with code.

To simplify further development, we will add an auxiliary function in the selection of elements by their selector.

const $ = el => document.querySelector(el);

This function is intended for those cases when we need to get only the first element by a given selector (most often it is used only to get an element by a given id).

Since we will be accessing the div element with the zone identifier several times, we will make a reference to this element as a constant.

const ZONE = $('#zone');

Class structure

4 classes will be used as the main structure of JavaScript:

In this case, the Substance and Piece classes will inherit from the Base class. And the Game class will be responsible for all interactions between elements.

class Base {}

class Substance extends Base {}

class Piece extends Base {}

class Game {}

And at the end we will add the creation of an instance of the Game class so that the script runs immediately after loading the JavaScript file.

const game = new Game();

If we currently open the index.html page in the browser, we will not see anything and when we click on the empty space, nothing will happen.

Instantiating Elements

Let’s work with the Base class and add a constructor to it, which will be a parameter and store a link to the element in the HTML structure.

class Base {
    constructor(parent) {
        this.parent = parent;
    }
}

The Piece class will be responsible for each element directly when the large Substance element explodes. Let’s add an additional characteristic to it: its positioning in space. And we’ll also add a createElement function, which will create a new element, add a class to this element, set the width and height, as well as position, and add the newly created element to the #zone container.

class Piece extends Base {
    constructor(parent) {
        super(parent);

        this.data = {
            position: {
                x: this.parent.data.position.x,
                y: this.parent.data.position.y
            }
        }

        this.createElement();
    }

    createElement() {
        this.el = document.createElement('div');
        this.el.className="spore";
        this.el.style.width = `200px`;
        this.el.style.height = `200px`;
        this.el.style.left = `${this.data.position.x}px`;
        this.el.style.top = `${this.data.position.y}px`;

        ZONE.appendChild(this.el);
    }
}

Now for the Substance class we need to add some additional fields as well. This will be a position in space and an array of instances of the Piece class.

class Substance extends Base {
    constructor(parent, params) {
        super(parent);

        this.data = {
            position: {
                x: params.position.x,
                y: params.position.y
            },
            pieces: []
        }
        
        this.data.pieces.push(new Piece(this));
    }
}

User interaction

To make the interactive part with the browser page work, we will add listening to the page click event in the Game class.

class Game {
    constructor() {
        this.substances = [];
        
        this.bindEvents();
    }

    bindEvents() {
        ZONE.addEventListener('click', ev => {
            this.substances.push(new Substance(this, {
                position: {
                    x: ev.clientX,
                    y: ev.clientY
                }
            }));
        });
    }
}

Now, when you click, a new element will appear in the same place where the click was made.

Dynamic appearance of new elements when you click on the page

Dynamic appearance of new elements when you click on the page

Logic for moving objects

We need to liven up a little the elements that appear on our page when clicked.

Let’s move on to the Game class and add a new random function, which will allow us to get a random number from a given range, since initially such a function does not exist in JavaScript.

class Game {
    constructor() {...}
    bindEvents() {...}

    static random(min, max) {
        return Math.round(Math.random() * (max - min) + min);
    }
}

And for the Piece class, you will need to implement additional logic that will allow you to randomly generate a motion direction vector and move the element every time the update function is called.

Let’s add two constants that will store the minimum and maximum speeds.

class Piece extends Base {
    MIN_SPEED = 3;
    MAX_SPEED = 8;

    constructor(parent) {
        super(parent);

        this.data = {
            position: {...},
            accelerations: {
                x: Game.random(-1, 0) === 0 ? Game.random(this.MIN_SPEED, this.MAX_SPEED) : Game.random(-this.MAX_SPEED, -this.MIN_SPEED),
                y: Game.random(-1, 0) === 0 ? Game.random(this.MIN_SPEED, this.MAX_SPEED) : Game.random(-this.MAX_SPEED, -this.MIN_SPEED),
            }
        }

        this.createElement();
    }
    
    createElement() {...}
    
    update() {
        this.data.position.x += this.data.accelerations.x;
        this.data.position.y += this.data.accelerations.y;

        this.draw();
    }
    
    draw() {
        this.el.style.top = `${this.data.position.y}px`;
        this.el.style.left = `${this.data.position.x}px`;
    }
}

Let’s add a loop to the Substance class that will iterate through the stored Piece instances and call the update function for each of them.

class Substance extends Base {
    constructor(parent, params) {...}

    update() {
        this.data.pieces.forEach(piece => {
            piece.update();
        });
    }
}

However, if we check the current result, our elements simply fly off the screen. So we need to add handling for cases to make them bounce off the edge of the screen.

Limiting the movement of objects

Let’s add two new constants to the beginning of the file: screen width and height.

const ZONE = …;

const SCREEN_WIDTH = window.innerWidth;
const SCREEN_HEIGHT = window.innerHeight;

In the Substance class you will need to add a new field maxSize to the data object.

class Substance extends Base {
    constructor(parent, params) {
        
        this.data = {
            maxSize: 200,
            ...
        }
    }
    
    update() {...}
}

And now let’s proceed directly to implementing the logic itself in the Piece class: add a new field, as we did with the Substance class, but simply call it size.

class Piece extends Base {
    constructor(parent) {
        this.data = {
            size: this.parent.data.maxSize,
            ...
        }
    }
}

Let’s add a new function that, each frame, will compare the current position and motion vector of an object with the extreme coordinates of the browser window, so that when these coordinates are reached, the object changes its motion vector to the opposite one. When calculating coordinates, you need to subtract the radius of the circle from the current position to find out its extreme point. Previously, the diameter of the circle was determined in the field data.size.

class Piece extends Base {
    ...
    
    constructor(parent) {...}
    
    createElement() {...}

    update() {
        this.checkEdge();

        ...
    }

    draw() {...}

    checkEdge() {
        const halfSize = this.data.size / 2;


        if (this.data.position.x - halfSize <= 0 && this.data.accelerations.x < 0) this.data.accelerations.x *= -1;
        if (this.data.position.x + halfSize >= SCREEN_WIDTH && this.data.accelerations.x > 0) this.data.accelerations.x *= -1;


        if (this.data.position.y - halfSize <= 0 && this.data.accelerations.y < 0) this.data.accelerations.y *= -1;
        if (this.data.position.y + halfSize >= SCREEN_HEIGHT && this.data.accelerations.y > 0) this.data.accelerations.y *= -1;
    }
}

If you open the index.html page and click several times anywhere on the page, each click will add a new circle that will move around the screen.

Conclusion

In this part, we implemented interaction with the page canvas, and also wrote logic for movement and limiting the movement of elements. In the next part we will complete this project and implement the merging of objects when they intersect, and then the subsequent “explosion” of a large object. And we’ll even paint them in different colors to make the movements look more colorful and spectacular!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *