const ABS = Math.abs
|
const calcLen = (v) => { // distance between two coordinate
|
return Math.sqrt(v.x * v.x + v.y * v.y)
|
}
|
const calcAngle = (a, b) => { // angle of the two vectors
|
var l = calcLen(a) * calcLen(b);
|
var cosValue;
|
var angle
|
if (l) {
|
cosValue = (a.x * b.x + a.y * b.y) / l
|
angle = Math.acos(Math.min(cosValue, 1))
|
angle = a.x * b.y - b.x * a.y > 0 ? -angle : angle
|
return angle * 180 / Math.PI
|
}
|
return 0
|
}
|
const generateCanvasId = () => { // generate a random string
|
const seeds = 'abcdefghijklmnopqrstuvwxyz'
|
const arr = seeds.split('').concat(seeds.toUpperCase().split('')).concat('0123456789'.split(''))
|
let m = arr.length;
|
let i
|
while (m) {
|
i = Math.floor(Math.random() * m--)
|
const temp = arr[m]
|
arr[m] = arr[i]
|
arr[i] = temp
|
}
|
return arr.slice(0, 16).join('')
|
}
|
|
export default {
|
props: {
|
width: { // width of the container
|
type: [String, Number],
|
default: '100%'
|
},
|
height: { // height of the container
|
type: [String, Number],
|
default: '100%'
|
},
|
cutWidth: { // cutter width
|
type: [String, Number],
|
default: '50%'
|
},
|
cutHeight: { // cutter height
|
type: [String, Number],
|
default: 0
|
},
|
minWidth: { // minWidth of the cutter
|
type: Number,
|
default: 50
|
},
|
minHeight: { // minHeight of the cutter
|
type: Number,
|
default: 50
|
},
|
center: { // autoCenter
|
type: Boolean,
|
default: true
|
},
|
src: String,
|
disableScale: Boolean, // disable to zoom
|
disableRotate: Boolean,
|
disableTranslate: Boolean,
|
disableCtrl: Boolean, // disable to resize the cutter
|
boundDetect: Boolean, // open boundary detection
|
freeBoundDetect: Boolean, // open boundary detection while doing rotation
|
keepRatio: Boolean, // keep the ratio of the cutter
|
disablePreview: Boolean, // disable preview after cutting
|
showCtrlBorder: Boolean, // show cutter border
|
resetCut: Boolean, // reset cut while img change
|
fit: {
|
type: Boolean,
|
default: true
|
},
|
imageCenter: Boolean, // auto center/middle for image
|
maxZoom: { // maximum scaling factor
|
type: Number,
|
default: 10 // can not be Infinity in baidu-MiniProgram
|
},
|
minZoom: { // minimum scaling factor
|
type: Number,
|
default: 1
|
},
|
angle: { // initial angle of rotation
|
type: Number,
|
default: 0
|
},
|
zoom: { // initial scaling factor
|
type: Number,
|
default: 1
|
},
|
offset: { // initial offset relative to the cutter left border
|
type: Array,
|
default () {
|
return [0, 0]
|
}
|
},
|
background: {
|
type: String,
|
default: '#000'
|
},
|
canvasBackground: { // background for the exported image
|
type: String,
|
default: '#fff'
|
},
|
canvasZoom: { // export multiples of the cutter size
|
type: Number,
|
default: 1
|
},
|
fileType: {
|
type: String,
|
default: 'png',
|
validator(t) {
|
return ['png', 'jpg'].includes(t)
|
}
|
},
|
quality: {
|
type: Number,
|
default: 1
|
},
|
maskType: { // type for mask
|
type: String,
|
default: "shadow"
|
},
|
circleView: Boolean // circle clip view
|
},
|
data() {
|
return {
|
transform: {
|
angle: 0,
|
translate: {
|
x: 0,
|
y: 0
|
},
|
zoom: 1
|
},
|
corner: {
|
left: 50,
|
right: 50,
|
bottom: 50,
|
top: 50
|
},
|
image: {
|
originWidth: 0,
|
originHeight: 0,
|
width: 0,
|
height: 0
|
},
|
ctrlWidth: 0,
|
ctrlHeight: 0,
|
view: false,
|
canvasId: ''
|
}
|
},
|
computed: {
|
transformMeta: function() {
|
const transform = this.transform
|
return `translate3d(${transform.translate.x}px, ${transform.translate.y}px, 0) rotate(${transform.angle}deg) scale(${transform.zoom})`
|
},
|
ctrlStyle: function() {
|
const corner = this.corner
|
let cssStr =
|
`left: ${corner.left}px;top: ${corner.top}px;right: ${corner.right}px;bottom: ${corner.bottom }px;`
|
if (this.maskType !== 'outline') {
|
cssStr += `box-shadow: 0 0 0 50000rpx rgba(0,0,0, ${this.view ? 0.8 : 0.4})`
|
} else {
|
cssStr += `outline: rgba(0,0,0, ${this.view ? 0.8 : 0.4}) solid 5000px`
|
}
|
return cssStr
|
}
|
},
|
watch: {
|
src: function() {
|
if (this.resetCut) this.resetCutReact()
|
this.initImage()
|
},
|
|
},
|
created() {
|
this.canvasId = generateCanvasId()
|
uni.getSystemInfo().then(result => {
|
result = result[1] || {
|
windowWidth: 375,
|
windowHeight: 736
|
}
|
this.ratio = result.windowWidth / 750
|
this.windowHeight = result.windowHeight
|
this.init()
|
this.initCanvas()
|
})
|
},
|
methods: {
|
toPx(str) {
|
if (str.indexOf('%') !== -1) {
|
return Math.floor(Number(str.replace('%', '')) / 100 * this.containerWidth)
|
}
|
if (str.indexOf('rpx') !== -1) {
|
return Math.floor(Number(str.replace('rpx', '')) * this.ratio)
|
}
|
return Math.floor(Number(str.replace('px', '')))
|
},
|
initCanvas() {
|
// #ifdef MP-ALIPAY
|
const context = uni.createSelectorQuery()
|
// #endif
|
// #ifndef MP-ALIPAY
|
const context = uni.createSelectorQuery().in(this)
|
// #endif
|
|
// get contianer size
|
context.select('.nice-cropper').boundingClientRect()
|
context.exec(res => {
|
this.containerWidth = res[0].width
|
this.containerHeight = res[0].height
|
this.initCut()
|
})
|
},
|
resetCutReact() { // init size and position of the cutter
|
this.ctrlWidth = Math.min(this.toPx(this.cutWidth), this.containerWidth)
|
if (this.cutHeight) {
|
this.ctrlHeight = Math.min(this.toPx(this.cutHeight), this.containerHeight)
|
} else { // 默认为正方形
|
this.ctrlHeight = Math.min(this.ctrlWidth, this.containerHeight)
|
}
|
const cornerStartX = this.center ? Math.floor((this.containerWidth - this.ctrlWidth) / 2) : 0
|
const cornerStartY = this.center ? Math.floor((this.containerHeight - this.ctrlHeight) / 2) : 0
|
this.cutRatio = this.ctrlHeight / this.ctrlWidth
|
this.corner = {
|
left: cornerStartX,
|
right: this.containerWidth - this.ctrlWidth - cornerStartX,
|
top: cornerStartY,
|
bottom: this.containerHeight - this.ctrlHeight - cornerStartY
|
}
|
},
|
initCut() {
|
this.resetCutReact()
|
this.initImage()
|
},
|
initImage() {
|
if (!this.src) return
|
const $this = this
|
uni.getImageInfo({
|
src: $this.src,
|
success: function(res) {
|
console.log("getImageInfo",res,$this)
|
$this.image.originWidth = res.width
|
$this.image.originHeight = res.height
|
$this.image.width = $this.fit ? $this.ctrlWidth : $this.image.originWidth
|
$this.image.height = res.height / res.width * $this.image.width
|
$this.img = res.path
|
$this.initImageSuccess()
|
},
|
fail: function(err) {
|
$this.image.originWidth = $this.ctrlWidth
|
$this.image.originHeight = $this.ctrlHeight
|
$this.image.width = $this.fit ? $this.ctrlWidth : $this.image.originWidth
|
$this.image.height = $this.ctrlHeight
|
$this.initImageSuccess()
|
}
|
})
|
},
|
initImageSuccess() {
|
const offset = [0, 0]
|
if (this.imageCenter) {
|
offset[0] = (this.ctrlWidth - this.image.width) / 2
|
offset[1] = (this.ctrlHeight - this.image.height) / 2
|
}
|
offset[0] += this.offset[0] || 0
|
offset[1] += this.offset[1] || 0
|
|
this.setTranslate(offset)
|
this.setZoom(this.zoom)
|
this.transform.angle = this.freeBoundDetect || !this.disableRotate ? this.angle : 0
|
|
this.setBoundary() // boundary detect
|
this.preview() // preview
|
this.draw()
|
},
|
init() {
|
this.pretouch = {}
|
this.handles = {}
|
this.preVector = {
|
x: 0,
|
y: 0
|
}
|
this.distance = 30
|
this.touch = {}
|
this.movetouch = {}
|
this.cutMode = false
|
this.params = {
|
zoom: 1,
|
deltaX: 0,
|
deltaY: 0,
|
diffX: 0,
|
diffY: 0,
|
angle: 0
|
}
|
},
|
start(e) {
|
if (!this.src) e.preventDefault()
|
const point = e.touches ? e.touches[0] : e
|
const touch = this.touch
|
const now = Date.now()
|
touch.startX = point.pageX
|
touch.startY = point.pageY
|
touch.startTime = now
|
this.doubleTap = false
|
this.view = false
|
clearTimeout(this.previewTimer)
|
if (e.touches.length > 1) {
|
var point2 = e.touches[1]
|
this.preVector = {
|
x: point2.pageX - this.touch.startX,
|
y: point2.pageY - this.touch.startY
|
}
|
this.startDistance = calcLen(this.preVector)
|
} else {
|
let pretouch = this.pretouch
|
this.doubleTap = this.pretouch.time && now - this.pretouch.time < 300 && ABS(touch.startX -
|
pretouch
|
.startX) < 30 && ABS(touch.startY - pretouch.startY) < 30 && ABS(touch.startTime -
|
pretouch
|
.time) < 300
|
pretouch = { // reserve the last touch
|
startX: this.touch.startX,
|
startY: this.touch.startY,
|
time: this.touch.startTime
|
}
|
}
|
},
|
move(e) {
|
if (!this.src) return
|
const point = e.touches ? e.touches[0] : e
|
if (e.touches.length > 1) { // multi touch
|
const point2 = e.touches[1]
|
const v = {
|
x: point2.pageX - point.pageX,
|
y: point2.pageY - point.pageY
|
}
|
|
if (this.preVector.x !== null) {
|
if (this.startDistance) { // zoom
|
const len = calcLen(v)
|
this.params.zoom = calcLen(v) / this.startDistance
|
this.startDistance = len(this.cutMode && !this.disableCtrl) ? this.setCut() : !this
|
.disableScale && this.pinch()
|
}
|
// rotate
|
this.params.angle = calcAngle(v, this.preVector)
|
(this.cutMode && !this.disableCtrl) ? this.setCut() : !this.disableRotate && this.rotate()
|
}
|
this.preVector.x = v.x
|
this.preVector.y = v.y
|
} else { // translate
|
const diffX = point.pageX - this.touch.startX
|
const diffY = point.pageY - this.touch.startY
|
this.params.diffY = diffY
|
this.params.diffX = diffX
|
if (this.movetouch.x) {
|
this.params.deltaX = point.pageX - this.movetouch.x
|
this.params.deltaY = point.pageY - this.movetouch.y
|
} else {
|
this.params.deltaX = this.params.deltaY = 0
|
}
|
if (ABS(diffX) > 30 || ABS(diffY) > 30) {
|
this.doubleTap = false
|
}
|
(this.cutMode && !this.disableCtrl) ? this.setCut(): !this.disableTranslate && this.translate()
|
this.movetouch.x = point.pageX
|
this.movetouch.y = point.pageY
|
}
|
if (!this.cutMode) this.setBoundary()
|
if (e.touches.length > 1) {
|
e.preventDefault()
|
}
|
},
|
end() {
|
if (this.doubleTap) this.$emit('doubleTap')
|
if (this.cutMode) this.setBoundary()
|
this.init()
|
if (!this.disablePreview) this.preview()
|
this.draw()
|
},
|
translate() {
|
const transform = this.transform.translate
|
const meta = this.params
|
transform.x += meta.deltaX
|
transform.y += meta.deltaY
|
},
|
pinch() {
|
this.transform.zoom *= this.params.zoom
|
},
|
rotate() {
|
this.transform.angle += this.params.angle
|
},
|
setZoom(scale) {
|
scale = Math.min(Math.max(Number(scale) || 1, this.minZoom), this.maxZoom)
|
this.transform.zoom = scale
|
},
|
setTranslate(offset) {
|
if (Array.isArray(offset)) {
|
const x = Number(offset[0])
|
const y = Number(offset[1])
|
this.transform.translate.x = isNaN(x) ? this.transform.translate.x : this.corner.left + x
|
this.transform.translate.y = isNaN(y) ? this.transform.translate.y : this.corner.top + y
|
}
|
},
|
setRotate(angle) {
|
this.transform.angle = Number(angle) || 0
|
},
|
setTransform(x, y, angle, scale) {
|
this.setTranslate([x, y])
|
this.setZoom(scale)
|
this.setRotate(angle)
|
},
|
setCutMode(type) {
|
if (!this.src) return
|
if (this.disableCtrl) return
|
this.cutMode = true
|
this.cutDirection = type
|
},
|
setCut() {
|
const corner = this.corner
|
const meta = this.params
|
this.setMeta(this.cutDirection, meta) // correct cutter position
|
if (this.keepRatio) {
|
if (this.cutDirection === 'lt' || this.cutDirection === 'rb') {
|
meta.deltaY = meta.deltaX * this.cutRatio
|
} else {
|
meta.deltaX = meta.deltaY / this.cutRatio
|
}
|
}
|
switch (this.cutDirection) {
|
case 'lt':
|
corner.top += meta.deltaY
|
corner.left += meta.deltaX
|
break
|
case 'rt':
|
corner.top += meta.deltaY
|
corner.right -= this.keepRatio ? -meta.deltaX : meta.deltaX
|
break
|
case 'rb':
|
corner.right -= meta.deltaX
|
corner.bottom -= meta.deltaY
|
break
|
case 'lb':
|
corner.bottom -= meta.deltaY
|
corner.left += this.keepRatio ? -meta.deltaX : meta.deltaX
|
break
|
}
|
this.ctrlWidth = this.containerWidth - corner.left - corner.right
|
this.ctrlHeight = this.containerHeight - corner.top - corner.bottom
|
},
|
setMeta(direction, meta) {
|
const {
|
ctrlWidth,
|
ctrlHeight,
|
minWidth,
|
minHeight
|
} = this
|
switch (direction) {
|
case 'lt':
|
if (meta.deltaX > 0 || meta.deltaY > 0) {
|
meta.deltaX = Math.min(meta.deltaX, ctrlWidth - minWidth)
|
meta.deltaY = Math.min(meta.deltaY, ctrlHeight - minHeight)
|
}
|
break
|
case 'rt':
|
if (meta.deltaX < 0 || meta.deltaY > 0) {
|
meta.deltaX = Math.max(meta.deltaX, minWidth - ctrlWidth)
|
meta.deltaY = Math.min(meta.deltaY, ctrlHeight - minHeight)
|
}
|
break
|
case 'rb':
|
if (meta.deltaX < 0 || meta.deltaY < 0) {
|
meta.deltaX = Math.max(meta.deltaX, minWidth - ctrlWidth)
|
meta.deltaY = Math.max(meta.deltaY, minHeight - ctrlHeight)
|
}
|
break
|
case 'lb':
|
if (meta.deltaX > 0 || meta.deltaY < 0) {
|
meta.deltaX = Math.min(meta.deltaX, ctrlWidth - minWidth)
|
meta.deltaY = Math.max(meta.deltaY, minHeight - ctrlHeight)
|
}
|
break
|
}
|
|
},
|
setBoundary() {
|
let zoom = this.transform.zoom
|
zoom = zoom < this.minZoom ? this.minZoom : (zoom > this.maxZoom ? this.maxZoom : zoom)
|
this.transform.zoom = zoom
|
if (!this.boundDetect || !this.disableRotate && !this.freeBoundDetect) return true
|
const translate = this.transform.translate
|
const corner = this.corner
|
const minX = corner.left - this.image.width + this.ctrlWidth - this.image.width * (zoom - 1) / 2
|
const maxX = corner.left + this.image.width * (zoom - 1) / 2
|
const minY = corner.top - this.image.height + this.ctrlHeight - this.image.height * (zoom - 1) / 2
|
const maxY = corner.top + this.image.height * (zoom - 1) / 2
|
translate.x = Math.floor(translate.x < minX ? minX : (translate.x > maxX ? maxX : translate.x))
|
translate.y = Math.floor(translate.y < minY ? minY : (translate.y > maxY ? maxY : translate.y))
|
},
|
preview() {
|
clearTimeout(this.previewTimer)
|
this.previewTimer = setTimeout(() => {
|
this.view = true
|
}, 500)
|
},
|
draw() {
|
// #ifdef MP-ALIPAY
|
const context = uni.createCanvasContext(this.canvasId)
|
// #endif
|
// #ifndef MP-ALIPAY
|
const context = uni.createCanvasContext(this.canvasId, this)
|
// #endif
|
const transform = this.transform
|
const corner = this.corner
|
const canvasZoom = this.canvasZoom
|
const img = this.image
|
context.save()
|
context.setFillStyle(this.canvasBackground)
|
this.$emit('beforeDraw', context, transform) // beforeDraw hook
|
|
const zoom = transform.zoom
|
context.fillRect(0, 0, this.ctrlWidth * canvasZoom, this.ctrlHeight * canvasZoom) // clear canvas
|
context.translate((transform.translate.x - corner.left + img.width / 2) * canvasZoom, (transform
|
.translate
|
.y - corner.top + img.height / 2) *
|
canvasZoom) // translate the canvas's orgin to the image center
|
context.rotate(transform.angle * Math.PI / 180)
|
context.translate(-img.width * zoom * 0.5 * canvasZoom, -img.height * zoom * 0.5 * canvasZoom)
|
context.drawImage(this.img, 0, 0, img.width * zoom * canvasZoom, img.height * zoom * canvasZoom)
|
context.restore()
|
this.$emit('afterDraw', context, {
|
width: this.ctrlWidth * canvasZoom,
|
height: this.ctrlHeight * canvasZoom
|
}) // afterDraw hook
|
context.draw(false, () => {
|
console.log("context.draw")
|
uni.canvasToTempFilePath({
|
canvasId: this.canvasId,
|
quality: this.quality || 1,
|
fileType: this.fileType,
|
success: (res) => {
|
console.log("canvasToTempFilePath",res)
|
this.$emit('cropped', res.tempFilePath, {
|
originWidth: this.image.originWidth,
|
originHeight: this.image.originHeight,
|
width: this.ctrlWidth * canvasZoom,
|
height: this.ctrlHeight * canvasZoom,
|
scale: zoom,
|
translate: {
|
x: transform.translate.x,
|
y: transform.translate.y
|
},
|
rotate: transform.angle
|
}) // draw callback
|
}
|
}, this)
|
})
|
}
|
}
|
}
|