Seguidor Interativo
Controle um líder (o quadrado vermelho) usando as teclas
QAOPpara fugir de um seguidor (o quadrado azul) e evitar colidir com as paredes.
Este exemplo ilustra o ciclo de animação:
let model = init_model();
let context = new_context();
let step = (ts) => {
model.update(ts);
context.render(model);
requestAnimationFrame(step);
}
requestAnimationFrame(step);
Em particular, os seguintes pontos:
- Usar um modelo para controlar uma animação por passos.
- Atualizar o modelo em função de eventos «externos». Neste caso, eventos do teclado.
- Aplicar conversões entre coordenadas cartesianas e polares para facilitar o controlo do movimento de um objeto gráfico.
- Fazer o movimento em termos de aceleração, velocidade e posição.
O Modelo
Um modelo tem um estado inicial que é atualizado ao longo da animação.
- O modelo é inicializado na função
init_model— esta função define um objeto com os parâmetros necessários para a animação, mais alguns usados na visualização e acrescenta alguns métodos a esse objeto para tornar a programação da animação mais sistemática e ergonómica. - O modelo é atualizado em cada passo da animação através do método
update. - A evolução do modelo depende de eventos externos, que são detetados assincronamente e processados durante a atualização.
Estado Inicial
Na construção do estado inicial são definidos os atributos e métodos que definem o modelo.
Esta animação tem um leader controlado com o teclado, e um follower que procura «apanhar» o líder. Portanto, o modelo necessita de parâmetros e métodos para:
- Representar a posição das «personagens».
- Aplicar as ações que decorrem do teclado.
- Atualizar o modelo.
- Proporcionar propriedades de visualização como «cor» ou «tamanho»
Movimento
Para controlar o movimento dos objetos define-se uma posição
pos: {x:, y:}e uma velocidadevel: {x:, y:}.
function init_model() {
const model = {
age: 0,
leader: {
pos: { x: 8, y: 8 },
vel: { x: 0, y: 0 },
size: 16,
color: "crimson",
A: 1000,
},
follower: {
pos: { x: 128, y: 128 },
vel: { x: 0, y: 0 },
max_speed: 50,
size: 8,
color: "steelblue",
},
background: "khaki",
width: 256,
height: 256,
action: 0,
};
// Further model definitions
return model;
}
Adicionalmente, o modelo tem os seguintes atributos:
ageé o número de passos decorridos desde que a animação iniciou.background,widtheheightdefinem propriedades visuais da animação (côr de fundo, largura e altura).actionrepresenta a ação a ser executada no passo atual. Por exemplo, «subir», «descer», etc. — o valor deste parâmetro é definido assincronamente quando o utilizador pressiona uma tecla e posteriormente processado para definir a aceleração doleader.
Controlo por Teclado
O movimento do
leaderé controlado pelas teclasQAOP(oStem de ser evitado pois já é usado para fazer pesquisas).
Este controlo tem duas fases:
- Deteção assíncrona do evento de teclado;
- Atualização adequada do modelo, durante a fase
update.
Para a deteção assíncrona usa-se o método document.addEventListener (veja a respetiva documentação aqui):
function init_model() {
const model = {
...
};
document.addEventListener("keydown", (e) => {
switch (e.key) {
case "Q":
case "q": // UP
model.action = 1;
break;
case "A":
case "a": // DOWN
model.action = 2;
break;
case "P":
case "p": // RIGHT
model.action = 3;
break;
case "O":
case "o": // LEFT
model.action = 4;
break;
case "r":
case "R": // RESET
model.action = 5;
break;
default: // NOOP
model.action = 0;
break;
}
});
// Further model definitions
return model;
}
Esta instrução faz com seja executada uma certa ação cada vez que é pressionada ("keydown") alguma daquelas teclas (QAOP e R). Neste caso essa ação é apenas definir o valor do parâmetro model.action de forma que:
| Ação | Teclas | model.action |
|---|---|---|
| Subir | Qq | 1 |
| Descer | Aa | 2 |
| Direita | Pp | 3 |
| Esquerda | Oo | 4 |
| Reset | Rr | 5 |
| Nada | tudo o resto | 0 |
Aqui pretende-se apenas detetar e registar o evento externo. As instruções a executar neste passo devem evitar cálculos prolongados. Para o registo do evento usa-se o parâmetro
model.actionque será devidamente processado durante a atualização (update) do modelo.
Tempos e Atualização
Os restantes atributos do modelo representam (1) momentos «importantes» e (2) o método de atualização.
function init_model() {
const model = { ...
};
document.addEventListener("keydown", (e) => {...
});
model.start = performance.now();
model.last_ts = model.start;
model.update = update;
return model;
}
O atributo model.last_ts é atualizado em cada passo da animação e regista o momento em que esse passo foi executado. Esta informação é importante para determinar o tempo decorrido desde o passo anterior.
Atualização do Modelo
A atualização do modelo é implementada no método
updatee segue uma determinada sequência.
- Atualização dos tempos — deste o passo anterior; número de passos, etc.
- Aplicação das ações — o valor do parâmetro
model.actiondetermina a aceleração a aplicar aoleader. - Velocidade, posição e colisões do
leader— a aceleração aplicada vai determinar a velocidade e, esta, a posição. Porém, oleaderpode colidir com os limites do modelo, pelo que é necessário detetar essas colisões e fazer as correções apropriadas. - Velocidade e posição do
follower— a nova posição doleaderdetermina um novo rumo para ofollower; esse cálculo é mais simples usando coordenadas polares.
Programação por Objetos em JavaScript
Um aspeto importante da programação
JavaScripté o uso do termo reservadothise o uso de algumas caraterísticas de programação por objetos; nomeadamente, o uso de métodos.
Na linguagem JavaScript é possível acrescentar atributos e métodos a objetos:
const target = document.getElementById("js:oop");
function greet(prompt) { // "this" is a owner of this method.
target.innerText = `${prompt} ${this.name} ${this.surname}!`;
}
const person = { name: "John", age: 34 }; // Object
person.surname = "Doe"; // Add an attribute
person.greet = greet; // Add a method
person.greet("Hi there"); // Use the method
O aspeto chave deste exemplo é o uso do termo reservado this na definição da função/método greet — é uma referência ao objeto que tem esse método.
Atualização dos Tempos
Para a animação por passos é necessário que a função de atualização do modelo tenha como entrada informação sobre o tempo.
Neste caso é usado o protocolo do método requestAnimationFrame(callback) (documentação) em que a função callback tem um único argumento timestamp; o momento em que o callback está a ser executado pelo requestAnimationFrame.
let step = (ts) => {
model.update(ts);
context.render(model);
requestAnimationFrame(step);
}
requestAnimationFrame(step);
O método de atualização do modelo é:
function update(ts) {
this.age += 1;
const dt = 0.001 * (ts - this.last_ts);
this.last_ts = ts;
// Further update instructions
}
O argumento ts é o timestamp (carimbo temporal; «agora» em milisegundos) e as primeiras instruções tratam do progresso do tempo:
this.age += 1;incrementa a «idade» do modelo;- As restantes duas instruções determinam:
- O tempo decorrido deste o passo anterior (
dt). - Atualizam o registo do momento do passo
this.last_ts = ts.
- O tempo decorrido deste o passo anterior (
Aplicação de Ações (Eventos Externos)
Estas instruções usam a representação da ação,
this.actiondefinida durante a deteção de eventos, para atualizar o modelo.
function update(ts) {
// Previous instructions
const a = { x: 0, y: 0 };
switch (this.action) {
case 1: // UP
a.y = -1.0;
break;
case 2: // DOWN
a.y = 1.0;
break;
case 3: // RIGHT
a.x = 1.0;
break;
case 4: // LEFT
a.x = -1.0;
break;
case 5: // RESET
// Homework: Reset the model
break;
case 0:
default:
break;
}
this.action = 0; // Experiment: remove this instruction!
// More instructions
}
Neste caso a atualização consiste em aplicar uma aceleração ao leader. Essa aceleração é um vetor a = {x:, y:} que, consoante a ação, define um movimento para a esquerda, direita, cima ou baixo.
A instrução
this.action = 0; // Experiment: remove this instruction!
faz com que uma ação definida num passo de atualização fique limitada a esse passo.
Experimente remover esta instrução e observe o efeito no movimento do
leader.
Velocidade, Posição e Colisões do Líder
A ação
this.actiondefine a aceleração,a, a aplicar aoleader. Segue-se a atualização da velocidade e posição doleader, tendo em conta as leis do movimento e as colisões com os limites do modelo.
As leis do movimento são: $$ \left\lbrace \begin{aligned} v & \gets v + a \delta_t, \cr p & \gets p + v \delta_t \end{aligned} \right. $$
A atualização das componentes horizontais (x) é feita com as seguintes instruções:
function update(ts) {
// Previous instructions
//
// LEADER X update
//
// vel.x
this.leader.vel.x += this.leader.A * a.x * dt;
// pos.x
this.leader.pos.x += this.leader.vel.x * dt;
// collision with the right wall
if (this.leader.pos.x > this.width - this.leader.size) {
this.leader.pos.x = this.width - this.leader.size;
this.leader.vel.x = 0;
}
// collision with the left wall
if (this.leader.pos.x < 0) {
this.leader.pos.x = 0;
this.leader.vel.x = 0;
}
//
// LEADER Y update
//
// Analogous to LEADER X update
// More instructions
}
A atualização da posição horizontal começa no «bloco» LEADER X update:
- A aceleração horizontal atualiza a velocidade horizontal.
- A velocidade horizontal atualiza a posição horizontal.
- São testadas as colisões horizontais e, se necessário, ajustadas as velocidade e posição horizontais.
Mais especificamente, a linha
this.leader.vel.x += this.leader.A * a.x * dt;
atualiza a velocidade por aplicação da lei
$$ v \gets v + a \delta_t $$
usando a aceleração e o tempo decorrido: a.x * dt. O parâmetro this.leader.A serve apenas para aumentar o efeito da aceleração.
Experimente fazer
model.leader.A: 1na funçãoinit_modele observe o efeito no movimento doleader.
A atualização da posição horizontal é feita em
this.leader.pos.x += this.leader.vel.x * dt;
que aplica
$$ p \gets p + v \delta_t. $$
Esta atualização pode ter criado uma colisão com uma parede. As colisões (horizontais) são detetadas em
function update(ts) {
// Previous instructions
// collision with the right wall
if (this.leader.pos.x > this.width - this.leader.size) {
...
}
// collision with the left wall
if (this.leader.pos.x < 0) {
...
}
// More instructions
}
e, quando é detetada uma colisão, o leader:
- É parado (na direção da colisão):
this.leader.vel.x = 0;. - É colocado «dentro» da área do modelo:
this.leader.pos.x = ...;.
A atualização da velocidade e da posição verticais (
LEADER Y update) é análoga:
- A aceleração atualiza a velocidade.
- A velocidade atualiza a posição.
- São testadas as colisões e, se necessário, ajustadas a posição e a velocidade.
Velocidade e Posição do Seguidor
O
follower(seguidor) desloca-se na direção doleadermas tem a velocidade limitada a um valor máximo (model.follower.max_speed).
Portanto, para determinar em que direção o follower se desloca, assim como a sua velocidade, é mais conveniente usarem-se coordenadas polares. A conversão entre coordenadas cartesianas (usadas no modelo) e polares usa as seguintes funções:
function cart2polar(p) {
return {
a: Math.atan2(p.y, p.x),
d: Math.hypot(p.x, p.y),
};
}
function polar2cart(q) {
return {
x: q.d * Math.cos(q.a),
y: q.d * Math.sin(q.a),
};
}
A direção (cartesiana) do ponto p para o ponto q e a respetiva distância são:
function dir(p, q) {
return {
x: q.x - p.x,
y: q.y - p.y,
};
}
function dist(p, q) {
return Math.hypot(q.x - p.x, q.y - p.y);
}
Agora a atualização da velocidade e posição do seguidor é:
function update(ts) {
// Previous instructions
// FOLLOWER
//
// Heading
const follower_heading_cart = dir(this.follower.pos, this.leader.pos);
const follower_heading_pol = cart2polar(follower_heading_cart);
//
// Speed proportional to distance to leader
// but limited by this.follower.max_speed
follower_heading_pol.d = Math.min(
this.follower.max_speed,
dist(this.follower.pos, this.leader.pos),
);
//
// Cartesian vel
this.follower.vel = polar2cart(follower_heading_pol);
//
// Position update
this.follower.pos.x += this.follower.vel.x * dt;
this.follower.pos.y += this.follower.vel.y * dt;
}
Com a instrução
follower_heading_pol.d = Math.min(
this.follower.max_speed,
dist(this.follower.pos, this.leader.pos),
);
a velocidade fica proporcional à distância entre o follower e o leader mas limitada por model.follower.max_speed.
A Visualização
A visualização do modelo consiste na construção (render) de uma imagem em cada passo da animação.
Usa-se o sistema C2D (i.e. o CanvasRenderingContext2D), acrescentando métodos específicos para a construção deste modelo.
As propriedades gráficas das «peças» desta animação são definidas pelos atributos
{
pos: {x: ..., y: ... },
size: ... ,
color: ...,
}
Os métodos de visualização são:
function render_piece(c) {
this.fillStyle = c.color;
this.fillRect(c.pos.x, c.pos.y, c.size, c.size);
}
function render_c2d(m) {
this.write(`AGE: ${m.age}`);
this.fillStyle = m.background;
this.fillRect(0, 0, m.width, m.height);
this.render_piece(m.leader);
this.render_piece(m.follower);
}
O método
this.write(...)«ainda» não está definido. Este método serve para escrever mensagens de texto num «terminal» de forma a proporcionar alguma informação não visual sobre a evolução do modelo.
O sistema gráfico (e o modelo) é inicializado da seguinte forma:
function main(target_c2d, target_terminal) {
const model = init_model();
const context_c2d = document.getElementById(target_c2d).getContext("2d");
context_c2d.canvas.width = model.width;
context_c2d.canvas.height = model.height;
context_c2d.render = render_c2d;
context_c2d.render_piece = render_piece;
const terminal = document.getElementById(target_terminal);
context_c2d.write = (text) => (terminal.innerHTML = text);
// More instructions
}
A instância context_c2d é aumentada com os métodos seguintes:
...
context_c2d.render = render_c2d;
context_c2d.render_piece = render_piece;
...
context_c2d.write = (text) => (terminal.innerHTML = text);
...
que proporcionam:
- A construção (render) do modelo:
context_c2d.render. - A construção (render) de cada «peça»:
context_c2d.render_piece. - Um sistema de mensagens de texto:
context_c2d.write.
O Ciclo de Animação
O ciclo de animação inclui a atualização seguida da visualização do modelo e é definido pela função
step.
function main(target_c2d, target_terminal) {
// Previous instructions
const step = function (ts) {
model.update(ts);
context_c2d.render(model);
requestAnimationFrame(step);
};
requestAnimationFrame(step);
}
- A instrução
model.update(ts);usa o métodoupdateacrescentado amodeleminit_model; - Em
context_c2d.render(model)o modelo é visualizado pelos métodos acrescentados ao contexto gráficocontext_c2d. - A primeira instrução
requestAnimationFrame(step);torna a executar a funçãostepquando o browser atualizar a página. - A segunda instrução
requestAnimationFrame(step);inicia a animação.
A Parte HTML
A animação «existe» num documento
HTML.
<script type="importmap">
{
"imports": {
"tween": "./lib/tweenjs/tween.esm.js",
"follower": "./follower.js"
}
}
</script>
<div style="display: grid; grid-template-columns: 256px 256px">
<canvas id="c2d:canvas"></canvas>
<div id="terminal" style="font-family:monospace;padding:0.5em;background:black;color:seagreen">TERMINAL</div>
</div>
<script type="module">
import main from "follower";
main("c2d:canvas", "terminal");
</script>
O Que Falta (Exercícios)
- Adicione «energia» ao
leader: cada vez que colide com uma parede perde alguma energia; se for «apanhado» pelo seguidor perde mais energia; - Aumente a dificuldade: Adicione novos seguidores de
xemxsegundos; - Acrescente PowerUps: O contrário dos seguidores; fogem do
leadermas dão pontos quando «apanhados». - Junte «obstáculos»: objetos estacionários e incontornáveis que tornam a vida do
leadermais interessante. - Implemente adaptações para outros sistemas gráficos sem alterar o modelo.