Referências

Modelos Básicos

Um documento «standalone» tem o seguinte conteúdo:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <script>
function main() {
    let gc = document
            .getElementById("acanvas")
            .getContext("2d");
    gc.fillStyle = "steelblue";
    gc.fillRect(0,0,256,256);
}
    </script>
</head>
<body>
    <canvas id="acanvas">
        <script>main();</script>
    </canvas>
</body>
</html>

embora seja preferível separar forma da função, isto é separar o documento HTML do código JavaScript. Nesse caso o documento HTML tem a estrutura

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <script src="prog.js">
    </script>
</head>
<body>
    <canvas id="acanvas">
        <script>main();</script>
    </canvas>
</body>
</html>

e o código JavaScript, no ficheiro prog.js:

function main() {
    let gc = document
                .getElementById("acanvas")
                .getContext("2d");
    gc.fillStyle = "steelblue";
    gc.fillRect(0,0,256,256);
}

O Canvas 2D Context funciona em modo imediato. O desenho é construído numa instância da «classe» CanvasRenderingContext2D (abreviada Canvas) que proporciona métodos e atributos de desenho. A hierarquia do grafo de cena não é explicitamente suportada mas resulta da forma como as funções de desenho são chamadas.

Em particular, podem ser implementadas funções que processam um modelo representado como um objeto JSON.

O processo básico de construção consiste em:

  1. Obter uma instância do contexto de desenho: let gc = document.getElementById("acanvas").getContext("2d");

  2. Definir propriedades de traço e/ou enchimento: gc.fillStyle = "steelblue";

  3. Usar métodos de geometria: gc.fillRect(0,0,256,256);

Objetos Básicos

Está disponível apenas uma forma básica, o retângulo, com três variantes:

  • encher o interior: gc.fillRect(x, y, width, height)

  • traçar o contorno: gc.strokeRect(x, y, width, height)

  • limpar: gc.clearRect(x, y, width, height) (este método $1 $2ma variante de fillRect em que a tinta é «preto transparente»)

Caminhos

Os caminhos são a principal ferramenta de desenho. O processo de construção de caminhos é o seguinte:

  1. iniciar o caminho: gc.beginPath() ou let p = new Path2D()

  2. definir o caminho, com as operações

    • mover para x,y: gc.moveTo(x,y)

    • linha para x,y: gc.lineTo(x,y)

    • quadrática para x,y com controlo em cx,cy: gc.quadraticCurveTo(cx,cy,x,y)

    • cúbica para x,y com controlos cx1,cy1 e cx2,cy2: gc.bezierCurveTo(cx1,cy1,cx2,cy2,x,y)

    • arco a apontar para x,y, com controlo em cx,cy e de raio r: gc.arcTo(cx,cy,x,y,r)

    • outras, incluindo gc.arc(), gc.ellipse() e gc.rect()

    • fechar o caminho: gc.closePath()

  3. traçar o caminho: gc.stroke() ou gc.stroke(path);

  4. encher o caminho: gc.fill(), gc.fill(regra) ou gc.fill(path), gc.fill(path,regra) em que regra é "nonzero" ou "evenodd";

Todas as coordenadas são relativas ao referencial atual (ver as transformações mais à frente).

Exemplos de Caminhos

Código Resultado
/**
 * Exemplo de caminho com um arco
 *
 * @param {path} p - o caminho
 */
function example_arcTo(c) {
    c.moveTo(64,64);
    c.lineTo(128,64);
    c.arcTo( 192,64, 192,192, 32);
    c.lineTo(192,192);
    c.moveTo(64,192);
}
/**
 * Exemplo de caminho com várias operações
 *
 * @param {path} p - o caminho
 */
function example_myPath(p) {
    p.moveTo(0,50);
    p.quadraticCurveTo( 50,0, 100,50 );
    p.quadraticCurveTo( 50,100, 0,50 );
    p.moveTo(25,40);
    p.lineTo(75,60);
    p.lineTo(75,40);
    p.lineTo(25,60);
    p.lineTo(25,40)
}

Transformações

As transformações são definidas por métodos do contexto:

  • translação: gc.translate(dx,dy)

  • rotação: gc.rotate(rad) (o ângulo é em radianos)

  • escala: gc.scale(sx,sy)

  • geral: gc.transform(a,b,c,d,e) que corresponde à matriz

Cada vez que um destes métodos é aplicado o estado interno do contexto muda, de forma a refletir a transformação aplicada. Isto é, as transformações são aplicadas no espaço do mundo.

O efeito das transformações persiste:

Código

Efeito

gc.fillRect(0,0,64,16);
gc.translate(32,32);
gc.fillRect(0,0,64,16);

Neste exemplo as instruções fillStyle produzem imagens diferentes embora ambas as instruções tenham exatamente os mesmos parâmetros!

Para «pequenos» desenhos isto não é propriamente grave mas, se estivermos a definir um modelo com mais do que uma dezena de objetos gráficos, pode tornar-se um problema considerável.

A forma de ultrapassar este problema consiste em «permanecer» no espaço do modelo, entrando no espaço do objeto apenas para definir a forma e voltar ao modelo logo de seguida. Para esse efeito os objetos gráficos são definidos no seu próprio espaço (espaços dos objetos) e «transportados» para o espaço do modelo. Para ajudar nesses «transportes», além das transformações, existem dois métodos do contexto:

  • guardar: gc.save() guarda o estado do contexto numa pilha;

  • repor: gc.restore() repõe o estado do contexto que estava no topo da pilha;

O processo de utilização destes dois métodos é o seguinte:

  1. Inicialmente, o contexto está no espaço do modelo;

  2. Cada objeto está definido (por uma função) no seu espaço do objeto;

  3. Guarda-se o estado do contexto;

  4. Aplica-se a transformação que transforma o espaço do objeto para o espaço do mundo;

  5. Constrói-se o objeto, chamando a função que o define;

  6. Repõe-se o estado do contexto, voltando para o espaço do mundo;

A implementação do exemplo do rosto, seguindo este processo fica como segue:

/**
 * Contorno do rosto definido no espaço do objeto: [-1,-1] x [-1,-1]
 *
 */
function contorno_1(c) {
    c.beginPath();
        c.ellipse( 0,0, 1,1, 0, 0,2*Math.PI);
    c.closePath();
}

/**
 * Nariz do rosto definido no espaço do objeto: [-1,-1] x [-1,-1]
 *
 */
function nariz_1(c) {
    c.beginPath();
        c.moveTo(0,-1);
        c.lineTo(-1,1);
        c.lineTo(1,1);
        c.lineTo(0,-1);
    c.closePath();
}

/**
 * Boca do rosto definida no espaço do objeto: [-1,-1] x [-1,-1]
 *
 */
function boca_1(c) {
    c.beginPath();
        c.rect(-1,-1,2,2);
    c.closePath();
}


/**
 * Olho do rosto definido no espaço do objeto: [-1,-1] x [-1,-1]
 *
 */
function olho_1(c) {
    c.beginPath();
        c.ellipse(0,0, 1,1, 0, 0,2*Math.PI);
    c.closePath();
}

/**
 * Rosto definido no espaço do objeto: [-1,1] x [-1,1]
 *
 */
function rosto_1(c) {
    c.strokeStyle = "black";
    c.lineWidth = 0.04;
    //
    //  Contorno
    //      centro: 0.0, 0.0
    //      tamanho: 2,2
    c.save();
        contorno_1(c);
    c.restore();
    c.fillStyle = "tan";
    c.fill();
    c.stroke();
    //
    //  Nariz
    //      centro: 0.0, 0.0
    //      tamanho: 0.2, 0.33
    c.save();   // «Entrar» no espaço do objeto
        c.translate(0.0,0.0);
        c.scale(0.2,0.33);
        nariz_1(c);
    c.restore(); // Repor «este espaço"
    c.fillStyle = "wheat";
    c.fill();
    c.stroke();
    //
    //  Boca
    //      centro: 0, 0.6
    //      tamanho: 0.33, 0.05
    c.save();
        c.translate(0,0.6);
        c.scale(0.33,0.05);
        boca_1(c);
    c.restore();
    c.fillStyle = "lightcoral";
    c.fill();
    c.stroke();
    //
    //  Olho Esquerdo
    //      centro: 0.33,0.35
    //      tamanho: 0.1,0.1
    c.save();
        c.translate(-0.33,-0.35);
        c.scale(0.25,0.25);
        olho_1(c);
    c.restore();
    c.fillStyle = "powderblue";
    c.fill();
    c.stroke();
    //
    //  Olho Direito
    //      centro: 0.66,0.35
    //      tamanho: 0.1,0.1
    c.save();
        c.translate(0.33,-0.35);
        c.scale(0.25,0.25);
        olho_1(c);
    c.restore();
    c.fillStyle = "powderblue";
    c.fill();
    c.stroke();
}

function example_spaces(c) {
    c.save();
        c.translate(50,50);
        c.scale(33,33);
        rosto_1(c);
    c.restore();
}

Embora este último exemplo pareça mais complicado do que os anteriores, na realidade está melhor estruturado e é muito mais flexível: Os objetos podem ser facilmente re-utilizados em diferentes posições, tamanhos, rotações e com aspetos diferentes.

  • Todos os sub-objetos estão definidos num «espaço próprio». Para este exemplo foi escolhido o «quadrado» [-1,1] x [-1,1] como espaço de todos os sub-objetos;

  • O objeto «principal» está definido, na função rosto_1, também no seu espaço; Cada sub-objeto é «transportado» dos seu sub-espaço próprio para o espaço deste objeto, efetivamente definido uma hierarquia de objetos;

  • O desenho final é construído «trazendo» o objeto «principal» para o espaço do mundo;

Além disso, o programador atento terá notado que existe imenso código repetido que pode (deve!) «passar» para funções. Por exemplo, os segmentos

...
c.save();
  c.translate(dx,dy);
  c.scale(sx,sy);
  caminho(c);
c.restore();
c.fillStyle = fs;
c.fill();
c.stroke();
...

também podem (devem!) ser implementados usando-se

function enter(c, dx, dy, sx, sy) {
    c.save();
    c.translate(dx,dy);
    c.scale(sx,sy);
}

function leave(c, fs, ss) {
    c.restore();
    c.fillStyle = fs;
    c.strokeStyle = ss;
    c.fill();
    c.stroke();
}

...
enter(c, dx, dy, sx, sy);
caminho(c);
leave(fs,ss);

O grafo de cena é implicitamente definido pela hierarquia de objetos que resulta da forma como cada função (que constrói um objeto gráfico) chama as funções dos seus sub-objetos. Este processo assenta no uso das transformações e de se salvar/repor o estado do contexto;

Aspeto

Traços

As propriedades do traços são (quase todas) definidas usando atributos do contexto:

  • espessura: gc.lineWidth = espessura;;

  • tracejado: gc.setLineDash(segmentos);;

  • extremidades: gc.lineCap = extremidade;. Pode ser "butt", "round" ou "square";

  • junções: gc.lineJoin = juncao. Pode ser "round", "bevel" ou "miter";

  • controlo das junções: gc.miterLimit = limite;

Tintas

A aplicação de tintas (cor sólida, gradiente ou mosaico) é feita através dos atributos do contexto:

  • traçar: gc.strokeStyle = tinta;

  • encher: gc.fillStyle = tinta;

As cores sólidas são definidas pela norma CSS Color Module Level 3.

Os gradientes podem ser lineares ou radiais e em ambos os casos segue o seguinte processo:

  1. É instanciado um gradiente: let g = gc.createLinearGradient(x1,y1,x2,y2); ou let g = gc.createRadialGradient(x1,y1,r1,x2,y2,r2);;

  2. São adicionados pontos de paragem ao gradiente: g.addColorStop(p,cor); em que p == 0.0 corresponde ao ponto inicial do gradiente (x1,y1), p == 1.0 ao ponto final (x2,y2) e cor é a cor que se deseja colocar no ponto correspondente;

  3. Define-se o atributo de traço ou de enchimento com o gradiente: gc.strokeStyle = g; ou gc.fillStyle = g;;

  4. Traça-se ou enche-se a figura "atual»: gc.stroke(); ou gc.fill();;

Por exemplo,

function demo_grads(c) {
    let g = c.createLinearGradient(0,0,64,0);
    g.addColorStop(0.0, "steelblue");
    g.addColorStop(1.0, "khaki");
    c.fillStyle = g;
    c.fillRect(0,0,64,12);
}

A aplicação de mosaicos passa por previamente «carregar» uma imagem mas há um problema de «timming» neste processo:

function demo_pattern(c) {
    let image = new Image();
    image.src = "best.png";
    image.onload = function() {
        let pattern = c.createPattern(image, "repeat");
        c.fillStyle = pattern;
        c.fillRect(0,0,256,128);
    }
}

A imagem (neste exemplo, best.png) é «carregada» num processo assíncrono com o programa que está a correr… Isto significa que é a imagem só fica disponível para ser usada quando for chamado o evento .onload() dessa imagem.

Textos

O render de texto é feito através dos seguintes métodos:

  • traçar: gc.strokeText(texto, x, y); desenha o contorno de «texto» na posição x,y;

  • encher: gc.fillText(texto, x,y); pinta o interior de «texto» na posição x,y;

As propriedades da fonte são especificadas pela norma CSS Fonts Module Level 3 e aplicadas com o atributo gc.font = "...";.