Изменение размеров и обрезка изображений с помощью Canvas

demosourse

В этом уроке я собираюсь показать, как можно изменять размеры и делать обрезку изображений с помощью HTML5 элемента <canvas>. Ну, и раз уж мы заговорили о canvas, то позаботимся также и о удобных элементах управления размерами, которые можно увидеть в распространенных приложениях обработки изображений.

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

Для этого создаём HTML5 элемент <canvas>, размещаем на нём необходимое изображение, а затем вытаскиваем данные нового изображения в виде data-URI. Большинство браузеров хорошо поддерживают эту технологию, так что ею можно пользоваться, но надо просто быть в курсе некоторых ограничений, связанных с поддержкой браузеров, вроде качества и производительности.

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

Ну что ж, давайте начинать!

Разметка

В нашей демонстрации мы начнем с существующего изображения:

<img class="resize-image" src="image.jpg" alt="Image" />

И это всё! Это весь код, который необходим нам для демонстрации.

CSS

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

.resize-container {
    position: relative;
    display: inline-block;
    cursor: move;
    margin: 0 auto;
}

.resize-container img {
    display: block
}

.resize-container:hover img,
.resize-container:active img {
    outline: 2px dashed rgba(222,60,80,.9);
}

Следующим делом определим позиции и стили для элементов изменения размеров. Это небольшие квадратики по углам изображения, которые нужно тянуть.

.resize-handle-ne,
.resize-handle-ne,
.resize-handle-se,
.resize-handle-nw,
.resize-handle-sw {
    position: absolute;
    display: block;
    width: 10px;
    height: 10px;
    background: rgba(222,60,80,.9);
    z-index: 999;
}

.resize-handle-nw {
    top: -5px;
    left: -5px;
    cursor: nw-resize;
}

.resize-handle-sw {
    bottom: -5px;
    left: -5px;
    cursor: sw-resize;
}

.resize-handle-ne {
    top: -5px;
    right: -5px;
    cursor: ne-resize;
}

.resize-handle-se {
    bottom: -5px;
    right: -5px;
    cursor: se-resize;
}

JavaScript

Начнём с определения переменных, инициализации Canvas и результирующего изображения.

var resizeableImage = function(image_target) {
    var $container,
    orig_src = new Image(),
    image_target = $(image_target).get(0),
    event_state = {},
    constrain = false,
    min_width = 60,
    min_height = 60,
    max_width = 800,
    max_height = 900,
    resize_canvas = document.createElement('canvas');
});

resizeableImage($('.resize-image'));

Определим функцию init, которая вызывается сразу. Эта функция оборачивает изображение в контейнер, создаёт элементы манипуляции размером, и создаёт копию оригинального изображения. Также мы присваиваем объект jQuery элемента контейнера отдельной переменной, так чтобы мы могли обратиться к нему позже, и навешиваем обработчик события mousedown для отслеживания начала перетаскивания одного из манипуляторов размера.

var resizeableImage = function(image_target) {

// ...
    init = function(){

        // создаем новое изображение с копией оригинала
        // При изменении размера всегда используем оригинал в качестве основы
        orig_src.src=image_target.src;

        // добавляем манипуляторы размера
        $(image_target).wrap('<div class="resize-container"></div>')
        .before('<span class="resize-handle resize-handle-nw"></span>')
        .before('<span class="resize-handle resize-handle-ne"></span>')
        .after('<span class="resize-handle resize-handle-se"></span>')
        .after('<span class="resize-handle resize-handle-sw"></span>');

        // Получаем переменную для контейнера
        $container =  $(image_target).parent('.resize-container');

        // Добавляем обработчик события
        $container.on('mousedown', '.resize-handle', startResize);
    };

//...

    init();
}

Функции startResize и endResize только лишь указывают браузеру, когда необходимо обращать внимание на факт передвижения курсора, и когда прекращать обращать на него внимание.

startResize = function(e){
    e.preventDefault();
    e.stopPropagation();
    saveEventState(e);
    $(document).on('mousemove', resizing);
    $(document).on('mouseup', endResize);
};

endResize = function(e){
    e.preventDefault();
    $(document).off('mouseup touchend', endResize);
    $(document).off('mousemove touchmove', resizing);
};

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

saveEventState = function(e){
  // Сохраняем изначальные данные событий и состояние контейнера
  event_state.container_width = $container.width();
  event_state.container_height = $container.height();
  event_state.container_left = $container.offset().left;
  event_state.container_top = $container.offset().top;
  event_state.mouse_x = (e.clientX || e.pageX || e.originalEvent.touches[0].clientX) + $(window).scrollLeft();
  event_state.mouse_y = (e.clientY || e.pageY || e.originalEvent.touches[0].clientY) + $(window).scrollTop();

  // Это фикс для мобильного Safari
  // По некоторым причинам прямое копирование тач-свойств недоступно
  if(typeof e.originalEvent.touches !== 'undefined'){
    event_state.touches = [];
    $.each(e.originalEvent.touches, function(i, ob){
      event_state.touches[i] = {};
      event_state.touches[i].clientX = 0+ob.clientX;
      event_state.touches[i].clientY = 0+ob.clientY;
    });
  }
  event_state.evnt = e;
}

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

resizing = function(e){
    var mouse={},width,height,left,top,offset=$container.offset();
    mouse.x = (e.clientX || e.pageX || e.originalEvent.touches[0].clientX) + $(window).scrollLeft();
    mouse.y = (e.clientY || e.pageY || e.originalEvent.touches[0].clientY) + $(window).scrollTop();

    width = mouse.x - event_state.container_left;
    height = mouse.y  - event_state.container_top;
    left = event_state.container_left;
    top = event_state.container_top;

    if(constrain || e.shiftKey){
        height = width / orig_src.width * orig_src.height;
    }

    if(width > min_width && height > min_height && width < max_width && height < max_height){
      resizeImage(width, height);
      // Без этого Firefox не будет пересчитывать размеры изображения до завершения перетаскивания
      $container.offset({'left': left, 'top': top});
    }
}

Далее мы добавим возможность сохранять пропорции окна изменения размера при зажатой клавише Shift, или при определённом значении переменной.

И, наконец, мы изменим размер изображения, но только если новая ширина и высота не выходит за границы переменных min и max, которые мы задали в начале.

Замечание: так как мы действительно изменяем размер изображения, а не только изменяем его атрибуты ширины и высоты, стоит задуматься о том, как часто вызывается функция resizeImage, для того, чтобы улучшить производительность.

Непосредственно изменение размеров изображения

Отрисовка изображения на canvas очень проста, и сводится к использованию метода drawImage. Первым делом мы устанавливаем ширину и высоту полотна, и всегда используем оригинальную копию изображения. Затем у canvas-а используем метод toDataURL, чтобы получить base64-кодированную строку, представляющую измененное изображение, и размещаем его на странице.

Полное описание всех параметров, которые можно использовать с методом drawImage можно найти в разделе “Обрезка” данного урока.

resizeImage = function(width, height){
    resize_canvas.width = width;
    resize_canvas.height = height;
    resize_canvas.getContext('2d').drawImage(orig_src, 0, 0, width, height);
    $(image_target).attr('src', resize_canvas.toDataURL("image/png"));
};

Слишком просто? Тут есть маленькая оговорка: изображение должно находиться на том же домене, что и страница, или на сервере должно быть включено Междоменное разделение ресурсов. Если эти условия не соблюдаются - можно столкнуться с проблемами.

Изменение размера из разных углов

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

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

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

Мы не можем изменить это стандартное поведение, но при изменении размеров из любого угла, отличного от правого нижнего, можно изменять позиционирование всего изображения так, что будет казаться, что неподвижными остаются противоположные углы и грани. Давайте дополним код функции resizing:

resizing = function(e){
  var mouse={},width,height,left,top,offset=$container.offset();
  mouse.x = (e.clientX || e.pageX || e.originalEvent.touches[0].clientX) + $(window).scrollLeft();
  mouse.y = (e.clientY || e.pageY || e.originalEvent.touches[0].clientY) + $(window).scrollTop();

  // По-разному размещаем изображение, в зависимости от перемещаемого угла и границ холста
  if( $(event_state.evnt.target).hasClass('resize-handle-se') ){
    width = mouse.x - event_state.container_left;
    height = mouse.y  - event_state.container_top;
    left = event_state.container_left;
    top = event_state.container_top;
  } else if($(event_state.evnt.target).hasClass('resize-handle-sw') ){
    width = event_state.container_width - (mouse.x - event_state.container_left);
    height = mouse.y  - event_state.container_top;
    left = mouse.x;
    top = event_state.container_top;
  } else if($(event_state.evnt.target).hasClass('resize-handle-nw') ){
    width = event_state.container_width - (mouse.x - event_state.container_left);
    height = event_state.container_height - (mouse.y - event_state.container_top);
    left = mouse.x;
    top = mouse.y;
    if(constrain || e.shiftKey){
      top = mouse.y - ((width / orig_src.width * orig_src.height) - height);
    }
  } else if($(event_state.evnt.target).hasClass('resize-handle-ne') ){
    width = mouse.x - event_state.container_left;
    height = event_state.container_height - (mouse.y - event_state.container_top);
    left = event_state.container_left;
    top = mouse.y;
    if(constrain || e.shiftKey){
      top = mouse.y - ((width / orig_src.width * orig_src.height) - height);
    }
  }

  // При желаниии - сохраняем соотношение сторон
  if(constrain || e.shiftKey){
    height = width / orig_src.width * orig_src.height;
  }

  if(width > min_width && height > min_height && width < max_width && height < max_height){
    // Для улучшения производительности частоту вызова функции resizeImage() необходимо ограничивать
    resizeImage(width, height);
    // Без этого Firefox не будет пересчитывать размеры изображения до завершения перетаскивания
    $container.offset({'left': left, 'top': top});
  }
}

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

Перемещаем изображение

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

init = function(){

    //...

    $container.on('mousedown', 'img', startMoving);
}

Теперь добавим функции startMoving и endMoving, сходные с функциями startResize и endResize.

startMoving = function(e){
    e.preventDefault();
    e.stopPropagation();
    saveEventState(e);
    $(document).on('mousemove', moving);
    $(document).on('mouseup', endMoving);
};

endMoving = function(e){
    e.preventDefault();
    $(document).off('mouseup', endMoving);
    $(document).off('mousemove', moving);
};

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

moving = function(e){
    var  mouse={};
    e.preventDefault();
    e.stopPropagation();
    mouse.x = (e.clientX || e.pageX) + $(window).scrollLeft();
    mouse.y = (e.clientY || e.pageY) + $(window).scrollTop();
    $container.offset({
        'left': mouse.x - ( event_state.mouse_x - event_state.container_left ),
        'top': mouse.y - ( event_state.mouse_y - event_state.container_top )
    });
};

Обрезка изображения

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

Добавим следующий HTML-код:

<div class="overlay">
    <div class="overlay-inner">
    </div>
</div>
<button class="btn-crop js-crop">Crop</button>

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

.overlay {
    position: absolute;
    left: 50%;
    top: 50%;
    margin-left: -100px;
    margin-top: -100px;
    z-index: 999;
    width: 200px;
    height: 200px;
    border: solid 2px rgba(222,60,80,.9);
    box-sizing: content-box;
    pointer-events: none;
}

.overlay:after,
.overlay:before {
    content: '';
    position: absolute;
    display: block;
    width: 204px;
    height: 40px;
    border-left: dashed 2px rgba(222,60,80,.9);
    border-right: dashed 2px rgba(222,60,80,.9);
}

.overlay:before {
    top: 0;
    margin-left: -2px;
    margin-top: -40px;
}

.overlay:after {
    bottom: 0;
    margin-left: -2px;
    margin-bottom: -40px;
}

.overlay-inner:after,
.overlay-inner:before {
    content: '';
    position: absolute;
    display: block;
    width: 40px;
    height: 204px;
    border-top: dashed 2px rgba(222,60,80,.9);
    border-bottom: dashed 2px rgba(222,60,80,.9);
}

.overlay-inner:before {
    left: 0;
    margin-left: -40px;
    margin-top: -2px;
}

.overlay-inner:after {
    right: 0;
    margin-right: -40px;
    margin-top: -2px;
}

.btn-crop {
    position: absolute;
    vertical-align: bottom;
    right: 5px;
    bottom: 5px;
    padding: 6px 10px;
    z-index: 999;
    background-color: rgb(222,60,80);
    border: none;
    border-radius: 5px;
    color: #FFF;
}

Дополним JavaScript следующей функцией и обработчиком события:

init = function(){

    //...

    $('.js-crop').on('click', crop);

};

crop = function(){
    var crop_canvas,
        left = $('.overlay').offset().left - $container.offset().left,
        top =  $('.overlay').offset().top - $container.offset().top,
        width = $('.overlay').width(),
        height = $('.overlay').height();

    crop_canvas = document.createElement('canvas');
    crop_canvas.width = width;
    crop_canvas.height = height;

    crop_canvas.getContext('2d').drawImage(image_target, left, top, width, height, 0, 0, width, height);
    window.open(crop_canvas.toDataURL("image/png"));
}

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

Для обрезки метод drawImage требует девяти параметров. Первый - изображение-источник. Следующие четыре параметра определяют, какая часть изображения будет использоваться (прямоугольник обрезки). Последние четыре параметра указывают, в каком месте холста начать отрисовывать изображение, и какого оно будет размера.

Добавляем тач-события и распознавание жестов

Мы добавили события для работы с мышью, теперь давайте добавим поддержку для устройств с сенсорным вводом.

Для событий mousedown и mouseup есть эквивалентные тач-события: touchstart и touchend; а для mousemove есть эквивалентное событие touchmove. Кому-то явно недостаёт чувства юмора, так как эти события можно легко было назвать “touchdown” и “touchup”.

Добавим touchstart и touchend везде, где у нас были определены обработчики событий mousedown и mouseup, а событие touchmove везде, где у нас есть mousemove.

// В функции init()...
$container.on('mousedown touchstart', '.resize-handle', startResize);
$container.on('mousedown touchstart', 'img', startMoving);

//В startResize() ...
$(document).on('mousemove touchmove', moving);
$(document).on('mouseup touchend', endMoving);

//В endResize()...
$(document).off('mouseup touchend', endMoving);
$(document).off('mousemove touchmove', moving);

//В  startMoving()...
$(document).on('mousemove touchmove', moving);
$(document).on('mouseup touchend', endMoving);

//В endMoving()...
$(document).off('mouseup touchend', endMoving);
$(document).off('mousemove touchmove', moving);

Так как мы изменяем размер изображения, то разумно будет предположить, что некоторые пользователи будут пробовать использовать распространённые жесты, вроде щипка. Существует библиотека под названием Hammer, которая значительно упрощает работу с жестами. Но так как нам нужен только щипок, то для нас она будет чересчур громоздкой. Давайте я покажу вам, как можно простым способом определить щипок без каких бы то ни было библиотек.

Возможно вы обратили внимание, что в функции saveEventState мы сохранили начальные данные жестов. Теперь мы ими воспользуемся.

Сначала проверим, что событие содержит два касания, и замерим расстояние между ними. Запомним это расстояние как изначальное, а потом будем постоянно замерять, насколько оно изменяется в процессе перемещения. Дополним функцию moving:

moving = function(e){
  var  mouse={}, touches;
  e.preventDefault();
  e.stopPropagation();

  touches = e.originalEvent.touches;
  mouse.x = (e.clientX || e.pageX || touches[0].clientX) + $(window).scrollLeft();
  mouse.y = (e.clientY || e.pageY || touches[0].clientY) + $(window).scrollTop();
  $container.offset({
    'left': mouse.x - ( event_state.mouse_x - event_state.container_left ),
    'top': mouse.y - ( event_state.mouse_y - event_state.container_top )
  });
  // Следим за щипком в процессе перемещения
  if(event_state.touches && event_state.touches.length > 1 && touches.length > 1){
    var width = event_state.container_width, height = event_state.container_height;
    var a = event_state.touches[0].clientX - event_state.touches[1].clientX;
    a = a * a;
    var b = event_state.touches[0].clientY - event_state.touches[1].clientY;
    b = b * b;
    var dist1 = Math.sqrt( a + b );

    a = e.originalEvent.touches[0].clientX - touches[1].clientX;
    a = a * a;
    b = e.originalEvent.touches[0].clientY - touches[1].clientY;
    b = b * b;
    var dist2 = Math.sqrt( a + b );

    var ratio = dist2 /dist1;

    width = width * ratio;
    height = height * ratio;
    // Для улучшения производительности частоту вызова функции resizeImage() необходимо ограничивать
    resizeImage(width, height);
  }
};

Делим текущее расстояние на изначальное, чтобы получить направление и коэффициент масштабирования изображения.

Ну вот и всё. Взгляните на демонстрационный пример, или скачайте архив.

В моих тестах Chrome предотвращал дефолтное поведение браузера при щипке (изменить масштаб страницы), а Firefox - нет.

Надеюсь, вы нашли этот урок полезным для себя. Я советую ознакомиться с техникой drag-n-drop, а также техниками загрузки файлов. Предлагаю заглянуть чуть дальше, и посмотреть, как люди комбинируют эти техники для создания прекрасных пользовательских интерфейсов.

Данный урок подготовлен для вас командой сайта ruseller.com
Источник урока: http://tympanus.net/codrops/2014/10/30/resizing-cropping-images-canvas/
Перевел: Станислав Протасевич
Урок создан: 24 Ноября 2014
Просмотров: 18846
Правила перепечатки


5 последних уроков рубрики "jQuery"

^ Наверх ^