Tirando fotos com JS

Hoje em dia todo computador, notebook ou smartphone tem uma (ou mais) câmera(s).

Antigamente, a única maneira de aproveitar essas câmeras em uma aplicação Web era usando Flash.

Mas, hoje em dia, podemos tirar fotos com JS. Para isso, vamos utilizar o WebRTC.

WebRTC é um padrão Web que define protocolos e APIs em JS para enviar vídeo, áudio e dados entre navegadores de maneira peer-to-peer e em tempo real. Está disponível no Chrome para Desktop e Android, Firefox, Opera e Edge.

Pra começar, um <video> e um <canvas>

Precisamos de uma estrutura mínima no nosso HTML.

A tag <video> com a propriedade autoplay, ficará responsável por mostrar para o usuário as imagens obtidas da câmera.

Para guardar a imagem da foto, vamos usar um <canvas>, que começara invisível.

Finalmente, precisaremos de um <button> para que o usuário informe o momento em que deve ser tirada a foto a partir do vídeo.

Os três elementos mencionados ficarão dentro de uma <div>.

<div class="camera">
  <video id="video" class="foto" autoplay>Vídeo não disponível.</video>
  <canvas id="canvas" class="foto" style="display: none;"></canvas>
  <button id="tira-foto">Tirar foto</button>
</div>

Um pouquinho de css

Para posicionar os elementos e definir uma borda, teremos um pequeno trecho de css:

.camera {
  width: 340px;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0.5em;
}
.foto {
  width: 320px;
  height: 240px;
  border: 1px solid black;
  margin: 1em;
}

Mude o CSS à vontade, para deixar mais estiloso!

Mostrando o vídeo

No nosso código JS, obteremos o elemento cujo id é video:

var video = document.querySelector('#video');

Para obter as imagens da câmera, precisamos usar o método getUserMedia do navigator.

Uma coisa chata é que a API ainda não está consolidada. Por isso, o getUserMedia tem prefixos diferentes para cada navegador:

  • navigator.webkitGetUserMedia – para o Chrome e Opera
  • navigator.mozGetUserMedia – para o Firefox
  • navigator.msGetUserMedia – para o Edge

Testaremos o getUserMedia correto, armazendo-o em uma variável e ajustando o this através de um bind com o objeto navigator:

var getUserMedia = (navigator.webkitGetUserMedia ||
                       navigator.mozGetUserMedia ||
                          navigator.msGetUserMedia).bind(navigator);

Ao chamar o getUserMedia, devemos passar 3 parâmetros:

  • um objeto de configuração que diz se deve ser obtido vídeo ou áudio da câmera, entre outros detalhes
  • uma função chamada quando houve sucesso ao obter mídia da câmera. Nessa função, obtemos um MediaStream, que devemos passar para a propriedade srcObject do nosso elemento video. Guardaremos o MediaStream em uma variável, para uso posterior.
  • uma função chamada em caso de erro
var configuracaoMedia = {video: true, audio: false};

var mediaStream;
function iniciaVideo (stream) {
  video.srcObject = stream;
  mediaStream = stream;
}

function trataErroMedia (erro) {
  console.error('Erro: ' + erro);
}

getUserMedia(configuracaoMedia, iniciaVideo, trataErroMedia);

O resultado até aqui pode ser encontrado em:
https://jsfiddle.net/alexandreaquiles/xqhLL2wm/1/

Obtendo uma foto

Devemos obter o canvas e o botão a partir de seus ids:

var canvas = document.querySelector('#canvas');
var botaoTiraFoto = document.querySelector('#tira-foto');

Precisamos registrar um tratador do evento de clique no botão.

botaoTiraFoto.addEventListener('click', function (e) {
  //tratamento aqui...
});

Dentro da função de tratamento do clique, devemos obter o contexto 2D a partir do canvas e utilizá-lo para desenhar uma imagem a partir do vídeo. Nesse momento, a foto é tirada.

Podemos obter os dados através do método toDataURL do canvas.

botaoTiraFoto.addEventListener('click', function (e) {
  canvas.getContext('2d').drawImage(video, 0, 0, 320, 240);
  var dados = canvas.toDataURL('image/png');
  //fazer algo com os dados...
});

O método toDataURL retorna uma Data URI, uma maneira de representar imagens e outros tipos de dados como um texto. Por exemplo, um .png com um pontinho preto pode ser representado como: data:image/png;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=

Podemos enviar essa Data URI para o servidor com uma chamada AJAX.

Do lado do servidor, você pode gravar a Data URI no BD para uso posterior como src de um <img> ou ainda transformá-lo em um arquivo.

Em PHP, usaríamos str_replace, base64_decode e file_put_contents para salvar a imagem em um arquivo no servidor.

Você pode testar o código até essa parte em:
https://jsfiddle.net/alexandreaquiles/xqhLL2wm/1/

Mostrando a foto e escondendo o vídeo

Quando o usuário clicar o botão, é interessante mostrarmos o <canvas> que tem a imagem da foto tirada e, em seguida, escondermos o vídeo da câmera.

botaoTiraFoto.addEventListener('click', function (e) {
  //código omitido...
  video.style.display = 'none';
  canvas.style.display = '';
});

Ao exibirmos a imagem, percebemos que há uma distorção. Para resolver, é importante fixar a altura e largura do <canvas>:

canvas.width = 320;
canvas.height = 240;

A câmera continuará filmando, mas a imagem ficará parada, sendo alterada só quando o usuário clicar no botão.

Você pode acompanhar o resultado em:
https://jsfiddle.net/alexandreaquiles/xqhLL2wm/2/

Parando a câmera ao obter foto

Para suspender a filmagem logo depois do clique no botão, podemos usar o seguinte código:

mediaStream.getVideoTracks().forEach(function (media) {
  media.stop();
});

É interessante alternar entre filmagem com a câmera e a exibição da foto tirada. Para isso, alternamos entre exibição do <video> e do <canvas>. Além disso, alternamos entre parar e voltar com o funcionamento da câmera.

botaoTiraFoto.addEventListener('click', function (e) {
  if (mediaStream) {
    //código omitido...
    mediaStream.getVideoTracks().forEach(function (media) {
      media.stop();
    });
    video.style.display = 'none';
    canvas.style.display = '';
    mediaStream = null;
  } else {
    getUserMedia(configuracaoMedia, iniciaVideo, trataErroMedia);
    video.style.display = '';
    canvas.style.display = 'none';
  }

O código até aqui está em:
https://jsfiddle.net/alexandreaquiles/xqhLL2wm/3/

Para funcionar no Chrome do Android

Com o código anterior, ocorre um bug quando acessado pelo Chrome do Android: é mostrada uma imagem preta no vídeo.

Para corrigí-lo, é necessário fixar as propriedades opcionais maxWidth e minWidth na configuração de getUserMedia:

var configuracaoMedia = {
  video: {
    optional: [
      { maxWidth: 320},
      { maxHeight: 240}
    ]
  }, 
  audio: false
};

O código final pode ser encontrado em:
https://jsfiddle.net/alexandreaquiles/xqhLL2wm/4/

Consideração importante: tem que ser HTTPS
Para que o código acima funcione no Chrome e Opera quando publicado em um servidor real, não em sua máquina local, é preciso que seja usado HTTPS.
Para o Firefox não há essa restrição.

Obtendo recursos embutidos em JARs com NIO.2

Digamos que no nosso projeto temos a seguinte estrutura:

.
└── src
    ├── meu-pacote
    │   └── nio2
    │       └── MinhaClasse.java
    └── config.xml

Dentro de MinhaClasse, estou interessado em obter o conteúdo do arquivo config.xml.

Para isso:

  • obtemos a URI do recurso através do método getResource de nosso classe seguido ao método toURI
  • utilizamos o método get da classe auxiliar Paths para obter um Path a partir da URI
  • criamos uma String a partir dos bytes retornados pelo método readAllBytes da classe auxiliar Files
public class MinhaClasse {
  public static void main(String[] args) throws URISyntaxException, IOException {

    URI uriDoRecurso = MinhaClasse.class.getResource("/config.xml").toURI();
    Path pathDoRecurso = Paths.get(uriDoRecurso);
    String conteudoDoRecurso = new String(Files.readAllBytes(pathDoRecurso));
    System.out.println(conteudoDoRecurso);

  }
}

Ao executarmos o código anterior de dentro de nossa IDE, o código tem o efeito esperado. Tudo funciona!

Mas e se exportarmos para um JAR?

Digamos que nosso JAR seja chamado minha-lib.jar. Para que a nossa classe seja executada por padrão na execução do JAR, conteria a configuração Main-Class do arquivo META-INF/MANIFEST.MF.

O conteúdo de minha-lib.jar seria o seguinte:

.
├── meu-pacote
│   └── nio2
│       └── MinhaClasse.class
├── config.xml
├── META-INF
│   └── MANIFEST.MF

Poderíamos então executar o JAR com o comando:

java -jar minha-lib.jar

Teríamos como resultado a seguinte exceção:

Exception in thread "main" java.nio.file.FileSystemNotFoundException
	at com.sun.nio.zipfs.ZipFileSystemProvider.getFileSystem(ZipFileSystemProvider.java:171)
	at com.sun.nio.zipfs.ZipFileSystemProvider.getPath(ZipFileSystemProvider.java:157)
	at java.nio.file.Paths.get(Paths.java:143)
	at meu-pacote.nio2.MinhaClasse.main(MinhaClasse.java:14)

Porque será que foi lançada a exceção FileSystemNotFoundException?

O que acontece é que um recurso que está embutido dentro de um JAR tem uma URI diferente. No nosso caso, seria parecida com: jar:file:/home/alexandre/minha-lib.jar!/config.xml

Observe que temos primeiramente o caminho até o JAR e, depois do !, o caminho do recurso que está embutido dentro do JAR.

Nossa primeira alteração no programa deve ser a detecção que temos um recurso de um JAR. Podemos fazer isso verificando se há o caracter !:

URI uriDoRecurso = MinhaClasse.class.getResource("/config.xml").toURI();
Path pathDoRecurso;
if(uriDoRecurso.toString().contains("!")) { //é de JAR
  //lógica para obter recurso do JAR...
} else {
  pathDoRecurso = Paths.get(uriDoRecurso);
}

Para obter a URI do recurso relativa à URI do JAR, devemos utilizar a ideia de sistema de arquivos do NIO.2, disponível a partir do Java 7. Um sistema de arquivos é abstraído através da classe abstrata java.nio.file.FileSystem. Por padrão, é utilizado o sistema de arquivos do sistema operacional. Porém, podemos utilizar um sistema de arquivos a partir de um JAR ou arquivo ZIP.

Os seguintes passos deverão ser tomados:

  • separar as partes da URI que são do JAR e do recurso dentro do JAR
  • criar uma URI que aponta para o JAR
  • criar um FileSystem a partir da URI do JAR
  • obter o Path do recurso relativo ao sistema de arquivos do JAR

Teremos, então, o código:

URI uriDoRecurso = MinhaClasse.class.getResource("/config.xml").toURI();
Path pathDoRecurso;
if(uriDoRecurso.toString().contains("!")) { //é de JAR
  String[] partesDaURI = uriDoRecurso.toString().split("!");
  URI uriDoJAR = URI.create(partesDaURI[0]);
  FileSystem fs = FileSystems.newFileSystem(uriDoJAR, new HashMap<String, String>());
  pathDoRecurso = fs.getPath(partesDaURI[1]);
} else {
  pathDoRecurso = Paths.get(uriDoRecurso);
}

Copiando arquivos de um diretório e seus sub-diretórios com Java 7+

Vamos dizer que temos um diretório chamado arquivos com o seguinte conteúdo:

.
└── arquivos
    ├── .bookignore
    ├── book.properties
    ├── imgs
        └── cover.jpg
    └── .gitignore

Queremos copiar todos os arquivos bem como o conteúdo do sub-diretório imgs para outro diretório chamado copia.

Fazer isso é fácil com os novos recursos do pacote java.nio disponíveis a partir do Java 7. Essas novidades foram definidas na JSR-203 e ficaram conhecidas como NIO.2.

Até o Java 1.3, havia somente o pacote java.io sem suporte adequado a abstrações de alto nível como channel, apenas com I/O bloqueante, suporte ruim a character encodings.

O Java 1.4 veio com o pacote java.nio que resolvia várias dessas limitações.

Com o NIO.2 do Java 7, foi simplificada a API do pacote java.nio, dado suporte a I/O assíncrono, criada uma abstração de sistema de arquivos, entre outras melhorias.

Para representar um diretório, devemos usar a classe Path, obtendo instâncias a partir da classe auxiliar Paths.

Path origem = Paths.get("./arquivos");
Path destino = Paths.get("./copia");

Para navegar no diretório de origem de forma a incluir sub-diretórios (ou seja, recursivamente) podemos usar o método estático walkFileTree da classe auxiliar Files. Precisamos passar um objeto que implementa a interface FileVisitor.

Files.walkFileTree(origem, new Copiador(origem, destino));

A classe Copiador define todos os métodos exigidos pela interface FileVisitor:

  • preVisitDirectory – chamado antes de visitar um novo sub-diretório
  • postVisitDirectory – chamado depois de visitar um sub-diretório e todos seus descendentes
  • visitFile – chamado ao visitar cada novo arquivo
  • visitFileFailed – chamando quando há falha em algum arquivo

Em cada método, devemos retornar um valor da enum FileVisitResult. Retornaremos FileVisitResult.CONTINUE, para continuar a navegação recursiva nos sub-diretórios.

class Copiador implements FileVisitor<Path> {
  private Path origem;
  private Path destino;
  public Copiador(Path origem, Path destino) {
    this.origem = origem;
    this.destino = destino;
  }
  @Override
  public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
    return FileVisitResult.CONTINUE;
  }
  @Override
  public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    //copia arquivo para destino...
    return FileVisitResult.CONTINUE;
  }
  @Override
  public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
    throw exc;
  }
  @Override
  public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
    return FileVisitResult.CONTINUE;
  }
}

Ainda não preenchemos a lógica de copiar os arquivos, que deveria ficar no método visitFile.

Antes disso, podemos simplificar um pouco o código herdando da classe auxiliar SimpleFileVisitor e sobre-escrevendo apenas o método necessário.

class Copiador extends SimpleFileVisitor<Path> {
  private Path origem;
  private Path destino;
  public Copiador(Path origem, Path destino) {
    this.origem = origem;
    this.destino = destino;
  }
  @Override
  public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    //copia arquivo para destino...
    return FileVisitResult.CONTINUE;
  }
}

No método visitFile, vamos pegar o caminho absoluto do arquivo que está sendo visitado transformando-o em um caminho relativo em relação ao diretório de origem.

Então, podemos obter o caminho do mesmo arquivo mas relativo ao destino.

Precisamos criar qualquer diretório “pai” intermediário necessário para o arquivo de destino, através do método estático createDirectories da classe auxiliar Files.

Aí, basta invocar o método estático copy de Files para efetuar a cópia.

No caso de algum diretório ou arquivo já existir, é lançada a exceção FileAlreadyExistsException, que vamos ignorar.

Teremos o código:

Path caminhoAbsoluto = file.toAbsolutePath();
Path caminhoRelativo = origem.relativize(caminhoAbsoluto);
Path arquivoDestino = destino.resolve(caminhoRelativo.toString());
try {
  Files.createDirectories(arquivoDestino);
  Files.copy(file, arquivoDestino);
} catch (FileAlreadyExistsException ex) {
  //se o diretorio ou arquivo ja existir, nao faz nada (nem sobreescreve)
}

Se quisermos sobre-escrever arquivos já existentes, basta passar o valor StandardCopyOption.REPLACE_EXISTING para o método copy.

Files.copy(file, target, StandardCopyOption.REPLACE_EXISTING);

Poderíamos utilizar uma classe anônima ao invés de ter uma classe que herda de SimpleFileVisitor. Juntando tudo, teríamos:

final Path origem = Paths.get("./src");
final Path destino = Paths.get("./copia");
Files.walkFileTree(origem, new SimpleFileVisitor<Path>() {
  @Override
  public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    Path caminhoAbsoluto = file.toAbsolutePath();
    Path caminhoRelativo = origem.relativize(caminhoAbsoluto);
    Path arquivoDestino = destino.resolve(caminhoRelativo.toString());
    try {
      Files.createDirectories(arquivoDestino);
    } catch (FileAlreadyExistsException ex) {
      //se o diretorio ja existir, nao faz nada (nem sobreescreve)
    }
    Files.copy(file, arquivoDestino, StandardCopyOption.REPLACE_EXISTING);
    return FileVisitResult.CONTINUE;
  }
});

Fila de Mensagens Mortas

O que acontece quando há uma exceção em um recebedor registrado em uma fila?

Por exemplo:

@MessageDriven(activationConfig = {
  @ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "jms/FILA"),
  @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue")
})
public class RecebedorMDB implements MessageListener {
  public void onMessage(Message msg) {
    throw new RuntimeException("Erro!");
  }
}

A classe acima é um Message Driven Bean (MDB) registrado em FILA que lança uma exceção não checada ao receber uma mensagem.

Se verificarmos a fila, a mensagem foi consumida. Só que aconteceu um erro!

De acordo com a especificação de mensageria do Java EE, o JMS (Java Message Service), um message listener que retorna uma RuntimeException é considerado um erro de programação e terá comportamento indefinido.

Aí entram detalhes da implementação: no HornetQ, o MOM (Message Oriented Middleware) do Wildfly 8.x.x, envia as mensagens que ocasionaram uma exceção para a fila DLQ. O nome vem da sigla para Dead Letter Queue, ou fila de mensagens mortas. É como se fosse um lugar para onde as mensagens vão morrer. O conceito também é conhecido como Dead Letter Channel ou Dead Message Queue.

Mas, na verdade, o recomendado é que os message listeners ou MDBs tratem todas as exceções. Uma boa solução é criarmos DLQs específicas para cada caso excepcional do processo de negócio. Ao detectarmos uma exceção, enviamos a mensagem para a DLQ correspondente.

O erro do criador do Ant

No livro Pragmatic Project Automation, publicado em 2004, há um trecho em que James Duncan Davidson (criador do Ant e do Tomcat) admite que foi um erro ter usado XML para descrever os passos do build do Ant.

Segue uma tradução livre:

O criador do Ant exorciza um de seus demônios
por James Duncan Davidson

A primeira versão do Ant não tinha esse monte de tags que você vê espalhadas pelos arquivos de build. Ao invés disso, era usado um arquivo de properties e a classe java.util.Properties para definir quais tasks deveriam ser executadas para um determinado target. Funcionou bem para pequenos projetos mas começou a entrar em colapso à medida que os projetos cresciam.

O motivo do colapso era a maneira que o Ant enxerga o mundo: um projeto é uma coleção de targets (alvos). Um target é uma coleção de tasks (tarefas). Cada task tem um conjunto de propriedades. É obviamente uma árvore hierárquica. Porém, arquivos .properties tem apenas mapeamentos chave=valor, nos quais essa estrututura de árvore não encaixa.

Eu queria um formato de arquivos hierárquico que capturasse a visão de mundo do Ant. Mas eu não queria criar meu próprio formato. Eu queria usar um formato padrão e, mais importante, eu não queria criar um parser. Eu queria reusar o trabalho dos outros. Eu queria o caminho mais fácil.

Na época, XML estava despontando no radar. A spec tinha sido finalizada, ainda que bem recentemente. SAX tinha se tornado um padrão de-facto, mas não tínhamos JAXP ainda. Eu estava convencido que XML era a next big thing depois do Java. Código portável e dados portáveis. Duas frases de efeito que ficam bem juntas.

E já que os dados no XML tem uma estrutura de árvore, parecia perfeito para o tipo de coisa que precisava ser expressado em um arquivo de build. Adicione o fato de que XML era um formato de texto editável manualmente, e parecia um casamento feito nos céus. E eu não precisava criar um parser. Trato feito.

Em retrospecto, XML provavelmente não foi a escolha correta como parecia. Eu tenho visto arquivos de build com centenas, e até milhares, de linhas e, nesses tamanhos, editar XML não é tão amigável como eu esperava. Além disso, quando você mistura XML e os mecanismos baseados em reflection do Ant que permitem estender e customizar tasks, você acaba com um ambiente que oferece o poder e a flexibilidade de uma linguagem de script — mas com a dor de cabeça de tentar expressar essa flexibilidade com tags no XML.

Minha intenção nunca foi que o formato de arquivo se transformasse em um linguagem de script. Afinal de contas, minha visão original do Ant era que teríamos a declaração de algumas propriedades que descreveriam o projeto e que as tasks escritas em Java fariam toda a lógica. Os atuais desenvolvedores do Ant em geral pensam o mesmo.

Mas quando eu fundi XML e reflection no Ant, fiz algo que é 70–80% de um ambiente de script. Só que não percebi na época. Negar que as pessoas iriam usar o XML do Ant como uma linguagem de script é equivalente a pedir para que finjam que açúcar não é doce.

Se eu soubesse o que sei agora, eu teria tentado usar uma linguagem de script de verdade, como JavaScript via o componente Rhino ou Python via Jython, com bindings para objetos Java que implementariam a funcionalidade expressadas nas tasks. Então, haveria uma forma natural de expressar lógica, e não estaríamos presos ao XML como formato que é muito ruim para o maneira como as pessoas querem usar a ferramenta.

Um processo de build é algo que comumente será customizado. Usar XML para definir os passos de um build foi um erro no Ant. E que foi herdado pelo Maven.

O poder de uma linguagem de script casa muito bem com a definição de um processo de build. E isso foi levado em conta em ferramentas mais novas como Gradle, que usa a linguagem Groovy.

Criando um jogo em Javascript [5/5] – Pontuação

Que tal finalizar nossa versão do jogo da galinha do Atari?

Fizemos o pano de fundo do jogo, no primeiro post.

A movimentação do personagem principal a partir do teclado foi feita no segundo post.

Alguns carros movimentados automaticamente foram inseridos no terceiro post.

No quarto post, o GAME OVER foi detectado ao ocorrer a colisão dos carros e nosso personagem principal.

Iniciando a pontuação

Vamos criar uma variável pontos para armazenar o número de pontos e também uma função desenhaPontos que, quando invocada, desenha os pontos no canto superior esquerdo da página.

var pontos = 0;
function desenhaPontos(){
    contexto.fillStyle = "black";
    contexto.font="12pt Monospace";
    contexto.fillText(pontos, 5, 20);
}

Na função do setTimeout, precisamos desenhar os pontos atuais. Devemos invocar a função que acabamos de criar depois de desenhar o fundo:

setInterval(function(){
    desenhaFundo();
    desenhaPontos();
    dilminha.desenhaImagem();
    carrinhoAmarelo.desenhaImagem();
    carrinhoAzul.desenhaImagem();
    carrinhoPolicia.desenhaImagem();

    //restante do código...
},50);

O resultado do código acima está disponível em:
http://a-dilminha.appspot.com/passo-a-passo/pontuacao/15-zerado.html

Pontuação zerada

Contando os pontos

No jogo do Atari, sempre que a galinha atravessa o lado de cima da rua ganhamos um ponto. Faremos o mesmo.

Quanto mais pra cima tiver o personagem principal, menor é o valor de y. Um valor menor ou igual a zero indica que o personagem atravessou a rua totalmente.

Vamos criar uma função passou que detecta se o personagem principal ultrapassou o limite superior da tela. Já que essa função não valerá pra os carros, vamos colocá-la diretamente como uma propriedade do objeto dilminha.

var dilminha = new Sprite("../../dilminha.png", 320, 400);
dilminha.passou = function(){
    if(this.y <= 0) {
        return true;
    }
    return false;
};

Sempre que houver alguma teclada pressionada no teclado, vamos verificar se a rua foi totalmente atravessada, invocando o método passou que acabamos de definir. Caso o personagem principal tenha ultrapassado o topo da tela, contaremos mais um ponto:

document.onkeydown = function(event) {
    //verificação de game over...
    //tratamento da tecla pressionada...
    if(dilminha.passou()){
        pontos++;
    }
}

O resultado do código acima está disponível em:
http://a-dilminha.appspot.com/passo-a-passo/pontuacao/16-pontos.html

Contagem de pontos

Bug na pontuação

Há um defeito na contagem dos pontos: se o personagem principal deslizar horizontalmente no topo da tela, são contados múltiplos pontos.

Veja:

Defeito na pontuação

Existem várias maneiras de resolver esse problema. Uma das mais simples é movermos o personagem principal lá pra baixo da tela assim que detectarmos que o topo da tela foi ultrapassado. Podemos implementar isso alterando o método passou:

dilminha.passou = function(){
    if(this.y <= 0) {
        this.y = canvas.height - this.altura;
        return true;
    }
    return false;
}

O resultado do código acima está disponível em:
http://a-dilminha.appspot.com/passo-a-passo/pontuacao/17-final.html

Defeito corrigido

Então é isso, pessoal

Poderíamos melhorar muito o joguinho: implementar uma colisão decente, salvar as pontuações em um servidor, fazer funcionar em dispositivos móveis…

Mas vamos parar por aqui! Já deu pra fazer algo razoável. Divertido, não?

Nosso código final ficou assim:

<html>
<body>
  <canvas id="canvas" width="640" height="480" style="border: solid 1px black; margin: 0px auto; display: block;"></canvas>
  <script>
    var gameOver = false;

    var canvas = document.getElementById("canvas");
    var contexto = canvas.getContext("2d");

    function desenhaFundo() {
      //preenche o fundo com cinza escuro
      contexto.fillStyle = "dimgray";
      contexto.fillRect(0, 0, canvas.width, canvas.height);

      //calcada superior
      contexto.fillStyle = "lightgray";
      contexto.fillRect(0, 0, canvas.width, 80);

      //calcada inferior
      contexto.fillStyle = "lightgray";
      contexto.fillRect(0, 380, canvas.width, 100);

      //faixas
      contexto.fillStyle = "white";
      for (var i = 0; i < 25; i++) {
        contexto.fillRect(i * 30 - 5, 185, 20, 4);
        contexto.fillRect(i * 30 - 5, 280, 20, 4);
      }
    }

    var pontos = 0;
    function desenhaPontos() {
      contexto.fillStyle = "black";
      contexto.font = "12pt Monospace";
      contexto.fillText(pontos, 5, 20);
    }

    function Sprite(caminhoDaImagem, xInicial, yInicial) {
      this.x = xInicial;
      this.y = yInicial;

      this.imagem = new Image();
      this.imagem.src = caminhoDaImagem;

      var that = this;
      this.imagem.onload = function () {
        that.largura = that.imagem.width;
        that.altura = that.imagem.height;
        that.desenhaImagem();
      }

      this.desenhaImagem = function () {
        contexto.drawImage(this.imagem, this.x, this.y, this.largura, this.altura);
        contexto.strokeStyle = "darkred";
        contexto.lineWidth = 0.2;
        contexto.strokeRect(this.x, this.y, this.largura, this.altura);
      }

      this.move = function (dx, dy) {
        this.x += dx;
        this.y += dy;

        //limites
        if (this.x > canvas.width) {
          this.x = -this.largura;
        } else if (this.x < -this.largura) {
          this.x = canvas.width;
        }
        if (this.y > canvas.height - this.altura + 5) {
          this.y -= dy;
        } else if (this.y <= -5) {
          this.y = canvas.height - this.altura;
        }
      }

      this.colidiu = function (outro) {
        var colidiuNoXTopo = outro.x >= this.x && outro.x <= (this.x + this.largura);
        var colidiuNoYTopo = outro.y >= this.y && outro.y <= (this.y + this.altura);
        var colidiuNoXBase = (outro.x + outro.largura) >= this.x && (outro.x + +outro.largura) <= (this.x + this.largura);
        var colidiuNoYBase = (outro.y + outro.altura) >= this.y && (outro.y + outro.altura) <= (this.y + this.altura);
        return (colidiuNoXTopo && colidiuNoYTopo) || (colidiuNoXBase && colidiuNoYBase);
      }

    }

    var dilminha = new Sprite("../../dilminha.png", 320, 400);
    dilminha.passou = function () {
      if (this.y <= 0) {
        this.y = canvas.height - this.altura;
        return true;
      }
      return false;
    }

    var carrinhoAmarelo = new Sprite("../../carrinho-amarelo.png", -10, 300);
    var carrinhoAzul = new Sprite("../../carrinho-azul.png", 560, 200);
    var carrinhoPolicia = new Sprite("../../carrinho-policia.png", 10, 100);

    document.onkeydown = function (event) {
      if (gameOver) {
        return;
      }

      switch (event.which) {
      case 37: //pra esquerda
        dilminha.move(-10, 0);
        break;
      case 38: //pra cima
        dilminha.move(0, -10);
        break;
      case 39: //pra direita
        dilminha.move(10, 0);
        break;
      case 40: //pra baixo
        dilminha.move(0, 10);
        break;
      }

      if (dilminha.passou()) {
        pontos++;
      }
    }

    setInterval(function () {
      desenhaFundo();
      desenhaPontos();

      dilminha.desenhaImagem();
      carrinhoAmarelo.desenhaImagem();
      carrinhoAzul.desenhaImagem();
      carrinhoPolicia.desenhaImagem();

      if (gameOver) {
        contexto.fillStyle = "red";
        contexto.font = "Bold 80px Sans";
        contexto.fillText("GAME OVER", canvas.width / 16, canvas.height / 2 + 20);
        return;
      }

      carrinhoAmarelo.move(7, 0);
      carrinhoAzul.move(-5, 0);
      carrinhoPolicia.move(10, 0);

      if (carrinhoAmarelo.colidiu(dilminha) || carrinhoAzul.colidiu(dilminha) || carrinhoPolicia.colidiu(dilminha)) {
        gameOver = true;
      }

    }, 50);
  </script>
</body>
</html>

O resultado do código acima está disponível em:
http://a-dilminha.appspot.com/index.html

Criando um jogo em Javascript [4/5] – Colisão

Vamos continuar nossa versão em Javascript do jogo da galinha do Atari.

No primeiro post, fizemos o pano de fundo do jogo.

Já no segundo post, fizemos o personagem principal, movendo-o a partir das setas do teclado e limitando até onde é possível ir.

Depois, no terceiro post, colocamos alguns carros que são movimentados automaticamente.

Agora, vamos detectar a colisão dos carros e nosso personagem principal, ocasionando um GAME OVER.

Detectando a colisão

Tanto o personagem principal como os três carrinhos são objetos criados a partir do construtor Sprite. Todos esses objetos tem as propriedades x, y, largura e altura.

Uma maneira de saber se houve colisão é considerar que as figuras são retângulos e verificar se houve intersecção.

Colisão de retângulos

Vamos definir um método colidiu no construtor Sprite que retorna true se houve intersecção entre os retângulos dos personagens:

function Sprite(caminhoDaImagem, xInicial, yInicial) {
    //restando do código...

    this.colidiu = function(outro){
        var colidiuNoXTopo = outro.x >= this.x && outro.x <= (this.x + this.largura);
        var colidiuNoYTopo = outro.y >= this.y && outro.y <= (this.y + this.altura);
        var colidiuNoXBase = (outro.x + outro.largura) >= this.x && (outro.x + + outro.largura) <= (this.x + this.largura);
        var colidiuNoYBase = (outro.y + outro.altura) >= this.y && (outro.y + outro.altura) <= (this.y + this.altura);
        return (colidiuNoXTopo && colidiuNoYTopo) || (colidiuNoXBase && colidiuNoYBase);
    }
}

Na função do setTimeout, logo após mover os carrinhos vamos verificar se algum colidiu com o personagem principal. Se houve colisão, vamos setar como verdadeira a variável gameOver.

Ainda na mesma função, depois de desenhar o fundo, o personagem principal e os carros, caso o jogo tenha terminado, vamos escrever com uma fonte vermelha bem grande o texto GAME OVER. Para isso, devemos utilizar a função fillText do contexto 2D. Também colocaremos um return antecipado, impedindo que os carros movam-se se o jogo tiver terminado.

Teremos então:

setInterval(function(){
    desenhaFundo();
    dilminha.desenhaImagem();
    carrinhoAmarelo.desenhaImagem();
    carrinhoAzul.desenhaImagem();
    carrinhoPolicia.desenhaImagem();

    if(gameOver){
        contexto.fillStyle = "red";
        contexto.font="Bold 80px Sans";
        contexto.fillText("GAME OVER", canvas.width/16, canvas.height/2+20);
        return;
    }

    carrinhoAmarelo.move(7, 0);
    carrinhoAzul.move(-5, 0);
    carrinhoPolicia.move(10, 0);

    if(carrinhoAmarelo.colidiu(dilminha)
        || carrinhoAzul.colidiu(dilminha)
        || carrinhoPolicia.colidiu(dilminha)){
        gameOver = true;
    }
},50);

Outra coisa que precisamos fazer é impedir que o personagem principal seja movido pelo teclado depois do GAME OVER. Para isso, faremos um retorno antecipado caso a variável gameOver esteja setada.

document.onkeydown = function(event) {
    if(gameOver){
        return;
    }
    //restante do código...
}

O resultado do código acima está disponível em:
http://a-dilminha.appspot.com/passo-a-passo/colidindo/13-colisao.html

Colisão do carro amarelo com o personagem

Desenhando um retângulo nas bordas

Perceba na imagem acima que há uma certa distância na colisão entre o carro amarelo e o personagem principal.

Será que o cálculo do método colidiu está errado? Na verdade, está OK. O problema é que fica difícil de ver onde começa e onde termina o retângulo da imagem.

Por isso, vamos alterar o método desenhaImagem para que seja desenhado uma borda em cada uma das imagens. Assim, teremos um feeback visual do porquê dos objetos estarem colidindo uns com os outros.

O ideal (mas complexo) seria fazer algum algoritmo de colisão mais sofisticado.

O retângulo pode ser desenhado usando a função strokeRect do contexto 2D.

this.desenhaImagem = function(){
    contexto.drawImage(this.imagem, this.x, this.y, this.largura, this.altura);
    contexto.strokeStyle="darkred";
    contexto.lineWidth = 0.2;
    contexto.strokeRect(this.x, this.y, this.largura, this.altura);
}

O resultado do código acima está disponível em:
http://a-dilminha.appspot.com/passo-a-passo/colidindo/14-box.html

Retângulos nas bordas

Tem mais

Implementamos a detecção da colisão entre os carros e o personagem principal. Caso haja colisão, acontece o GAME OVER.

Falta o mais importante do jogo: a pontuação. Será o assunto do próximo post!