Доброго времени суток, уважаемые хаброжители.
В этой статье я расскажу о том, как при помощи HTML5 можно сделать простенькое приложение, которое будет генерировать ASCII-арты на основе обычных изображений. Статья ориентирована на тех, кто только начинает свое знакомство с такой замечательной технологией, как HTML5, коим являюсь и я. Профессионалы вряд ли найдут для себя что то новое.
Копался я недавно в интернете в поисках обоев и наткнулся на одно интересное изображение(1.1мб). И меня “зацепила” идея рисовать изображения разноцветными буквами. Порывшись в интернете узнал, что это называется ASCII-art. Ну и конечно же первая мысль: “А запилю ка я приложение, что бы мои любимые обои таким образом нарисовало!”
Сказано — сделано. Есть время, есть желание — почему бы не попробовать.
Было решено реализовывать приложение в браузере. Я давно смотрел на HTML5 и облизывался, да все никак руки не доходили поиграться. А что? Технология модная, перспективная, почему бы не попробовать? Да и проект не сложный, для изучения чего то нового — самое то. На этом и остановился.
Приложение должно соответствовать следующим требованиям:
Понятно, что вопрос о поддержке старых браузеров не встает.
Для начала, набросаем html-разметку. Страница приложения делится на три логических части:
Для начала разберем способ загрузки исходного изображения.
Для того, что бы получить доступ к выбранному пользователем файлу, без отправки его она сервер используется класс
Теперь у нас есть исходное изображение в виде data:URL. Что с ним можно сделать? Его можно использовать в качестве значения атрибута
Вот, так намного нагляднее. Теперь самое главное: необходимо обработать это изображение.
Мы не будем сохранять настройки каждый раз, когда пользователь их меняет. Вместо этого мы считаем их всего один раз, непосредственно перед обработкой изображения.
Теперь перейдем непосредственно к генерации нашего арта.
Весь процесс можно разбить на несколько этапов:
Для того, что бы узнать цвета пикселей исходного изображения, необходимо создать канву и нанести изображение на нее. Сначала добавим канву на страницу:
Теперь зададим ей такие же размеры, как и у исходного изображения и нанесем это изображение на нее. А затем получим информацию о канве, а как следствие — об исходном изображении.
Метод
Теперь у нас есть необходимая информация. Вот только представлена она не в самой лучшей форме. Это одномерный массив, где первые четыре элемента описывают первый пиксель (rgba), элементы с пятого по восьмой — второй пиксель и т.д. до конца. Как с таким работать, я слабо представляю. Поэтому давайте приведем эту кучу чисел в человеческий вид.
Теперь мы имеем двумерный массив где каждый пиксель представлен объектом. С ним и будем работать дальше.
Как получить точный размер символа? Не размер шрифта, а область, которую символ занимает на экране? Что бы не заморачиваться, просто создадим временный span с этим символом и замерим его размер.
Внимательный читатель скорее всего заметил, что учитывается не вся высота символа, а только 80%. Это сделано потому, что видимая часть буквы занимает не всю отводимую ей высоту. Из-за этого на итоговом изображении появляются пустые горизонтальные линии между строчками. Особенно они заметны, если буквы большого размера. Я пристрелялся, так что бы при разных размерах шрифта расстояние между строчками было минимальным — получилось 80%. Так и оставим.
Теперь необходимо составить “карту символов” — список, содержащий информацию о каждом символе, из которых будет формироваться итоговое изображение. Необходимо знать координаты символа и его цвет.
В качестве цвета символа будем использовать цвет пикселя, находящегося в центре области исходного изображения, занимаемой этим символом. Ну или рядом с ним, в случае области с нечетным количеством пикселей по одной из сторон.
Так же определим функцию, которая будет возвращать очередной символ из введенного пользователем текста. А при достижении конца, начинать сначала.
Теперь у нас есть все, что нужно: список позиций и цветов символов, из которых будем формировать изображение и функция, которая эти символы будет возвращать. Давайте уже сгенерируем наш арт.
Отлично! Наш арт готов. Осталось только показать его пользователю.
Поздравляю, наш генератор готов! Более того, он даже работает.
Вот несколько примеров:
Здесь можно посмотреть на то, что у нас получилось.
А здесь лежат исходники. Они несколько отличаются от тех, что описаны в статье (функционал разнесен по классам), но логика работы та же.
Есть несколько интересных моментов:
Сегодня мы познакомились с некоторыми интересными и полезными сторонами 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;
Теперь перейдем непосредственно к генерации нашего арта.
Обработка изображения
Весь процесс можно разбить на несколько этапов:
- получение данных об исходном изображении. А точнее — нам нужен цвет каждого пикселя;
- расчет размеров символов, при помощи которых будет формироваться арт;
- расчет цвета каждого символа и его цвета;
- непосредственно генерация арта;
- представление арта в виде изображения, что бы пользователь мог сохранить плод своих стараний.
Получение данных об исходном изображении
Для того, что бы узнать цвета пикселей исходного изображения, необходимо создать канву и нанести изображение на нее. Сначала добавим канву на страницу:
<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
А вот с прозрачным фоном ~300Kb
Здесь можно посмотреть на то, что у нас получилось.
А здесь лежат исходники. Они несколько отличаются от тех, что описаны в статье (функционал разнесен по классам), но логика работы та же.
Есть несколько интересных моментов:
- в хроме приложение работает в разы быстрее, чем в других браузерах;
- при обработке большого изображения (больше чем 1000х1000). Хром отказывается открывать его в новом окне, при этом убивая вкладку с сообщением “нехватка памяти”. В других браузерах это работает, хотя и медленно. Я думаю, это связанно с тем, что при передаче изображения через url строка получается слишком длинной;
Заключение
Сегодня мы познакомились с некоторыми интересными и полезными сторонами HTML5, такими как работа с канвой и загрузка файлов. При этом мы написали работоспособное и, что самое главное, практически полезное приложение. Конечно, это еще не конец. Приложение надо доработать:
- сейчас, если зайдет пользователь с устаревшим браузером, приложение ничего не скажет, просто не будет работать. Надо добавить проверку на поддержку используемых технологий. Так же необходимо добавить проверку вводимых пользователем данных;
- если размер исходного изображения не кратен размеру символа, внизу и справа появляются пустые полосы. Необходимо решить эту проблему.
- ну и кроссбраузерность. У меня была возможность проверить только в chromium 25, chrome 27, firefox 21, opera 12 и safari на айфоне (версию не нашел). В остальных браузерах надо тестировать и исправлять баги.
Но это уже будет потом. Здесь я хотел показать только сам принцип работы.
На этом я заканчиваю. Спасибо, что дочитали до конца. Надеюсь, вы нашли для себя что то полезное. Стройного вам кода.