A Tela é o Quadro - Desenhando fontes com matemática
Para começar, falaremos sobre Bitmaps. Esta é a forma mais ingênua de desenhar fontes, pois um bitmap nada mais é do que uma imagem pronta. Abaixo, por exemplo, temos uma letra que ocupa um espaço de 6 pixels de altura por 6 pixels de largura.
No momento, você consegue visualizar facilmente porque o tamanho dos pixels está em 10 pixels. No entanto, se você alterar o tamanho para 1 pixel, verá que não é possível ler o que está na tela."
Como mostrado, o maior problema das fontes de bitmap é que elas não são escaláveis. Ou seja, para que possamos mudar o tamanho das fontes, teríamos que:
Criar uma nova fonte com cada uma das letras desenhadas no novo tamanho.
Rasterizar a fonte em uma outra escala.
Vamos ver o que acontece na segunda opção.
Essa é uma função simples de escala. Ela recebe como parâmetros os dados da letra que você quer desenhar e a escala na qual deseja aumentar.
Ela funciona simplesmente duplicando os pixels existentes tanto no eixo X quanto no Y.
function scaleGlyph (glyph, scale) {
const newWidth = Math.ceil(glyph.width * scale);
const newHeight = Math.ceil(glyph.height * scale);
const newPixels = Array.from({ length: newHeight }, () =>
Array(newWidth).fill(0)
);
for (let y = 0; y < glyph.height; y++) {
for (let x = 0; x < glyph.width; x++) {
const value = glyph.pixels[y][x];
const newX = Math.floor(x * scale);
const newY = Math.floor(y * scale);
for (let dy = 0; dy < scale; dy++) {
for (let dx = 0; dx < scale; dx++) {
if (newY + dy < newHeight && newX + dx < newWidth) {
newPixels[newY + dy][newX + dx] = value;
}
}
}
}
}
return {
width: newWidth,
height: newHeight,
pixels: newPixels,
};
};
O problema desse tipo de escala é que as fontes acabam obtendo um aspecto de bloco, o que traz a sensação de uma imagem com baixa resolução.
Outra forma é aplicando uma escala utilizando interpolação linear. Essa técnica consiste em tirar uma média de todos os pontos originais ao redor, em vez de simplesmente copiar o bloco inteiro, repetindo cegamente o que há no pixel. No entanto, isso agora resulta em um aspecto de imagem borrada, e essa característica se acentua quanto maior a diferença entre o tamanho original e o tamanho final.
function lerp(x0, v0, x1, v1, x) {
if (x0 === x1) {
return v0;
}
return v0 + (v1 - v0) * ((x - x0) / (x1 - x0));
}
function bilinearInterpolate(Q11, Q21, Q12, Q22, x, y) {
if (
Q11.x !== Q12.x ||
Q21.x !== Q22.x ||
Q11.y !== Q21.y ||
Q12.y !== Q22.y
) {
console.error(
"Error: The provided points do not form a proper rectangle for bilinear interpolation."
);
}
const x1 = Q11.x;
const x2 = Q21.x;
const y1 = Q11.y;
const y2 = Q12.y;
const R1 = lerp(x1, Q11.value, x2, Q21.value, x);
const R2 = lerp(x1, Q12.value, x2, Q22.value, x);
const P = lerp(y1, R1, y2, R2, y);
return P;
}
Com isso temos os exemplos a baixo,
Como utilizar uma só fonte para vários tamanhos?#
Na matemática, existem equações que desenham um gráfico na tela. Os exemplos mais comuns são:
Função quadrática#
Função inversa multiplicativa#
Para mover nossas equações, podemos somar um valor qualquer após o resultado da exponenciação e, assim, movemos nossa equação no eixo Y
Para mover nossa equação na horizontal, adicionamos esse valor antes de elevá-lo ao quadrado.
Então, já temos uma maneira de representar nossas curvas utilizando equações matemáticas.
Mas antes de desenharmos, vamos aprender sobre mais uma coisa: curvas de Bézier. Ela é uma curva polinomial expressa como a interpolação linear entre alguns pontos representativos, chamados de pontos de controle.
No exemplo abaixo, temos 3 pontos: P0, P1 e P2, onde P0 e P2 são os pontos representativos e P1 é o ponto de controle.
Você pode mover os exemplos abaixo e ver o resultado.
Desenhando uma letra com vetores#
Com o conceito de Bézier, fica até intuitivo como podemos desenhar uma letra usando matemática: basta organizar pontos em sequência e misturar linhas retas com curvas de Bézier, fazendo com que o P2 de uma termine exatamente onde começa o P0 da outra.
Aliás, uma reta também pode ser feita com Bézier; basta alinhar todos os pontos. Dessa forma, fica ainda mais claro como a interpolação atua na curva de Bézier.
Com isso, já podemos agora pensar em como transformar isso em um bitmap. Para fazer isso, precisamos primeiramente rasterizar essa fonte, começando por traduzir as curvas de Bézier em linhas compatíveis com a resolução da tela. Isso acontece porque a tela do computador é uma matriz de pixels; logo, precisamos transformar curvas em pixels legíveis ao olho humano.
Feito isso, a última coisa que se precisa é preencher a letra. Essa parte pode ser feita por um processo chamado scanline, que consiste em lançar um raio e contar quantas vezes esse raio vai tocar uma das paredes da letra. Se o número de toques for par, o pixel está representado fora da letra; se for ímpar, ele está dentro.
Perceba que, no exemplo da letra ‘O’, há uma falha na renderização. Ela está aí de propósito: o processo de renderizar fontes é complicado e cheio de edge cases que só aumentam quanto mais aprofundamos no assunto.
O que quero demonstrar com essa falha é que, além de contar quantas vezes sua linha corta a letra, deve-se também estar ciente se a linha está cortando ela mesma novamente.
Bem, e com isso, concluímos esta etapa do processo de renderização das fontes. Daqui a uns dias, vou publicar outros dois artigos sobre o tema para complementar o assunto da palestra. Eles serão sobre Unicode e Text Shaping.
Muito obrigado, e até a próxima! 😊
Referencias#
- A Brief look at Text Rendering - VoxelRifts (YouTube)
- Coding Adventure: Rendering Text -Sebastian Lague (YouTube)
- The Math Behind Font Rasterization | How it Works - GamesWithGame (YouTube)
- Text Rendering Hates You - Aria Desires
- Multi-channel signed distance field generator - Viktor Chlumský[Valve] (GitHub)
- Harfbuzz[Google] - (GitHub)