Доброго времени суток, уважаемые хаброжители.
В этой статье я расскажу о том, как при помощи 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, chrome 27, firefox 21, opera 12 и safari на айфоне (версию не нашел). В остальных браузерах надо тестировать и исправлять баги.
Но это уже будет потом. Здесь я хотел показать только сам принцип работы.
На этом я заканчиваю. Спасибо, что дочитали до конца. Надеюсь, вы нашли для себя что то полезное. Стройного вам кода.
Автор: HaruAtari