Статьи

Оптимизация анимации холста

Я работал над небольшой анимацией холста для  ffconf 2015  и реализовал две важные оптимизации, которые я упустил в прошлом.

Конечный результат: больше нет гудящего вентилятора на моем ноутбуке.

В результате получается простая ретро-анимация, которая продлится всего несколько дней, поэтому я включил здесь улучшенную версию:

Распознать анимацию? Попробуйте включить целевой компьютер 😉

Ради краткости (и, фактически, написания этого поста в обычные несколько часов ), я просто собираюсь поговорить о том, что я изменил.

Пиннинг ФПС

Я знал, что «правильный» подход requestAnimationFrame заключается в использовании  (rAF) для анимации, но исторически я сталкивался с проблемами в том, что частота вызовов моей функции обновления была слишком, слишком часто. Это может привести к слишком быстрой анимации  или к перегреву процессора.

Одним приятным преимуществом rAF для анимации является то, что она перестает срабатывать, когда вкладка не в фокусе (т.е. если вы переключаетесь на другую вкладку). Принимая во внимание, что он  setInterval не только не выберет нужное время, но и будет  продолжать  работать, сжигая батарею.

TIL requestAnimationFrame  передает  метку времени  с высоким разрешением для обратного вызова.

Используя метку времени, мы можем получить дельту последнего запуска, и если, и только если, последний кадр был нарисован X FPS назад, тогда мы нарисуем новый кадр. Например:

var lastFrameTime = 0;
function draw(elapsedTime) {
  // calculate the delta since the last frame
  var delta = elapsedTime - (lastFrameTime || 0);

  // queue up an rAF draw call
  window.requestAnimationFrame(draw);

  // if we *don't* already have a first frame, and the
  // delta is less than 33ms (30fps in this case) then
  // don't do anything and return
  if (lastFrameTime && delta < 33) {
    return;
  }
  // else we have a frame we want to draw at 30fps...

  // capture the last frame draw time so we can work out
  // a delta next time.
  lastFrameTime = elapsedTime;

  // now do the frame update and render work
  // ...
}

Минимизируйте свои краски

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

function draw() {
  // ... calculate x, y, scale, etc
  // makes the shape: |_|
  ctx.beginPath();
  ctx.moveTo(x, y);
  ctx.lineTo(x, y + y2);
  ctx.lineTo(x + x2, y + y2);
  ctx.lineTo(x + x2, y);
  ctx.stroke();
  ctx.closePath();
}

// update is called on a new frame
function update() {
  // ... update state then draw:
  for (i = 0; i < boxes.length; i++) {
    boxes[i].draw();
  }
}

Это будет повторяться для каждой «коробки», анимируемой к зрителю. Так как я просто рисую линии, я могу объединить все это за один раз и сгруппировать коллективные фигуры под одним путем, а затем выполнить  один  штрих:

function draw() {
  // ... calculate x, y, scale, etc
  // makes the shape: |_|
  ctx.moveTo(x, y);
  ctx.lineTo(x, y + y2);
  ctx.lineTo(x + x2, y + y2);
  ctx.lineTo(x + x2, y);
}

// update is called on a new frame
function update() {
  // ... update state then draw:
  ctx.beginPath();
  for (i = 0; i < boxes.length; i++) {
    boxes[i].draw();
  }
  ctx.stroke();
  ctx.closePath();
}

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

Одиночный обработчик rAF

Если у вас запущено более одной анимации, вам нужно настроить несколько обратных вызовов  requestAnimationFrame, но наличие нескольких обратных вызовов rAF не является идеальным и начинает становиться громоздким, когда вы работаете с другими, которые  также  хотят поставить в очередь до их анимации.

Вы действительно хотите, чтобы все было обработано в  одном  обработчике rAF.

Я написал небольшую суть  raf.js,  которая позволяет мне выполнять все мои вызовы rAF через один обработчик (и добавил некоторые тонкости, такие как пиннинг FPS и  runningлогический флаг).