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

Иногда, не совсем понятно, как и зачем использовать этот вид математики для создания анимации. Сегодня я хочу показать вам пример, как мы можем использовать тригонометрию в сочетании с нашей банальной физикой для создания забавной анимации.

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

Как когда-то сказала Тина Тернер, на что способна любовь способен треугольник?



Первый элемент, требуемый для нашей анимации – это рычаг. Базовая часть довольно проста, мы можем нарисовать полукруг, чтобы создать следующее.

function drawBase() {
  ctx.beginPath();
  ctx.arc(leverPosition.x, leverPosition.y, 40, Math.PI, 0, false);
  ctx.closePath();
  ctx.lineWidth = 5;
  ctx.fillStyle = '#c1c0c8';
  ctx.fill();
}


Теперь нам нужно нарисовать сам рычаг. Мы знаем, что нам нужен рычаг длиной 200 пикселей, и нам нужно его направить под углом 70° от «основания». Чтобы нарисовать линию, вам нужно знать начальную (x1,y1) и конечную точку (x2,y2). На данном этапе мы знаем нашу начальную точку (основание), но не знаем конечную точку в пространстве x/y.
img


Глядя на схему выше, мы видим, что наш рычаг, расстояние x и расстояние y до нашей второй точки составляют стороны треугольника. Мы знаем расстояние рычага и угол, но нам нужно вычислить стороны x и y. Пришло время тригонометрии!

Вот опять наш рычаг, разбитый на три угла: A, B и C и три стороны: a, b и c:

img


Первый тригонометрический факт, который нам нужно знать для решения нашего треугольника, это то, что три угла в треугольнике всегда в сумме дают 180°

A + B + C = 180

В данном случае нам известен угол C (70°) и угол B (правый угол — 90°), поэтому мы можем найти угол A:

A = 180 — B — C

Теперь у нас есть все три угла, и мы можем попробовать найти наши стороны a (x) и c (y). Для этого мы можем воспользоваться теоремой синусов:

a / sin(A) = b / sin(B) = c / sin(C)

Нам известны значения угла C, угла B и стороны b, поэтому мы можем найти сторону c:

c / sin(C) = b / sin(B) c = b / sin(B) * sin(C)

Наконец, мы можем найти сторону a:

a / sin(A) = c / sin(C) a = c / sin(C) * sin(A)

Вот функция JavaScript, которая может найти стороны x/y треугольника нашего рычага при известном угле С. Не забывайте, что для тригонометрических функций JavaScript, таких как Math.sin, углы должны быть указаны в радианах, а мы их считали в градусах. Я люблю использовать простую функцию помощника, которая помогает отслеживать конвертацию.

function calculateTriangleXY(C) {
  var B = 90;
  // solve for A
  var A = 180 - C - B;
  // solve c and a
  var c = (leverLength * Math.sin(toRadians(C))) / Math.sin(toRadians(B));
  var a = (c * Math.sin(toRadians(A))) / Math.sin(toRadians(C));
  // return x/y sides
  return {x: a, y: c}
}

function toRadians(deg) {
  return deg / 180 * Math.PI;
}


Теперь у нас есть функция, способная найти стороны x/y треугольника, и мы можем нарисовать наш рычаг за счет вычитания этих значений из нашей базовой точки, чтобы получить конечную точку линии.

function drawLever() {
  var xy = calculateTriangleXY(currentAngle);
  ctx.beginPath();
  ctx.moveTo(leverPosition.x, leverPosition.y);
  ctx.lineTo(leverPosition.x - xy.x, leverPosition.y - xy.y);
  ctx.lineWidth = 4;
  ctx.strokeStyle = '#7b4b3d';
  ctx.stroke();
}


Рычаг у нас уже есть, теперь нужно сделать так, чтобы пользователь мог с ним взаимодействовать. Мы можем это сделать, записав движение мышки, и проверив, что мышь привязывается к «области запуска» рычага. При этом «область запуска» представляет собой длинный тонкий треугольник под углом, а потому это не так просто, как отметить положение мышки внутри нормальной рамки x/y.
img


Мы можем использовать тригонометрию для решения и этой проблемы! Когда пользователь держит мышку в области запуска рычага, он создает еще один треугольник с углом 70°. Так как мы знаем положение y мышки и угол рычага, мы можем проверить, каким должно быть положение x, если мышь находится в области запуска. Если x нашей мыши находится рядом с x треугольника, тогда мы можем предположить, что она находится в области запуска. Давайте обновим нашу функцию calculateTriangleXY, чтобы мы могли сообщить ей положение y, а также угол, чтобы получить положение х.

function calculateTriangleXY(C, y) {
  var B = 90;
  // solve for A
  var A = 180 - C - B;
  // if we only have angle C we need to solve edge c first
  var c = y || (leverLength * Math.sin(toRadians(C))) / Math.sin(toRadians(B));
  var a = (c * Math.sin(toRadians(A))) / Math.sin(toRadians(C));
  return {x: a, y: c}
}


Теперь мы можем проверить положение мыши, и если она находится в области запуска, мы изменим курсор на указатель, чтобы пользователь понимал, что объект кликабелен, и установим переменную hitting в значение «истина».

// get x given current angle & y pos
var xy = calculateTriangleXY(currentAngle, leverPosition.y - mousePos.y);
// if mouse x is close to the triangle x, we're in the hit area
if ((mousePos.x > leverPosition.x - xy.x - 20) && (mousePos.x < leverPosition.x - xy.x + 20)) {
    canvas.style.cursor = 'pointer';
    hitting = true;
} else {
    canvas.style.cursor = 'auto';
    hitting = false;
}


Теперь мы знаем, когда пользователь наводит мышь на область запуска! Чудесно! Это значит, что когда пользователь кликнет мышью, мы сможем начать «движение» рычага. Когда пользователь кликнет и потянет, нам нужно сделать так, чтобы рычаг соответствовал положению мыши. Чтобы это сделать, нам нужно определить угол рычага при имеющемся положении x/y мыши. Найти этот угол мы можем с помощью, как вы уже догадались – тригонометрии!

В этом примере мы знаем стороны a и c благодаря позиции x/y мыши, и угол B (90°).

img


Чтобы найти угол C, нам сначала нужно найти сторону b. Для этого мы можем использовать теорию косинусов.

b2 = a2 + c2 ? (2 * a * c * cos(B))

Как только мы найдем сторону b, мы можем найти угол С.

Вот функция для этого:
unction calculateTriangleAngle(x,y) {
  var A, C;
  var B = 90;
  var c = y;
  var a = x;
  var b = Math.sqrt(Math.pow(a, 2) + Math.pow(c, 2) - 2 * a * c * Math.cos(toRadians(B)));
  C = Math.asin(Math.sin(toRadians(B)) / b * c);
  return C;
}



После этого мы можем установить соответствующий угол рычага, что приведет к движению с помощью мыши!

var angle = toDegrees(calculateTriangleAngle(leverPosition.x - mousePos.x, leverPosition.y - mousePos.y));
if (angle < 10) {
  release();
  return;
}
if (angle > 80) angle = 80;
currentAngle = angle;


Наконец, мы можем добавить функцию для «высвобождения» рычага, либо когда пользователь отпускает, либо когда дотягивает рычаг до самого конца. Помните, в 1 части этого блога я объясняла, что вы можете задать скорость в направлении x и y? Вы также можете задать скорость вращения, и именно это мы здесь и сделаем. Мы зададим скорость vr относительно того, насколько далеко был оттянут рычаг, так чтобы он отправлялся обратно до нашего целевого угла в 70°.

function release() {
  pulling = false;
  vr = (70 - currentAngle) * 0.06;
}


Собрав все это вместе, мы получим рычаг, который можно натягивать и отпускать!

Давайте заставим пингвинов летать


Сначала нам нужно создать объект «пингвин», который мы будем использовать для генерирования пингвинов.

function Penguin(x, y, r) {
  var _this = this;
  var img;
  this.x = x;
  this.y = y;
  this.r = targetAngle;

  (function() {
    img = new Image();
    img.src = 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/53148/penguin.png';
  })();

  this.update = function() {
    // rotating the penguin
    ctx.save();
    ctx.translate(_this.x, _this.y); 
    if(_this.latched) ctx.rotate(toRadians(currentAngle));
    else ctx.rotate(toRadians(_this.r));
    // translating a little so it sits further down the ledge
    ctx.translate(24, -16);
    ctx.drawImage(img, -20, -20, 40, 40);
    ctx.restore();
  }
}


Теперь мы можем создать пингвина и разместить его на рычаге.

function addPenguin() {
  var penguin = new Penguin(140,120);
  penguin.latched = true;
  penguins.push(penguin);
  loadedPenguin = penguin;
}


Вы наверняка заметили, что мы установили свойство penguin.latched в значение «true». Это для того, чтобы функция обновления обновляла вращение пингвина по мере вращения рычага, создавая ощущение, что пингвин «привязан» к рычагу. Нам нужно опустить пингвина, когда рычаг вернется в наш изначальный угол после запуска. Мы можем поставить отметку для этого момента в нашу функцию updateRotation и запустить пингвина.

if (released && currentAngle > targetAngle) {
  released = false;
  throwPenguin();
}


«Запуск» пингвина подразумевает присвоение пингвину значений vx, vy и vr, что запустит его в воздух, как только мы обновим функцию обновления Penguin с помощью банальной физики.

function throwPenguin() {
  loadedPenguin.vx = vr*4;
  loadedPenguin.vy = vr*-1.86;
  loadedPenguin.vr = 0.5;
  loadedPenguin.latched = false;
  loadedPenguin = null;
  setTimeout(addPenguin, 500);
}
// added to Penguin.update --
// penguin is launched!
if(!_this.latched && _this.vx) {
  _this.vy += gravity;
  _this.x += _this.vx;
  _this.y += _this.vy;
  _this.r += _this.vr;
  _this.vx *= 0.98;
  _this.vy *= 0.98;

  // check for hitting edges
  if (_this.x + 28 > canvasWidth) {
    _this.x = canvasWidth - 28;
    _this.vx *= bounce;
    _this.vr *= bounce;
  }

  if (_this.y + 38 > canvasHeight) {
    _this.y = canvasHeight - 38;
    _this.vy *= bounce;
    _this.vr *= bounce;
  } 

  if (Math.abs(_this.vx) < 0.001) _this.vx = 0;
  if (Math.abs(_this.vy) < 0.001) _this.vy = 0;
}


Зацените готовое демо

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


  1. jauseg
    09.07.2015 18:09

    а какже инерция вращения после того, как пингвин отделился от палки?


    1. lanseg
      09.07.2015 18:21

      Для игры пингвина вполне можно считать материальной точкой


  1. kahi4
    09.07.2015 18:21
    +2

    Я просто оставлю это здесь

    Ну и разложить гипотенузу на x, y в 4 синуса — это мощно!

    Как оказалось, я забыла еще и тригонометрию.

    Простите, но в таком случае вам (точнее, автору оригинала), стоит почитать «Геометрия, 7 класс»


  1. vvovas
    09.07.2015 18:25
    +4

    Какая завуалированная реклама=)

    А по теме: в колледже понравился проект, в котором была реализована система из нескольких грузов разной массы, связанных между собой пружинами. Все это было приделано к подвесам, и была гравитация. После запуска было интересно следить за раскачиванием грузов.


  1. Temirkhan
    09.07.2015 20:25

    После десятого пущенного пингвинчика имею
    TypeError: loadedPenguin is null

    Также нет обратного вектора силы. То есть, толчок создает инерцию лишь в правую часть оси x.


  1. alkresin
    10.07.2015 10:13

    Что такое «креативный кодинг»?


  1. KvanTTT
    16.07.2015 01:12

    Судя по первой статье, конец немного предсказуем…