Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Seguidor Interativo

Controle um líder (o quadrado vermelho) usando as teclas QAOP para fugir de um seguidor (o quadrado azul) e evitar colidir com as paredes.

TERMINAL

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.

  1. 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.
  2. O modelo é atualizado em cada passo da animação através do método update.
  3. 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 velocidade vel: {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, width e height definem propriedades visuais da animação (côr de fundo, largura e altura).
  • action representa 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 do leader.

Controlo por Teclado

O movimento do leader é controlado pelas teclas QAOP (o S tem de ser evitado pois já é usado para fazer pesquisas).

Este controlo tem duas fases:

  1. Deteção assíncrona do evento de teclado;
  2. 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çãoTeclasmodel.action
SubirQq1
DescerAa2
DireitaPp3
EsquerdaOo4
ResetRr5
Nadatudo o resto0

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.action que 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 update e segue uma determinada sequência.

  1. Atualização dos tempos — deste o passo anterior; número de passos, etc.
  2. Aplicação das ações — o valor do parâmetro model.action determina a aceleração a aplicar ao leader.
  3. Velocidade, posição e colisões do leader — a aceleração aplicada vai determinar a velocidade e, esta, a posição. Porém, o leader pode colidir com os limites do modelo, pelo que é necessário detetar essas colisões e fazer as correções apropriadas.
  4. Velocidade e posição do follower — a nova posição do leader determina um novo rumo para o follower; 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 reservado this e 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.

Aplicação de Ações (Eventos Externos)

Estas instruções usam a representação da ação, this.action definida 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.action define a aceleração, a, a aplicar ao leader. Segue-se a atualização da velocidade e posição do leader, 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:

  1. A aceleração horizontal atualiza a velocidade horizontal.
  2. A velocidade horizontal atualiza a posição horizontal.
  3. 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: 1 na função init_model e observe o efeito no movimento do leader.

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:

  1. É parado (na direção da colisão): this.leader.vel.x = 0;.
  2. É 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:

  1. A aceleração atualiza a velocidade.
  2. A velocidade atualiza a posição.
  3. 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 do leader mas 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);
}
  1. A instrução model.update(ts); usa o método update acrescentado a model em init_model;
  2. Em context_c2d.render(model) o modelo é visualizado pelos métodos acrescentados ao contexto gráfico context_c2d.
  3. A primeira instrução requestAnimationFrame(step); torna a executar a função step quando o browser atualizar a página.
  4. 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 x em x segundos;
  • Acrescente PowerUps: O contrário dos seguidores; fogem do leader mas dão pontos quando «apanhados».
  • Junte «obstáculos»: objetos estacionários e incontornáveis que tornam a vida do leader mais interessante.
  • Implemente adaptações para outros sistemas gráficos sem alterar o modelo.