Признание: На самом деле я не очень хорошо знаю физику. Сказать, что я обладаю базовыми знаниями – это сильно преувеличить. Последний раз я знала что-то достаточно сложное по физике 11 лет назад. Мне было 17, и я знала ровно столько, чтобы получить твердую четверку на выпускных экзаменах в школе, после которых я немедленно выкинула всю эту информацию из своей головы, чтобы освободить место для более важных вещей, таких как тексты песен из альбома «Nellyville» Nelly.

Но позже я поняла, что я могла бы использовать некоторые физические расчеты, чтобы создавать крутую анимацию с canvas. Черт побери! К счастью, я довольно быстро осознала, что мне не обязательно вспоминать программу старших классов. Достаточно просто схитрить.

Банальная физика


Мне нравится называть это банальной физикой, так как мы используем очень базовое понимание векторов и пишем JavaScript, который в какой-то мере воспроизводит эти векторы, но никоим образом не представляет собой реальную физическую среду. Преимущество банальной физики заключается в том, что вам на самом деле даже не нужно понимать теорию физики за ее пределами. Вам нужно понимать только то, как ее использовать для анимации.

Векторы


Вектор – это математический объект, характеризующийся величиной и направлением. Скорость и ускорение – это векторы. Если вы скажете, что собираетесь ехать в северо-восточном направлении со скоростью 35 миль в час, вы опишете вектор.

Создаем скорость с помощью JavaScript


Наверняка, вы уже знаете, что такое скорость, но я все равно вам расскажу. Вектор скорости – это вектор, который состоит из скорости и направления. Когда мы воспроизводим скорость в JavaScript, мы делим ее на два вектора – скорость в направлении x и скорость в направлении y. Здесь мы имеем дело с пикселями и анимацией JavaScript, поэтому мы можем описать нашу скорость с точки зрения количества пикселей в кадр. Квадрату в примере присвоена скорость в направлении х в размере 2 пикселей на каждый кадр, и скорость в направлении y в размере 2 пикселей на каждый кадр. В результате мы получаем линейное диагональное движение.

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var width = canvas.width = window.innerWidth;
var height = canvas.height = window.innerHeight;

// position
var x = 0;
var y = 0;
// velocity
var vx = 2;
var vy = 2;

// animation loop
function animate() {
  // clear canvas
  ctx.clearRect(0, 0, width, height);
  // draw square
  ctx.fillRect(x,y,20,20);
  
  // update position with velocity
  x += vx;
  y += vy;
  requestAnimationFrame(animate);
  
  // if square is out of view reset position to the start
  if (y > height || x > width) {
    x = 0;
    y = 0;
  }
}

animate();


Создаем ускорение с JavaScript


Ускорение – это коэффициент изменения скорости объекта. И как же мы воспроизводим это в JavaScript? У нас есть скорость vx и vy, и мы можем добавить ускорение в направлении x или y в каждом кадре. То есть, если ускорение равно 0,5 пикселей, мы добавим 0,5 пикселя к скорости x и скорости y в каждом кадре. Это заставит наш квадратик ускориться.

Невероятно простой и забавный пример – давайте сделаем фонтан из частиц!


Фонтан из частиц – это первый эффект банальной физики, который я анимировала. В частности, я сделала его с помощью Flash и ActionScript, но сейчас мы можем сделать это с JavaScript и canvas. По сути, я планирую использовать объект «частица» для создания фонтана.

Чтобы сделать фонтан, нам нужно сгенерировать некоторое количество частиц. Каждая частица должна иметь свое положение и скорость (vx и vy). Начальной позицией мы сделаем центр canvas, после чего зададим произвольную скорость частиц, чтобы они «распылялись» из нашего фонтана.

function Particle(x, y, vx, vy, size, color, opacity) {

  this.update = function() {
    x += vx;
    y += vy;
  }

  this.draw = function() {
    ctx.globalAlpha = opacity;
    ctx.fillStyle = color;
    ctx.fillRect(x, y, size, size);
  } 
}

function createParticle(i) {
  // initial position in middle of canvas
  var x = width*0.5;
  var y = height*0.5;
  // randomize the vx and vy a little - but we still want them flying 'up' and 'out'
  var vx = -2+Math.random()*4;
  var vy = Math.random()*-3;
  // randomize size and opacity a little & pick a color from our color palette
  var size = 5+Math.random()*5;
  var color = colors[i%colors.length];
  var opacity =  0.5 + Math.random()*0.5;
  var p = new Particle(x, y, vx, vy, size, color, opacity);
  particles.push(p);
}


В результате должно получиться что-то вроде этого (возможно, вам придется нажать «rerun», чтобы их увидеть).

Частицы просто распыляются вверх и никогда не падают вниз. Нам нужно добавить немного «силы притяжения», чтобы заставить частицы падать. Мы уже знаем, как это сделать, потому что силу притяжения, по сути, можно воспроизвести с помощью ускорения в направлении y (вниз). Давайте установим значение силы притяжения (ускорения) равное 0,04 пикселя. Также мы немного уменьшим прозрачность каждого кадра, чтобы каждая частица в результате растворялась. После этого мы сделаем нулевую прозрачность в точке, в которой мы будем «возвращать» нашу частицу в центр фонтана.

Листинг результата
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var width = canvas.width = window.innerWidth;
var height = canvas.height = window.innerHeight;
var particles = [];
var colors = ['#029DAF', '#E5D599', '#FFC219', '#F07C19', '#E32551'];
var gravity = 0.04;

function initParticles() {
  for (var i = 0; i < 200; i++) {
    setTimeout(createParticle, 20*i, i);
  }
}

function createParticle(i) {
  // initial position in middle of canvas
  var x = width*0.5;
  var y = height*0.5;
  // randomize the vx and vy a little - but we still want them flying 'up' and 'out'
  var vx = -2+Math.random()*4;
  var vy = Math.random()*-3;
  // randomize size and opacity a little & pick a color from our color palette
  var size = 5+Math.random()*5;
  var color = colors[i%colors.length];
  var opacity =  0.5 + Math.random()*0.5;
  var p = new Particle(x, y, vx, vy, size, color, opacity);
  particles.push(p);
}

function Particle(x, y, vx, vy, size, color, opacity) {
  
  function reset() {
    x = width*0.5;
    y = height*0.5;
    opacity = 0.5 + Math.random()*0.5;
    vx = -2+Math.random()*4;
    vy = Math.random()*-3;
  }
  
  this.update = function() {
    // if a particle has faded to nothing we can reset it to the starting position
    if (opacity - 0.005 > 0) opacity -= 0.005 ;
    else reset();
    
    // add gravity to vy
    vy += gravity;
    x += vx;
    y += vy;
  }
  
  this.draw = function() {
    ctx.globalAlpha = opacity;
    ctx.fillStyle = color;
    ctx.fillRect(x, y, size, size);
  } 
}

function render() {
  ctx.clearRect(0, 0, width, height);
  for (var i = 0; i < particles.length; i++) {
    particles[i].update();
    particles[i].draw();
  }
  requestAnimationFrame(render);
}


// resize
window.addEventListener('resize', resize);
function resize() {
  width = canvas.width = window.innerWidth;
  height = canvas.height = window.innerHeight;
}


// init
initParticles();
render();



Таким образом, у нас получится бесконечный фонтан частиц! А во второй части, мы запустим в полет нашего первого пингвина.

Комментарии (6)


  1. Zenitchik
    08.07.2015 19:09
    -3

    Конечных разностей чтоли?


  1. SerafimArts
    08.07.2015 20:57
    +3

    Помнится тоже когда-то подобным страдал, даже примерчик есть небольшой: jsbin.com/quxigoruho/1/edit?output

    Увы, с коллизиями фигур менее шарообразных форм, тем более общими — работать намного сложнее. Всё что у меня получилось из этого — глючит и ломается. Так что интересно было бы узнать что будет в будущих статьях (это ведь ч.1, но ничего особо интересного тут нет, простите). Если не сложно (уже есть что-то в планах), спасибо.


    1. SerafimArts
      09.07.2015 00:23
      +1

      Ох, не заметил, это перевод. Увидел, что ещё есть две части, да, довольно интересное продолжение, спасибо за работу по переводу, будем ждать-с.


  1. ertaquo
    08.07.2015 21:01
    +6

    Весь пост ради рекламы в конце? Тут физикой практически даже не пахнет. Написали бы заголовок типа «Простейший генератор частиц на JS Canvas своими руками», что ли… www.gamedev.ru/code/articles/?id=4249 — вот толковая статья по физике.


    1. KvanTTT
      08.07.2015 23:25

      Ах, да помню ее, читал с интересом… Пытался в свое время тоже графику только на паскале реализовать. Уж 10 лет прошло…


  1. kahi4
    09.07.2015 00:48
    +2

    Есть одно мелкое и невероятно важное замечание: а время отрисовки кадра кто учитывать будет? Т.е.

        vy += gravity * dt;
        x += vx  * dt;
        y += vy * dt;
    

    Ну и да, стоило бы сказать, что это метод Эйлера (с почему-то выкинутым h) в чистом виде, только размазанный на объекты. Ну и ожидаемо, что частицы будут «тормозиться» при отлете.

    Ну и немного конструктивной критики:
    Скрытый текст
    Метод draw не должен привязываться к глобальному ctx, он ему должен передаваться в виде аргумента (мало ли у вас не один canvas будет, а несколько слоев).

    Почему в функции render вызывается функция update? Тогда логичнее будет назвать mainLoop.

    Хорошим тоном является сразу привести список частей, из которых будет состоять повествование, а то пока это тянет на часть 1.1, даже на 1.1.1.

    createParticle(i) и reset делают одно и то же. Не хорошо, не хорошо! Раз учите людей что-то делать, не учите хотя бы плохому.

    ctx.globalAlpha = opacity;
    

    Кто обратно альфаканал в 1 выставлять будет?

    function createParticle(i)
    {
      var x = width*0.5;
      var y = height*0.5;
    ...
    }
    

    С чего вдруг частица сама решает где ей появиться? В теории, должен быть метод createParticleSystem, который принимает координаты центра, направление (в общем случае), начальное ускорение, ну и т.д.

    *много бубнения по поводу определения вектора. А нам, дуракам, половину семестра читали векторы, а тут в одно предложение влезло! Ну и вы описали радиус-вектор, покуда забыли еще точку приложения этого вектора указать, а не пометили это отдельно. Не хорошо вводить в заблуждение!

    Вижу, что перевод, но перевод, на мой взгляд, неудачной статьи, пронизанной попыткой гуманитария описать то, в чем он сам слабо понимает. Если выкинуть слово «физика» — еще куда не шло. И да, помнится, вот ровно эту статью кто-то уже переводил на хабре, по крайней мере ошибки там были такие же.