Image cropping component for vue.js
A Pen by Edward Lance Lorilla on CodePen.
<template id="vueCropper"> | |
<div> | |
<input type="file" @change="selectFile" /> | |
<div v-if="cropping" | |
ref="cropperdiv" | |
:style="cropperDivStyle"> | |
<canvas ref="canvas" | |
:width="canvasWidth" | |
:height="canvasHeight" | |
@mousemove="moveMouse" | |
@mousedown="startDrag" | |
@mouseup="stopDrag" | |
@dragover="stopDrag"> | |
</canvas> | |
</div> | |
<img v-bind:src="croppedImage" :style="roundCorners" /> | |
</div> | |
</template> | |
<div id="app"> | |
<h1>Vue.js Image Cropper Component</h1> | |
<vue-cropper></vue-cropper> | |
</div> |
Vue.component('vueCropper', { | |
template: '#vueCropper', | |
data: function () { | |
return { | |
cropperDivWidth: '100%', //this can be anything passed to css width | |
cropperDivHeight: false, //this should be either false or number (in pixels) | |
cropperDivMaxHeight: 600, //this should be either false or number (in pixels). Must be number if cropperDivHeight is false | |
cropping: false, | |
image: false, | |
imageWidth: 0, | |
imageHeight: 0, | |
canvasWidth: 300, | |
canvasHeight: 150, | |
ctx: false, | |
mainStroke: 'rgba(255,255,255,0.99)', //color of the line around cropping area. Set to rgba(0,0,0,0) for no line | |
lineDash: [5,3], //dash style of line around cropping area, set to empty array for solid line ([]) | |
overlayStyle: 'rgba(0,0,0,0.4)', //overlay around cropped area | |
fillStyle: 'rgba(0,0,0,0.7)', //corner handles style, fill | |
strokeStyle: 'rgba(255,255,255,0.7)', //corner handles style, stroke | |
hoverFillStyle: 'rgba(255,255,255,0.4)', //corner handles style, fill on hover | |
hoverStrokeStyle: 'white', //corner handles style, stroke on hover | |
x: 20, y: 20, w: 200, h: 100, //initial cropping values, x, y, width, height | |
markerSize: 20, | |
deltaX: 0, deltaY: 0, | |
dragged: false, | |
aspectRatio: 2, //aspect ratio of the cropping area. false for non locked ratio, or number for locked | |
croppedWidth: 400, //desired width of cropped image | |
croppedHeight: 200, //desired height of cropped image. If width/height is not equal to aspect ratio, final image will be distorted | |
minWidth: 10, //minimal cropping area width | |
minHeight: 10, //minimal cropping area height | |
croppedImage: false, | |
circle: false //for cropping round images. If set to true, it will change aspect ratio of cropping area to 1. CroppedWidth and croppedHeight should be equal, otherwise strange effects will occur | |
}; | |
}, | |
computed: { | |
cropperDivStyle: function () { | |
return {width: this.cropperDivWidth, height: this.cropperHeight+'px', textAlign: 'center'}; | |
}, | |
cropperHeight: function () { | |
return this.cropperDivHeight ? this.cropperDivHeight : this.cropperDivMaxHeight; | |
}, | |
cropperWidth: function () { | |
return this.cropping && this.$refs.cropperdiv.offsetWidth; | |
}, | |
cropperDivRatio: function () { | |
return this.cropperWidth/this.cropperHeight; | |
}, | |
imageRatio: function () { | |
return this.imageWidth/this.imageHeight; | |
}, | |
markers: function () { | |
return { | |
nw: {x: this.x - this.markerSize/2, y: this.y - this.markerSize/2}, | |
ne: {x: this.x + this.w - this.markerSize/2, y: this.y - this.markerSize/2}, | |
sw: {x: this.x - this.markerSize/2, y: this.y + this.h - this.markerSize/2}, | |
se: {x: this.x + this.w - this.markerSize/2, y: this.y + this.h - this.markerSize/2} | |
}; | |
}, | |
cw: function () { | |
return this.croppedWidth || this.w; | |
}, | |
ch: function () { | |
return this.croppedHeight || this.h; | |
}, | |
roundCorners: function () { | |
if (this.circle) {return {borderRadius: '100%'};} | |
else return false; | |
} | |
}, | |
methods: { | |
selectFile: function (evt) { | |
var file = evt.currentTarget.files[0]; | |
var reader = new FileReader(); | |
var cropper = this; | |
reader.onload = function (evt) { | |
cropper.cropping = true; | |
if (cropper.circle) {cropper.aspectRatio = 1; cropper.h = cropper.w;} | |
var image = new Image(); | |
image.src = evt.target.result; | |
image.onload = function() { | |
cropper.image = image; | |
cropper.drawing = true; | |
cropper.imageWidth = image.width; | |
cropper.imageHeight = image.height; | |
cropper.canvasWidth = cropper.cropperWidth; | |
cropper.canvasHeight = cropper.cropperHeight; | |
if (cropper.imageRatio < cropper.cropperDivRatio) { | |
cropper.canvasWidth = cropper.canvasHeight * cropper.imageRatio; | |
} | |
if (cropper.imageRatio > cropper.cropperDivRatio) { | |
cropper.canvasHeight = cropper.canvasWidth / cropper.imageRatio; | |
} | |
Vue.nextTick(function () { | |
var canvas = cropper.$refs.canvas; | |
cropper.ctx = canvas.getContext('2d'); | |
cropper.ctx.drawImage(cropper.image, 0, 0, cropper.canvasWidth, cropper.canvasHeight); | |
}); | |
}; | |
}; | |
reader.readAsDataURL(file); | |
}, | |
moveMouse: function (event) { | |
if (event === undefined) return false; | |
var doc = document.documentElement; | |
var scrollLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); | |
var scrollTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); | |
var x = event.clientX - event.target.offsetLeft + scrollLeft; | |
var y = event.clientY - event.target.offsetTop + scrollTop; | |
var ctx = this.ctx; | |
//draw the image | |
ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); | |
ctx.drawImage(this.image, 0, 0, this.canvasWidth, this.canvasHeight); | |
//update coords | |
if (this.dragged) this.updateCoords(x,y); | |
//draw crop area and handles | |
this.drawSelection(ctx,x,y); | |
//crop image | |
var scaleX = this.imageWidth / this.canvasWidth; | |
var scaleY = this.imageHeight / this.canvasHeight; | |
resultCanvas = document.createElement('canvas'); | |
resultCanvas.width = this.cw; | |
resultCanvas.height = this.ch; | |
resultCanvas.getContext('2d').drawImage(this.image, this.x * scaleX, this.y * scaleY, this.w * scaleX, this.h * scaleY, 0, 0, this.cw, this.ch); | |
this.croppedImage = resultCanvas.toDataURL(); | |
}, | |
drawSelection: function (ctx,x,y) { | |
this.drawOverlay(ctx); | |
this.$refs.canvas.style.cursor = 'default'; | |
ctx.beginPath(); | |
if (!this.circle) {ctx.rect(this.x, this.y, this.w, this.h);} | |
else { | |
ctx.arc(this.x + this.w / 2, this.y + this.h / 2, this.w / 2, 0, 2 * Math.PI); | |
} | |
if (ctx.isPointInPath(x, y)) { | |
this.$refs.canvas.style.cursor = 'move'; | |
} | |
ctx.setLineDash(this.lineDash); | |
ctx.strokeStyle = this.mainStroke; | |
ctx.stroke(); | |
ctx.setLineDash([]); | |
for (var p in this.markers) { | |
var rectangle = this.markers[p]; | |
ctx.beginPath(); | |
ctx.rect(rectangle.x, rectangle.y, this.markerSize, this.markerSize); | |
ctx.fillStyle = this.fillStyle; | |
ctx.strokeStyle = this.strokeStyle; | |
if (ctx.isPointInPath(x, y)) { | |
ctx.fillStyle = this.hoverFillStyle; | |
ctx.strokeStyle = this.hoverStrokeStyle; | |
this.$refs.canvas.style.cursor = p+'-resize'; | |
} | |
ctx.fill(); | |
ctx.stroke(); | |
} | |
}, | |
drawOverlay: function (ctx) { | |
ctx.fillStyle = this.overlayStyle; | |
ctx.fillRect(0,0,this.canvasWidth, this.y); | |
ctx.fillRect(0,this.y, this.x, this.h); | |
ctx.fillRect(this.x+this.w, this.y, this.canvasWidth - (this.x+this.w), this.h); | |
ctx.fillRect(0, this.y+this.h, this.canvasWidth, this.canvasHeight - (this.y+this.h)); | |
if (this.circle) { | |
ctx.beginPath(); | |
ctx.arc(this.x + this.w / 2, this.y + this.h / 2, this.w / 2, Math.PI, 1.5 * Math.PI); | |
ctx.lineTo(this.x, this.y); | |
ctx.closePath(); | |
ctx.fill(); | |
ctx.beginPath(); | |
ctx.arc(this.x + this.w / 2, this.y + this.h / 2, this.w / 2, 1.5 * Math.PI, 2 * Math.PI); | |
ctx.lineTo(this.x + this.w, this.y); | |
ctx.closePath(); | |
ctx.fill(); | |
ctx.beginPath(); | |
ctx.arc(this.x + this.w / 2, this.y + this.h / 2, this.w / 2, 0, 0.5 * Math.PI); | |
ctx.lineTo(this.x + this.w, this.y + this.h); | |
ctx.closePath(); | |
ctx.fill(); | |
ctx.beginPath(); | |
ctx.arc(this.x + this.w / 2, this.y + this.h / 2, this.w / 2, 0.5 * Math.PI, Math.PI); | |
ctx.lineTo(this.x, this.y + this.h); | |
ctx.closePath(); | |
ctx.fill(); | |
} | |
}, | |
startDrag: function (event) { | |
var doc = document.documentElement; | |
var scrollLeft = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0); | |
var scrollTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0); | |
var x = event.clientX - event.target.offsetLeft + scrollLeft; | |
var y = event.clientY - event.target.offsetTop + scrollTop; | |
var ctx = this.ctx; | |
for (var p in this.markers) { | |
var rectangle = this.markers[p]; | |
ctx.beginPath(); | |
ctx.rect(rectangle.x, rectangle.y, this.markerSize, this.markerSize); | |
if (ctx.isPointInPath(x, y)) { | |
this.dragged = p; | |
this.deltaX = x - rectangle.x; | |
this.deltaY = y - rectangle.y; | |
return;} | |
} | |
ctx.beginPath(); | |
if (!this.circle) {ctx.rect(this.x, this.y, this.w, this.h);} | |
else { | |
ctx.arc(this.x + this.w / 2, this.y + this.h / 2, this.w / 2, 0, 2 * Math.PI); | |
} | |
if (ctx.isPointInPath(x, y)) { | |
this.dragged = 'all'; | |
this.deltaX = x - this.x; | |
this.deltaY = y - this.y; | |
return;} | |
}, | |
stopDrag: function () { | |
this.dragged = false; | |
this.deltaX = 0; | |
this.deltaY = 0; | |
}, | |
updateCoords: function (x,y) { | |
var newX, newY, newW, newH; | |
if (this.dragged == 'all') { | |
newX = x - this.deltaX; | |
newY = y - this.deltaY; | |
newW = this.w; | |
newH = this.h; | |
} | |
else { | |
var ox = this.dragged[1] == 'w' ? 'e' : 'w'; | |
var oy = this.dragged[0] == 'n' ? 's' : 'n'; | |
var oppositeIdx = oy+ox; | |
if (ox == 'e') { | |
newX = x - this.deltaX + this.markerSize / 2; | |
newW = this.markers[oppositeIdx].x - newX + this.markerSize / 2; | |
} | |
else { | |
newX = this.x; | |
newW = x - this.deltaX - this.markers[oppositeIdx].x; | |
} | |
if (oy == 's') { | |
newY = y - this.deltaY + this.markerSize / 2; | |
newH = this.markers[oppositeIdx].y - newY + this.markerSize / 2; | |
} | |
else { | |
newY = this.y; | |
newH = y - this.deltaY - this.markers[oppositeIdx].y; | |
} | |
} | |
if (this.aspectRatio) {newH = newW / this.aspectRatio;} | |
if (newX < 0) newX = 0; | |
if (newY < 0) newY = 0; | |
if (newX + newW > this.canvasWidth) { | |
newW = this.canvasWidth - newX; | |
if (this.aspectRatio) {newH = newW / this.aspectRatio;} | |
} | |
if (newY + newH > this.canvasHeight) { | |
newH = this.canvasHeight - newY; | |
if (this.aspectRatio) {newW = newH * this.aspectRatio;} | |
} | |
if (newW < this.minWidth) {newW = this.minWidth; newH = newW / this.aspectRatio;} | |
if (newH < this.minHeight) {newH = this.minHeight; newW = newH * this.aspectRatio;} | |
this.x = newX; | |
this.y = newY; | |
this.w = newW; | |
this.h = newH; | |
} | |
} | |
}) | |
var app = new Vue({ | |
el: '#app', | |
}); |
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.min.js"></script> |
Image cropping component for vue.js
A Pen by Edward Lance Lorilla on CodePen.