В этой статье я расскажу о том, как при помощи HTML5 можно сделать простенькое приложение, которое будет генерировать ASCII-арты на основе обычных изображений. Статья ориентирована на тех, кто только начинает свое знакомство с такой замечательной технологией, как HTML5, коим являюсь и я. Профессионалы вряд ли найдут для себя что то новое.
Дело было вечером, делать было нечего
Копался я недавно в интернете в поисках обоев и наткнулся на одно интересное изображение(1.1мб). И меня “зацепила” идея рисовать изображения разноцветными буквами. Порывшись в интернете узнал, что это называется ASCII-art. Ну и конечно же первая мысль: “А запилю ка я приложение, что бы мои любимые обои таким образом нарисовало!”
Сказано — сделано. Есть время, есть желание — почему бы не попробовать.
Было решено реализовывать приложение в браузере. Я давно смотрел на HTML5 и облизывался, да все никак руки не доходили поиграться. А что? Технология модная, перспективная, почему бы не попробовать? Да и проект не сложный, для изучения чего то нового — самое то. На этом и остановился.
Постановка задачи
Приложение должно соответствовать следующим требованиям:
- наличие двух способов загрузки исходного изображения: через поле выбора файла и перетаскиванием в специальную область (далее будем называть «область приема»);
- отсутствие сложных настроек. Только самое необходимое: цвет фона, используемый текст и размер шрифта;
- возможность обработки изображений с прозрачным фоном;
- работа должна происходить только в браузере, без обращений к серверу и без перезагрузки страницы.
Понятно, что вопрос о поддержке старых браузеров не встает.
Для начала, набросаем html-разметку. Страница приложения делится на три логических части:
<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>
<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>
<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;
Теперь перейдем непосредственно к генерации нашего арта.
Обработка изображения
Весь процесс можно разбить на несколько этапов:
- получение данных об исходном изображении. А точнее — нам нужен цвет каждого пикселя;
- расчет размеров символов, при помощи которых будет формироваться арт;
- расчет цвета каждого символа и его цвета;
- непосредственно генерация арта;
- представление арта в виде изображения, что бы пользователь мог сохранить плод своих стараний.
Получение данных об исходном изображении
Для того, что бы узнать цвета пикселей исходного изображения, необходимо создать канву и нанести изображение на нее. Сначала добавим канву на страницу:
<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;
Итоги
Поздравляю, наш генератор готов! Более того, он даже работает.
Вот несколько примеров:
Здесь можно посмотреть на то, что у нас получилось.
А здесь лежат исходники. Они несколько отличаются от тех, что описаны в статье (функционал разнесен по классам), но логика работы та же.
Есть несколько интересных моментов:
- в хроме приложение работает в разы быстрее, чем в других браузерах;
- при обработке большого изображения (больше чем 1000х1000). Хром отказывается открывать его в новом окне, при этом убивая вкладку с сообщением “нехватка памяти”. В других браузерах это работает, хотя и медленно. Я думаю, это связанно с тем, что при передаче изображения через url строка получается слишком длинной;
Заключение
Сегодня мы познакомились с некоторыми интересными и полезными сторонами HTML5, такими как работа с канвой и загрузка файлов. При этом мы написали работоспособное и, что самое главное, практически полезное приложение. Конечно, это еще не конец. Приложение надо доработать:
- сейчас, если зайдет пользователь с устаревшим браузером, приложение ничего не скажет, просто не будет работать. Надо добавить проверку на поддержку используемых технологий. Так же необходимо добавить проверку вводимых пользователем данных;
- если размер исходного изображения не кратен размеру символа, внизу и справа появляются пустые полосы. Необходимо решить эту проблему.
- ну и кроссбраузерность. У меня была возможность проверить только в chromium 25, chrom 27, firefox 21, opera 12 и safari на айфоне (версию не нашел). В остальных браузерах надо тестировать и исправлять баги.
Но это уже будет потом. Здесь я хотел показать только сам принцип работы.
На этом я заканчиваю. Спасибо, что дочитали до конца. Надеюсь, вы нашли для себя что то полезное. Стройного вам кода.
ссылка на оригинал статьи http://habrahabr.ru/post/182782/
Добавить комментарий