.. _sec-Interacao:
Elementos de interação: botões, teclado e mouse
===============================================
Programas simples costumam receber um arquivo de entrada, processá-lo até o final e terminam devolvendo todos os recursos que haviam alocado para a máquina. Um programa interativo precisa ficar "dormindo" para evitar consumir recursos da máquina até receber algum comando da usuária ou usuário. Esse comando é executado e, tipicamente, o programa volta a dormir até receber o próximo comando.
Por isso, programas interativos são orientados a eventos. Um evento nesse caso pode corresponder a uma ação do usuário, como um clique em um botão, tecla, ou movimentação do mouse.
Assim, um programa orientado a eventos deve associar as interrupções geradas por dispositivos de entrada (quando, por exemplo, um botão é pressionado) a rotinas de **callback**. Em nosso caso, vamos escrever funções em JavaScript específicas para tratar cada tipo de evento.
Tipos de eventos
----------------
Os eventos podem ser classificados segundo a fonte da interrupção. São duas as principais fontes: usuários e sistema.
**Eventos do usuário**: como clique do mouse, movimento do mouse, clique em botões da interface gráfica, cliques no teclado etc.
**Eventos do sistema**: dentre os diversos eventos gerados pelo sistema podemos citar o evento de redesenho ou *redisplay*. Esse evento é chamado, por exemplo, para redesenhar o conteúdo de uma janela que esteja parcialmente escondida sob outra janela que é movida ou fechada. O sistema também pode gerar eventos quando uma janela muda de tamanho, ou quando é tempo para gerar um novo quadro de uma animação, como veremos na próxima aula.
Nessa aula vamos tratar de alguns elementos básicos de interação que vão nos permitir criar interfaces gráficas simples usando o teclado, mouse e botões.
Programação orientada a eventos
-------------------------------
Os eventos gerados por usuários tem sua origem em algum dispositivo físico, como o teclado e o mouse. Uma interface é composta por elementos gráficos. Cada um deles precisa ser registrado para receber algum evento. Por exemplo, devemos registar o canvas para receber eventos do mouse, ou um botão virtual para receber um clique. Ao registrar esses elementos, devemos associar a função de callback a ser chamada para tratar o evento.
Os exemplos a seguir mostram alguns programas que criam e registram botões, e também mostram como podemos usar o mouse e o teclado para interagir com o canvas.
Elementos do HTML: input button e range
---------------------------------------
O elemento ```` do HTML oferece vários tipos usados para a criação de formulários HTML, como campos de texto, botões (``button``) e barras de intervalo (``range``). Vamos a seguir ilustrar o uso do tipo ``button``.
Como usar
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A janela abaixo pode ser acessada diretamente no JSitor pelo link `https://jsitor.com/XZl51Dy2Q `__.
.. raw:: html
A interface é composta por 2 botões, como exibidos na aba ``Browser`` do JSitor.
Clique em cada botão para ver o comportamento de cada um.
O ``Botao01``, que aparece com a mensagem `Clique aqui!`, faz uma mensagem aparecer no canvas com o número de cliques recebidos por esse botão. O console do JavaScript também exibe uma mensagem semelhante.
O ``Botao02`` também conta o número de cliques, mas ao invés de enviar uma mensagem ao canvas, o próprio valor do botão é alterado para exibir o número de cliques. Explore a interface do JSitor até se sentir confortável com os recursos que esse ambiente online oferece.
.. admonition:: JSitor
O `JSitor `__ é uma ferramenta de código aberto que permite a edição online de código HTML, CSS e JavaScript. O código fica distribuído em abas e o resultado pode ser observado clicando no botão ``Run``. Você pode dar um *fork* dos projetos para estendê-los ou criar seus próprios projetos usando a sua própria no `Github `__.
Nossos exemplos foram desenvolvidos para rodarem localmente, na sua máquina, onde cada arquivo deve possuir um nome específico, com nomes diferentes dos adotados pelo JSitor. Por isso, ao invés de fazer o download dos exemplos direto pelo JSitor, procure criar arquivos com os nomes indicados ou, ainda, edite os nomes no arquivo ``index.html`` para serem compatíveis com os seus arquivos.
Clique na aba ``HTML`` do JSitor para abrir o arquivo ``botao.html``.
O trecho abaixo ilustra como podemos criar botões usando o elemento ````.
.. code:: HTML
Esse trecho cria dois botões (``type="button"``).
O campo ``value`` define o texto exibido no botão.
A aparência dos botões está configurada no arquivo ``estilo.css``. Basta clicar na aba ``CSS`` para ver a classe ``styled``.
Cada ``id`` é usado para acessar o botão correspondente no arquivo JavaScript ``interface.js`` (clique na aba ``JavaScript`` do JSitor para ver o conteúdo desse arquivo). O trecho de código abaixo é responsável pelo registro dos elementos da interface e duas funções de callback.
.. code:: JavaScript
//==================================================================
// Interface e callbacks
// sugestão: organize os elementos da sua interface usando objetos
// Para saber mais sobre objetos em JS:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects#defining_methods
var interface = {
b01Clicks: 0,
b02Clicks: 0,
}
/**
* Registra os elementos HTML responsáveis pela interação no objeto
* interface e os associa às rotinas de callback.
*/
function construaInterface() {
// monta estrutura com os elementos da interface
interface.botao01 = document.getElementById('Botao01');
interface.botao02 = document.getElementById('Botao02');
// registro das funções de callback
// associa um evento de um elemento a uma função
interface.botao01.onclick = callbackBotao01;
interface.botao02.onclick = callbackBotao02;
}
O uso de variáveis globais em aplicativos gráficos é comum pois simplifica o protótipo das funções.
No entanto, para evitar alguma confusão que pode ser criada por um grande número de variáveis globais, vamos usar objetos para encapsular os elementos e atributos que controlam os botões. Assim, o objeto ``interface`` foi criado com os atributos ``b01Clicks``e ``b02Clicks``, que contam o número de cliques recebidos por cada botão.
A função ``construaInterface`` inicialmente insere no objeto ``interface`` referências para cada botão do documento usando o ``id`` de cada um. Cada elemento interativo pode responder a eventos diferentes. No caso de um botão, o evento mais comum é o ``onclick``. O comando
:code:`interface.botao01.onclick = callbackBotao01;`
diz ao JavaScript que, quando o botão ``interface.botao01`` receber um evento ``onclick``, a função ``callbackBotao01`` deve ser chamada. O JavaScript permite outras formas de definir e registrar as funções de callback. Nesse curso vamos adotar essa forma por possuir uma sintaxe mais simples e próxima da utilizada por outras linguagens. Dessa forma, as funções de callback são definidas separadamente como no trecho abaixo:
.. code:: JavaScript
// ------------------------------------------------------------------
// funções de CallBack
/**
* Trata os eventos relacionados ao botao 01
* @param {*} e
*/
function callbackBotao01(e) {
ctx.clearRect(0, 0, width, height);
desenhe_texto( `você clicou ${++interface.b01Clicks} vezes`, 100, 50 );
console.log(`clicou em B01: ${interface.b01Clicks}`);
}
// ------------------------------------------------------------------
/**
* Trata do evento: clique no botao 02
* @param {*} e
*/
function callbackBotao02(e) {
interface.botao02.value = `B ${++interface.b02Clicks}`;
console.log(`clicou em B02: ${interface.b02Clicks}`);
}
As funções de callback costumam receber um objeto ``evento`` contendo informações sobre o evento. No caso do clique do botão, as informações não estão sendo utilizadas.
Para o tratamento do ``Botao01``, é necessário limpar o canvas para reescrever as novas mensagens usando a função ``desenhe_texto``. Já no tratamento do ``Botao02``, o valor do botão (``interface.botao02.value``) recebe uma nova string. Nos dois casos, uma mensagem de depuração é enviada ao console.
Como usar
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A janela abaixo pode ser acessada diretamente no JSitor pelo link `https://jsitor.com/v793_DTZG `__.
.. raw:: html
Cada elemento ``range`` corresponde a um botão móvel que pode ser deslocado para alterar o valor de uma componente de cor. Cada vez que um valor é alterado, a cor do retângulo no canvas é alterada, exibindo a nova cor.
Clique na aba ``HTML`` do JSitor para abrir o arquivo ``range.html``.
O trecho abaixo ilustra como podemos criar 3 barras de intervalo usando o elemento ````.
.. code:: HTML
As 3 barras tem valor inicial ``value="127"``, e podem modificar esse valor no intervalo ``min="0" max="255"`` com passo ``step= "1"``.
O trecho de código abaixo registra os elementos da interface e as funções de callback para cada barra.
.. code:: JavaScript
function construaInterface() {
// monta estrutura com os elementos da interface
interface.barraR = document.getElementById('Barra R');
interface.barraG = document.getElementById('Barra G');
interface.barraB = document.getElementById('Barra B');
// registro das funções de callback
// associa um evento de um elemento a uma função
interface.barraR.onchange = callbackBarraMudeCor;
interface.barraG.onchange = callbackBarraMudeCor;
interface.barraB.onchange = callbackBarraMudeCor;
// chama a função de callback para desenho inicial
callbackBarraMudeCor();
}
O objeto interface guarda referências para cada uma das barras.
Observe que as três barras devem responder ao evento ``onchange`` usando **a mesma** função ``callbackBarraMudeCor``, definida a seguir.
.. code:: JavaScript
// ------------------------------------------------------------------
// funções de CallBack
/**
* Trata os eventos das 3 barras para mudança de cor.
* Observe que a mesma função de callback é utilizada
* para as 3 barras.
* @param {*} e
*/
function callbackBarraMudeCor(e) {
let r = interface.barraR.value;
let g = interface.barraG.value;
let b = interface.barraB.value;
let novacor = `rgb(${r}, ${g}, ${b})`;
console.log("cor = ", novacor);
ctx.fillStyle=novacor;
desenheFillRect(50,50,100,50);
}
Essa função lê o estado atual de cada barra e cria uma nova cor RGB, usada para pintar o retângulo no canvas utilizando o trecho de código abaixo.
.. code:: JavaScript
// ------------------------------------------------------------------
// funções de desenho
/**
* desenha um retangulo prenchido com o cor
* atual no canto (x,y) com dimensao (w,h)
* @param {number} x
* @param {number} y
* @param {number} w
* @param {number} h
*/
function desenheFillRect(x, y, w, h) {
let quad = new Path2D();
quad.moveTo( x, y);
quad.lineTo( x+w, y);
quad.lineTo( x+w, y+h);
quad.lineTo( x, y+h);
quad.closePath();
ctx.fill(quad);
}
Apesar do canvas 2d oferecer funções diretas para desenhar retângulos, vamos de agora em diante adotar a forma usando ``Path2D`` para que você se acostume, desde já, a definir os objetos usando vértices.
Teclado
-------
As interfaces gráficas atuais são capazes de apresentar várias janelas e, em muitos aplicativos, como o seu próprio navegador é comum possuir elementos que aceitam a entrada de texto pelo teclado. Nesses casos, quando há várias janelas que podem receber os eventos do teclado, apenas a janela ativa (ou com foco) recebe esses eventos.
A janela abaixo pode ser acessada diretamente no JSitor pelo link `https://jsitor.com/24GJMAR9E `__.
.. raw:: html
Clique na aba ``Browser`` e digite algumas teclas. O console exibe mensagens para quando as teclas são pressionadas (``onkeydown``) e quando elas são soltas (``onkeyup``).
Para nossas aplicações, os eventos do teclado poderiam ser recebidas pelo canvas. No entanto, para conseguir um comportamento mais robusto, vamos associar os eventos do teclado à própria janela do navegador.
O seguinte trecho de código associa as funções de callback à janela para os eventos ``onkeydown`` e ``onkeyup``.
.. code:: JavaScript
window.onkeydown = callbackKeyDown;
window.onkeyup = callbackKeyUp;
O tratamento desses eventos é realizado pelas seguintes funções:
.. code:: JavaScript
// ------------------------------------------------------------------
// funções de CallBack
/**
* Trata o evento keyDown - tecla pressionada. A função
* limpa a tela e desenha uma nova mensagem.
* @param {*} event
* @returns
*/
function callbackKeyDown(event) {
const keyName = event.key;
console.log("tecla Down = ", keyName)
if (keyName === 'Control') {
interface.controlDown = true;
return ;
}
let msg = `Tecla pressionada: ${keyName}`;
if (interface.controlDown) {
msg = `Tecla pressionada: Ctrl + ${keyName}`;
}
ctx.clearRect(0,0,width,height);
ctx.fillText(msg, 20, 65);
}
// ------------------------------------------------------------------
/**
* Trata o evento keyUp - tecla solta. A combinação
* de informações com keyDown permite a combinação de
* teclas como control e alt.
*
* @param {*} event
*/
function callbackKeyUp(event) {
const keyName = event.key;
console.log("tecla Up = ", keyName)
if (keyName === 'Control') {
interface.controlDown = false;
}
}
Observe nesse exemplo que a detecção de combinações de teclas com a tecla ``Control`` é realizado usando a variável ``interface.controlDown``.
Mouse
-----
Clicar e arrastar o mouse enquanto um botão se encontra pressionado permite criar interações poderosas. Nesse exemplo vamos ver como usar os eventos ``onmousedown``, ``onmouseup`` e ``onmousemove`` para desenhar com o mouse no canvas usando linhas de cores aleatórias.
A janela abaixo pode ser acessada diretamente no JSitor pelo link `https://jsitor.com/nPk_BW-9l `__.
.. raw:: html
Clique na aba ``Browser`` e clique e mantenha pressionado o botão esquerdo do mouse para desenhar no canvas, arrastando o mouse.
O seguinte trecho de código do arquivo ``mouse.js`` registra as funções de callback no canvas:
.. code:: JavaScript
canvas.onmousedown = onMouseDownCallback;
canvas.onmouseup = onMouseUpCallback;
canvas.onmousemove = onMouseMoveCallback;
Os eventos ``onmousedown`` e ``onmouseup`` são acionados quando o botão esquerdo do mouse é pressionado e depois solto, respectivamente. Já o evento ``onmousemove`` ocorre sempre que o mouse é arrastado.
As funções que tratam desses eventos são ilustradas abaixo. A ideia é manter o estado do botão, armazenado no atributo ``interface.buttonDown``. Quando o botão é pressionado, seu estado se torna ``true``, e essa posição é armazenada. O programa também sorteia uma nova cor para desenhar a linha.
O sorteio da cor é realizado usando a função ``random`` do módulo ``Math`` do JS, que retorna um número real no intervalo [0.0, 1.0]. Esse real é convertido para um valor inteiro usando a função ``Math.floor``.
Veja a função ``sorteieCor`` no arquivo ``mouse.js`` para ver os detalhes da implementação.
.. code:: JavaScript
function onMouseDownCallback( e ) {
interface.setXY( e.offsetX, e.offsetY );
interface.buttonDown = true;
ctx.strokeStyle = sorteieCor();
}
function onMouseUpCallback( e ) {
if (interface.buttonDown == true) {
interface.desenheLinha(e.offsetX, e.offsetY);
interface.clear();
}
}
function onMouseMoveCallback( e ) {
if (interface.buttonDown == true) {
interface.desenheLinha( e.offsetX, e.offsetY );
}
}
Com o botão apertado, quando o canvas recebe um evento de movimento, a função ``onMouseMoveCallback`` desenha a linha da última posição registrada até a nova posição. Quando o botão é solto, a função ``onMouseUpCallback`` desenha o último trecho e limpa a interface.
Nesse exemplo, aproveitamos para criar um objeto ``inteface`` com atributos e métodos, como no trecho abaixo.
.. code:: JavaScript
var interface = {
// atributos
mouseX : 0,
mouseY : 0,
buttonDown : false,
// métodos
clear : function() {
this.mouseX = 0;
this.mouseY = 0;
this.buttonDown = false;
},
setXY : function(x, y) {
this.mouseX = x;
this.mouseY = y;
}
};
// Veja a seguir uma forma de estender objetos,
// criando a chave 'desenheLinha' e associando uma função
// veja que a gente poderia ter incluído desenheLinha
// direto na definição acima.
/**
* Método para desenhar uma linha a partir da última posição
* até a nova.
* @param {*} novoX - nova pos X do mouse
* @param {*} novoY - nova pos Y do mouse
*/
interface.desenheLinha = function(novoX, novoY) {
ctx.beginPath();
ctx.moveTo(this.mouseX, this.mouseY);
ctx.lineTo(novoX , novoY);
ctx.stroke();
ctx.closePath();
this.setXY(novoX, novoY);
};
Os **atributos** ``mouseX``, ``mouseY`` e ``buttonDown`` são como variáveis que armazenam valores ou propriedades do objeto.
Os **métodos** ``clear``, ``setXY`` e ``desenheLinha`` são funções, ou ações que o objeto é capaz de realizar.
Observe que, para acessar internamente os atributos e métodos, é necessário utilizar o prefixo ``this``.
Novos atributos e métodos podem ser adicionados posteriormente, como no caso do método ``desenheLinha``.
Onde estamos e para onde vamos?
--------------------------------
Nessa aula vimos como criar interfaces simples que permitem a entrada de comandos por meio do mouse, teclado e outros elementos do HTML. Como esses comandos podem ser recebidos a qualquer instante, os programas interativos, ao invés de esperar pelos comandos, respondem a eventos gerados pelos dispositivos de entrada. Para implementar um programa baseado em eventos precisamos
criar uma rotina de **callback**
para cada evento que precisa ser tratado e
associar cada rotina aos eventos no início do programa.
Além dos usuários e usuárias, o próprio sistema precisa também tratar alguns eventos para, por exemplo, redesenhar o conteúdo de uma janela que estava "escondida", total ou parcialmente, por outra janela que foi fechada ou movida e, no caso de animações, gerar periodicamente um evento para atualizar a animação. Esse é o tema de nossa próxima aula.
Exercícios
----------
1. Escreva um programa de desenhe um quadrado vermelho de lado 50 centrado em um canvas branco (ou qualquer outra cor diferente de vermelho) de dimensão :math:`400x400`. Seu programa deve permitir que a posição do quadrado seja controlada por 4 botões usando o ```` do HTML. Organize os botões na forma de um losango, com os botões com valores "^" e "v" nos cantos superior e inferior do losango, e os botões "<" e ">" na linha intermediária do losango para controlar os movimentos laterais. Use uma barra de intervalo [1,10] (``range``) para controlar o passo do deslocamento, ou seja, se a barra estiver em 5, então cada clique move o quadrado de 5 pixels em alguma direção. Parte opcional: permita também que o quadrado seja controlado pelas teclas ``i``, ``j``, ``k`` e ``m``.
2. Modifique o programa anterior para controlar a posição do quadrado usando o mouse. Ao clicar com o botão da esquerda sobre o mouse, o centro do quadrado é movido para a posição clicada. Ao manter o botão pressionado e arrastar o mouse, o quadrado deve ser arrastado também.
3. Modifique o programa anterior para criar um quadrado novo quando o botão direito do mouse é clicado. Invente uma forma também de definir o tamanho do quadrado, por exemplo, usando um segundo clique ou mantendo o botão da direita pressionado e controlar o tamanho arrastando o mouse.
4. Modifique o programa para desenhar linhas com o mouse para que a grossura da linha seja controlada pelo teclado. Por exemplo, ao clicar na seta "para cima", a grossura da linha é incrementada e ao clicar na seta "para baixo" a grossura é decrementada caso for positiva.
Para saber mais
---------------
Para saber mais recomendamos as seguintes leituras:
* `Elemento input `__;
* `tipo button `__;
* `tipo range `__;
* `Eventos do teclado `__;
* `Eventos do mouse `__;
..