0 added
0 removed
Original
2026-01-01
Modified
2026-02-21
1
<p><a>#Руководства</a></p>
1
<p><a>#Руководства</a></p>
2
<ul><li>18 июн 2020</li>
2
<ul><li>18 июн 2020</li>
3
<li>0</li>
3
<li>0</li>
4
</ul><p>В этой статье мы расскажем, как написать простой редактор для аватарок, чтобы облегчить жизнь пользователям.</p>
4
</ul><p>В этой статье мы расскажем, как написать простой редактор для аватарок, чтобы облегчить жизнь пользователям.</p>
5
<p> vlada_maestro / shutterstock</p>
5
<p> vlada_maestro / shutterstock</p>
6
<p>Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.</p>
6
<p>Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.</p>
7
<p>Если на сайте можно загрузить аватарку, то хорошо бы добавить минимальный редактор, который позволит обрезать изображение. Реализовать это относительно просто, и все стороны остаются в выигрыше: пользователю удобнее делать всё на сайте, не уходя с него.</p>
7
<p>Если на сайте можно загрузить аватарку, то хорошо бы добавить минимальный редактор, который позволит обрезать изображение. Реализовать это относительно просто, и все стороны остаются в выигрыше: пользователю удобнее делать всё на сайте, не уходя с него.</p>
8
<p>Редактор берёт уже сохранённую фотографию и выводит её на страницу. Далее пользователь выбирает нужный ему фрагмент с помощью мыши или вводит значения в форме.</p>
8
<p>Редактор берёт уже сохранённую фотографию и выводит её на страницу. Далее пользователь выбирает нужный ему фрагмент с помощью мыши или вводит значения в форме.</p>
9
<p>После сохранения сайт отправляет запрос<a>PHP</a>-скрипту, который обрезает фотографию и помещает её в отдельный файл. В итоге пользователь видит ссылку с результатом работы редактора.</p>
9
<p>После сохранения сайт отправляет запрос<a>PHP</a>-скрипту, который обрезает фотографию и помещает её в отдельный файл. В итоге пользователь видит ссылку с результатом работы редактора.</p>
10
<p>Здесь мы покажем только важные фрагменты приложения, а полный исходный код вы найдёте в <a>репозитории на GitHub</a>.</p>
10
<p>Здесь мы покажем только важные фрагменты приложения, а полный исходный код вы найдёте в <a>репозитории на GitHub</a>.</p>
11
<p>Для начала нужно сверстать саму страницу в <a>HTML</a>:</p>
11
<p>Для начала нужно сверстать саму страницу в <a>HTML</a>:</p>
12
<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><p>Сразу же добавляем стили:</p>
12
<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><p>Сразу же добавляем стили:</p>
13
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; }<p>Получается достаточно минималистичная форма:</p>
13
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; }<p>Получается достаточно минималистичная форма:</p>
14
<p>Изображение выводится с помощью тега<em>img</em>, а поверх него находится элемент<em>canvas</em>. На данном этапе позиция холста не указана - она прописывается скриптом и зависит от позиции изображения.</p>
14
<p>Изображение выводится с помощью тега<em>img</em>, а поверх него находится элемент<em>canvas</em>. На данном этапе позиция холста не указана - она прописывается скриптом и зависит от позиции изображения.</p>
15
<p>Дальше берём какое-нибудь изображение, чтобы его редактировать. Я воспользовался сайтом<a>thispersondoesnotexist.com</a>, который генерирует фотографию несуществующего человека.</p>
15
<p>Дальше берём какое-нибудь изображение, чтобы его редактировать. Я воспользовался сайтом<a>thispersondoesnotexist.com</a>, который генерирует фотографию несуществующего человека.</p>
16
<em>Только мелкие детали выдают, что это не настоящая фотография</em><p>Для начала получим из DOM нужные нам объекты:</p>
16
<em>Только мелкие детали выдают, что это не настоящая фотография</em><p>Для начала получим из DOM нужные нам объекты:</p>
17
//Холст и его контекст 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");<p>Выполним инициализацию - загрузим изображение, наложим на него холст, заполним поля и так далее:</p>
17
//Холст и его контекст 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");<p>Выполним инициализацию - загрузим изображение, наложим на него холст, заполним поля и так далее:</p>
18
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(); //Эта функция будет рассмотрена чуть позже }<p>Чтобы проверить, работает ли функция нормально, я уже добавил рамку на холст:</p>
18
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(); //Эта функция будет рассмотрена чуть позже }<p>Чтобы проверить, работает ли функция нормально, я уже добавил рамку на холст:</p>
19
<p>Давайте рассмотрим код, который отвечает за выделение:</p>
19
<p>Давайте рассмотрим код, который отвечает за выделение:</p>
20
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(); }<p>Объект<em>selection</em>хранит данные о том, зажата ли кнопка мыши, какие координаты курсора на холсте, разрешение и позиция выделенной области. Функция<em>DrawSelection ()</em>отрисовывает рамку:</p>
20
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(); }<p>Объект<em>selection</em>хранит данные о том, зажата ли кнопка мыши, какие координаты курсора на холсте, разрешение и позиция выделенной области. Функция<em>DrawSelection ()</em>отрисовывает рамку:</p>
21
<p>Остаётся написать код, который двигает эту область:</p>
21
<p>Остаётся написать код, который двигает эту область:</p>
22
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; }<p>Функции<em>MouseDown ()</em>,<em>MouseMove ()</em>и <em>MouseUp ()</em>вызываются при действиях мыши над холстом: зажатии кнопки мыши, передвижении курсора и отпускании кнопки соответственно. Как работают<em>CheckSelection ()</em>и <em>Update (),</em>можно увидеть в полном коде в репозитории.</p>
22
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; }<p>Функции<em>MouseDown ()</em>,<em>MouseMove ()</em>и <em>MouseUp ()</em>вызываются при действиях мыши над холстом: зажатии кнопки мыши, передвижении курсора и отпускании кнопки соответственно. Как работают<em>CheckSelection ()</em>и <em>Update (),</em>можно увидеть в полном коде в репозитории.</p>
23
<p>Давайте посмотрим, как это работает:</p>
23
<p>Давайте посмотрим, как это работает:</p>
24
<p>Заключительный этап - пишем функцию для отправки запроса PHP-скрипту, который будет обрезать изображение:</p>
24
<p>Заключительный этап - пишем функцию для отправки запроса PHP-скрипту, который будет обрезать изображение:</p>
25
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(); }<p>Эта функция вызывается при нажатии на кнопку<em>Save</em>. Если изображение успешно обрезано, то появится ссылка на него - для этого с объекта<em>newImg</em>будет удалён класс<em>a_hidden.</em></p>
25
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(); }<p>Эта функция вызывается при нажатии на кнопку<em>Save</em>. Если изображение успешно обрезано, то появится ссылка на него - для этого с объекта<em>newImg</em>будет удалён класс<em>a_hidden.</em></p>
26
<p>PHP-скрипт получает разрешение и позицию нового фрагмента, а также разрешение самого холста. Затем он загружает старое изображение, вырезает из него нужный нам фрагмент и сохраняет в файл<em>newphoto.jpg.</em></p>
26
<p>PHP-скрипт получает разрешение и позицию нового фрагмента, а также разрешение самого холста. Затем он загружает старое изображение, вырезает из него нужный нам фрагмент и сохраняет в файл<em>newphoto.jpg.</em></p>
27
<?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!"; } ?><p>В итоге мы получаем новое изображение, на котором будет выделенный фрагмент. Его разрешение зависит от размера оригинала, а не от размера холста.</p>
27
<?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!"; } ?><p>В итоге мы получаем новое изображение, на котором будет выделенный фрагмент. Его разрешение зависит от размера оригинала, а не от размера холста.</p>
28
<p>Вы можете сделать так, чтобы разрешения совпадали, - для этого измените функцию<em>imagecopyresized ()</em>:</p>
28
<p>Вы можете сделать так, чтобы разрешения совпадали, - для этого измените функцию<em>imagecopyresized ()</em>:</p>
29
imagecopyresized($output, $source, 0, 0, $newLeft, $newTop, $newWidth, $newHeight, $width, $height);<p>Вы можете добавить возможность менять разрешение выделенной области с помощью мыши - для этого в функции<em>MouseMove ()</em>проверим, в какой области находится курсор, когда пользователь двигает мышь. Если курсор рядом с углом рамки, то меняем размер области, в другом случае - позицию.</p>
29
imagecopyresized($output, $source, 0, 0, $newLeft, $newTop, $newWidth, $newHeight, $width, $height);<p>Вы можете добавить возможность менять разрешение выделенной области с помощью мыши - для этого в функции<em>MouseMove ()</em>проверим, в какой области находится курсор, когда пользователь двигает мышь. Если курсор рядом с углом рамки, то меняем размер области, в другом случае - позицию.</p>
30
<p>В полях формы логичнее выводить значения относительно оригинального изображения, а не холста, - это будет работать, но потребует дополнительных вычислений.</p>
30
<p>В полях формы логичнее выводить значения относительно оригинального изображения, а не холста, - это будет работать, но потребует дополнительных вычислений.</p>
31
<p>Также вы можете придумать что-нибудь самостоятельно, если достаточно хорошо владеете JavaScript, HTML и CSS.</p>
31
<p>Также вы можете придумать что-нибудь самостоятельно, если достаточно хорошо владеете JavaScript, HTML и CSS.</p>
32
<a>Научитесь: Профессия Фронтенд-разработчик + ИИ Узнать больше</a>
32
<a>Научитесь: Профессия Фронтенд-разработчик + ИИ Узнать больше</a>