É cada vez mais comum vermos jogos que fazem uso de cálculos físicos com extrema precisão, se você já jogou Angry Birds, ou Cut the Rope por exemplo, já deve ter se impressionado com o quanto "realísticos" esses jogos se parecem. A verdade é que simular eventos físicos não é um assunto novo na computação, alias, muito pelo contrário, anos de estudo já foram dedicados a essa área, e por conta disso, hoje em dia é possível com apenas um google, encontrar diversas bibliotecas que nos permitem criar simulações físicas extremamente avançadas. E não para por aí, além de darem suporte a uma gigantesca variedade de linguagens, grande parte delas estão disponíveis gratuitamente, e na maior parte das vezes, com o código fonte aberto. Claro que é impossível não se maravilhar enquanto se navega pelas dezenas de exemplos dos mais variados casos que essas bibliotecas exibem, e é óbvio que isso faz com que nossa imaginação comece a maquinar as ideias mais mirabolantes possíveis, já que aparentemnte todos os problemas envolvendo física estão resolvidos!

Quem dera...

Infelizmente, toda a empolgação acaba no primeiro projeto real que vamos fazer usando qualquer uma dessas bibliotecas. É normal pensar: "mas é só alterar um pouco esse exemplo, e depois pegar esse outro pedaço de código do Stackoverflow, e então..." ... e então... você vai notar que nada vai funcionar do jeito como você imaginou. O exemplo que antes funcionava tão perfeitamente, não é tão simples de ser adaptado para o que têm em sua mente.

O que não notamos de primeiro momento, é que apesar dessas engines trazerem muitas coisas prontas, elas também trazem um alto nível de complexidade, e não só a nível de código, mas principalmente a nível de conceito. Muitas vezes utilizar uma dessas bibliotecas sem a base técnica necessária pode mais atrapalhar do que ajudar. Por exemplo, sem um conhecimento de vetores e trignometria nós não seríamos sequer capazes de realmente compreender a documentação básica de qualquer uma delas. Por esse motivo, se você quer trabalhar com projetos que envolvam física, é essencial conhecer esses conceitos básicos isolados de qualquer tipo de biblioteca ou abstração. Essa base é tão importante que você irá notar que várias (na verdade, mais do que você imagina) das funcionalidades que essas engines físicas fornecem, podem ser implementadas do zero sem muitas dificuldades.

A base de tudo

Se existe algo que serve de base para qualquer simulação física, essa base é o vetor. Qualquer engine ou artigo sobre o assunto não só fará um uso extenso deles, como também assumirá um prévio conhecimento em cálculos vetoriais. Agora, é preciso deixar claro que quando eu digo vetores eu não estou falando de vetores como coleção de valores (os arrays), ou de desenhos vetoriais. Na verdade, vetor tem várias definições, e a que iremos ver nesse artigo é (tirado da wikipedia):

Um conjunto de elementos geométricos, denominados segmentos de reta orientados, que possuem todos a mesma intensidade (denominada norma ou módulo), mesma direção e mesmo sentido.

Se é a primeira vez que você se depara com o assunto, essa definição provavelmente mais atrapalhou do que ajudou. Mas basicamente, vetor é uma coleção de valores que descrevem uma posição relativa num espaço.

Vetor indo do ponto a ao ponto b

Agora antes de entender exatamente o que são vetores, é interessante entender o porquê eles são tão importantes para nossas simulações físicas. Para isso, nada melhor que um exemplo prático, que em um primeiro momento será implementado da maneira mais simplista possível (sem o uso de vetores), e posteriormente utilizando vetores.

Sobre os exemplos

Todos os códigos serão demonstrados usando Coffeescript. Se você ainda não conhece essa linguagem, vale uma rápida consulta em sua documentação para se familiarizar com a sintaxe. Basicamente é uma forma menos sofrida mais elegante de se escrever Javascript.

Para a demonstração gráfica dos exemplos iremos utilizar a API canvas do HTML5, que mesmo sendo uma tecnologia relativamente recente, existe muito material disponível para consulta.

Por fim, muitos dos códigos exibidos na página serão uma versão um pouco simplificada da implementação em si, escondendo, por exemplo, os detalhes da renderização dos objetos na tela ou tratamento de eventos (como movimento de mouse, clicks, etc). De qualquer forma, você pode consultar o código completo de todos os exemplos nesse repositório no github.

O hello world das simulações físicas

Como o primeiro exemplo, vamos criar um dos cenário mais comuns em tutoriais de física: um círculo que rebate na tela. A estrutura do programa é bem simples: um loop infinito que a irá alterar a posição do círculo a cada iteração. Como esse loop roda 60 vezes por segundo, o que iremos ter é uma ilusão de movimento, já que a cada iteração (também chamado de frame) o círculo estará em uma nova posição.

class Ball
  constructor: (@x, @y) ->
    # Setting some random velocities here
    @xVelocity = 3
    @yVelocity = 1.5

    # The size of the ball
    @radius = 10

  # Increment the ball position based on its current velocity
  update: ->
    @x += xVelocity
    @y += yVelocity

  # Inverts the ball velocity when it hits any of the bounds of the canvas
  checkBounds: (area) ->
    # Inverting the x velocity when the ball touches the left or right side of the screen
    @xVelocity *= -1 if @x > area.width  or @x < 0

    # Inverting the y velocity when the ball touches the up or down side of the screen
    @yVelocity *= -1 if @y > area.height or @y < 0

  # Draw the ball on the screen
  draw: (context) ->
    context.fillCircle @x, @y, @radius

# Creating the ball instance and positioning it on a random position on the screen
ball = new Ball(10, 10)

# Let's use the HTML5 canvas to render the example.
canvas  = getElementById("example01")
context = canvas.getContext("2d")

infiniteLoop =->
  ball.update()
  ball.checkBounds(canvas)
  ball.draw(context)

# Calls the infiniteLoop function 60 times per second
setInterval 1000 / 60, infiniteLoop
# Note we are using setInterval here for simplicity's sake, as it is not the recommended way.
# You should always use requestAnimationFrame API for canvas drawing. Check out the example file
# for more information on how to use it.

Apesar de simples, o exemplo é interessante porque deixa bem evidente dois conceitos cruciais que servirão de base para todas as simulações físicas que envolvam movimento:

  • Posição: as propriedades x e y do círculo
  • Velocidade: a quantidade de pixels que a posição será alterada a cada iteração do loop, representadas pelas variáveis xVelocity e yVelocity

E claro que poderíamos refinar a simulação adicionando outras propriedades físicas, como por exemplo:

  • Aceleração: quantidade de variação de velocidade a cada iteração. Seria representada por algo como xAcceletation e yAcceleration
  • Vento: xWind e yWind
  • Gravidade: xGravity e yGravity
  • Fricção: xFriction e yFriction

Agora se pararmos para observar, podemos notar claramente que todas essas propriedades (aceleração, vento, etc) sempre precisam de dois valores para serem representadas: um x e um y. Isso acontece porque estamos trabalhando num contexto 2D, se a nossa simulação fosse em 3D, notaríamos que iríamos precisar de ainda mais um valor, que seria referente ao eixo z.

Sabendo-se disso, e se agrupássemos esses dois valores em algum tipo de estrutura, de modo generalizar (e simplificar) as coisas? Afinal, não ficaria mais organizado se ao invés de escrever dessa maneira:

@x = 4
@y = 8

@xVelocity = 1.5
@yVelocity = 3

@xWind = 0.3
@yWind = 0.01

Escrevêssemos dessa?

@position = new Vector(4, 8)
@velocity = new Vector(1, 3)
@wind     = new Vector(0.3, 0.01)

Sim, acabamos de escrever nossos primeiros vetores! Claro que no momento eles não parecem trazer muitas vantagens, mas não se preocupe, isso é só o começo.

Conhecendo os vetores

Como já vimos no início a definição básica de vetores, podemos resumi-los aqui como a diferença entre dois pontos num espaço. Relembrando o que foi demonstrado no exemplo anterior, nós estamos alterando a posição do círculo a cada iteração por um número de pixels horizontais e um número de pixels verticais (foi o que chamamos de velocidade), que, matematicamente se traduz para:

position = position + velocity

Observando a fórmula acima podemos então afirmar que a velocidade é um vetor, já que ela descreve a diferença entre dois pontos: o ponto atual do objeto, e o ponto que o objeto vai estar após a iteração.

Vetor de velocidade

Mas agora você pode se perguntar, e a posição? É também considerada um vetor? Afinal, apesar de ela também ter as propriedades x e y, ela não descreve a diferença entre dois pontos, ela apenas especifica uma coordenada. A resposta para essa pergunta é bastante debatida, tanto que algumas linguagens (como por exemplo o Java) utilizam classes distintas para especificar uma coordenada e um vetor. Em contrapartida, a maior parte das linguagens e engines físicas simplificam esse caso e tratam essa coordenada também como um vetor, já que uma outra forma descrever a posição é como a diferença entre a origem para a sua posição, o que elimina "burocracia" de ter duas classes que representam a mesma coisa só que com nomes diferentes. Para simplificar, vamos também tratar uma coordenada como um vetor.

Vetor de posição

Mas voltando ao exemplo, tínhamos:

position = x, y
velocity = xVelocity, yVelocity

Vamos criar nossa classe Vector que irá armazenar esses valores:

class Vector
  constructor: (x, y) ->
    @x = x
    @y = y

E reescrever a classe Ball para tratar sua posição e sua velocidade como dois vetores:

class Ball
  constructor: (x, y) ->
    @position = new Vector(x, y)
    @velocity = new Vector(3, 1.5)

Com isso, podemos finalmente implementar nosso algorítimo de movimento usando vetores! Apenas relembrando, na implementação original nós tínhamos:

@x += xVelocity
@y += yVelocity

E o que gostaríamos de fazer agora seria:

@position = @position + @velocity

Infelizmente como estamos trabalhado com Javascript nos exemplos, você já deve saber que essa sintaxe não é permitida pela linguagem, já que não podemos implementar uma função de soma de dois objetos da classe Vector utilizando o simbolo + (na verdade, a única linguagem que conheço que permitiria tal sintaxe é o Ruby). Resumindo, o Javascript não sabe como somar dois vetores como ele sabe como somar dois inteiros ou até mesmo "somar" duas strings utilizando o operador +, logo, a única opção que nos resta é utilizar uma sintaxe um pouco diferente para esse caso.

Somando vetores

Já temos uma primeira versão da nossa classe Vector que basicamente é uma estrutura que possui as propriedades x e y. Agora teremos que implementar um método nessa classe que irá somar essas propriedades com as propriedades de um outro vetor. Mas antes, é importante entender exatamente o que significa somar dois vetores. Para isso, vamos primeiro nos familiarizar com algumas notações matemáticas que serão usadas daqui para frente.

Vetores normalmente são representados com as letras em negrito e/ou com uma seta em cima do seu nome. Para facilitar a escrita, vamos utilizar apenas as letras em negrito para diferenciar um vetor de um escalar (escalar se refere a um valor inteiro ou decimal, como as propriedades x e y).

Com isso claro, vamos retomar no caso da soma. Já sabemos que cada vetor tem duas propriedades: um x e um y. Para somar um vetor com outro, basta somar as propriedades x e y de ambos.

Para ficar mais claro, supondo que temos os vetores a e b:

a = (3, 4)
b = (6, 1)

Podemos dizer que:

c = a + b

É equivalente a:

cx = ax + bx
cy = ay + by

Portanto:

cx = 3 + 6
cy = 4 + 1

E portanto:

c = (9, 5)

Extremamente simples, não? Vamos então implementar o método add na nossa classe Vector:

class Vector
  constructor: (@x, @y) ->

  add: (vector) ->
    @x += vector.x
    @y += vector.y

E finalmente terminar a refatoração do nosso exemplo:

class Ball
  constructor: (x, y) ->
    @position = new Vector x, y
    @velocity = new Vector 3, 1.5
    @radius = 10

  update: ->
    # Incrementing the ball position by adding the velocity vector to the position vector
    @position.add(@velocity)

  checkBounds: (area) ->
    # Of course we can access the x and y properties of a vector directly
    @velocity.x *= -1 if @position.x > area.width  or @position.x < 0
    @velocity.y *= -1 if @position.y > area.height or @position.y < 0

  draw: (context) ->
    # As the canvas API doesn't support passing vectors as arguments, so we must inform the x and y scalars
    context.fillCircle @position.x, @position.y, @radius

Você deve estar nesse momento pensando: "espere aí, é só isso? Fizemos tudo isso e o código não mudou quase nada!". De fato, utilizar vetores não irá magicamente fazer que seus programas simulem perfeitamente conceitos físicos. É importante entender que isso ainda não é o suficiente para que você compreenda o poder de organizar (e pensar) usando vetores. Nos próximos exemplos, vamos abordar algumas situações mais complexas que talvez deixe isso mais claro, mas por hora, vamos continuar com alguns outros conceitos básicos.

Subtraindo, multiplicando e dividindo vetores

Como você já imaginava, soma não é a única operação realizada com vetores. Na verdade, além das básicas (soma, subtração, divisão e multiplicação) existem ainda diversas outras (veja por exemplo, os métodos da classe Vector2d do java).

Começando pela subtração, que como você já deve imaginar, funciona da mesma maneira que a soma, só iremos (obviamente) trocar o operador:

class Vector
  sub: (vector) ->
    @x -= vector.x
    @y -= vector.y

Agora multiplicação é um pouco diferente. Nós não multiplicamos um vetor por outro, como fazemos com a soma e subtração. Nós multiplicamos um vetor por um escalar. Sendo assim, em muitas linguagens você não encontrará um método multiply, você irá encontrar um método chamado scale, já que o que a multiplicação (e a divisão também) faz é escalar um vetor. Podemos dizer, por exemplo que queremos dobrar ou triplicar o tamanho de um vetor, bem como podemos dizer que queremos reduzi-lo pela metade.

Novamente, supondo que temos um vetor a:

a = (-7, -3)

Vamos criar um vetor b três vezes maior que o a:

b = a * 3

Isso é equivalente a:

bx = -7 * 3
by = -3 * 3

Portanto:

b = (-21, -9)

Vetor sendo escalado

Sendo assim, a implementação do nosso método:

class Vector
  mult: (scalar) ->
    @x *= scalar
    @y *= scalar

E claro, a divisão funciona da mesma maneira:

class Vector
  div: (scalar) ->
    @x /= scalar
    @y /= scalar

Agora uma coisa importante que precisamos notar: todos os métodos que implementamos alteram o estado do vetor em si. Então, é preciso tomar muito cuidado, porque você pode ficar tentado a fazer esse tipo de coisa:

a = new Vector(5, 5)
b = new Vector(2, 2)
c = a.sub(b)

Deve ficar claro que isso não irá funcionar como o esperado. O que esse código faz na realidade é alterar o valor do vetor a para (3, 3), e não retornar um novo vetor com esse valor para ser atribuído a c. Sendo assim, em vários casos é útil poder executar uma operação e retornar o resultado em outro vetor, para isso, vamos ter que criar método estáticos na nossa classe Vector com nossas já conhecidas operações básicas.

class Vector
  @add: (v1, v2) ->
    new Vector v1.x + v2.x, v1.y + v2.y

  @sub: (v1, v2) ->
    new Vector v1.x - v2.x, v1.y - v2.y

  @mult: (vector, scalar) ->
    new Vector vector.x * scalar, vector.y * scalar

  @div: (vector, scalar) ->
    new Vector vector.x / scalar, vector.y / scalar

Agora é possível fazer a operação acima, só que de uma maneira um pouco diferente:

a = new Vector(5, 5)
b = new Vector(2, 2)
c = Vector.sub(a, b)

Com isso concluímos as operações básicas. Mas ainda não é tudo (nem perto disso). Na verdade, conhecer esses conceitos abrem portas para entender outras importantes propriedades e funções de um vetor.

Magnitude

Como ficou claro na multiplicação e divisão, sabemos que é possível aumentar e diminuir vetores facilmente. Mas e se quisermos saber qual o tamanho exato de um vetor? Como você já deve ter notado, todo o vetor se parece com um triangulo retângulo quando juntarmos seus pontos:

Vetor de posição

Acontece que ele não só se parece com um triângulo, um vetor é triângulo retângulo, e como nós já temos os dois lados do triângulo (os catetos), basta utilizarmos o teorema de Pitagoras para encontrar a Hipotenusa (que representa o tamanho de qualquer vetor).

Vetor de posição

A fórmula, caso alguém tenha faltado das aulas de trigonometria do colegial, é super simples:

A soma dos quadrados dos catetos é igual ao quadrado da hipotenusa.

Teorema de pitágoras

class Vector
  magnitude: ->
    Math.sqrt @x * @x + @y * @y

Normalização

Conhecendo o conceito de magnitude podemos finalmente entender um dos conceitos mais importantes em cálculos vetoriais: a normalização.

Agora, normalização é algo já bem conhecido e aplicado em várias situações. O processo consiste em tornar um valor "normal", ou "padrão". No nosso caso, um vetor "padrão" (chamado de vetor unitário) é um vetor que tenha uma magnitude de valor 1. Ou seja, pra normalizar nosso vetor, basta reduzirmos (ou aumentarmos em alguns casos) seu tamanho para 1. A parte interessante disso é que como só seu tamanho é alterado, sua direção é mantida intacta (iremos entender o porquê isso é importante em breve).

E como podemos ajustar o tamanho de um vetor para exatamente 1? Simples, basta dividirmos cada uma de suas propriedades (no nosso caso, os valores de x e y) pela magnitude do vetor:

Vetor de posição

A implementação fica simples, já que os métodos para divisão e magnitude já estão disponíveis:

class Vector
  normalize: ->
    @div(@magnitude())

Programando movimento com vetores

Até agora só vimos os conceitos básicos de vetores mas não fizemos nada prático que realmente justifique o uso deles, logo, é possível que eles continuem parecendo pouco úteis. A verdade é que leva um tempo para realmente perceber o quanto importante é saber utiliza-los. Para tentar "acelerar" esse processo, vamos explorar alguns casos um pouco mais complexos, onde a utilização correta de vetores pode tornar a implementação muito mais simples tanto de escrever como de compreender.

Para começar, vamos ver algo muito utilizado em qualquer simulação física: aceleração.

Como já vimos, velocidade é a quantidade de variação de posição. A aceleração é a quantidade de variação de velocidade. Então podemos dizer que a aceleração afeta a velocidade e essa por sua vez afeta a posição.

velocity = velocity + acceleration
position = position + velocity

Que traduzindo para código, seria:

velocity.add(acceleration)
position.add(velocity)

Como é possível notar, podemos alterar tanto a posição e a velocidade através da aceleração, com isso, nunca será necessário alterar os valores de velocidade e posição diretamente (apenas, é claro, no processo de inicialização). Essa "ideia" de não alterar diretamente os valores de velocidade e posição, por exemplo, é muito comum em qualquer engine física, por isso quando quisermos fazer um objeto se mover pela tela, temos que pensar em algorítimos para manipular sua aceleração. Para ficar claro, vamos ver alguns algorítimos bastante conhecidos:

  1. Aceleração constante
  2. Aceleração aleatória
  3. Aceleração até um ponto específico

Aceleração constante

Sem dúvida o algoritmo mais simples e básico de aceleração, onde o objeto irá ganhar velocidade gradualmente. Para isso, vamos voltar ao nosso exemplo dos círculos:

class Ball
  constructor: (x, y) ->
    @position = new Vector x, y

    # Let's start with no velocity at all
    @velocity = new Vector 0, 0

    # NEW acceleration property
    @acceleration = new Vector(0.005, 0.01)

Podemos notar que por enquanto as únicas modificações feitas foram zerar a velocidade e adicionar uma nova propriedade para a aceleração. Outro ponto bastante importante de se observar é que a aceleração tem valores muito pequenos, isso é necessário já que a cada iteração do nosso loop, iremos somar esses valores na velocidade do círculo, e como temos 60 iterações desse loop por segundo, se o valor for alto, o objeto irá ganhar aceleração muito rapidamente, tirando todo o "efeito" que queremos visualizar.

Mas continuando, agora vamos alterar o método update para adicionar aceleração a nossa velocidade:

update: ->
  @velocity.add @acceleration
  @position.add @velocity

Tudo irá funcionar perfeitamente com um porém: a velocidade nesse caso tende ao infinito, ou seja, se deixarmos o exemplo rodando por algum tempo, a velocidade acumulada será tão grande que não será mais possível ver o círculo na tela (de tão rápido que ele irá se mover!). Com isso, precisamos pensar em uma maneira de limitar nossa velocidade. Lembrando que como a velocidade é um vetor, podemos dizer então que se conseguissemos limitar o tamanho desse vetor, iriamos conseguir limitar a velocidade máxima.

Para isso, basta checarmos se a magnitude (lembrando que magnitude é o tamanho do vetor) é maior que um certo número (um escalar), caso seja nada é feito, mas caso não seja, o que iremos fazer é normalizar o vetor (ajustando então, seu tamanho para 1) e depois multiplica-lo pelo tamanho máximo informado.

class Vector
  limit: (max) ->
    if @mag() > max
      do @normalize
      @mult(max)

Com isso, podemos agora limitar nossa velocidade facilmente:

update: ->
  @velocity.add @acceleration
  @velocity.limit 15 # Let's not allow a velocity greater than 15
  @position.add @velocity

Uma última modificação que faremos para que seja possível visualizar melhor o efeito de aceleração constante será que ao invés dos círculos rebaterem nas bordas, iremos "transporta-los" para o lado inverso (da mesma forma como é feito jogo Asteroids por exemplo). Para isso, basta alterarmos o método checkBounds:

checkBounds: (area) ->
  @position.x = 0           if @position.x > area.width 
  @position.x = area.width  if @position.x < 0

  @position.y = 0           if @position.y > area.height
  @position.y = area.height if @position.y < 0

E o resultado podemos ver no seguinte exemplo:

Aceleração aleatória

Vamos usar o exemplo anterior como base e modifica-lo para que ele gere uma aceleração diferente a cada iteração do nosso loop. Mas antes, para simplificar a implementação, vamos fazer uma função que retorna um número aleatório dentro de um range, já que o javascript não fornece algo do tipo nativamente:

random = (min, max) -> Math.random() * (max - min) + min

Com isso, vamos agora redefinir o método update e gerar uma aceleração aleatória a cada frame:

update: ->
  @acceleration = new Vector random(-1, 1), random(-1, 1)
  @acceleration.normalize()

  @velocity.add @acceleration
  @velocity.limit 15
  @position.add @velocity

Apesar de não ser necessário normalizar a aceleração, isso torna a implementação mais flexível para atender dois casos comuns:

  • escalar a aceleração para um valor constante
@acceleration = new Vector random(-1, 1), random(-1, 1)
@acceleration.normalize()
@acceleration.div(2)
  • escalar a aceleração para um valor aleatório
@acceleration = new Vector random(-1, 1), random(-1, 1)
@acceleration.normalize()
@acceleration.div(random(1, 2))

E o resultado dos dois casos:

Esse exemplo é de extrema importância pois mostra que aceleração não é apenas usada para fazer com que objetos acelerem ou desacelerem, mas que também pode ser usada para qualquer mudança de velocidade, seja essa uma mudança de magnitude (que é o que faz um objeto andar mais rápido ou mais devagar), ou uma mudança de direção.

Aceleração em direção a um ponto

Como último exemplo, vamos implementar a aceleração direcionada a um ponto. Esse ponto, pode ser qualquer coisa, seja ele um outro objeto, ou, como no nosso caso, o mouse. Sendo assim, teremos que implementar um algorítimo que acelere em direção ao ponteiro do mouse.

Para esse tipo de simulação nós sempre iremos precisar calcular duas coisas: a magnitude e a direção.

Computar a magnitude creio que já está bem claro, já temos o método magnitude que retorna esse valor. Agora para a direção precisamos montar um vetor que vai da posição atual do objeto até o ponteiro do mouse (podemos dizer que precisamos de um vetor que aponta para o mouse). Para isso, vamos fazer uma simples subtração: vamos pegar o ponto do mouse e subtrair o ponto do objeto.

direction = mouse - position

Subtração do objeto para a posição do mouse

Traduzindo para nossa linguagem:

direction = Vector.sub @mouse, @position

Com isso criamos nosso vetor direction, que irá apontar para o ponteiro do mouse. Agora muito cuidado: se utilizássemos esse vetor como aceleração, nosso objeto iria aparecer no ponteiro do mouse instantaneamente. Talvez isso seja útil em alguma simulação, mas para nosso caso, queremos limitar o quão rápido o nosso objeto irá em direção ao mouse, ou seja, nós queremos limitar a magnitude desse vetor.

Para isso, vamos normalizar o vetor (que lembrando irá manter sua direção, mas irá fixar sua magnitude ao valor 1) e como ele normalizado podemos facilmente escalar sua magnitude.

direction = Vector.sub @mouse, @position
direction.normalize()
direction.mult 0.5 # Here we are multiplying the acceleration by 0.5 pixles per frame

E sobrescrevendo mais uma vez nosso método update:

update: ->
  direction = Vector.sub @mouse, @position
  direction.normalize()
  direction.mult 0.5

  @acceleration = direction

  @velocity.add @acceleration
  @velocity.limit MAX_SPEED
  @position.add @velocity

E temos esse resultado (instanciando vários objetos para um wow effect!):

E nossa implementação completa:

class Ball
  MAX_SPEED = 10

  constructor: (x, y) ->
    @position = new Vector x, y
    @velocity = new Vector 0, 0
    @radius = 10

  update: (mouse) ->
    direction = Vector.sub mouse, @position
    direction.normalize()
    direction.div 2

    acceleration = direction

    @velocity.add acceleration
    @velocity.limit MAX_SPEED
    @position.add @velocity

  draw: (context) ->
    context.fillCircle @position.x, @position.y, @radius

canvas  = getElementById("canvas")
context = canvas.getContext("2d")

random = (min, max) -> Math.random() * (max - min) + min

balls = []
for index in [1..10]
  balls.push new Ball(random(0, canvas.width), random(0, canvas.height))

mouse = new Vector 0, 0

canvas.addEventListener "mousemove", ->
  mouse.x = event.offsetX
  mouse.y = event.offsetY

infiniteLoop =->
  for ball in balls
    ball.update(mouse)
    ball.draw(context)

setInterval infiniteLoop, 1000 / 60

Fechando

Ainda existem (muitos) outros assuntos que podem ser abordados que utilizam vetores para simular eventos do mundo real. Por exemplo, você deve ter notado, que na nossa última simulação os círculos não "param" quando chegam no mouse, pelo contrário, eles "passam" por ele e depois precisam voltar (ficando nesse ciclo infinitamente). Isso acontece porque não estamos limitando a força máxima que o vetor pode acelerar (lembre-se: estamos limitando apenas a velocidade). Em futuros artigos, iremos estudar vários dos algorítimos do Crayg Reynolds, e um deles é o arrival, que trata exatamente esse caso: um objeto que acelera até um ponto e desacelera a ponto de parar quando finalmente chega em seu destino.

Outra grande vantagem que ganhamos "quase de graça" quando utilizamos vetores, e que não foi explorada nesse artigo, é a conversão desses cálculos para um "mundo" 3D. Para isso, bastava inicializar/manipular os vetores utilizando mais um eixo (convencionalmente chamado de eixo z). Converter os exemplos mostrados nessa página para suportar uma terceira dimensão é algo trivial, mas que ficará para um próximo artigo.

Por hora, espero que tenha ficado claro algumas das vantagens do uso de vetores em simulações físicas, bem como as propriedades e operações que eles possuem.

Agradecimentos

A maior parte tanto do conteúdo quanto dos exemplos desse artigo não foram só baseados como transcritos de livros e tutoriais escritos pelo Daniel Shiffman e pelo Craig Reynolds. Portanto, todos os créditos devem ser dados a eles.

Opensource

A classe Vector que criamos pode ser encontrada para download em seu próprio repositório no github. Além de estar 100% coberta por testes (e ter alguns outros métodos adicionais), ela também dá suporte a vetores 3D.

Outra biblioteca usada nos exemplos é o canvas-extensions, que adiciona alguns métodos úteis no context do canvas do HTML5 (como por exemplo, o fillCircle, que foi muito usado nesse artigo).