Cейчас все больше вычислений стараются производить на стороне клиента, чтобы сократить нагрузку на сервер и увеличить производительность веб-приложения в целом. Я уже писал о сжатии изображений на стороне клиента, до его отправки на сервер; сегодня же речь пойдет о кадрировании изображения (инструмент crop) средствами HTML5 canvas.
Очень часто, регистрируясь на каком-либо сервисе, он подтягивает фото из соц. сети и предлагает выбрать необходимый фрагмент для использования его как аватарки. Реализуют пободный функционал обычно на HTML5 canvas, и в этот раз мы будем использовать также библиотеку fabricjs. Это отличный инструмент, который включает объектную модель для работы с canvas и ряд других инструментов, о которых вы можете почитать на официальном сайте проекта.
Работая в Adobe Photoshop, мы привыкли, что при кадрировании появляется рамка, а все, что вне ее, затемняется. Также в последних версиях появились направляющие линии, которые помогают выровнять объект. Предлагаю нечто подобное реализовать и для нашего кроупа: блоки, затемняющие все, кроме выделенной области и вспомогательные линии, которые по горизонтали и вертикали делят выделенную область на 3 равные части.
Для выделения желаемой области будем использовать скрытый прямоугольный объект, при изменении размеров которого будем пересчитывать параметры всех вспомогательных элементов - блоков затемнения и направляющих линий.
И перейдем непосредственно к реализации:
function init_image_helper(iw, ih, img){ var bgcolor = '#000000', // Цвет затемнения opacity = 0.7, // Прозрачность затемнения bordercolor = 'rgba(155,155,155,0.8)', // Цвет рамок cornerColor = 'rgba(155,155,155,0.6)', // Фон управляющих точек cornerStrokeColor = 'rgba(100,100,100,1)', // Цвет рамок управляющих точек cornerSize = 10, // Размер управляющих очек showHelpers = true; // Показывать вспомогательные линии var src = img.attr('src'); img.wrap('<div class="crop_wrap" />'); // Создаем вспомогательные элементы var crop_area = img.parent(); crop_area.html( '<div class="crop_controlls">'+ '<button class="crop_ok">Готово</button> '+ '<button class="crop_cancel">Отмена</button>'+ '</div>'+ '<canvas id="c"></canvas>'); var cw = iw; var ch = ih; // Создаем холст var canvas = new fabric.Canvas('c', { width: cw, height: ch, backgroundColor: "#ffffff" }); canvas.uniScaleTransform = true; // Добавляем изображение на холст var imgInstance; var fimg = fabric.Image.fromURL(src, function(imgInstance) { imgInstance.set({ left: 0, top: 0, width: iw, height: ih, selectable: false, evented: false, lockMovementX: true, lockMovementY: true, lockRotation: true, lockScalingX: true, lockScalingY: true, lockUniScaling: true, hasControls: false, hasBorders: false }); canvas.add(imgInstance); canvas.sendToBack(imgInstance); }); var crop_on = true; var crop2 = new fabric.Rect({ left: Math.ceil(cw/4), top: Math.ceil(ch/4), fill: '#ffffff', width: Math.ceil(cw/2), height: Math.ceil(ch/2), hasRotatingPoint: false, opacity:0, strokeWidth: 0, transparentCorners: false, borderColor: bordercolor, borderDashArray: [5, 5], cornerColor: cornerColor, cornerStrokeColor: cornerStrokeColor, cornerSize: cornerSize }); var p = crop2; var x1 = Math.ceil(p.left); var x2 = Math.ceil(p.left+p.width); var y1 = Math.ceil(p.top); var y2 = Math.ceil(p.top+p.height); // Прямоугольники затемнения var crop_l = makecrop({left: 0, top: 0, width: x1, height: ch}), crop_r = makecrop({left: x2, top: 0, width: cw-x2, height: ch}), crop_t = makecrop({left: x1, top: 0, width: x2-x1, height: y1}), crop_b = makecrop({left: x1, top: y2, width:x2-x1, height: ch-y2}); // Вспомогательные линии if (showHelpers){ var crop_help_1 = make_line({x1: Math.ceil((x2-x1)/3)+x1, y1: y1, x2: Math.ceil((x2-x1)/3)+x1, y2: y2}), crop_help_2 = make_line({x1: Math.ceil((x2-x1)/3*2)+x1, y1: y1, x2: Math.ceil((x2-x1)/3*2)+x1, y2: y2}), crop_help_3 = make_line({x1: x1, y1: Math.ceil((y2-y1)/3)+y1, x2: x2, y2: Math.ceil((y2-y1)/3)+y1 }); crop_help_4 = make_line({x1: x1, y1: Math.ceil((y2-y1)/3*2)+y1, x2: x2, y2: Math.ceil((y2-y1)/3*2)+y1 }); } // helper_block; $('.crop_controlls').css({ 'top': y2-$('.crop_controlls').outerHeight()-10+'px', 'left': x2-$('.crop_controlls').outerWidth()-10+'px' }); // Добавляем объекты на холст canvas.add(crop2); canvas.add(crop_l); canvas.add(crop_r); canvas.add(crop_t); canvas.add(crop_b); if (showHelpers){ canvas.add(crop_help_1); canvas.add(crop_help_2); canvas.add(crop_help_3); canvas.add(crop_help_4); } canvas.setActiveObject(crop2); canvas.on({ 'object:moving': onCroping, 'object:scaling': onCroping }); // Пересчитываем при изменении области function onCroping(e){ var p = e.target, wi = Math.ceil(p.width * p.scaleX), hi = Math.ceil(p.height * p.scaleY); if (p.top < 0){ p.set({ 'top': 0}); } if (p.left < 0){ p.set({ 'left': 0}); } if (wi+p.left > cw){ p.set({ 'left': cw-wi}); } if (hi+p.top > ch){ p.set({ 'top': ch-hi}); } if (wi > cw){ p.set({ 'scaleX': 1, 'width': cw, 'left': 0}); } if (hi > ch){ p.set({ 'scaleY': 1, 'height': ch, 'top': 0}); } wi = Math.ceil(p.width * p.scaleX), hi = Math.ceil(p.height * p.scaleY); var x1 = Math.ceil(p.left); var x2 = Math.ceil(p.left+wi); var y1 = Math.ceil(p.top); var y2 = Math.ceil(p.top+hi); crop_l.set({ 'width': x1}); crop_r.set({ 'left': x2, 'width': cw-x2}); crop_t.set({ 'left': x1, 'width': x2-x1, 'height': y1}); crop_b.set({ 'left': x1, 'width': x2-x1, 'top': y2, 'height': ch-y2}); if (showHelpers){ crop_help_1.set({x1: Math.ceil((x2-x1)/3)+x1, y1: y1, x2: Math.ceil((x2-x1)/3)+x1, y2: y2}), crop_help_2.set({x1: Math.ceil((x2-x1)/3*2)+x1, y1: y1, x2: Math.ceil((x2-x1)/3*2)+x1, y2: y2}), crop_help_3.set({x1: x1, y1: Math.ceil((y2-y1)/3)+y1, x2: x2, y2: Math.ceil((y2-y1)/3)+y1 }); crop_help_4.set({x1: x1, y1: Math.ceil((y2-y1)/3*2)+y1, x2: x2, y2: Math.ceil((y2-y1)/3*2)+y1 }); } canvas.renderAll(); // helper_block; $('.crop_controlls').css({ 'top': y2-$('.crop_controlls').outerHeight()-10+'px', 'left': x2-$('.crop_controlls').outerWidth()-10+'px' }); } function calc_crop(p){ } // Обрезаем function makecrop(coords) { return new fabric.Rect({ left: coords.left, top: coords.top, fill: bgcolor, width: coords.width, height: coords.height, selectable: false, evented: false, lockMovementX: true, lockMovementY: true, lockRotation: true, lockScalingX: true, lockScalingY: true, lockUniScaling: true, hasControls: false, hasBorders: false, opacity: opacity, strokeWidth: 0, hasBorder: false }); } // Генерируем вспомогательную линию function make_line(coords){ return new fabric.Line([coords.x1, coords.y1, coords.x2, coords.y2], { selectable: false, evented: false, lockMovementX: true, lockMovementY: true, lockRotation: true, lockScalingX: true, lockScalingY: true, lockUniScaling: true, hasControls: false, strokeDashArray: [5,5], stroke: bordercolor }); } // применить $('.crop_ok').on('click', function(){ if(crop_on){ var x = crop2.left, y = crop2.top, width = crop2.width * crop2.scaleX, height = crop2.height * crop2.scaleY; canvas.remove(crop_t, crop_b, crop_r, crop_l, crop2); if (showHelpers){ canvas.remove(crop_help_1, crop_help_2, crop_help_3, crop_help_4); } } canvas.deactivateAll(); if (!fabric.Canvas.supports('toDataURL')) { alert('This browser doesn\'t provide means to serialize canvas to an image'); }else{ var resultimg = canvas.toDataURL({ format: 'jpeg', left: x, top: y, width: width, height: height }); crop_area.html('<img src="'+resultimg+'" alt="" class="cropd" />'); } return false; }); // Отменить $('.crop_cancel').on('click', function(){ crop_area.html('<img src="'+src+'" alt="" />'); return false; }); }