Hello, dear Habr members!
Today we're going to talk about creating a mini-game that you can use to decorate your website or just leave as a screensaver. We will divide the development of the project into two parts: we will start with basic object movement and finish with creating a full-fledged project. This course is suitable both for beginners who have already learnt a bit of JavaScript, HTML and CSS and for experienced programmers.
The second part of the lesson.
Final demo of the first part of the lesson:
Let's get started!
Project Structure
First, we will need to create a basic project structure. This will be 3 empty files.
index.html
assets
├── style.css
└── main.js
Preparing the project
In index.html we will create a basic HTML5 structure with style.css and JavaScript main.js.
<!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="assets/style.css">
</head>
<body>
<script src="assets/main.js"></script>
</body>
</html>
In style.css we will write styles to change the default indentation.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
HTML and CSS
Now we can get to the most interesting part - development!
Let's add the following structure to the tag in index.html.
<div id="board">
<div id="zone"></div>
</div>
Here we will use two blocks: "board" and "zone" - which we will need in order to 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 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 as one circle.
Then, in the developer tools, let's find the second circle, which is currently superimposed on the first one, and change its positioning. Let's add the left positioning styles (left property) for the second circle. You can pick the necessary value yourself to see this effect!
This effect will look beautiful when we have dozens of such elements, and even in different colours!
After testing, let's remove the div blocks with the spore class, and edit the style.css file as well.
.spore {
width: 200px;
height: 200px;
border-radius: 50%;
background: cyan;
position: absolute;
transform: translate(-50%, -50%);
}
These styles are final, and we will not edit them anymore, as well as the HTML file.
JavaScript
Finally, we will start filling our main.js file with code.
To simplify further development, we will add an auxiliary function to select 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 used only to get an element by a given id).
Since we will refer to the div element with the zone identifier several times, we will make the reference to this element as a constant.
const ZONE = $('#zone');
Class structure
As the basic structure of JavaScript we will use 4 classes:
Base
Substance
Piece
Game
The Substance and Piece classes will inherit from the Base class. And the Game class will be responsible for all interaction between the 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 will be launched immediately after loading the JavaScript file.
const game = new Game();
If we open the index.html page in the browser at this moment, we won't see anything and nothing will happen when we click on the empty space.
Creating instances of elements
Let's work with the Base class and add a constructor to it, which will be a parameter and store a reference to the element in the HTML structure.
class Base {
constructor(parent) {
this.parent = parent;
}
}
The Piece class will be directly responsible for each element when a large Substance element is "exploded". Let's add an additional characteristic to it: its positioning in space. And let's also add a createElement function that will create a new element, add a class to this element, set the width and height and 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 also need to add some additional fields. These will be the position in the 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 get the interactive part with the browser page working, we will add listening for the page click event to 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.
Logic of moving objects
We need to liven up the elements that appear on our page when clicked.
Let's go to the Game class and add a new function random, which will allow us to get a random number from a given range, because initially such a function doesn't exist in JavaScript.
class Game {
constructor() {...}
bindEvents() {...}
static random(min, max) {
return Math.round(Math.random() * (max - min) + min);
}
}
And for the Piece class we will need to implement additional logic that will generate a vector of movement direction randomly and will move the item at each call of the update function.
Let's add two constants that will store the minimum and maximum velocities.
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 loop through the stored Piece instances and call the update function for each one.
class Substance extends Base {
constructor(parent, params) {...}
update() {
this.data.pieces.forEach(piece => {
piece.update();
});
}
}
However, if you check the current result, our pieces just fly off the screen. So we need to add case handling 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 screen height.
const ZONE = …;
const SCREEN_WIDTH = window.innerWidth;
const SCREEN_HEIGHT = window.innerHeight;
In the Substance class, we will need to add a new maxSize field 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: let's add a new field like we did with the Substance class, but let's just call it size.
class Piece extends Base {
constructor(parent) {
this.data = {
size: this.parent.data.maxSize,
...
}
}
}
Let's add a new function that will compare the current position and motion vector of the object with the extreme coordinates of the browser window every frame, so that when these coordinates are reached, the object will change its motion vector to the opposite one. When calculating the coordinates, it is necessary to subtract the radius of the circle from the current position to find out its edge point. Previously, the diameter of the circle was defined in the data.size field.
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;
}
}
And let's add a new loop function to the Game class, which will internally call itself via the requestAnimationFrame method and call the update method for each element at each function run.
class Game {
constructor() {
...
this.loop();
}
loop() {
this.substances.forEach((substance => {
substance.update();
}))
requestAnimationFrame(_ => this.loop());
}
bindEvents() {...}
static random(min, max) {...}
}
If you open the index.html page and click anywhere on the page several times, each click will add a new circle that will move around the screen.
Conclusion
In this part we have implemented the interaction with the page canvas, as well as written logic for moving and restricting the movement of elements. In the next part we will finish this project and implement merging of objects when they intersect, and then the subsequent "explosion" of a large object. And we will even paint them in different colours to make the movements look more colourful and spectacular!