Criando artes de texto usando imagens

Convertendo imagens para ascii art usando o valor da intensidade das cores cinzentas.

Postado em 16 fevereiro 2023

Atualizado em 16 fevereiro 2023



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:

  1. Carrega a imagem no canvas
  2. Converte a cor da imagem para o cinza
  3. 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.

ascii art exemplo 1

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:
resultado da ascii arte 2

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%B@$";

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.

representação de array e pixels de uma imagem

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.

imagem colorida para 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.

teste final de arte de texto

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.

Postagens mais vistas

Os 5 principais componentes do computador

Os 5 principais componentes do computador são a unidade de controle, unidade aritmética e lógica, memória, dispositivo de entrada e dispositivo de saída.

Portas TCP e UDP

A porta é um número de 16 bits que é adicionado no final do endereço IP, insinuando qual aplicativo está vinculado e atuando nessa porta.

LAN

Rede local de computadores (LAN) é um conjunto de computadores ou dispositivos conectados uns aos outros de forma isolada em um pequeno local.

Retornar aos projetos