В этой статье вы увидите, как сделать генератор 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)]; });
Вы можете заметить две глобальные переменные, которые еще не упоминались:
intervalASCII_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 пройдет открытый урок, на котором разберем основы, необходимые для работы с анимацией, и создадим анимированный приветственный экран приложения. Регистрация по ссылке.
