Como construir um componente Timer - Angular 2+
Já comentei em outros posts que normalmente conseguimos atingir o mesmo resultado através de caminhos diferentes. Neste texto vou mostrar como construir um componente temporizador utilizando SVG de uma forma bem simples, com o <path>
. Novamente, há outras formas de chegarmos neste resultado, esta é uma delas.
Estruturando a lógica com JavaScript + SVG
Uma das formas de trabalharmos com gráficos vetoriais em HTML é através da utilização de SVG. Considere o código abaixo:
<svg width="80"
height="80"
style="border: 1px dashed #ff0000">
<path d="M 0 0 L 80 80"
stroke="#000"
fill="transparent"></path>
</svg>
Criamos um elemento <svg>
com 80px
de largura e altura e com linhas tracejadas em vermelho na borda. Dentro deste <svg>
temos um <path>
que permite passarmos um conjunto de instruções dentro do atributo d
para desenharmos algo. Abaixo está a descrição das instruções:
M 0 0
-> posicione o cursor/"lápis" nas coordenadasx=0 y=0
(canto superior esquerdo)L 80 80
-> faça uma linha de onde o cursor/"lápis" está, até as coordenadasx=80 y=80
(canto inferior direito)
O resultado do código acima é representado na imagem a seguir:
Como vamos precisar manipular o <svg>
de forma dinâmica, vamos utilizar JavaScript para construir exatamente o mesmo cenário do código acima:
const SVG_SIZE = 80;
const svg = document.createElementNS(
"http://www.w3.org/2000/svg",
"svg"
);
svg.setAttribute("height", SVG_SIZE);
svg.setAttribute("style", "border: 1px dashed #ff0000");
svg.setAttribute("width", SVG_SIZE);
document.body.appendChild(svg);
// Cria o <path>
const path = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
path.setAttribute("d", "M 0 0 L 80 80");
path.style.fill = "transparent";
path.style.stroke = "#000";
svg.appendChild(path);
Criando um círculo em SVG
Agora que sabemos como construir uma linha no <svg>
utilizando o <path>
, vamos calcular quais seriam as coordenadas de 0° a 360° para construirmos um círculo.
Observação: existe um elemento chamado <circle>
no SVG, porém ele não provê alguns comportamentos que serão utilizados no componente Timer descritos neste texto.
Link para mais detalhes sobre o <circle>
.
O centro do círculo (timer) será no meio do <svg>
, então podemos simplesmente dividir o tamanho do <svg>
por 2 para encontrarmos o ponto médio na horizontal (x) e vertical (y):
const SVG_SIZE = 80;
const xyCentroTimer = SVG_SIZE / 2;
Para calcular as coordenadas do circulo podemos utilizar como base:
- ângulo que queremos obter as coordenadas
- raio do circulo
- ponto central
Veja na imagem abaixo que podemos encontrar o x
calculando a seguinte expressão (seno(angulo) * raio) + cx
, onde cx
é o ponto central no eixo x
. A mesma lógica se aplica para encontrar y
, porém com o cosseno.
Traduzindo isto para código temos:
const SVG_SIZE = 80;
const raio = SVG_SIZE / 2;
const xyCentroTimer = SVG_SIZE / 2;
// Exemplo do cálculo para 0°
const radianoZeroGraus = 0 /* graus */ * (Math.PI / 180);
const x1 = xyCentroTimer
+ (Math.sin(radianoZeroGraus) * raio);
const y1 = xyCentroTimer
- (Math.cos(radianoZeroGraus) * raio);
// Exemplo do cálculo para 90°
const radiano90Graus = 90 /* graus */ * (Math.PI / 180);
const x2 = xyCentroTimer
+ (Math.sin(radiano90Graus) * raio);
const y2 = xyCentroTimer
- (Math.cos(radiano90Graus) * raio);
console.log(x1, y1); // Resultado: 40, 0
console.log(x2, y2); // Resultado: 80, 40
Agora que estruturamos o cálculo das coordenadas do circulo, vamos pegar o código acima, adicionar um loop for
para calcular as coordenadas a cada 90° e ver o resultado no console:
const SVG_SIZE = 80;
const raio = SVG_SIZE / 2;
const xyCentroTimer = SVG_SIZE / 2;
const coordenadas = [];
for (let grau = 0; grau < 360; grau = grau + 90) {
const radianos = grau * (Math.PI / 180);
// Observação: adicionei o Math.round só
// para ajudar na avaliação inicial. Depois
// vamos removê-lo
const x = xyCentroTimer
+ Math.round(Math.sin(radianos) * raio);
const y = xyCentroTimer
- Math.round(Math.cos(radianos) * raio);
coordenadas.push([x, y]);
}
// Resultado na imagem abaixo
console.table(coordenadas);
O console.table()
exibe os dados de forma tabular no console conforme a imagem acima. Para mais detalhes: https://developer.mozilla.org/pt-BR/docs/Web/API/Console/table
Olhando o resultado acima podemos ver que a lógica está correta. Então vamos juntar o código do cálculo das coordenadas com o código que constrói um SVG:
const SVG_SIZE = 80;
const raio = SVG_SIZE / 2;
const xyCentroTimer = SVG_SIZE / 2;
// Cria o <svg>
const svg = document.createElementNS(
"http://www.w3.org/2000/svg",
"svg"
);
svg.setAttribute("height", SVG_SIZE);
svg.setAttribute("style", "border: 1px dashed #ff0000");
svg.setAttribute("width", SVG_SIZE);
document.body.appendChild(svg);
// Cria o <path>
const path = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
path.style.fill = "transparent";
path.style.stroke = "#000";
svg.appendChild(path);
const coordenadas = [];
// ******************************************
// IMPORTANTE:
//
// Veja que estamos calculando as coordenadas
// a cada 90 graus. Mais adiante vamos
// calcular a cada grau
// ******************************************
for (let grau = 0; grau <= 360; grau = grau + 90) {
const radianos = grau * (Math.PI / 180);
const x = xyCentroTimer
+ (Math.sin(radianos) * raio);
const y = xyCentroTimer
- (Math.cos(radianos) * raio);
if (grau === 0) {
coordenadas.push(`M ${x} ${y}`);
} else {
coordenadas.push(`L ${x} ${y}`);
}
}
//
// O join(' ') concatena o array separando cada
// item por um espaço (' ')
//
path.setAttribute("d", coordenadas.join(' '));
Abaixo temos o resultado da renderização calculando as coordenadas a cada:
- 90°
- 45°
- 1°
Criando o visual do Timer
Agora que estruturamos um circulo, vamos ajustar o script para criar dois circulos. O circulo do fundo será cinza claro e o circulo de cima será de outra cor para indicar o progresso do timer.
Para chegarmos neste resultado vamos ter as seguintes etapas:
- Calcular todas as coordenadas do circulo
- Passar todas as coordenadas para o circulo do fundo, para dar o visual de circulo cinza
- Passar algumas coordenadas para o circulo que representa o progresso do timer - depois vamos evoluir este ponto
const STROKE_CIRCULO_CIMA = "#FF0000";
const STROKE_CIRCULO_FUNDO = "#CCC";
const STROKE_WIDTH_CIRCULO_CIMA = 2;
const STROKE_WIDTH_CIRCULO_FUNDO = 1;
const SVG_SIZE = 80;
function criarCirculo(svg, stroke, strokeWidth) {
const circulo = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
circulo.style.fill = "transparent";
circulo.style.stroke = stroke;
circulo.style.strokeWidth = strokeWidth;
svg.appendChild(circulo);
return circulo;
}
function calculaCoordenadas(graus, raio, xyCentroCirculo) {
const radiano = graus * (Math.PI / 180);
return {
x: xyCentroCirculo
+ (Math.sin(radiano) * raio),
y: xyCentroCirculo
- (Math.cos(radiano) * raio),
};
}
const svg = document.createElementNS(
"http://www.w3.org/2000/svg",
"svg"
);
svg.setAttribute("width", SVG_SIZE);
svg.setAttribute("height", SVG_SIZE);
document.body.appendChild(svg);
const xyCentroTimer = SVG_SIZE / 2;
const raio = (SVG_SIZE / 2) - STROKE_WIDTH_CIRCULO_CIMA * 2;
const circuloFundo = criarCirculo(
svg,
STROKE_CIRCULO_FUNDO,
STROKE_WIDTH_CIRCULO_FUNDO + "px"
);
const circuloCima = criarCirculo(
svg,
STROKE_CIRCULO_CIMA,
STROKE_WIDTH_CIRCULO_CIMA + "px"
);
const { x: x0, y: y0 } = calculaCoordenadas(0, raio, xyCentroTimer);
const coordenadas = [`M ${x0} ${y0}`];
for (let grau = 1; grau <= 360; grau++) {
const { x, y } = calculaCoordenadas(grau, raio, xyCentroTimer);
coordenadas.push(`L ${x} ${y}`);
}
circuloFundo.setAttribute("d", coordenadas.join(" "));
// **********************************************
// IMPORTANTE
//
// Temos 360 itens dentro do array coordenadas.
// Vamos pegar por exemplo, os 35 primeiros itens
// para desenhar um pedaço do circulo de cima
// simulando uma pequena passagem de tempo
// **********************************************
circuloCima.setAttribute("d", coordenadas.slice(0, 35).join(" "));
Até este ponto estruturamos todo o código. Ainda falta ajustarmos o script para que o circulo de cima cresça conforme o tempo for passando. Vamos fazer este ajuste direto na criação do componente Angular.
Criando o componente no Angular
O template do componente é bem simples. Basicamente temos o <svg>
com dois <path>
dentro. O primeiro <path>
será o circulo cinza e o outro será a progressão do timer na cor vermelha:
Veja que temos algumas variáveis como svgSize
, circuloTimerCoordenadas
, entre outras. Essas variáveis estão na implementação da classe do componente conforme abaixo:
Analisando o código podemos destacar alguns pontos:
renderizarTimer()
constrói o visual inicial do componente com o circulo cinza por trás e guarda as coordenadas do circulo completo (360°)ngOnChanges
é chamado toda vez que houver uma alteração no valor de algum@Input
de fora para dentro do componente. Por exemplo, considere o seguinte trecho:<app-timer [tempoSegundos]="xyz" ...
Quando o componente receber o valor dexyz
ou quando este valor for alterado ongOnChanges
será chamado.processarContagemRegressiva
é o responsável por verificar quanto tempo se passou desde que o métodoiniciarContagem
foi chamado. Ele atualiza o valor da variávelcirculoTimerCoordenadas
que por sua vez atualiza o tamanho do circulo do timer (veja notimer.component.html
)
Implementação do módulo:
Código para testar o componente:
Considerações
Com certeza este não é o timer mais bonito que você já viu, mas é um ótimo tema para estudo. Particularmente já utilizei o SVG para a construção de gráficos e o resultado ficou ótimo.
Este componente ainda pode ser evoluído. Podemos adicionar um @Output
para nos avisar quando o timer finalizar, podemos adicionar um texto indicando quando tempo falta, etc.
Abaixo está o link com o código completo em funcionamento: