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

в 11:32, , рубрики: canvas, filereader, html, html5, javascript, метки: , , ,

Генератор 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];

Генератор ASCII артов на HTML5Внимательный читатель скорее всего заметил, что учитывается не вся высота символа, а только 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

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

~750Kb

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

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

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

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

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

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

Заключение

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

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

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

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

Автор: HaruAtari

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js