falbar Масштабирование и изменение размеров картинок с помощью canvas

Масштабирование и изменение размеров картинок с помощью canvas

28 июля 2018 Перевод Туториал 766 0

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

Реклама

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

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

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

На конечный результат можно посмотреть, перейдя на страницу «демо», ссылка на которую находиться в конце статье и там найти ссылку со всем кодом.

Начнем же наш не простой путь!

Немного верстки для нашего урока

Мы начнем с верстки исходного изображения, которое будет подвергаться обработке:

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

Все! Для этого инструмента HTML больше не понадобится.

Добавим чуть-чуть 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, 0.9);
}

Теперь определим координаты и свойства элементов resize-handle. Это маленькие квадратики, при помощи которых пользователь будет увеличивать или уменьшать изображение:

.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, 0.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

При работе с 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();
}

Единственная задача написанных на JavaScript методов 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».

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

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

Меняем размеры изображения JavaScript

Переместить изображение на холст можно при помощи drawImage. Сначала нужно установить высоту и ширину холста, пользуясь командами JavaScript resize, а затем воспользоваться только исходной копией изображения. К холсту применяется команда toDataURL, благодаря чему мы получаем закодированную в Base64 новую версию фото, которая помещается на страницу.

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

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

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

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

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

izmenenie-razmera-kartinki-s-raznyh-uglov

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

Нельзя изменить то, что задано по умолчанию, но можно схитрить. Когда пользователь потянет за размерную ручку, находящуюся не в правом нижнем углу, будет меняться положение всего изображения таким образом, чтобы казалось, что противоположный угол и прилежащие к нему стороны остаются на своих местах. Для этого нужно обновить метод 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);
        }
    }

    // Сохраняем пропорции при нажатой клавише shift
    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});
    }
}

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

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

Из-за того, что размер фото меняется с любого угла, картинка может непроизвольно переместиться. Нужно позволить пользователям поместить картинку обратно в центр «рамки». Добавим в 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)
    });
};

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

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

Пропишем следующий HTML:

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

Очень важны атрибуты блока overlay: положение, высота и ширина. Они нужны для того, чтобы понять, какая часть картинки обрезается. Необходимо также учесть, что «рамка» должна быть заметна на фоне любого цвета. Для этого нужно создать полупрозрачную обводку вокруг «рамки».

.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(), только в этом случае мы не передаем данные о ширине и высоте: она сама их «возьмет» из элемента overlay.

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

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

Созданный инструмент умеет получать события от мыши. Теперь адаптируем ее для сенсорных устройств.

Mousedown и mouseup соответствуют touchstart и touchend (они выполняют те же функции, только получают данные о касаниях, а не о перемещении курсора), а mousemove – touchmove.

Заменим все mousedown, mouseup и 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() мы храним исходную информацию о касании. Теперь она пригодится.

Сначала определим, содержит ли событие 2-ва прикосновения, и измерим расстояние от одного до другого. Это наш «исходник». Затем программа должна постоянно отслеживать, как меняется расстояние между этими касаниями. Обновим код функции 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 не дал браузеру отреагировать на масштабирование двумя пальцами так, как по умолчанию (этот жест используется для увеличения или уменьшения веб-страницы).

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

Реклама
Комментариев еще не оставлено
no_avatar