Javascript может делать много интересных вещей. Введение элемента холста в HTML5 еще больше расширило возможности Javascript. Так что же такое элемент холста? Это элемент, который действует как контейнер для рисованной графики javascript. Хотя мы не будем демонстрировать это здесь, вы можете продвинуть свои анимации еще дальше, взаимодействуя с ними с помощью прослушивателей и обработчиков событий. Вместо этого мы сделаем простую анимацию с кружками разного размера, прыгающими по экрану.

Сначала давайте создадим и настроим наш элемент холста в нашем index.html файле:

index.html
....
<body>
    <canvas></canvas>
    <script src="canvas.js"></script>
</body>
....

Тег <html> документа не занимает весь экран. Поскольку мы планируем сделать нашу анимацию полной страницей и у нас есть доступ к элементу window, который заполняет весь экран, мы воспользуемся javascript для доступа к его свойствам innerHeight и innerWidth и установим эти значения для высоты и ширины холста. Это гарантирует, что наш холст займет всю высоту и ширину окна браузера. Поскольку значение поля по умолчанию для тега <body> не равно 0, нам также необходимо обновить его:

canvas.js
const canvas = document.querySelector('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const body = document.querySelector('body');
body.style.margin = 0;

Затем мы создадим переменную c в нашем файле javascript, которая будет вызывать метод .getContext() на нашем холсте и передавать 2d в качестве аргумента. Это возвращает контекст рисования на холсте и по существу превращает c в суперобъект, который имеет доступ ко всем свойствам и функциям рисования, которые нам нужны для рисования 2d-элементов:

canvas.js
const c = canvas.getContext('2d')

Вот несколько ключевых методов, которые мы будем использовать в c: .beginPath(), .strokeStyle() и .stroke(). .beginPath() делает именно то, что говорит, он начинает новый путь, по которому мы можем рисовать. Метод .strokeStyle() используется для изменения цвета, градиента или рисунка наших объектов, а метод .stroke() - это то, что на самом деле рисует наш объект.

Давайте создадим круг. Мы можем сделать это, сначала позвонив c.beginPath(), чтобы указать, что мы хотим начать новый путь, а затем позвонив c.arc(). Этот метод принимает аргументы для координаты x центра круга, координаты y центра круга, радиуса, начального угла в радианах и конечного угла в радианах. Он также принимает необязательный аргумент против часовой стрелки. А пока мы жестко закодируем значения для x, y и радиуса. Наконец, мы можем задать цвет нашему кругу и, наконец, получить его на странице, вызвав c.strokesStyle() и c.stroke():

canvas.js
c.beginPath();
c.arc(200, 200, 30, 0, Math.PI * 2, false );
c.strokeStyle = "pink";
c.stroke();

Теперь, когда у нас есть круг, давайте заставим его что-то делать. Нам понадобится функция, назовем ее animate, которая вызывает requestAnimationFrame(). requestAnimationFrame() принимает функцию в качестве аргумента и был введен как альтернатива традиционному способу создания анимации, который включал рекурсивный вызов .setTimeout(). Мы собираемся передать нашу недавно созданную функцию animate в качестве аргумента для requestAnimateFrame(), что по сути создаст бесконечный цикл. Это то, что мы хотим, так как мы будем немного изменять координаты x и y нашего круга при каждой перерисовке страницы. Это создаст иллюзию движения нашего круга по странице:

canvas.js
const animate = () => {
    requestAnimationFrame(animate)
}
animate()

Теперь поместим код, который мы ранее написали, чтобы создать круг в animate. Прямо сейчас этот код просто перерисовывает круг на месте, поскольку мы фактически не обновили координаты x и y:

canvas.js
const animate = () => {
    requestAnimationFrame(animate);
c.beginPath();
    c.arc(200, 200, 30, 0, Math.PI * 2, false );
    c.strokeStyle = "pink";
    c.stroke();
}
animate()

Мы хотим изменять значения x и y нашего круга при каждом обновлении, так как это создаст иллюзию движения. Сначала мы создадим переменные для координат x и y окружностей вне нашей функции. Затем мы будем увеличивать их при каждом обновлении, задав для x и y значения x += 1 и y += 1. Нам также нужно будет очищать холст каждый раз при обновлении страницы, чтобы изображения не накладывались друг на друга. Мы делаем это, вызывая c.clearRect() which, который принимает в качестве аргументов координату по оси X начальной точки холста, координату оси Y начальной точки холста, ширину холста и его высоту. Мы хотим очистить весь холст, поэтому установим эти значения на 0, 0, innerWidth, innerHeight:

canvas.js
let x = 200;
let y = 200;
const animate = () => {
requestAnimationFrame(animate);
c.clearRect(0, 0, innerWidth, innerHeight)
    c.beginPath();
    c.arc(x, y, 30, 0, Math.PI * 2, false );
    c.strokeStyle = "pink";
    c.stroke();
    x += 1
    y += 1
}
animate()

Теперь круг движется! Но за кадром. Мы собираемся это исправить, но сначала давайте подумаем о том, что делают x += 1 и y += 1. Они увеличивают положение x и y нашего круга на один пиксель при каждой перерисовке. Мы можем думать об этом как о нашей скорости, так как в настоящее время она установлена, наша скорость равна 1. Давайте сделаем это немного более динамичным и установим переменные dx и dy вне нашей функции, которые будут представлять скорость x и y. Теперь внутри нашей функции мы можем вместо этого увеличивать x на dx и y на dy. Теперь нам нужны некоторые условные выражения, чтобы координаты кругов x и y не превышали высоту и ширину нашего элемента холста. Мы можем сделать это, изменив знак наших значений скорости прямо перед тем, как мы превысим значения innerHeight или innerWidth нашего холста:

canvas.js
let x = 200;
let dx = 5;
let y = 200;
let dy = 5;
let radius = 30
const animate = () => {
    requestAnimationFrame(animate);
c.clearRect(0, 0, innerWidth, innerHeight);
    c.beginPath();
    c.arc(x, y, radius, 0, Math.PI * 2, false );
    c.strokeStyle = "pink";
    c.stroke();
if(x + radius > innerWidth || x - radius < 0){
        dx = -dx
    }
if(y + radius > innerHeight || y - radius < 0){
        dy = -dy
    }
    x += dx;
    y += dy;
}
animate()

Затем мы хотим рандомизировать скорость и положение нашего круга, чтобы каждый раз путь не совпадал. Мы можем начать с рандомизации x и y, что даст кругу другую начальную точку всякий раз, когда мы перезагружаем страницу. Мы также хотим рандомизировать скорости, чтобы они могли быть положительными или отрицательными, чтобы изменить направление, в котором кажется, что круг движется при обновлении:

canvas.js
let x = Math.random() * innerWidth;
let dx = (Math.random() - 1) * 10;
let y = Math.random() * innerHeight;
let dy = (Math.random() - 1) * 10;
let radius = 30;
const animate = () => {
    requestAnimationFrame(animate);
c.clearRect(0, 0, innerWidth, innerHeight);
    c.beginPath();
    c.arc(x, y, radius, 0, Math.PI * 2, false );
    c.strokeStyle = "pink";
    c.stroke();
if(x + radius > innerWidth || x - radius < 0){
        dx = -dx
    }
if(y + radius > innerHeight || y - radius < 0){
        dy = -dy
    }
    x += dx;
    y += dy;
}
animate()

Это здорово, у нас есть круг, и он движется. Но не было бы лучше, если бы у нас была сотня движущихся кругов? Для этого нам нужно создать группу круговых объектов. Самый быстрый и эффективный способ сделать это - создать класс круга, из которого мы можем создавать новые экземпляры объектов круга. Таким образом, все они будут иметь доступ к одним и тем же методам через прототипное наследование. Каждому из них потребуются собственные значения x, y, dx, dy и radius, поэтому мы передадим их в функцию-конструктор в качестве аргументов. Затем мы можем переместить код, который мы изначально использовали для рисования круга, в функцию, которую мы создадим в нашем классе, под названием draw(). Нам понадобится вторая функция update(), которая обновит скорость, поэтому мы можем создать ее и в нашем классе. Мы снова можем взять ранее написанный код, обновляющий скорость наших кругов, и поместить его в нашу новую функцию обновления. Нам нужно будет вызвать draw() внутри update(), чтобы действительно отобразить круг на экране, а затем вызвать update() в animate()

canvas.js
class Circle {
    constructor(x, y, dx, dy, radius){
        this.x = x;
        this.y = y;
        this.dx = dx;
        this.dy = dy;
        this.radius = radius
    }
draw() {
        c.beginPath();
        c.arc(this.x, this.y, this.radius, 0, Math.PI*2, false);
        c.strokeStyle = "pink";
        c.stroke();
    }
update() {
        if(this.x + this.radius > innerWidth || this.x - this.radius < 0){
            this.dx = -this.dx
        }
        if(this.y + this.radius > innerHeight || this.y - this.radius < 0){
            this.dy = -this.dy
        }
this.x += this.dx;
        this.y += this.dy;
        this.draw();
    }
}
let x = Math.random() * innerWidth;
let dx = (Math.random() - 0.5) * 10;
let y = Math.random() * innerHeight;
let dy = (Math.random() - 0.5) * 10;
let radius = Math.random() * 50;
const circle = new Circle(x, y, dx, dy, radius)
const animate = () => {
    requestAnimationFrame(animate);
c.clearRect(0, 0, innerWidth, innerHeight)
    circle.update()
}
animate()

Пока у нас остался только один круг. Мы можем решить эту проблему, используя простой цикл for, чтобы создать столько круговых объектов, сколько мы хотим, в данном случае 100, и поместить их в массив. Мы также переместим объявления переменных для x, y, dx, dy и radius в этом цикле:

canvas.js
....
const circleArray = []
for(let i = 0; i < 100; i++){
let radius = Math.random() * 50
    let x = Math.random() * (innerWidth - radius * 2) + radius;
    let dx = (Math.random() - 1.5) * 5
    let y = Math.random() * (innerHeight - radius * 2) + radius;
    let dy = (Math.random() - 1.5) * 5
circleArray.push(new Circle(x, y, dx, dy, radius))
}
....

Теперь у нас есть все экземпляры нашего круга, но как нам их показать? Что ж, у нас все они есть в массиве, поэтому давайте создадим еще один цикл for и поместим его в animate(), где мы вызываем update для всех только что созданных кругов. Если вы помните, у нас есть draw() и update(). draw() создает новый круг со значениями, которые мы передаем. update() затем регулирует положение круга по x и y, используя значения скорости, которые мы передали. Затем он вызывает draw(), чтобы фактически получить круг на странице. Помещая цикл for, который проходит через все наши круги на нашей анимированной странице, мы одновременно рисуем и обновляем положение каждого созданного нами круга каждый раз, когда страница перерисовывается:

cavans.js
class Circle {
    constructor(x, y, dx, dy, radius){
        this.x = x;
        this.y = y;
        this.dx = dx;
        this.dy = dy;
        this.radius = radius;
        this.color = Math.floor(Math.random()*16777215).
        toString(16);
}
draw() {
        c.beginPath();
        c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false );
        c.fillStyle = `#${this.color}`;
        c.fill();
        c.strokeStyle = `#${this.color}`;
        c.stroke();
    }
update() {
        if(this.x + this.radius > innerWidth || this.x - this.radius < 0){
            this.dx = -this.dx
        }
        if(this.y + this.radius > innerHeight || this.y - this.radius < 0){
            this.dy = -this.dy
        }
this.x += this.dx;
        this.y += this.dy;
this.draw()
    }
}
const circleArray = [];
for(let i = 0; i < 100; i++){
    let radius = Math.random() * 50
    let x = Math.random() * (innerWidth - radius * 2) + radius;
    let dx = (Math.random() - 1.5) * 5
    let y = Math.random() * (innerHeight - radius * 2) + radius;
    let dy = (Math.random() - 1.5) * 5
circleArray.push(new Circle(x, y, dx, dy, radius))
}
const animate = () => {
    requestAnimationFrame(animate);
    c.clearRect(0, 0, innerWidth, innerHeight)
for(let i = 0; i < circleArray.length; i++){
        circleArray[i].update()
    }
}
animate()

Наконец, все наши круги движутся в разных направлениях и с разной скоростью! Это была лишь верхушка айсберга с точки зрения того, чего можно достичь с помощью элемента холста HTML и javascript.