Referências
-
Norma em HTML Canvas 2D Context e WHATWG - HTML Living Standard.
-
Documentação em Mozilla Developer Network.
-
Tutorial em Mozilla Developer Network.
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:
-
Obter uma instância do contexto de desenho:
let gc = document.getElementById("acanvas").getContext("2d");
-
Definir propriedades de traço e/ou enchimento:
gc.fillStyle = "steelblue";
-
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 defillRect
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:
-
iniciar o caminho:
gc.beginPath()
oulet p = new Path2D()
-
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 emcx,cy
:gc.quadraticCurveTo(cx,cy,x,y)
-
cúbica para
x,y
com controloscx1,cy1
ecx2,cy2
:gc.bezierCurveTo(cx1,cy1,cx2,cy2,x,y)
-
arco a apontar para
x,y
, com controlo emcx,cy
e de raior
:gc.arcTo(cx,cy,x,y,r)
-
outras, incluindo
gc.arc()
,gc.ellipse()
egc.rect()
-
fechar o caminho:
gc.closePath()
-
-
traçar o caminho:
gc.stroke()
ougc.stroke(path)
; -
encher o caminho:
gc.fill()
,gc.fill(regra)
ougc.fill(path)
,gc.fill(path,regra)
em queregra
é"nonzero"
ou"evenodd"
;
Todas as coordenadas são relativas ao referencial atual (ver as transformações mais à frente).
Exemplos de Caminhos
Código | Resultado |
---|---|
|
|
|
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 |
|
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:
-
Inicialmente, o contexto está no espaço do modelo;
-
Cada objeto está definido (por uma função) no seu espaço do objeto;
-
Guarda-se o estado do contexto;
-
Aplica-se a transformação que transforma o espaço do objeto para o espaço do mundo;
-
Constrói-se o objeto, chamando a função que o define;
-
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:
-
É instanciado um gradiente:
let g = gc.createLinearGradient(x1,y1,x2,y2);
oulet g = gc.createRadialGradient(x1,y1,r1,x2,y2,r2);
; -
São adicionados pontos de paragem ao gradiente:
g.addColorStop(p,cor);
em quep == 0.0
corresponde ao ponto inicial do gradiente (x1,y1
),p == 1.0
ao ponto final (x2,y2
) ecor
é a cor que se deseja colocar no ponto correspondente; -
Define-se o atributo de traço ou de enchimento com o gradiente:
gc.strokeStyle = g;
ougc.fillStyle = g;
; -
Traça-se ou enche-se a figura "atual»:
gc.stroke();
ougc.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çãox,y
; -
encher:
gc.fillText(texto, x,y);
pinta o interior de «texto
» na posiçãox,y
;
As propriedades da fonte são especificadas pela norma CSS Fonts Module Level 3 e aplicadas com o atributo gc.font = "...";
.