Работаем с двумерной графикой в JavaScript
Создание реалистичной анимации физических процессов может казаться сложной задачей, но это не так. Используемые для этого алгоритмы могут быть очень простыми и при этом точно воспроизводить такие физические явления, как движение, ускорение и гравитация (притяжение).
Хотите узнать, как эти алгоритмы реализуются в JS?
Равномерное движение и движение с ускорением
Для равномерного движения мы можем использовать следующий код:
Здесь x и y — это координаты объекта, vx и vy — скорость объекта по горизонтальной и вертикальной осям, соответственно, dt (time delta — дельта времени) — время между двумя отметками таймера, что в JS равняется двум вызовам requestAnimationFrame.
Например, если мы хотим переместить объект, находящийся в точке с координатами 150, 50, на юго-запад, мы можем сделать следующее (одна отметка таймера или один шаг):
x = 150 += -1 * 0.1 - > 149.9 y = 50 += 1 * 0.1 - > 50.1
Равномерное движение — это скучно, поэтому давайте придадим нашему объекту ускорение:
Здесь ax и ay — это ускорение по осям x и y, соответственно. Мы используем ускорение для изменения скорости (vx/vy). Теперь, если мы возьмем предыдущий пример и добавим ускорение по оси x (на запад), то получим следующее:
vx = -1 += -1 * 0.1 - > -1.1 // vx += ax * dt vy = 1 += 0 * 0.1 - > 1 // vy += ay * dt x = 150 += -1.1 * 0.1 - > 149.89 // x += vx * dt; объект переместился дальше на -0.01 y = 50 += 1 * 0.1 - > 50.1 // y += vy * dt
Гравитация
Мы научились перемещать отдельные объекты. Как насчет того, чтобы научиться перемещать их относительно друг друга? Это называется гравитацией или притяжением. Что нам нужно сделать для этого?
Вот что мы хотим получить:
Для начала вспомним несколько уравнений из старших классов.
Сила, приложенная к телу, рассчитывается по следующей формуле:
F = m * a… сила равна массе, умноженной на ускорение
a = F / m… из этого мы можем сделать вывод, что сила действует на объект с ускорением
Если мы применим это к двум взаимодействующим объектам, то получим следующее:
Выглядит сложно (по крайней мере, для меня), поэтому давайте разбираться. В данном уравнении |F| — это величина силы, которая одинакова для обоих объектов, но направлена в противоположные стороны. Объекты представлены массами m_1 и m_2. k — это гравитационная постоянная и r — расстояние между центрами масс объектов. Все еще непонятно? Вот иллюстрация:
Если мы хотим сделать что-то интересное, нам потребуется больше двух объектов.
На этом изображении мы видим два оранжевых объекта, притягивающих черный с силами F_1 и F_2, однако нас интересует равнодействующая сила F, которую мы можем вычислить следующим образом:
- сначала мы рассчитываем силы F_1 и F_2, используя предыдущую формулу:
- затем переводим все в векторы:
Отлично, у нас есть все необходимые расчеты. Как нам перевести это в код? Я не буду утомлять вас промежуточными этапами и сразу приведу готовый код с комментариями. Если вам понадобится больше информации, можете написать мне, я обязательно отвечу на все ваши вопросы.
function moveWithGravity(dt, o) < // o - массива объектов, с которыми мы работаем for (let o1 of o) < // нулевой счетчик (сумматор) сил каждого объекта o1.fx = 0 o1.fy = 0 >for (let [i, o1] of o.entries()) < // для каждой пары объектов for (let [j, o2] of o.entries()) < if (i < j) < // чтобы не делать одного и того же для той же пары дважды let dx = o2.x - o1.x // вычисляем расстояние между центрами объектов let dy = o2.y - o1.y let r = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) if (r < 1) < // чтобы избежать деления на 0 r = 1 >// вычисляем равнодействующую для этой пары; k = 1000 let f = (1000 * o1.m * o2.m) / Math.pow(r, 2) let fx = f * dx / r let fy = f * dy / r o1.fx += fx // сила первого объекта o1.fy += fy o2.fx -= fx // сила второго объекта в противоположной направлении o2.fy -= fy > > > for (let o1 of o) < // для каждого объекта обновляем. let ax = o1.fx / o1.m // ускорение let ay = o1.fy / o1.m o1.vx += ax * dt // скорость o1.vy += ay * dt o1.x += o1.vx * dt // позицию o1.y += o1.vy * dt >>
Столкновение
Движущиеся тела иногда сталкиваются. От столкновения происходит либо выталкивание одних объектов другими, либо отскакивание одних объектов от других. Сначала поговорим о выталкивании:
Прежде всего, нам необходимо определить, что имело место столкновение:
class Collision < constructor(o1, o2, dx, dy, d) < this.o1 = o1 this.o2 = o2 this.dx = dx this.dy = dy this.d = d >> function checkCollision(o1, o2) < let dx = o2.x - o1.x let dy = o2.y - o1.y let d = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) if(d < o1.r + o2.r)< return < collisionInfo: new Collision(o1, o2, dx, dy, d), collided: true >> return < collisionInfo: null, collided: false >>
Мы объявляем класс Collision, представляющий два столкнувшихся объекта. В функции checkCollision мы сначала вычисляем разницу между координатами x и y объектов, затем вычисляем их фактическое расстояние d. Если сумма радиусов объектов меньше, чем расстояние между ними, значит имело место столкновение этих объектов — возвращаем объект Collision.
Далее нам нужно определить направление смещения и его величину (магнитуду):
n_x = d_x / d… это вектор
n_y = d_y / d
s = r_1 + r_2 — d… это «величина» столкновения (см. картинку ниже)
В JS это может выглядеть так:
function resolveCollision(info) < // "info" - это объект Collision из предыдущего примера let nx = info.dx / info.d // вычисляем векторы let ny = info.dy / info.d let s = info.o1.r + info.o2.r - info.d // вычисляем глубину проникновения info.o1.x -= nx * s/2 // сдвигаем первый объект на половину величины столкновения info.o1.y -= ny * s/2 info.o2.x += nx * s/2 // сдвигаем второй объект в противоположную сторону info.o2.y += ny * s/2 >
Отскакивание
Завершающая часть пазла — реализация отскакивания одного объекта от другого при столкновении. Я не буду приводить всех математических расчетов, поскольку это сделает статью очень длинной и скучной, ограничусь лишь тем, что упомяну о законе сохранения импульса и законе сохранения энергии, которые помогают прийти к следующей волшебной формуле:
k = -2 * ((o2.vx — o1.vx) * nx + (o2.vy — o1.vy) * ny) / (1/o1.m + 1/o2.m)… *Магия*
Как мы можем использовать волшебную k? Мы знаем, в каком направлении будут двигаться объекты, но не знаем на какое расстояние. Это и есть k. Вот как вычисляется вектор (z), показывающий, куда должны переместиться объекты:
function resolveCollisionWithBounce(info) < let nx = info.dx / info.dy let ny = info.dy / info.d let s = info.o1.r + info.o2.r - info.d info.o1.x -= nx * s/2 info.o1.y -= ny * s/2 info.o2.x += nx * s/2 info.o2.y += ny * s/2 // магия. let k = -2 ((info.o2.vx - info.o1.vx) * nx + (info.o2.vy - info.o1.vy) * ny) / (1/info.o1.m + 1/info.o2.m) info.o1.vx -= k * nx / info.o1.m // то же самое, только добавили "k" и поменяли "s/2" на "m" info.o1.vy -= k * ny / info.o1.m info.o2.vx += k * nx / info.o2.m info.o2.vy += k * ny / info.o2.m >
Заключение
В статье много уравнений, но большинство из них очень простые. Надеюсь, статья хоть немного помогла вам понять, как в JS реализуются физические явления и процессы.
Анимация на JavaScript используя Canvas
Двумерная и трехмерная анимация, создаваемая как традиционными, так и компьютерными методами, основана на одном и том же принципе: если ряд статичных изображений показать в достаточно быстром темпе, то человеческий глаз свяжет их вместе и примет за непрерывное движение.
Наиболее очевидный и распространенный вид анимации в компьютерной графике – это спрайтовая анимация.
Спрайт — графический объект в компьютерной графике, чаще всего растровое изображение, свободно перемещающееся по экрану.
Наиболее простой вид спрайта – это прямоугольное изображение. Для начального его определения необходимо задать размер и положение.
Перемещение спрайта по экрану определяет изменение координат его положения с течением времени.
Т.е. необходима функция, которая срабатывает с определенным интервалом времени. И такая функция в JS есть, и не одна.
- setTimeout позволяет вызвать функцию один раз через определённый интервал времени.
- setInterval позволяет вызывать функцию регулярно, повторяя вызов через определённый интервал времени.
setTimeout
Синтаксис:
let timerId = setTimeout ( func | code , [ delay ] , [ arg1 ] , [ arg2 ] , . )
Параметры:
func|code — Функция или строка кода для выполнения. Обычно это функция. По историческим причинам можно передать и строку кода, но это не рекомендуется.
delay — Задержка перед запуском в миллисекундах (1000 мс = 1 с). Значение по умолчанию – 0.
arg1, arg2… — Аргументы, передаваемые в функцию.
Отмена через clearTimeout
Вызов setTimeout возвращает «идентификатор таймера» timerId , который можно использовать для отмены дальнейшего выполнения.
let timerId = setTimeout(. ); clearTimeout(timerId);
setInterval
Метод setInterval имеет такой же синтаксис как setTimeout :
let timerId = setInterval ( func | code , [ delay ] , [ arg1 ] , [ arg2 ] , . )
Все аргументы имеют такое же значение. Но отличие этого метода от setTimeout в том, что функция запускается не один раз, а периодически через указанный интервал времени.
Чтобы остановить дальнейшее выполнение функции, необходимо вызвать clearInterval ( timerId ) .
Пример анимации линейного спрайтового движения:
html> head> meta charset="utf-8" /> script src="test.js">script> head> body onload="init()"> canvas id="tutorial" width="500" height="500">canvas>br /> input type="button" onclick="start()" value="Пуск"> input type="button" onclick="stop()" value="Стоп"> body> html>
var x; var Idint; var ctx; function init(){ x=10; ctx = document.getElementById('tutorial').getContext('2d'); } function draw() { ctx.fillStyle = 'white'; ctx.fillRect(x,10,50,50); x=x+3; ctx.fillStyle = 'blue'; ctx.fillRect(x,10,50,50); } function start() { Idint = setInterval(draw, 100); } function stop() { clearInterval(Idint); }
В действительности лучше отображать меньшее количество кадров в секунду, но сделать это количество постоянным. Дело в том, что наш глаз воспринимает небольшие отклонения в частоте, и несколько выпавших кадров режут глаз больше, чем более низкое количество кадров в секунду. Вот здесь на помощь приходит встроенный в HTML5 API requestAnimationFrame.
Преимущества requestAnimationFrame
requestAnimationFrame дает браузеру возможность контролировать, сколько кадров он может обработать. Вместо того, чтобы требовать от браузера отображать кадры, которые в итоге выпадут, вы разрешаете ему показывать кадры тогда, когда они обработаны, и с постоянной частотой. Польза от этого двояка:
- Анимация выглядит более плавной, поскольку уровень кадров в секунду остается постоянной.
- Процессор не перегружается задачами по рендерингу, а может обрабатывать и другие задачи во время рендеринга анимации. Вообще браузер может определить тот уровень кадров в секунду, который будет оптимален для задач, которые браузер выполняет одновременно с анимацией.
let requestId = requestAnimationFrame ( callback )
Такой вызов планирует запуск функции callback на ближайшее время, когда браузер сочтёт возможным осуществить анимацию.
Пример анимации линейного спрайтового движения:
var x; var Idint; var ctx; function init(){ x=10; ctx = document.getElementById('tutorial').getContext('2d'); } function draw() { if (x450) { ctx.fillStyle = 'white'; ctx.fillRect(x,10,50,50); x=x+3; ctx.fillStyle = 'blue'; ctx.fillRect(x,10,50,50); Idint = requestAnimationFrame(draw); } else {cancelAnimationFrame(Idint);} } function start() { Idint = requestAnimationFrame(draw); } function stop() { cancelAnimationFrame(Idint); }