Технология Canvas Core — как реализовать сложную анимацию

внешний интерфейс браузер игра Canvas

Это пятая часть серии заметок об изучении и просмотре холста. Полные заметки см.технология сердцевины холста.

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

основная логика

Анимация, которую мы понимаем, должна заключаться в том, что некоторые свойства объектов, такие как цвет, размер, положение, прозрачность и т. д., изменяются с течением времени. Единицей для оценки степени потока анимации является частота обновления анимации, которая обычно является частотой кадров браузера в браузере. Чем выше частота кадров, тем плавнее анимация. В современных браузерах мы обычно используемrequestAnimationFrameдля выполнения анимации.

let raf = null;
let lastFrame = 0;

//动画
function animate(frame) {
  // todo:这里可以执行一些动画更新
  console.log(frame)
  raf = requestAnimationFrame(animate);
  lastFrame = frame;
}

function start() {
  // 一些初始化的操作
  init();

  // 执行动画
  animate(performance.now());
}

function stop() {
  cancelAnimationFrame(raf);
}

Общая структура такова, черезrequestAnimationFrameНепрерывно выполнять в следующем кадре браузераanimate,а такжеanimateФункция принимает в качестве аргумента отметку времени начала выполнения текущего кадра. Если вы хотите прервать текущую анимацию, просто вызовитеcancelAnimationFrame, то он не будет выполняться в следующем кадреanimateфункция. Время выполнения предыдущего кадра можно использоватьframe - lastFrame, а затем на основе этой разницы можно рассчитать частоту кадров текущей анимации следующим образом:

let fps = 0;
let lastCalculateFpsTime = 0;
function calculateFps(frame) {
  if (lastFrame && (fps === 0 || frame - lastCalculateFpsTime > 1000)) {
    fps = 1000 / (frame - lastFrame);
    lastCalculateFpsTime = frame;
  }
}
//动画
function animate(frame) {
  // todo:这里可以执行一些动画更新
  calculateFps(frame);
  raf = requestAnimationFrame(animate);
  lastFrame = frame;
}

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

фактор времени

При рисовании анимации мы должны ориентироваться на время, а не на частоту кадров текущего браузера. Поскольку разные браузеры будут иметь разную частоту кадров, а один и тот же браузер может иметь разную частоту кадров при разных нагрузках на графический процессор, поэтому наша анимация должна быть основана на времени, чтобы обеспечить одинаковую скорость в одно и то же время. Изменения анимации согласованы. . Например, когда мы рассматриваем вертикальное падение мяча, мы должны задать скорость падения мяча v, и тогда по формулеs = v * t, получить расстояние перемещения мяча в текущий период времени и вычислить координаты в текущем кадре.

  /* 初始化 */
  private init() {
    this.fps = 0; 
    this.lastFrameTime = 0;
    this.speed = 5; // 设置小球初速速度为:5m/s
    this.distance = 50; //设置小球距离地面高度为:50m
    let pixel = this.height - this.padding * 2;
    if (this.distance <= 0) {
      this.pixelPerMiter = 0;
    } else {
      this.pixelPerMiter = (this.height - this.padding * 2) / this.distance;
    }
  }

В приведенном выше коде при инициализации мы устанавливаем начальную скорость мяча равной5m/s, высота мяча от земли равна50m, и вычислил отношение физической высоты к высоте пикселяpixelPerMiter, это значение пригодится при вычислении координат шара позже.

  /* 更新 */
  private update() {
    if (this.fps) {
      this.ball.update(1000 / this.fps); //更新小球
    }
  }

Затем при обновлении положения мяча в каждом кадре мы передаем значение времени предыдущего кадра вball.update.

  /* 移动 */
  static move(ball: Ball, elapsed: number) {
    //小球是静止状态,不更新
    if (ball.isStill) {
      return;
    }
    let { currentSpeed } = ball;
    let t = elapsed / 1000; //elapsed是毫秒, 而速度单位是m/s,所以要除1000
    let distance = ball.currentSpeed * t; 
    if (ball.offset + distance > ball.verticalHeight) {
      ////如果小球是否已经超过实际高度,则落到地面了
       ball.isStill = true;
       ball.currentSpeed = 0;
       ball.offset = ball.verticalHeight;
    } else {
      ball.offset += distance;
    }
  }

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

  /* 绘制小球 */
  public render(ctx: CanvasRenderingContext2D) {
    let { x, y, radius, offset, pixelPerMiter } = this;
    ctx.save();
    ctx.translate(x, y + offset * pixelPerMiter); //offset * pixelPerMiter得到下落的像素
    ctx.beginPath();
    ctx.arc(0, 0, radius, 0, Math.PI * 2, false);
    ctx.fill();
    ctx.stroke();
    ctx.restore();
  }

Наконец, при нанесении мяча, во-первых, в соответствии с фактической высотой каплиoffsetи соотношение фактической высоты до высоты пикселей рассчитывается ранее, чтобы получить значение пикселя шарика, падающего на экран (offset * pixelPerMiter).

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

физические факторы

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

  /* 创建小球 */
  private createBall() {
    let { width, height, padding, speed, radius, pixelPerMiter, distance } = this;
    this.ball = new Ball(width / 2, padding - radius, radius, { verticalHeight: distance, pixelPerMiter, useGravity: true });
    this.ball.setSpeed(speed);
    this.ball.addBehavior(Ball.move);
  }

При создании мяча мы дали параметрuserGravityЧтобы указать, следует ли использовать гравитационное ускорение, здесь мы устанавливаем его какtrue, а так же передаем начальные координаты и радиус шара, а так же начальную скорость и т.д.

const GRAVITY = 9.8; //重力加速度9.8m/s  
  /* 移动 */
  static move(ball: Ball, elapsed: number) {
    // ...
    //如果应用了重力加速度,则更新速度
    if (ball.useGravity) {
      ball.currentSpeed += GRAVITY * t;
    }
   // ...
  }

Затем при обновлении мяча добавляем расчет текущей скорости мяча, по формулеv = g * tРассчитайте скорость предыдущего кадра, чтобы со временем скорость мяча действительно увеличивалась, и мяч падал все быстрее и быстрее.

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

//创建小球
this.ball = new Ball(width / 2, padding - radius, radius, { verticalHeight: distance, pixelPerMiter, useGravity: true, useRebound: true });

При создании шара мы прошлиuseRebound:true, что указывает на то, что текущий мяч применил эффект отскока.При обновлении мяча необходимо определить, что направление текущей скорости меняется на противоположное, и размер уменьшается до 0,6 раза, когда мяч ударяется о землю.Этот коэффициент 0,6 является только эмпирическое значение, в игре его можно корректировать для достижения нужного эффекта. Чем больше коэффициент, тем выше отскок.

  /* 移动 */
  static move(ball: Ball, elapsed: number) {
    //小球是静止状态,不更新
    if (ball.isStill) {
      return;
    }
    let { currentSpeed } = ball;
    let t = elapsed / 1000; //elapsed是毫秒, 而速度单位是m/s,所以要除1000
    //更新速度
    if (ball.useGravity) {
      ball.currentSpeed += GRAVITY * t;
    }
    let distance = ball.currentSpeed * t; 
    if (ball.offset + distance > ball.verticalHeight) {
      //落到地面了
      //使用反弹效果
      if (ball.useRebound) {
        ball.offset = ball.verticalHeight;
        ball.currentSpeed = -ball.currentSpeed * 0.6; //速度方向取反,大小乘0.6
        if ((distance * ball.pixelPerMiter) / t < 1) {
          //当前移动距离小于1px,应该静止了,
          ball.isStill = true;
          ball.currentSpeed = 0;
        }
      } else {
        ball.isStill = true;
        ball.currentSpeed = 0;
        ball.offset = ball.verticalHeight;
      }
    } else {
      ball.offset += distance;
    }
  }
}

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

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

Вот полный онлайн-пример моего мяча в свободном падении.

Деформация временной шкалы

Анимация длится в течение определенного периода времени. Мы можем заранее указать определенное значение продолжительности, чтобы анимация продолжала выполняться в течение этого периода, как в CSS3.animation-duration, а затем, искажая временную шкалу, вы можете заставить анимацию выполнять нелинейное движение, такое как наше обычноеЛегкость в действии,Расслабься,Эффект легкости входа и выходаЖдать.

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

eplased = actualElapsed * effectPercent/compeletePercent

линейная функция,

  static linear() {
    return function(percent: number) {
      return percent;
    };
  }

Легкость в функции,

  static easeIn(strength: number = 1) {
    return function(percent: number) {
      return Math.pow(percent, strength * 2);
    };
  }

Функция облегчения,

  static easeOut(strength: number = 1) {
    return function(percent: number) {
      return 1 - Math.pow(1 - percent, strength * 2);
    };
  }

Функция легкого входа и выхода,

  static easeInOut() {
    return function(percent: number) {
      return percent - Math.sin(percent * Math.PI * 2) / (2 * Math.PI);
    };
  }

Вот полный онлайн-пример деформации моей временной шкалы..

Более сложные функции ослабления включают пружинные эффекты, кривые Безье и т. Д. Для получения подробной информации см.EasingFunctions.

резюме

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

При создании игр на холсте в основном используются анимации.Если объекты двигаются, будут столкновения.Например,когда наш мяч падает,происходит столкновение мяча с землей.Мы выполнили простое обнаружение столкновений. В следующей заметке мы подробно обсудим, как выполнять обнаружение столкновений в холсте.