Я разработал генератор ASCII-арта в Node JS

от автора

В этой статье вы увидите, как сделать генератор ASCII-арта из изображения.

Результат:

но сначала

что такое ASCII-арт?

ASCII-арт — это метод графического дизайна, который использует компьютеры для презентации и он состоит из изображений, собранных вместе из 95 печатных символов, определенных стандартом ASCII от 1963 года, и ASCII-совместимых наборов символов с проприетарными расширенными символами.

Необходимые условия

Для данного проекта мне хочется применить свои знания JS, поэтому я буду использовать:

npm i sharp readline-sync

Этапы программы:

Когда я думал об ASCII-арте, то представлял, что он создается с помощью какого-то алгоритма детекции краев. Как же я ошибался — для создания ASCII-арта из изображения вам потребуется:

  • превратить изображение в черно-белое;

  • изменить размер изображения;

  • заменить все черно-белые пиксели на символы, определяющие яркость и темноту/тень.

Итак, давайте приступим. Сначала я создам файл package.json, сделав следующее:

npm init

Как только я получу свой пакет, то создам index.js файл, где будет находиться мой код.

Когда это будет сделано, я импортирую все зависимости, необходимые для этого проекта, следующим образом:

const sharp = require("sharp"); const readlineSync = require("readline-sync"); const fs = require("fs");

Тогда давайте сначала запросим у пользователя изображение, которое он хочет преобразовать.

Получение пользовательского ввода

Для этого я создам функцию loadFileFromPath и в ней буду получать данные от пользователя следующим образом:

var filePath = readlineSync.question("What's the file path ");

Зачем нам нужен readlineSync?

Вероятно, вам интересно, что представляет собой пакет readlineSync. Он позволяет нам вводить данные в консоль синхронно; поскольку JS-узел является асинхронным, код продолжает выполняться, поэтому мы используем пакет для того, чтобы дождаться ввода данных пользователем.

Далее я проверю, корректен ли путь или нет, с помощью операторов try/catch, как здесь:

try {     const file = await sharp(filePath);     return file;   } catch (error) {     console.error(error);   }

и вся функция выглядит следующим образом:

const loadFileFromPath = async () => {   var filePath = readlineSync.question("What's the file path ");   try {     const file = await sharp(filePath);     return file;   } catch (error) {     console.error(error);   } };

Преобразование в черно-белое

Для этого я сначала создам функцию convertToGrayscale с таким параметром пути, как здесь:

const convertToGrayscale = async (path) => {   // code };

В этой функции я загружу изображение, изменю его цветовые значения на черно-белые и, наконец, верну черно-белый результат.

const convertToGrayscale = async (path) => {  const img = await path;   const bw = await img.gamma().greyscale();  return bw; };

Изменение размера изображения

Для этого я сначала создам функцию resizeImg с параметрами bw и newWidth = 100 следующим образом:

const resizeImg = async (bw, newWidth = 100) => {   //code };

Затем я буду ждать ч/б изображение и ожидать результат переменной blackAndWhite, потом получу метаданные для доступа к свойствам размеров.

const resizeImg = async (bw, newWidth = 100) => {   const blackAndWhite = await bw;   const size = await blackAndWhite.metadata(); };

далее вычисляем пропорции изображения, для этого просто делим ширину на высоту и получаем необходимое соотношение. Затем мы рассчитываем нашу новую высоту:

const ratio = size.width / size.height; newHeight = parseInt(newWidth * ratio);

Потом мы окончательно изменяем размер изображения и возвращаем его в таком виде:

const resized = await blackAndWhite.resize(newWidth, newHeight, {     fit: "outside",   }); return resized;

Вся функция должна выглядеть следующим образом:

const resizeImg = async (bw, newWidth = 100) => {   const blackAndWhite = await bw;   const size = await blackAndWhite.metadata();   const ratio = size.width / size.height;   newHeight = parseInt(newWidth * ratio);   const resized = await blackAndWhite.resize(newWidth, newHeight, {     fit: "outside",   });    return resized; };

Преобразование пикселей в ASCII-символы 

Для этого я сначала создам функцию pixelToAscii с параметром img следующим образом:

const pixelToAscii = async (img) => {  //code };

Затем я создам переменную для хранения img с ключевым словом await. Потом получу массив пикселей изображения и сохраню его в переменной pixels.

var newImg = await img; const pixels = await newImg.raw().toBuffer(); };

Дальше создам переменную characters, которая будет содержать пустую строку. Затем я пройдусь по каждому пикселю из массива и введу ASCII-символ в созданную ранее строку:

characters = ""; pixels.forEach((pixel) => {     characters = characters + ASCII_CHARS[Math.floor(pixel * interval)];   });

Вы можете заметить две глобальные переменные, которые еще не упоминались:

  • interval

  • ASCII_CHARS

Я объясню вам, что они из себя представляют:

  • ASCII_CHARS — это переменная, в которой хранятся все ASCII-символы:

ASCII_CHARS = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ".split(   "" );
  • interval — это ascii, который должен быть присвоен цвету (интенсивность).

charLength = ASCII_CHARS.length; interval = charLength / 256;

Теперь мы знаем, что это за переменные, давайте вернемся к функции, сейчас она должна выглядеть следующим образом:

const pixelToAscii = async (img) => {   var newImg = await img;   const pixels = await newImg.raw().toBuffer();   characters = "";   pixels.forEach((pixel) => {     characters = characters + ASCII_CHARS[Math.floor(pixel * interval)];   });   return characters; };

Теперь все шаги сделаны, давайте создадим ядро приложения:

Главная функция

Для этого я сначала создам функцию main с параметрами newWidth = 100 следующим образом:

const main = async (newWidth = 100) => {   //code };

В этой функции я создам функцию с названием: *newImgData, которая будет равна всем тем функциям, которые мы создали ранее, вложенным следующим образом:

const main = async (newWidth = 100) => {   const newImgData = await pixelToAscii(     resizeImg(convertToGrayscale(loadFileFromPath()))   ); };

Затем я выясню длину моих символов и создам пустую переменную с именем ASCII следующим образом:

const pixels = newImgData.length; let ASCII = "";

Потом переберу список пикселей:

for (i = 0; i < pixels; i += newWidth) {     let line = newImgData.split("").slice(i, i + newWidth);     ASCII = ASCII + "\n" + line;   }

По существу, я делаю разбиение на строки. Получаю размер newWidth, нарезаю массив как строку этой newWidth и затем добавляю символ \n для перехода к следующей строке.

Экспорт в текстовый файл

И, наконец, в той же функции для сохранения текста в файл я сделал следующее:

setTimeout(() => {     fs.writeFile("output.txt", ASCII, () => {       console.log("done");     });   }, 5000);

В результате мы получили ASCII-арт генератор из изображения! И, конечно же, не забудьте про main() для первого вызова функции.

Законченный код должен выглядеть следующим образом:

const sharp = require("sharp"); const readlineSync = require("readline-sync"); const fs = require("fs");  ASCII_CHARS = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ".split(   "" ); charLength = ASCII_CHARS.length; interval = charLength / 256; var newHeight = null; const main = async (newWidth = 100) => {   const newImgData = await pixelToAscii(     resizeImg(convertToGrayscale(loadFileFromPath()))   );   const pixels = newImgData.length;   let ASCII = "";   for (i = 0; i < pixels; i += newWidth) {     let line = newImgData.split("").slice(i, i + newWidth);     ASCII = ASCII + "\n" + line;   }    setTimeout(() => {     fs.writeFile("output.txt", ASCII, () => {       console.log("done");     });   }, 5000); };  const convertToGrayscale = async (path) => {   const img = await path;   const bw = await img.gamma().greyscale();   return bw; };  const resizeImg = async (bw, newWidth = 100) => {   const blackAndWhite = await bw;   const size = await blackAndWhite.metadata();   const ratio = size.width / size.height;   newHeight = parseInt(newWidth * ratio);   const resized = await blackAndWhite.resize(newWidth, newHeight, {     fit: "outside",   });    return resized; };  const pixelToAscii = async (img) => {   var newImg = await img;   const pixels = await newImg.raw().toBuffer();   characters = "";   pixels.forEach((pixel) => {     characters = characters + ASCII_CHARS[Math.floor(pixel * interval)];   });   return characters; };  const loadFileFromPath = async () => {   var filePath = readlineSync.question("What's the file path ");   try {     const file = await sharp(filePath);     return file;   } catch (error) {     console.error(error);   } }; main();

Чему я научился в ходе работы над этим проектом?

Этот проект был очень интересным. Я впервые обнаружил, что можно вложить функции, также выяснил, как работает ASCII-арт, узнал об асинхронной проблеме js-узла для пользовательского ввода и о том, как ее решить, и, наконец, как сделать некоторые простые манипуляции с изображениями.


Анимации на сайте давно перестали быть каким-то ноу-хау. Это неотъемлемая часть любого интерфейса. Скоро в OTUS пройдет открытый урок, на котором разберем основы, необходимые для работы с анимацией, и создадим анимированный приветственный экран приложения. Регистрация по ссылке.


ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/654777/