123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538 |
- 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()
- },
- async initImage() {
- if (!this.src) return
- const [err, res] = await uni.getImageInfo({
- src: this.src
- })
- if(err) {
- this.$emit("error", err)
- } else {
- this.$emit('load', res)
- }
- // init image size
- this.image.originWidth = err ? this.ctrlWidth : res.width
- this.image.originHeight = err ? this.ctrlHeight : res.height
- this.image.width = this.fit ? this.ctrlWidth : this.image.originWidth
- this.image.height = err ? this.ctrlHeight : res.height / res.width * this.image.width
- this.img = res.path
- 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
- }
- !this.cutMode && this.setBoundary()
- if (e.touches.length > 1) {
- e.preventDefault()
- }
- },
- end() {
- this.doubleTap && this.$emit('doubleTap')
- this.cutMode && this.setBoundary()
- this.init()
- !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
- 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, () => {
- uni.canvasToTempFilePath({
- canvasId: this.canvasId,
- quality: this.quality || 1,
- fileType: this.fileType,
- success: (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)
- })
- }
- }
- }
|