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 |
---|
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);
}