Пишем простой редактор аватарок на JavaScript

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

Если на сайте можно загрузить аватарку, то хорошо бы добавить минимальный редактор, который позволит обрезать изображение. Реализовать это относительно просто, и все стороны остаются в выигрыше: пользователю удобнее делать всё на сайте, не уходя с него.

Как работает редактор

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

После сохранения сайт отправляет запрос PHP-скрипту, который обрезает фотографию и помещает её в отдельный файл. В итоге пользователь видит ссылку с результатом работы редактора.

Здесь мы покажем только важные фрагменты приложения, а полный исходный код вы найдёте в репозитории на GitHub.

Евгений Кучерявый

Пишет о программировании, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.


Вёрстка формы

Для начала нужно сверстать саму страницу в HTML:

<div class="wrapper">
    <main class="main">
   	 <div class="main__content">
   		 <div class="avatar">
   			 <img id="image" class="image">
   			 <canvas id="canvas" class="canvas">
   				 Your browser does not support JS or HTML5!
   			 </canvas>
   		 </div>
   		 <p>
   			 <input type="number" name="widthBox" id="widthBox" value="100" min="100" title="Width">
   			 ×
   			 <input type="number" name="heightBox" id="heightBox" value="100" min="100" title="Height">
   		 </p>
   		 <p>
   			 <label>Top: <input type="number" name="topBox" id="topBox" value="0" min="0" title="Top"> </label><br><br>
   			 <label>Left: <input type="number" name="leftBox" id="leftBox" value="0" min="0" title="Left"></label>
   		 </p>
   		 <p>
   			 <button class="button" id="saveBtn">Save</button>
   		 </p>
   		 <p>
   			 <a href="images/newphoto.jpg" target="_blank" class="a a_hidden" id="newImg">Open new photo</a>
   		 </p>
   	 </div>
    </main>
</div>

Сразу же добавляем стили:

body, html
{
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    overflow: hidden;
    font-size: 16px;
    font-family: helvetica, arial;
    background: #f6f6f6;
    color: #111;
}

.wrapper
{
    width: 100%;
    height: 100%;
    display: table;
}

.main
{
    display: table-cell;
    vertical-align: middle;
}

.main__content
{
    padding: 5px;
    text-align: center;
}

.image
{
    display: block;
    margin: 15px auto;
    border: 5px dashed #ddd;
    background: #fff;
    border-radius: 15px;
    max-width: 60%;
    max-height: 600px;
}

.canvas
{
    display: block;
    max-width: 60%;
    max-height: 600px;
    position: absolute;
    border: 0;
    border-radius: 15px;
    cursor: move;
}

.input
{
    display: inline-block;
    vertical-align: middle;
    margin: 5px;
}

.button
{
    display: inline-block;
    vertical-align: middle;
    margin: 5px;
    background: #6694f6;
    border: 0;
    border-radius: 15px;
    padding: 10px 25px;
    color: #fff;
    cursor: pointer;
    box-shadow: 0 0 10px rgba(0,0,0,0.5);
    font-size: 16px;
}

.button:hover
{
    background: #8acef1;
}

.button_disabled
{
    background: #555;
    cursor: not-allowed;
}

.a, .a:visited
{
    color: #6694f6;
}

.a:hover
{
    text-decoration: none;
}

.a_hidden
{
    opacity: 0;
}

Получается достаточно минималистичная форма:

Изображение выводится с помощью тега img, а поверх него находится элемент canvas. На данном этапе позиция холста не указана — она прописывается скриптом и зависит от позиции изображения.

Дальше берём какое-нибудь изображение, чтобы его редактировать. Я воспользовался сайтом thispersondoesnotexist.com, который генерирует фотографию несуществующего человека.

Только мелкие детали выдают, что это не настоящая фотография

Пишем JS-скрипт

Для начала получим из DOM нужные нам объекты:

//Холст и его контекст
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

//Поля ввода
const widthBox = document.getElementById("widthBox");
const heightBox = document.getElementById("heightBox");
const topBox = document.getElementById("topBox");
const leftBox = document.getElementById("leftBox");

//Кнопка сохранения
const saveBtn = document.getElementById("saveBtn");

//Ссылка на новое изображение
const newImg = document.getElementById("newImg");

Выполним инициализацию — загрузим изображение, наложим на него холст, заполним поля и так далее:

const image = document.getElementById("image");

image.addEventListener("load", function () { Init(); });

image.src = "images/photo.jpg";

window.addEventListener("resize", function () { Init(); });

function Init()
{
    canvas.width = image.width;
    canvas.height = image.height;

    canvas.setAttribute("style", "top: " + (image.offsetTop + 5) + "px; left: " + (image.offsetLeft + 5) + "px;");

    leftBox.setAttribute("max", image.width - 100);
    topBox.setAttribute("max", image.height - 100);

    widthBox.setAttribute("max", image.width);
    heightBox.setAttribute("max", image.height);

    DrawSelection(); //Эта функция будет рассмотрена чуть позже
}

Чтобы проверить, работает ли функция нормально, я уже добавил рамку на холст:

Давайте рассмотрим код, который отвечает за выделение:

var selection =
{
    mDown: false,
    x: 0,
    y: 0,
    top: 50,
    left: 50,
    width: 100,
    height: 100
};

function DrawSelection()
{
    ctx.fillStyle = "rgba(0, 0, 0, 0.7)";

    ctx.clearRect(0, 0, canvas.width, canvas.height);

    ctx.fillRect(0, 0, canvas.width, canvas.height);

    ctx.clearRect(selection.left, selection.top, selection.width, selection.height);

    ctx.strokeStyle = "#fff";

    ctx.beginPath();

    ctx.moveTo(selection.left, 0);
    ctx.lineTo(selection.left, canvas.height);

    ctx.moveTo(selection.left + selection.width, 0);
    ctx.lineTo(selection.left + selection.width, canvas.height);

    ctx.moveTo(0, selection.top);
    ctx.lineTo(canvas.width, selection.top);

    ctx.moveTo(0, selection.top + selection.height);
    ctx.lineTo(canvas.width, selection.top + selection.height);

    ctx.stroke();
}

Объект selection хранит данные о том, зажата ли кнопка мыши, какие координаты курсора на холсте, разрешение и позиция выделенной области. Функция DrawSelection () отрисовывает рамку:

Остаётся написать код, который двигает эту область:

function MouseDown(e)
{
    //Говорим, что кнопка была зажата
    selection.mDown = true;
}

function MouseMove(e)
{
    if(selection.mDown) //Проверяем, зажата ли кнопка
    {
	//Получаем координаты курсора на холсте
   	 selection.x = e.clientX - canvas.offsetLeft;
   	 selection.y = e.clientY - canvas.offsetTop;

	//Меняем позицию выделенного фрагмента
   	 selection.left = selection.x - selection.width / 2;
   	 selection.top = selection.y - selection.height / 2;

	//Проверяем, не выходит ли фрагмент за границы холста
   	 CheckSelection();

	//Ввод новых значений в поля, отрисовка рамки
   	 Update(); 
    }
}

function MouseUp(e)
{
    //Отпускаем кнопку
    selection.mDown = false; 
}

Функции MouseDown (), MouseMove () и MouseUp () вызываются при действиях мыши над холстом: зажатии кнопки мыши, передвижении курсора и отпускании кнопки соответственно. Как работают CheckSelection () и Update (), можно увидеть в полном коде в репозитории.

Давайте посмотрим, как это работает:

Заключительный этап — пишем функцию для отправки запроса PHP-скрипту, который будет обрезать изображение:

function Save()
{
    var xhr = new XMLHttpRequest();

    var params = "width=" + widthBox.value + "&height=" + heightBox.value + "&top=" + topBox.value + "&left=" + leftBox.value + "&cw=" + canvas.width + "&ch=" + canvas.height;

    xhr.open("GET", "editor.php?" + params, true);

    xhr.onload = function ()
    {
   	 if (xhr.status != 200)
   	 {
   		 console.log(xhr.status + ": " + xhr.statusText);
   	 }
   	 else
   	 {
   		 console.log(xhr.responseText);

   		 if (xhr.responseText == "ok")
   		 {
   			 newImg.className = "a";
   		 }
   		 else
   		 {
   			 alert("Ошибка!");
   		 }
   	 }
    };

    xhr.send();
}

Эта функция вызывается при нажатии на кнопку Save. Если изображение успешно обрезано, то появится ссылка на него — для этого с объекта newImg будет удалён класс a_hidden.

Пишем PHP-скрипт

PHP-скрипт получает разрешение и позицию нового фрагмента, а также разрешение самого холста. Затем он загружает старое изображение, вырезает из него нужный нам фрагмент и сохраняет в файл newphoto.jpg.

<?php
$filename = "images/photo.jpg";

if(isset($_GET['width'])
&& isset($_GET['height'])
&& isset($_GET['left'])
&& isset($_GET['top'])
&& isset($_GET['cw'])
&& isset($_GET['ch']))
{
    $width = $_GET['width'];
    $height = $_GET['height'];
    $top = $_GET['top'];
    $left = $_GET['left'];
    $cw = $_GET['cw'];
    $ch = $_GET['ch'];

    //Получаем размеры старого изображения
    list($oldWidth, $oldHeight) = getimagesize($filename);

    //Вычисляем новые размеры и позицию фрагмента
    //Для этого сначала разделим значение, например, ширину на ширину холста - и получим новую ширину в процентах
    //Затем этот процент нужно умножить на ширину оригинальной фотографии - так мы получим новое значение
    $newWidth = ($width / $cw) * $oldWidth;
    $newHeight = ($height / $ch) * $oldHeight;
    $newLeft = ($left / $cw) * $oldWidth;
    $newTop = ($top / $ch) * $oldHeight;

    //Создаём изображение с новыми размерами
    $output = imagecreatetruecolor($newWidth, $newHeight);
    $source = imagecreatefromjpeg($filename);

    imagecopyresized($output, $source, 0, 0, $newLeft, $newTop, $newWidth, $newHeight, $newWidth, $newHeight);

    //Сохранение нового изображения
    $result = imagejpeg($output, "images/newphoto.jpg");

    if($result)
    {
   	 echo "ok";
    }
    else
    {
   	 echo "string"; "fail";
    }
}
else
{
    echo "Error!";
}

?>

В итоге мы получаем новое изображение, на котором будет выделенный фрагмент. Его разрешение зависит от размера оригинала, а не от размера холста.

Вы можете сделать так, чтобы разрешения совпадали, — для этого измените функцию imagecopyresized ():

imagecopyresized($output, $source, 0, 0, $newLeft, $newTop, $newWidth, $newHeight, $width, $height);

Как улучшить редактор аватарок

Вы можете добавить возможность менять разрешение выделенной области с помощью мыши — для этого в функции MouseMove () проверим, в какой области находится курсор, когда пользователь двигает мышь. Если курсор рядом с углом рамки, то меняем размер области, в другом случае — позицию.

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

Также вы можете придумать что-нибудь самостоятельно, если достаточно хорошо владеете JavaScript, HTML и CSS. Изучить эти языки вы можете на нашем курсе по Frontend-разработке.

Курс

Frontend-разработчик


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

Хочешь получать крутые статьи по программированию?
Подпишись на рассылку Skillbox