Доброго времени суток! Разрабатывая сайт я подошел к тому, что мне необходимо добавить функцию добавления аватара для пользователей на десктопе и мобильных устройствах. Долго искал материалы, даже сначала решил добавить библиотеку с готовыми функциями, уже написанную кем-то (не помню как называлась статья, но она точно была на хабре и там были рассмотрены некоторые библиотеки). После тестирования этих подключенных библиотек я решил написать все на JavaScript и PHP (за исключением использования ajax для работы с php) при помощи HTML5. Для демонстрации я создал страницу для хабра: демо для habrahabr.

Начнем с общей части. Разметка.

Тут все просто. Внутри главного блока (в моем случае 400x400px) я создал блок с изображением, абсолютный блок для вычисления координат обрезки (200х200px) и так же абсолютный блок на всю ширину рабочей области, который я назвал тачпадом (400х400px). Сразу обращаю Ваше внимание что размеры этих блоков сугубо индивидуальные.

Функции для десктопа.

Все начинается с функции onmousedown. Первое что я в нее добавил это изменение масштаба изображения нажатием левой кнопки мыши с зажатой клавишей shift. Работаем с тачпадом. Для передачи координат php обработчику я создал 4 input type=«hidden» со значениями id как x1 — координата смещения обрезки изображения по оси x, y1 — соответственно по оси y, w — ширина исходного изображения, h — высота исходного изображения.

//добавляем исходное изображение
var image = document.getElementById('image');
//добавляем тачпад
var a = document.getElementById('touch_pad');
//и input-ы координат и размеров	
var x1 = document.getElementById('x1');
var y1 = document.getElementById('y1');
var w = document.getElementById('w');
var h = document.getElementById('h');
//функция после нажатия левой кнопки мыши
a.onmousedown = function(e){
//если нажат shift, то объявляем функцию изменения масштаба изображения
	if(e.shiftKey){
//получаем коэффициент положения курсора 
                var koefx = e.clientX + e.clientY;
//получаем ширину изображения
		var d = image.offsetWidth;
//после движения мышью начинаем функцию вычисления 
		a.onmousemove = function(e){
//получаем новый коэффициент положения курсора, чтоб получить разницу от koefx
		var reli = e.clientX + e.clientY;
//и задаем стили изображению по полученному коэффициенту масштаба
		image.style.width = d + reli*2 - koefx*2 + 'px';
		var wid = image.width;
		var heig = image.height;
//поскольку ширина и высота тачпада 400px, а блока обрезки 200px и он находится в центре -
//мы смещаем результат на 100px по x и y
		var ll = 100 - image.offsetLeft;
		var tt = 100 - image.offsetTop;
//и вписываем в форму новые координаты x и y и задаем новую высоту и ширину изображения
		x1.setAttribute('value', ll);
		y1.setAttribute('value', tt);
		w.setAttribute('value', wid);
		h.setAttribute('value', heig);
		}
	}

Как видите здесь все просто, чуть позже мы объявим функцию для изменения масштаба при помощи прокрутки колеса мыши, что даст дополнительный функционал пользователю.

Далее начнем функцию изменения положения исходного изображения, если shift не зажали:

else {
//получаем текущие координаты курсора
	var x = e.pageX;
	var y = e.pageY;
//получаем координаты исходного изображения
	var lleft = image.offsetLeft;
	var ttop = image.offsetTop;
//и создаем коэффициенты по осям x и y 
	var lleft = x - lleft;
	var ttop = y - ttop;
//после движения мышью начинаем функцию вычисления
	a.onmousemove = function(e){
//получаем новые координаты курсора
		x = e.pageX;
		y = e.pageY;
//и заново получаем координаты изображения
		var l = image.offsetLeft;
		var t = image.offsetTop;
//здесь получаем коэффициент разницы между изображением и блоком обрезки 
		var ll = 100 - Number(l);
		var tt = 100 - Number(t);
//и наконец задаем стили и вписываем в форму полученные данные		
		image.style.marginLeft = x - lleft;
		image.style.marginTop = y - ttop;		
		var wid = image.width;
		var heig = image.height;
		x1.setAttribute('value', ll);
		y1.setAttribute('value', tt);
		w.setAttribute('value', wid);
		h.setAttribute('value', heig);
	}
	}

//при срабатывании события mouseup очищаем функцию mousemove
	a.onmouseup = function(){
	a.onmousemove = function(){}
	}
return	
}

Как видите здесь все очень просто, что касается получения координат обрезки и изменения масштаба.
Далее нужно сделать все то же, но уже с методами touchstart и touchmove для мобильных устройств:

a.ontouchstart = function(e){	
//начинаем функцию если пальец или пальцы коснулись сенсора
//если количество касаний равняется двум, то вызываем функцию изменения масштаба scal(), которую рассмотрим позже
	if(e.targetTouches.length == 2){
		scal(e);
	}
//если касание одно, то начинаем функцию перемещения изображения	
	if(e.targetTouches.length == 1){
//каждое касание записывается в массив targetTouches и так, как у нас касание одно
//мы берем первое касание, указывая [0], то есть как в случае с курсором (он всего один)
//мы вместо e (в функциях mousedown и mousemowe нам достаточно было одной переменной)
//используем e с методом targetTouches первого касания [0] и прописываем это все
//в переменную handl
	var handl = e.targetTouches[0];
	var x = handl.pageX;
	var y = handl.pageY;
	var lleft = image.offsetLeft;
	var ttop = image.offsetTop;
	var lleft = x - lleft;
	var ttop = y - ttop;
    a.ontouchmove = function(e) {
		var handle = e.targetTouches[0]
	    x = handle.pageX;
		y = handle.pageY;
		var l = image.offsetLeft;
		var t = image.offsetTop;
		var ll = 100 - Number(l);
		var tt = 100 - Number(t);
		
		image.style.marginLeft = x - lleft;
		image.style.marginTop = y - ttop;
		a.ontouchend = function(){
		var wid = image.width;
		var heig = image.height;
		x1.setAttribute('value', ll);
		y1.setAttribute('value', tt);
		w.setAttribute('value', wid);
		h.setAttribute('value', heig);
		}
		}		
	}
	return
}

То есть, в принципе, функции одинаковы, за исключением что при работе с сенсором нам доступно использование более одного «курсора».

Дальше чуть сложней. Если касаний все-таки 2, то нужно прописать функцию изменения масштаба, а там есть свои подводные камни. Во-первых при увеличении/уменьшении нужно сделать так, чтоб менялся масштаб изображения не от левого верхнего угла, а от центра. Во-вторых, коэффициент всегда должен быть положительным числом и принимать значение «ноль» при срабатывании функции вычисления. И так поехали:

//объявляем функцию изменения масштаб на тач-устройствах
function scal(e){	
//прописываем в переменную touc массив с касаниями	
		var touc = e.targetTouches;
		var cur_w = image.offsetWidth;
//получаем первый коэффициент следующим образом	
		var koef = Math.sqrt(Math.pow((touc[0].clientX - touc[1].clientX), 2) + Math.pow((touc[0].clientY - touc[1].clientY), 2));
//начинаем функцию после события touchmove и начинаем вычислять новый коэффициент
			a.ontouchmove = function(e){
				var tou = e.targetTouches;
				var relis = Math.sqrt(Math.pow((tou[0].clientX - tou[1].clientX), 2) + Math.pow((tou[0].clientY - tou[1].clientY), 2));						
				var im = image.offsetWidth;
//задаем размер изображения разницу коэффициентов
				image.style.width = cur_w + relis*2 - koef*2  + 'px';
			}
//при срабатывании события touchend прописываем изменения
			a.ontouchend = function(e){		
		var wid = image.width;
		var heig = image.height;
		var ll = 100 - image.offsetLeft;
		var tt = 100 - image.offsetTop;
		x1.setAttribute('value', ll);
		y1.setAttribute('value', tt);
		w.setAttribute('value', wid);
		h.setAttribute('value', heig);
			}
		
}

Осталось добавить изменения масштаба при прокрутке колеса мыши на десктопе:

//здесь все предельно просто
a.onwheel = function(e){
	var t = e.deltaY/5;
	var d = image.offsetWidth;	
	image.style.width = d-t;
		var wid = image.width;
		var heig = image.height;
		var ll = 100 - image.offsetLeft;
		var tt = 100 - image.offsetTop;
		x1.setAttribute('value', ll);
		y1.setAttribute('value', tt);
		w.setAttribute('value', wid);
		h.setAttribute('value', heig);
	return
}


Обработчик полученного изображения каждый выбирает сам, лично я использую следующий скрипт:

<?
if(isset($_POST['crop'])){
$filename = 'img.jpg';
$new_filen = 'ava/img.jpg';
list($current_width, $current_height) = getimagesize($filename);

$x1 = $_POST['x'];

$y1 = $_POST['y'];

$w = $_POST['w'];

$h = $_POST['h'];  

$new = imagecreatetruecolor($w, $h);
$current_image = imagecreatefromjpeg($filename);
imagecopyresampled($new, $current_image, 0, 0, 0, 0, $w, $h, $current_width, $current_height);
$final = imagecreatetruecolor(200, 200);
imagecopy($final, $new, 0, 0, $x1, $y1, 200, 200);
$fin_creat = imagejpeg($final, $new_filen, 100);
$handle = fopen($new_filen, "r");
unlink($filename );
}

?>

Спасибо за внимание!
Поделиться с друзьями
-->

Комментарии (24)


  1. bromzh
    01.11.2016 22:13
    +1

    Зачем сервер, если есть canvas и всё можно делать в браузере?
    http://camanjs.com/examples/
    https://mattketmo.github.io/darkroomjs/


    1. jwwiskey
      02.11.2016 00:13
      -1

      Никто не спорит, что canvas — очень хорошая версия. Но она куда сложнее и поэтому подойдет не каждому.


      1. bromzh
        02.11.2016 01:23
        +1

        Чем сложнее? Вы всё равно используете внешние функции для манипуляции с изображениями (пусть и в стандартной поставке PHP). С таким же успехом можно подключить готовую JS-библиотеку для обработки картинок и добавить UI. Впрочем, работа с изображениями напрямую довольно проста — достаточно знать немного линейной алгебры и пару алгоритмов.


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


        1. jwwiskey
          02.11.2016 02:50
          -1

          Если Вы заметили, то я описывал функции с уже имеющимся изображением. У меня на сайте сначала происходит добавление изображения, которое пользователь может загружать хоть терабайтовым, которое урезается до нужных мне размеров, а после попадает под эти самые функции. И если, как я упомянул, мы имеем дело с добавлением аватара для пользователя, значит речь идет о базе данных, где хранятся эти самые пользователи. А зачастую с базой работают посредством php скриптом. Повторюсь «зачастую». Никто не говорит что этот метод единственный. А библиотеками я не привык пользоваться, поскольку в них имеется масса ненужного.


          1. bromzh
            02.11.2016 11:45
            +4

            А библиотеками я не привык пользоваться, поскольку в них имеется масса ненужного.

            Ага, и поэтому тащите jquery ради одной её функции ajax.


            1. jwwiskey
              02.11.2016 12:26

              Я пользуюсь только jquery. Помимо ajax (который немаловажный) я для других целей использую еще несколько методов. Именно потому что я использую jquery не пытаюсь перегружать еще каким-то библиотеками. Это сугубо мое мнение и не является конечным для читателей.


              1. bromzh
                02.11.2016 13:54
                +1

                Именно потому что я использую jquery не пытаюсь перегружать еще каким-то библиотеками.

                Экономите на трафике? Но вы же каждый раз пересылаете изображение туда-сюда. Я вот потыкал кнопку поворота 800 раз, и трафик с вашим сервером составил 160 Мб. С канвасом и обработкой в браузере он был бы около нуля.


                Если же боитесь тормозов, то стоит сперва заменить jquery на более быстрые аналоги.


                1. muxa_ru
                  02.11.2016 14:09
                  -1

                  А завтра у вас на сайте редизайн и аватарка станет не 100x100, а 200x200.

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

                  А Вы что будете делать? Просить людей перезалить картинки? Отресайзите старые и будете счастливы от размытости и многократного сжатия в jpeg?


                  1. bromzh
                    02.11.2016 14:25
                    +1

                    Мой посыл не в том, чтобы хранить только обработанные картинки, а в том, чтобы обработку эту делать в браузере средствами HTML5 (которые автор не использует), а на сервер посылать уже обработанную картинку и, возможно, доп. инфу про эту обработку. Оригинал можно хранить на своё усмотрение.


                    Смотрите, пользователь хочет обработать свою аватарку. Он случайно жмёт на кнопку поворота. В авторском варианте это сразу перезапишет аватарку. А так как всё в jpg, то часть информации потеряется. Пользователь попытается вернуть исходное положение, что в итоге приведёт к ещё одной перезаписи файла и ухудшению качества.


                    В моём варианте, пока пользователь не нажмёт кнопочку "отправить", изображение будет меняться только в браузере. Тут пользователь может поворачивать картинку сколько угодно, качество результата это не изменит. Если же изменения надо отменить, можно просто не отправлять их.


                    В итоге, в моём варианте 1000 поворотов картинки вызовет 0 операций с файловой системой на сервере, трафик с сервером составит 0 байт. В авторском варианте 1000 поворотов вызовет около 1000 операций записи и 1000 операции чтения на сервере, обменяется с сервером большим количеством трафика (1000 * размер картинки) и окончательно убьёт качество результата без возможности отмены этого.


                    1. muxa_ru
                      02.11.2016 15:00

                      Что Вы будете делать после того как произойдёт редизайн сайта и встанет потребность увеличить аватарки с 100x100 до 200x200?

                      У него на сервере будет исходник и последовательность действий- он перегенерирует аватарку.

                      У Вас на сервере будет конечные 100x100. Ваши действия?


                      1. bromzh
                        02.11.2016 15:11
                        +1

                        У Вас на сервере будет конечные 100x100. Ваши действия?

                        С чего вы так решили? У меня на сервере хранятся и оригиналы и обработанные картинки. Действия при редизайне аналогичные.


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


                        1. muxa_ru
                          02.11.2016 15:23

                          Разницы в том, что у Вас на сервер уходит КОНЕЧНЫЙ РЕЗУЛЬТАТ, а у него вся последовательность действий.

                          И если эту последовательность действий записать, то потом её можно воспроизвести с одной единственной записью в конце.


                          1. bromzh
                            02.11.2016 16:09
                            +3

                            Простым движением руки и парой строчек кода я могу записывать действия и отправлять их на сервер вместе с результатом.


                            Но на самом деле надо делать по-другому: отправлять аватар в непожатом виде, а уже на сервере генерить из него разные thumbnails.


    1. dom1n1k
      07.11.2016 13:06

      Так браузер же не даст просто так вынуть из canvas загруженную пользователем картинку по политикам безопасности.
      Все равно сервер нужен.


      1. bromzh
        07.11.2016 15:30

        Сервер в любом случае нужен, чтобы посылать туда результат. А про ограничения из-за политик не знаю, можно пример? Я помню, что делал кроп изображений в браузере через canvas, и никаких проблем не было.


        1. dom1n1k
          07.11.2016 18:07

          Можно погуглить по ошибке
          Uncaught SecurityError: Failed to execute 'getImageData'


      1. raveclassic
        07.11.2016 18:21

        Просто ее грузить в канвас нужно через FileAPI/FileReader и readAsDataURL


        1. sskozin
          19.02.2017 17:04
          +1

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


  1. raveclassic
    02.11.2016 02:24
    +2

    A стоит ли на мобильном устройстве заливать изображение через полудохлое соединение на сервер для переваривания PHP и последующего выкачивания обратно, когда все необходимые инструменты для работы есть на клиенте? Контент изображения с устройства можно получить, редактировать как угодно тоже можно, даже в фоновом потоке, можно даже с камеры картинку получить, зачем тут сервер?

    PS. Очень не хочу показаться брюзгой, но, пожалуйста, отформатируйте код.


    1. jwwiskey
      02.11.2016 02:53

      У меня все изображения пользователей хранятся на сервере и поэтому не использовать сервер не получится. Это не редактор картинок, а загрузка изображений на сервер для последующего использования не только для одного пользователя


      1. sumanai
        02.11.2016 12:11
        +1

        Только вот загрузка полноразмерной фотографии в 16мп и загрузка аватара 100х100- это две большие разности.


        1. jwwiskey
          02.11.2016 12:23

          Перед попаданием фотографии в окно изменения аватара у меня она проходит обработку и урезается до приемлемых размеров и после обработки ширина и высота ее составляет 200пикселей.


          1. sumanai
            02.11.2016 12:40

            Это уже в обратку. Для получения этого 200 пиксельного изображения вы заставляете пользователей грузить десяток мегабайт фотографии с его 100500 мегапиксельной камеры.
            И отсылка на сервер для поворота- перебор, можно повернуть в браузере при помощи CSS, а при финальной отправке просто учесть угол поворота. А то у вас после 4 поворотов получится не оригинальное изображение, а каша артефактов сжатия JPEG.


            1. bromzh
              02.11.2016 14:06
              +2

              А то у вас после 4 поворотов получится не оригинальное изображение, а каша артефактов сжатия JPEG.

              Ага. Вот что случается, если повернуть картинку много-много раз.


              Осторожно, шакалы

              image