Pull to refresh

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

HTML *Image processing *Canvas *
Translation
Tutorial
Original author: Mike Riethmuller


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

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

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

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

С этой целью мы начинаем работу!

Разметка


В нашем демо мы будем работать с одним заданным изображением:
<img class="resize-image" src="image.jpg" alt="Image" />


Все! Другого HTML нам не нужно.

CSS


CSS-код тоже будет не очень большим. Определим стили для resize-container и самого изображения.

.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 handles’. Это небольшие квадратики, находящиеся в углах изображений, которые мы перетаскиваем для изменения размеров картинки.

.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'));


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

var resizeableImage = function(image_target) {
 
// ...
    init = function(){
 
        // Create a new image with a copy of the original src
        // When resizing, we will always use this original copy as the base
        orig_src.src=image_target.src;
 
        // Add resize handles
        $(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>');
 
        // Get a variable for the container
        $container =  $(image_target).parent('.resize-container');
 
        // Add events
        $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){
  // Save the initial event details and container state
  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();
 
  // This is a fix for mobile safari
  // For some reason it does not allow a direct copy of the touches property
  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); 
      // Without this Firefox will not re-calculate the the image dimensions until drag end
      $container.offset({'left': left, 'top': top});       
    }
}


Затем добавим опцию для ограничения размеров изображения с помощью клавиши Shift или переменной.

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

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

Новые размеры изображения


Рисовать изображения в Canvas так же просто, как и drawImage. Мы задаем высоту и длину картинки, а затем предоставляем оригинал. Также используем toDataURL для получения Base64-encoded версии результата операции.

Здесь приведены все объяснения доступных для данной операции параметров.

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")); 
};


Слишком просто? Есть одна оговорка: изображение должно быть размещено на одном домене с нашей страницей, либо на сервере должна быть активирована функция CORS. Если это не так, то у Вас возникнут проблемы с ‘tainted canvas’.

Увеличение через другие вершины



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

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



Мы можем поменять код так, чтобы при растягивании картинки за любой угол она менялась. Давайте обновим функцию 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();
   
  // Position image differently depending on the corner dragged and constraints
  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);
    }
  }
 
  // Optionally maintain aspect ratio
  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){
    // To improve performance you might limit how often resizeImage() is called
    resizeImage(width, height); 
    // Without this Firefox will not re-calculate the the image dimensions until drag end
    $container.offset({'left': left, 'top': top});
  }
}


Теперь мы проверяем, какой из resize-handle был задействован, и применяем необходимые изменения.

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


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

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 в canvas. Первый - исходное изображение. Следующие четыре - место, задействованное под операцию. Еще четыре - координаты места, в котором стоит начать рисование в canvas, и какого размера будет изображение.

Добавление прикосновений и распознавания жестов


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

Для mousedown и mouseup есть эквивалентные операторы - touchstart и touchend, а для mousemove есть touchmove. Нужно быть внимательным, чтобы не перепутать их с touchup и touchdown (А то будет смешно).

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

// In init()...
$container.on('mousedown touchstart', '.resize-handle', startResize);
$container.on('mousedown touchstart', 'img', startMoving);
 
//In startResize() ...
$(document).on('mousemove touchmove', moving);
$(document).on('mouseup touchend', endMoving);
 
//In endResize()...
$(document).off('mouseup touchend', endMoving);
$(document).off('mousemove touchmove', moving);
 
//In  startMoving()...
$(document).on('mousemove touchmove', moving);
$(document).on('mouseup touchend', endMoving);
 
//In 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 )
  });
  // Watch for pinch zoom gesture while moving
  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;
    // To improve performance you might limit how often resizeImage() is called
    resizeImage(width, height);
  }
};


На основе этих данных мы уменьшаем или увеличиваем наше изображение и корректируем его высоту и длину.



На этом все. Вы можете открыть демо или скачать исходный код.

В моих тестах Chrome лучше справился с обработкой жестов и движением "сжатия", а Firefox на него не среагировал.

Надеюсь, Вам понравилась моя статья.
Tags:
Hubs:
Total votes 29: ↑23 and ↓6 +17
Views 32K
Comments Comments 8