Animação por passos

Embora flexíveis, nos tweens todos os valores estão determinados antes da animação começar. Uma vez iniciado, o tween vai seguir um «percurso» fixo, previsível.

Quando a animação dum objeto depende doutros objetos ou eventos, é necessário ir além dos tweens.

Por exemplo:

  • Com movimentos baseados na física é preciso tratar da aceleração, colisões com outros objetos, etc.
  • Os comportamentos reativos («seguir ...», «olhar para ...», etc) dependem dos valores de outros objetos.
  • Os objetos controlados por interação (do teclado, rato, etc) dependem de valores externos ao próprio modelo.

m = initial_model();  // Inicialização do modelo
while (true) {        // Ciclo de animação
    m.update()          // Atualizar o modelo
    m.render()          // Construir a imagem
}

A estrutura básica da animação por tempo, com modelos parametrizados e o ciclo seguinte é válida em geral: a animação resulta da variação dos parâmetros calculada na atualização, quer esta variação resulte de tweens ou de outros cálculos.


Atualização Dinâmica

Intervalo entre fotogramas consecutivos
Intervalo entre fotogramas consecutivos
A atualização dos parâmetros adaptar-se ao intervalo entre fotogramas consecutivos.

Quando o tempo decorrido entre dois fotogramas não é constante, a atualização dos parâmetros do modelo tem de adaptar-se a essa variação.

Os tweens fazem automaticamente esta adaptação: O «valor» do tween é atualizado «internamente».

Para animações gerais, é necessário proporcionar, ao ciclo de animação, informação sobre o tempo: A forma universal e prática para lidar com a variação do tempo no ciclo da animação consiste em passar informação temporal para guiar a atualização dos parâmetros do modelo.

Revisitar o Ciclo de Animação

A função requestAnimationFrame é o elemento fundamental das animações na web (veja a documentação completa em MDN).

window.requestAnimationFrame(callback)
  • A função indicada como argumento (callback) é «chamada» cada vez que o sistema (por exemplo, o browser) está pronto para iniciar um novo passo da animação.
  • Quando a função callback é «chamada» recebe um único argumento, timestamp que assinala esse preciso momento.

function render(element, model) {
  const count   = model.count;
  const time    = Math.round(model.time * 0.001);
  const elapsed = Math.round(model.elapsed);
  const fps     = Math.round(1000 * model.count / model.time);
  element.innerHTML = `<table>` +
    `<tr><td>Count</td><td>Time</td><td>Elapsed (dt)</td><td>FPS</td></tr>` +
      `<tr>` +
      `<td> ${count} </td>` +
      `<td> ${time}s </td>` +
      `<td> ${elapsed}ms </td>` +
      `<td> ${fps} </td>` +
      `</tr>` +
  `</table>`;
}

let element = document.getElementById("anim:steps:counter");
let model = {
    count: 0, time: 0, elapsed: 0
};

let start_time = performance.now();
let last_time = performance.now();

animation_step = function (timestamp) {
    let progress = timestamp - start_time;
    let elapsed = timestamp - last_time;
    last_time = timestamp;
    model.count += 1;
    model.time = progress;
    model.elapsed = elapsed;
    render(element, model);
    requestAnimationFrame(animation_step);
}

requestAnimationFrame(animation_step);

Exemplo: Movimentos Baseados na Física

Para ilustrar uma aplicação da animação por passos usamos uma (simples) simulação física do movimento uniformemente acelerado.

Esta animação não é possível usando apenas tweens porque as colições e as interações modificam o modelo de formas que não podem ser antecipadas.

  • Uma «bola» vermelha saltita numa «caixa» azul.
  • A «bola» está sujeita à força da gravidade (constante ).
  • Um clique na «caixa» dá um impulso (com força ) à «bola».
  • As colisões amortecem a velocidade da «bola» (com fator ).

As leis do movimento (de Newton) definem o estado de uma partícula em termos de posição , velocidade e aceleração e como variam a posição e a velocidade em função da posição, velocidade, aceleração e do tempo decorrido :

porque

const model = {
    x: 64, y: 64,           //  ball position
    vx: 0, vy: 0,           //  ball velocity
    ax: 0, ay: 0,           //  ball acceleration
    r: 16,                  //  ball radius
    min_x: 0, max_x: 256,   //  box bounds: x
    min_y: 0, max_y: 256,   //  box bounds: y
    G: 0.25e-3, D: 0.8,     //  physics constants
    K: 0.8,                 //  kick strength
    kick: true,             //  kick flag
};

// Record time of the animation start
const start = performance.now();

// Record time of the previous step
let prev_ts = performance.now();

// Record time of the step start
const start_ts = performance.now();

// Animation step function
const step = function (ts) {
  const dt = ts - prev_ts;
  prev_ts = ts;

  // update acceleration
  if (model.kick) {   // KICK
      model.ax = model.K * linmap(model.x, model.min_x, model.max_x, -1, 1);
      model.ay = model.K * linmap(model.y, model.min_y, model.max_y, -1, 1);
      model.kick = false;
  } else {                // NO KICK
      model.ax = 0.0;
      model.ay = model.G; // gravity
  }

  // update velocity
  model.vx = model.vx + model.ax * dt;
  model.vy = model.vy + model.ay * dt;

  // update position
  model.x = model.x + model.vx * dt;
  model.y = model.y + model.vy * dt;

  // check collisions
  if (model.x - model.r < model.min_x) {
    model.vx = -model.D * model.vx;
    model.x = model.min_x + model.r;
  }
  if (model.x + model.r > model.max_x) {
    model.vx = -model.D * model.vx;
    model.x = model.max_x - model.r;
  }
  if (model.y - model.r < model.min_y) {
    model.vy = -model.D * model.vy;
    model.y = model.min_y + model.r;
  }
  if (model.y + model.r > model.max_y) {
    model.vy = -model.D * model.vy;
    model.y = model.max_y - model.r;
  }

  // Render the model in the graphics system
  render(context, model);
  requestAnimationFrame(step);
}
requestAnimationFrame(step);
}