Генератор ASCII-артов на HTML5

от автора

Доброго времени суток, уважаемые хаброжители.

В этой статье я расскажу о том, как при помощи HTML5 можно сделать простенькое приложение, которое будет генерировать ASCII-арты на основе обычных изображений. Статья ориентирована на тех, кто только начинает свое знакомство с такой замечательной технологией, как HTML5, коим являюсь и я. Профессионалы вряд ли найдут для себя что то новое.

Дело было вечером, делать было нечего

Копался я недавно в интернете в поисках обоев и наткнулся на одно интересное изображение(1.1мб). И меня “зацепила” идея рисовать изображения разноцветными буквами. Порывшись в интернете узнал, что это называется ASCII-art. Ну и конечно же первая мысль: “А запилю ка я приложение, что бы мои любимые обои таким образом нарисовало!”
Сказано — сделано. Есть время, есть желание — почему бы не попробовать.

Было решено реализовывать приложение в браузере. Я давно смотрел на HTML5 и облизывался, да все никак руки не доходили поиграться. А что? Технология модная, перспективная, почему бы не попробовать? Да и проект не сложный, для изучения чего то нового — самое то. На этом и остановился.

Постановка задачи

Приложение должно соответствовать следующим требованиям:

  • наличие двух способов загрузки исходного изображения: через поле выбора файла и перетаскиванием в специальную область (далее будем называть «область приема»);
  • отсутствие сложных настроек. Только самое необходимое: цвет фона, используемый текст и размер шрифта;
  • возможность обработки изображений с прозрачным фоном;
  • работа должна происходить только в браузере, без обращений к серверу и без перезагрузки страницы.

Понятно, что вопрос о поддержке старых браузеров не встает.

Для начала, набросаем html-разметку. Страница приложения делится на три логических части:

1. Область загрузки исходного изображения

<h2>Source image</h2> <div class="row">     <div class="source-image-area-out">         <!-- Область для перетаскивания изображения (область приема) -->         <div id="source-image-area">             Drop source image here...         </div>     </div> </div> <div class="row">     <!-- Поле для выбора файла -->     <label for="source-image-file" style="width: 150px">Or select this here:</label>     <input type="file" id="source-image-file" /> </div> 

2. Область настроек

<h2>Settings</h2> <div class="left">     <!-- Используемый текст -->     <div class="row">         <label for="input-used-text">Used text:</label>         <input type="text" id="input-used-text" value="B" />     </div>     <!-- Размер шрифта -->     <div class="row">         <label for="input-font-size">Font size:</label>         <input type="number" id="input-font-size" min="3" max="20" step="1" value="8" style="width: 65px" /> px     </div> </div> <div class="right">     <!-- Цвет фона -->     <div class="row">         <label for="input-background-color">Background:</label>         <input type="color" id="input-background-color" />     </div>     <!-- Прозрачность фона -->     <div class="row">         <label for="input-background-transparent">Transparent:</label>         <input type="checkbox" id="input-background-transparent" />     </div> </div> 

3. Область предпросмотра

<h2>Previews</h2> <!-- Получившееся изображение --> <div id="preview-result" class="left">     <img src="" id="image-result" alt="" /> </div> <!-- Исходное изображение --> <div id="preview-source" class="right">     <img src="" id="image-source" alt="" /> </div> 

Загрузка исходного изображения

Для начала разберем способ загрузки исходного изображения.
Для того, что бы получить доступ к выбранному пользователем файлу, без отправки его она сервер используется класс FileReader. Его метод readAsDataURL() возвращает содержимое файла в виде схемы data:URL. Ну что же, давайте попробуем.

// Содержимое файла, которое будет загружено из поля ввода. var fileData = null;  // Загружает изображение из поля выбора файла. var loadFromField = function(event) {     loadFile(event.target.files[0]; };  // Загружает изображение из “области приема”. var loadFromArea = function(event) {     event.stopPropagation();     event.preventDefault();     loadFile(event.dataTransfer.files[0]); }; // Обработчик события dragover “области приема”. var areaDragOverHandler = function(event) {     event.stopPropagation();     event.preventDefault();     event.dataTransfer.dropEffect = "copy"; };  // Загружает выбранное изображение. // Записывает содержимое файла в виде строки в переменную fileData. var loadFile = function(file) {     var reader = new FileReader();     reader.onload = function(data)     {         fileData = data.target.result;     }     reader.readAsDataURL(file); }  // Присваиваем необходимые обработчики. // Для поля выбора файла document.getElementById("source-image-file").addEventListener("change", loadFromField, false); // Для “области приема”. document.getElementById("source-image-area").addEventListener("drop", loadFromArea, false); document.getElementById("source-image-area").addeventListener("dragover", areaDragOverHandler, false); 

Теперь у нас есть исходное изображение в виде data:URL. Что с ним можно сделать? Его можно использовать в качестве значения атрибута src для изображения. Поэтому давайте покажем пользователю исходное изображение.

var sourceImage = document.getElementById("mage-source"); sourceImage.src = fileData; 

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

Настройки

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

var usedText = document.getElementById("input-used-text").value; var fontSize = document.getElementById("input-font-size").value; var backgroundColor = (document.getElementById("input-background-transparent").checked == true) ? "rgba(0,0,0,0)" : document.getElementById("input.background-color").value; 

Теперь перейдем непосредственно к генерации нашего арта.

Обработка изображения

Весь процесс можно разбить на несколько этапов:

  1. получение данных об исходном изображении. А точнее — нам нужен цвет каждого пикселя;
  2. расчет размеров символов, при помощи которых будет формироваться арт;
  3. расчет цвета каждого символа и его цвета;
  4. непосредственно генерация арта;
  5. представление арта в виде изображения, что бы пользователь мог сохранить плод своих стараний.

Получение данных об исходном изображении

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

<canvas id="canvas"></canvas> 

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

var canvas = document.getElementById("canvas"); var context = canvas.getContext("2d"); canvas.width = sourceImage.width; canvas.height = sourceImage.height; context.drawImage(sourceImage, 0, 0); var sourseData = context.getImageData(0, 0, canvas.width, canvas.height).data; 

Метод getImageData() возвращает информацию о канве. Поле data содержит описание каждого пикселя, как раз то, что нам надо.

Теперь у нас есть необходимая информация. Вот только представлена она не в самой лучшей форме. Это одномерный массив, где первые четыре элемента описывают первый пиксель (rgba), элементы с пятого по восьмой — второй пиксель и т.д. до конца. Как с таким работать, я слабо представляю. Поэтому давайте приведем эту кучу чисел в человеческий вид.

var getPixelsGrid = function(source) {     var res = [];     for (var i = 0; i < source.length; i += 4) {         var y = Math.floor(i / (canvas.width * 4));         var x = (i - (y * canvas.height * 4)) / 4;         if (typeof res[x] === "undefined") {             res[x] = [];         }         res[x][y] = {             r: source+0],             g: source[i+1],             b: source[i+2],             a: source[i+3]         }     }     return res; } var pixelsGrid = getPixelsGrid(sourseData); 

Теперь мы имеем двумерный массив где каждый пиксель представлен объектом. С ним и будем работать дальше.

Расчет размеров символа

Как получить точный размер символа? Не размер шрифта, а область, которую символ занимает на экране? Что бы не заморачиваться, просто создадим временный span с этим символом и замерим его размер.

var countUsedTextSize = function(symbol, size) {     var block = document.createElement("span");     block.innerHTML = symbol;     block.style.fontSize = size + "px";     block.style.fontFamily = "Monospace";     document.body.appendChild(block);     var re = [(block.offsetWidth, Math.floor(block.offsetHeight * 0.8)]     document.body.removeChild(block);     return re; }; // Передаем первый символ, из введенного пользователем текста. var size = countUsedTextSize(usedText[0], fontSize); var usedTextWidth = size[0] var usedTextHeight[1]; 

Внимательный читатель скорее всего заметил, что учитывается не вся высота символа, а только 80%. Это сделано потому, что видимая часть буквы занимает не всю отводимую ей высоту. Из-за этого на итоговом изображении появляются пустые горизонтальные линии между строчками. Особенно они заметны, если буквы большого размера. Я пристрелялся, так что бы при разных размерах шрифта расстояние между строчками было минимальным — получилось 80%. Так и оставим.

Расчет положения и цвета символов

Теперь необходимо составить “карту символов” — список, содержащий информацию о каждом символе, из которых будет формироваться итоговое изображение. Необходимо знать координаты символа и его цвет.

В качестве цвета символа будем использовать цвет пикселя, находящегося в центре области исходного изображения, занимаемой этим символом. Ну или рядом с ним, в случае области с нечетным количеством пикселей по одной из сторон.

var getAvgPixelsList = function(grid) {     var res = [];     var stepX = usedTextWidth;     var stepY = usedTextHeight;     var countStepsX = canvas.width / stepX;     var countStepsY = canvas.height / stepY;      for (var y = 0; y < countStepsY; y++) {         for (var x = 0; x < countStepsX; x++) {             res.push({                 x: x * stepX,                 y: y * stepY,                 r: grid[x * stepX][y * stepY].r,                 g: grid[x * stepX][y * stepY].g,                 b: grid[x * stepX][y * stepY].b,                 a: grid[x * stepX][y * stepY].a             });         }     }      return res; }; var avgPixelsList = getAvgPixelsList(pixelsGrid); 

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

var nextUsedChart = 0; var getNextUsedChart = function() {     var re = usedText.substring(nextUsedChart, nextUsedChart+1);     nextUsedChart++;     if(nextUsedChart == str.length) {         nextUsedChart = 0;     }     return re; }; 

Генерация арта

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

var getResultData = function(list) {     // Очищает канву от исходного изображения и заливает фон.     context.clearRect(0, 0, canvas.width, canvas.height);     context.fillStyle =  backgroundColor;     context.fillRect(0, 0, canvas.width, canvas.height);        // Наносит символы.     for (var i = 0; i < list.length; i++) {         var px = list[i];         context.fillStyle = "rgba(" + px.r +", " + px.g + ", " + px.b + ", " + px.a + ")";         context.font = fontSize + "px Monospace";         context.fillText(getNextUsedChart(), px.x, px.y);     }      return canvas.toDataURL(); }; var resultData = getResultData(avgPixelsList); 

Отлично! Наш арт готов. Осталось только показать его пользователю.

var resultImage = document.getElementById("image-result"); resultimage.src = resultData; 

Итоги

Поздравляю, наш генератор готов! Более того, он даже работает.
Вот несколько примеров:

~150Kb


~750Kb


image

А вот с прозрачным фоном ~300Kb


Здесь можно посмотреть на то, что у нас получилось.
А здесь лежат исходники. Они несколько отличаются от тех, что описаны в статье (функционал разнесен по классам), но логика работы та же.

Есть несколько интересных моментов:

  • в хроме приложение работает в разы быстрее, чем в других браузерах;
  • при обработке большого изображения (больше чем 1000х1000). Хром отказывается открывать его в новом окне, при этом убивая вкладку с сообщением “нехватка памяти”. В других браузерах это работает, хотя и медленно. Я думаю, это связанно с тем, что при передаче изображения через url строка получается слишком длинной;

Заключение

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

  • сейчас, если зайдет пользователь с устаревшим браузером, приложение ничего не скажет, просто не будет работать. Надо добавить проверку на поддержку используемых технологий. Так же необходимо добавить проверку вводимых пользователем данных;
  • если размер исходного изображения не кратен размеру символа, внизу и справа появляются пустые полосы. Необходимо решить эту проблему.
  • ну и кроссбраузерность. У меня была возможность проверить только в chromium 25, chrom 27, firefox 21, opera 12 и safari на айфоне (версию не нашел). В остальных браузерах надо тестировать и исправлять баги.

Но это уже будет потом. Здесь я хотел показать только сам принцип работы.

На этом я заканчиваю. Спасибо, что дочитали до конца. Надеюсь, вы нашли для себя что то полезное. Стройного вам кода.

ссылка на оригинал статьи http://habrahabr.ru/post/182782/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *