5. 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(o) usuária(o). 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(a) usuário(a), 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.

5.1. 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.

5.2. 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.

5.3. Elementos do HTML: input button e range

O elemento <input> 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.

5.3.1. Como usar <input type=”button”>

A janela abaixo pode ser acessada diretamente no JSitor pelo link https://jsitor.com/XZl51Dy2Q.

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.

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 <input type="button">.

<input id="Botao01" class="styled" type="button" value="Clique aqui!"></input>
<input id="Botao02" class="styled" type="button" value="B 0"></input>

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.

//==================================================================
// 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

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 sintaxe por ser mais próxima da utilizada por outras linguagens. Dessa forma, nossas funções de callback serão definidas separadamente como no trecho abaixo:

// ------------------------------------------------------------------
// 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 detalhes sobre o evento que chamou a função. No caso do clique do botão, as informações não estão sendo utilizadas pois, nesse exemplo simples, já sabemos qual botão foi clicado.

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.

5.3.2. Como usar <input type=”range”>

A janela abaixo pode ser acessada diretamente no JSitor pelo link https://jsitor.com/v793_DTZG.

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 <input type="range">.

<input id="Barra R" type="range" value="127" min="0" max="255" step="1" name="Vermelho"></input>
<label for="redLabel">Red</label>

<input id="Barra G" type="range" value="127" min="0" max="255" step="1" name="Verde"></input>
<label for="greenLabel">Green</label>

<input id="Barra B" type="range" value="127" min="0" max="255" step="1" name="Azul"></input>
<label for="blueLabel">Blue</label>

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.

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.

// ------------------------------------------------------------------
// 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.

// ------------------------------------------------------------------
// 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, de agora em diante vamos adotar a forma usando Path2D para que você se acostume a pensar em desenhar objetos usando vértices e polilinhas.

5.4. 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.

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.

window.onkeydown = callbackKeyDown;
window.onkeyup   = callbackKeyUp;

O tratamento desses eventos é realizado pelas seguintes funções:

// ------------------------------------------------------------------
// 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.

5.5. 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.

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:

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.

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 interface com atributos e métodos, como no trecho abaixo.

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.

5.6. 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 das(os) usuárias(os), 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.

5.7. 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 \(400x400\). Seu programa deve permitir que a posição do quadrado seja controlada por 4 botões usando o <input type="button"> 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.

5.8. Para saber mais

Para saber mais recomendamos as seguintes leituras: