- Introdução
- Ferramentas utilizadas
- Criando o front-end com HTML e CSS
- Criando o back-end com javascript
- Conclusão
Introdução
Arte de texto ou Ascii art é uma técnica de desenhar imagens usando caracteres. Esse projeto irá converter uma imagem normal para arte de texto usando javascript puro. A conversão é feita em 3 etapas:
- Carrega a imagem no canvas
- Converte a cor da imagem para o cinza
- Converte os pixels em caracteres usando a intensidade de cada pixel
Quanto mais escuro for o pixel, menos espaço branco terá o caracter. Esse assunto será discutido mais adiante. O valor inicial usado como referência será a cor do pixel que é representada em RGB.

Ferramentas utilizadas
Esse projeto usa javascript puro como linguagem de programação. Porém, o servidor roda no node.js e o gerenciamento de bibliotecas é feito com o npm. A única biblioteca usada será o http-server, usado para iniciar o servidor.
O projeto usará HTML e CSS no front-end e javascript no back-end. É importante levar em consideração que a fonte utilizada no projeto tenha caracteres do mesmo tamanho para que não haja distorção da imagem de texto.
A imagem carregada no servidor será desenhada no canvas. As informações do canvas serão usadas para desenhar a arte de texto.
Criando o front-end com HTML e CSS
Primeiro de tudo, criamos o diretório do projeto e instalamos a biblioteca responsável pelo servidor. Nesse projeto, alguns diretórios e arquivos são criados pelo terminal.
mkdir ascii_art
cd ascii_art
npm install -g http-server
Logo em seguida, criamos o arquivo index.html, arquivo raiz do projeto responsável por exibir o formulário ao usuário.
touch index.html
Após criar o arquivo raiz, criamo a caixa de entrada (input) para o upload do arquivo de imagem e o canvas para exibir a imagem.
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Conversor de Ascii Art</title>
<link rel="stylesheet" href="main.css" />
</head>
<body>
<h1>Conversor de Ascii Art</h1>
<p>
<input id="file" type="file" name="picture" style="width: 100%;" />
</p>
<pre id="asciiImage"></pre>
<canvas id="preview"></canvas>
<script src="main.js"></script>
</body>
</html>
No código acima, também temos um tag “pre”, local onde a arte de texto será extraída. Em seguida, criamos o arquivo CSS com o nome de “main.css”, responsável pelo estilo do HTML acima.
pre {
font-family: 'Courier New', 'monospace';
margin: 1rem auto;
font-size: 1pt;
line-height: 1pt;
letter-spacing: 1pt;
}
* {
margin: 0;
}
body {
padding: 2rem 3rem;
font-family: 'VT323', monospace;
line-height: 3rem;
font-size: 18px;
}
header {
display: flex;
align-items: baseline;
font-size: 18px;
}
A porção mais importante do arquivo CSS acima é o “pre”. Atributos como tipo de fonte, tamanho e espaço entre os caracteres são definidos. Caso esses atributos estejam com um valor desequilibrado definido, a arte de texto sofre deformações.
Criando o back-end com javascript
Criamos um arquivo chamado “main.js” e escrevemos o algoritmo de arte de texto. Primeiro, obtemos a instância dos elementos principais do HTML usando javascript.
let canvas = document.getElementById("preview");
let file = document.getElementById('file');
let asciiImage = document.getElementById('asciiImage');
let context = canvas.getContext("2d");
Os elementos acima se comportarão da seguinte maneira:
| Elemento | Papel |
|---|---|
| canvas | Exibi imagem original |
| file | Abriga a instância dos arquivos carregado |
| asciiImage | Exibi a arte de texto |
| context | Obtenção de pixels e tamanho de imagem |
Em seguida, queremos exibir o conteúdo da imagem carregada no canvas. Usamos a função do javascript “onchange” para detectar mudanças de estado.
file.onchange = function(e) {
// Mesmo selecionando múltiplas imagens, só trabalharemos com a primeira
let file = e.target.files[0];
let reader = new FileReader();
reader.onload = function(event) {
let image = new Image();
image.onload = function() {
// Muda o tamanho do canvas para o mesmo tamanho que a imagem
canvas.width = image.width;
canvas.height = image.height;
// Imagem está em base64 quando carregada
// drawImage desenha a imagem no canvas
context.drawImage(image, 0, 0);
};
// Passa o conteúdo carregado para a imagem e detecta a mudança
image.src = event.target.result;
};
// Finaliza
reader.readAsDataURL(file);
};
Uma vez que o código acima tenha sido implementado, já é possível testar o resultado. Usamos o comando abaixo no diretório do projeto para iniciar o servidor.
http-server
Após carregar a imagem, temos uma simples imagem colorida como mostra o resultado abaixo:

Seguindo adiante, definimos o “ramp”, variável que armazena os caracteres que representam a densidade de cada pixel da imagem. Quanto mais escuro for a imagem, maior será o caracter utilizado. No site de Paul Borke, ele explica um pouco sobre como funciona.
const rampForGray = " .'`^\",:;Il!i><~+_-?][}{1)(|\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%[email protected]$";
A variável acima é uma string. Cada pixel da imagem será representa com um dos caracteres disponíveis na string acima. Os pixels brancos (branco é a cor menos densa possível) serão representados com o espaço que fica na extremidade esquerda do valor da variável “rampForGray”. Quanto mais preto for o pixel da imagem, mais denso ele será, usando um caracter mais próxima a extremidade direita da string acima.
Porém, antes de medir a intensidade do cinza, temos que converter a imagem de colorida para cinza. Isso é possível fazendo uma simples conta com o valor RGB de cada pixel. Existem algumas fórmulas que podem ser usadas para mudar a imagem para cinza, duas delas são:
- Intensidade do pixel em escala de cinza = 0.299 * R + 0.587 * G + 0.114 * B
- Intensidade do pixel em escala de cinza = (R + G + B) / 3
Qualquer uma das duas fórmulas irá funcionar. RGB representa a iniciante das cores vermelho, verde e azul.
// Formula para deixar imagem colorida em cinza
const convertToGray = function(r, g, b) {
return (r + g + b) / 3;
};
const convertImageToGray = function(width, height) {
let imageData = context.getImageData(0, 0, width, height);
for (let i = 0; i < imageData.data.length; i += 4) {
// Obtem o RGB do pixel atual
let r = imageData.data[i];
let g = imageData.data[i + 1];
let b = imageData.data[i + 2];
// Transforma a cor do pixel em cinza
let gray = convertToGray(r, g, b);
imageData.data[i] = gray;
imageData.data[i + 1] = gray;
imageData.data[i + 2] = gray;
}
context.putImageData(imageData, 0, 0);
}
A função acima irá transformar a imagem colorida em cinza. “getImageData” irá obter um array contendo os pixels da imagem. No loop, adicionamos 4 a variável “i”, pois cada pixel ocupa 4 elementos no array.

Após definir a função acima, basta invocarmos a mesma logo após a função “drawImage”, como mostra abaixo:
...
context.drawImage(image, 0, 0);
convertImageToGray(canvas.width, canvas.height)
...
O resultado será uma imagem colorida convertida para uma imagem cinza.

Após a conversão basta converter cada pixel para texto usando a intensidade do cinza como valor referente. A variável “gray” usada dentro da função “convertImageToGray” pode ter um valor de 0 até 255. Quanto mais claro o pixel é mais alto é o valor. Portanto, zero é a cor totalmente preta e o 255 é a cor totalmente branca. Usando esse valor do “gray”, criamos uma faixa (range) entre 0 e o tamanho da string da variável “rampForGray”. O tamanho da variável é obtida com a linha abaixo:
const rampLength = rampForGray.length;
Usando uma função de mapeamento obtida no StackOverflow, temos a seguinte função pronta para o uso:
function mapRange(value, low1, high1, low2, high2) {
return low2 + (high2 - low2) * (value - low1) / (high1 - low1);
}
A função acima será usada para ajustar um range de 0 até 255 para 0 até “rampLength”. Dentro da função “convertImageToGray” adicionamos mais código:
const charIndex = mapRange(gray, 0, 255, 0, rampLength);
// Arredonda o número
const charIndexfloor = Math.floor(charIndex);
// Seleciona o caracter ideal para a intensidade da imagem
const character = rampForGray.charAt(charIndexfloor);
// Adiciona o caracter do pixel na imagem
asciiCharacters += character;
Antes do loop, iniciamos a variável “asciiCharacters” como uma string vazia:
asciiCharacters = "";
Temos que iniciar uma nova linha quando necessário. Isso pode ser feito com uma simples condição ainda dentro do loop usando o atributo “width” da imagem.
if (i % (imageData.width * 4) == 0 && i != 0) {
asciiCharacters += "\n";
}
Multiplicamos o tamanho da imagem por 4, pois são elementos por pixel. Caso a contagem dos elementos de uma linha dividido pelo tamanho da imagem multiplicado por 4 dê um valor zero, temos uma nova linha adicionada ao string.
Finalmente, após o loop acabar, passamos a string para o elemento no HTML.
asciiImage.textContent = asciiCharacters;
Ao carregarmos a imagem veremos que a arte de texto ficará extremamente larga. Isso acontece pelo grande número de pixels por linha. Podemos diminuir a largura da imagem ignorando alguns pixels. Podemos mudar o loop da seguinte maneira:
...
for (let i = 0; i < imageData.data.length; i += (4 * 4)) {
...
Multiplicamos o número de elementos por pixel que é 4, por 4. Isso fará com que usemos apenas 1 pixel a cada 4 pixels, assim os outros 3 pixels são ignorados. Assim, podemos diminuir a largura da arte de texto. O único problema disso, é que a imagem perde informação, perdendo alguns detalhes quando muito complexa.

Conclusão
O resultado final do código pode ser conferido no github. Esse projeto nos faz entender como a estrutura de pixels de imagens funcionam no HTML e javascript. Cada pixel armazenado no array da imagem possui 4 elementos que representam o RGBA da imagem. É possível usar o valor da intensidade das cores para representar cada pixel da imagem usando diversos conjuntos de caracteres.