<template> <view class="tm-avatarCrop fixed t-0 l-0 black" :style="{ width: `${width}px`, height: `${height}px` }"> <tm-sticky model="bottom"> <view class="fulled flex-center mb-32"> <tm-button size="m" text @click="$emit('cancel')">取消</tm-button> <tm-button size="m" @click="selectedImage">选择图片</tm-button> <tm-button size="m" @click="saveImage">确定</tm-button> </view> </tm-sticky> <view class="flex-center"><canvas id="AvatarCrop" canvas-id="AvatarCrop" :style="{ width: `${area_width}px`, height: `${area_height}px` }"></canvas></view> <movable-area class="absolute t-0 l-0 zIndex-n1" :style="{ width: `${width}px`, height: `${height}px` }"> <movable-view :out-of-bounds="false" @scale="movaScaleChange" @change="movaChange" :x="areview_x" :y="areview_y" direction="all" :scale="true" :style="{ width: `${scale_w}px`, height: `${scale_h}px` }" > <image v-show="image_src" @load="loadImage" :src="image_src" :style="{ width: `${scale_w}px`, height: `${scale_h}px` }"></image> </movable-view> </movable-area> <view :style="{ width: `${width}px`, height: `${height}px` }" class="absolute tm-avatarCrop-bodywk t-0 l-0 zIndex-n16"> <view class="tm-avatarCrop-area relative" :class="[isArc ? 'rounded' : '']" :style="{ width: `${area_width}px`, height: `${area_height}px`, top: `${posArray[0].y}px`, left: `${posArray[0].x}px` }" > <view class="flex-center text-size-s" :style="{ height: pos_size + 'px' }">宽:{{ Math.floor(area_width) }},高:{{ Math.floor(area_height) }}</view> <block v-for="(item, index) in 4" :key="index"> <view v-if="(isRatio == true && index !== 3) || index == 3" :key="index" :style="{ width: `${pos_size}px`, height: `${pos_size}px` }" @touchstart.stop.prevent="m_start($event, index)" @touchmove.stop.prevent="m_move($event, index)" @touchend.stop.prevent="m_end($event, index)" @mousedown.stop.prevent="m_start($event, index)" @mousemove.stop.prevent="m_move($event, index)" @mouseup.stop.prevent="m_end($event, index)" @mouseleave="m_end($event, index)" class="tm-avatarCrop-pos absolute black opacity-5" :class="[ 'tm-avatarCrop-pos-'+index, index == 0?'tm-avatarCrop-area-top-left':'', index == 1?'tm-avatarCrop-area-top-right': '', index == 2?'tm-avatarCrop-area-bottom-left': '', index == 3?'tm-avatarCrop-area-bottom-right': '' ]" :id="`${index}`" > <tm-icons style="line-height: 0;" color="white" v-if="index !== 3" :size="22" dense name="icon-expand-alt"></tm-icons> <tm-icons style="line-height: 0;" color="white" v-else :size="22" dense name="icon-arrows-alt"></tm-icons> </view> </block> </view> </view> </view> </template> <script> /** * 图片裁剪 * @property {Number} area-width = [] 默认300,裁剪框的宽度 * @property {Number} area-height = [] 默认300,裁剪框的高度 * @property {Number} quality = [] 默认1,图片压缩质量0-1 * @property {String} fileType = [jpg|png] 默认 png * @property {Boolean} is-ratio = [] 默认false,是否允许用户调整裁剪框的大小 * @property {Boolean} is-arc = [] 默认false,是否圆形 */ import tmIcons from '@/tm-vuetify/components/tm-icons/tm-icons.vue'; import tmButton from '@/tm-vuetify/components/tm-button/tm-button.vue'; import tmSticky from '@/tm-vuetify/components/tm-sticky/tm-sticky.vue'; import tmImages from '@/tm-vuetify/components/tm-images/tm-images.vue'; export default { name: 'tm-avatarCrop', props: { areaWidth: { type: Number, default: 300 }, areaHeight: { type: Number, default: 300 }, //是否允许用户调整距形区域的大小 。 isRatio: { type: Boolean, default: false }, //是否圆形。 isArc: { type: Boolean, default: false }, quality: { type: Number, default: 1 }, fileType:{ type:String, default:'png' }, confirm: { type: Function, default: function(data) { return function(data) {}; } } }, computed: {}, components: { tmIcons, tmButton, tmSticky, tmImages }, data() { return { width: 0, height: 0, canvanid: 'AvatarCrop', showCanva: false, area_width: 0, area_height: 0, prevent_left: 0, prevent_top: 0, old_x: 0, old_y: 0, posArray: [], endDrage: true, pos_size: 24, image_src: '', scale_w: 0, //图片缩放的宽, scale_h: 0, //图片缩放的高。 scale: 1, real_w: 0, real_h: 0, scale_areview_x: 0, scale_areview_y: 0, areview_x: 0, areview_y: 0, areview_new_x: 0, areview_new_y: 0, isAddImage: false }; }, destroyed() {}, created() { let sys = uni.getSystemInfoSync(); this.width = sys.windowWidth; this.height = sys.screenHeight; this.area_width = uni.upx2px(this.areaWidth); this.area_height = uni.upx2px(this.areaHeight); let dr = []; for (let i = 0; i < 4; i++) { dr.push({ x: 0, y: 0 }); } dr[0].x = (this.width - this.area_width) / 2; dr[0].y = (this.height - this.area_height) / 2; this.posArray = [...dr]; }, async mounted() { let t = this; await this.jishunTopData(); }, methods: { async jishunTopData() { let t =this; this.$nextTick(async function() { this.listData = []; uni.$tm.sleep(100).then(s=>{ let psd = uni.createSelectorQuery().in(t); psd.select('.tm-avatarCrop-pos-0').boundingClientRect() .select('.tm-avatarCrop-pos-1').boundingClientRect() .select('.tm-avatarCrop-pos-2').boundingClientRect() .select('.tm-avatarCrop-pos-3').boundingClientRect() .exec(function(p){ let list = p; let dr = [...t.posArray]; for (let i = 0; i < list.length; i++) { if(list[i]){ dr.splice(parseInt(list[i].id), 1, { x: list[i].left, y: list[i].top }); } } t.posArray = [...dr]; }) }) }); }, async loadImage(e) { this.isAddImage = true; this.posArray.splice(0, 1, { x: (this.width - this.area_width) / 2, y: (this.height - this.area_height) / 2 }); await this.jishunTopData(); this.$nextTick(async function() { let img_w = e.detail.width; let img_h = e.detail.height; this.real_w = img_w; this.real_h = img_h; let ratio_w = img_w >= this.width ? this.width : img_w; let ratio_h = ratio_w / (img_w / img_h); this.scale_w = ratio_w; this.scale_h = ratio_h; //图片宽大于高度时, this.areview_y = (this.height - this.scale_h) / 2; this.areview_x = (this.width - this.scale_w) / 2; this.areview_new_x = this.areview_x; this.areview_new_y = this.areview_y; }); }, selectedImage() { let t = this; uni.chooseImage({ count: 1, sizeType: ['compressed'], success: function(res) { t.image_src = res.tempFilePaths[0]; } }); }, saveImage() { if (!this.image_src) { uni.$tm.toast('未选择图片'); return; } let t = this; this.$nextTick(async function() { let scale_x = this.posArray[0].x - this.areview_new_x; let scale_y = this.posArray[0].y - this.areview_new_y; //计算真实的xy时,需要通过原有的缩放的大小通过比例来放大或者缩小到真实在源图片上的坐标。 let real_x = (this.real_w / (this.scale_w * this.scale)) * scale_x; let real_y = (this.real_h / (this.scale_h * this.scale)) * scale_y; let real_w = (this.real_w / (this.scale_w * this.scale)) * this.area_width; let real_h = real_w; if (this.isRatio) { real_h = (this.real_h / (this.scale_h * this.scale)) * this.area_height; } const ctx = uni.createCanvasContext('AvatarCrop', this); if (!ctx) return; //如果把框移动到图片外,则不要截取。 if (real_x < 0 || real_y < 0) { uni.$tm.toast('请把框移入图片中'); return; } if (this.isArc) { ctx.beginPath(); ctx.arc(this.area_width / 2, this.area_width / 2, this.area_width / 2, 0, 2 * Math.PI); ctx.clip(); } ctx.drawImage(this.image_src, real_x, real_y, real_w, real_h, 0, 0, this.area_width, this.area_height); uni.showLoading({ title: '...' }); function getimage() { let rtx = uni.getSystemInfoSync().pixelRatio; let a_w = t.area_width * rtx; let a_h = t.area_height * rtx; console.log(a_w,a_h); uni.canvasToTempFilePath( { x: 0, y: 0, width: a_w, height: a_h, canvasId: 'AvatarCrop', quality:t.quality, fileType:t.fileType, success: function(res) { // 在H5平台下,tempFilePath 为 base64 uni.hideLoading(); t.$nextTick(function() { t.$emit('confirm', { width:a_w, height: a_h, src: res.tempFilePath }); t.confirm({ width: a_w, height: a_h, src: res.tempFilePath }); }); }, fail: function(res) { uni.$tm.toast('请重试'); } }, t ); } ctx.draw(true, function() { getimage(); }); }); }, movaChange(e) { //当添加新图片时,这里的执行要比添加时的慢。因此会覆盖前面设置的xy if (!this.isAddImage) { //移动后,真实的x,y已经得到,不需要再计算缩放的xy //(因为uniapp的bug缩放后返回 的xy始终是原有的xy而不是真实的) this.scale_areview_x = 0; this.scale_areview_y = 0; this.areview_new_x = e.detail.x; this.areview_new_y = e.detail.y; } else { this.isAddImage = false; } }, movaScaleChange(e) { //通过缩放,计算出缩放后的x,y的和原有的x,y之间的差值得到真实的x,y。 //(因为uniapp的bug缩放后返回 的xy始终是原有的xy而不是真实的) let scale_x = -(this.scale_w - this.scale_w * e.detail.scale) / 2; let scale_y = (this.scale_h - this.scale_h * e.detail.scale) / 2; this.areview_new_x = -scale_x; this.areview_new_y = this.posArray[0].y - Math.abs(scale_y); // 保存缩放的比例。 this.scale = e.detail.scale; }, m_start(event, index) { event.preventDefault(); event.stopPropagation(); const ctx = uni.createCanvasContext('AvatarCrop', this); if (ctx) { ctx.clearRect(0, 0, this.area_width, this.area_height); ctx.draw(); } var touch; if (event.type.indexOf('mouse') == -1 && event.changedTouches.length == 1) { touch = event.changedTouches[0]; } else { touch = { pageX:event.pageX, pageY:event.pageY } } // #ifdef APP-VUE if (index == 0 || index == 2) { this.old_x = touch.pageX; } else if (index == 1 || index == 3) { this.old_x = touch.pageX + this.area_width; } if (index == 0 || index == 1) { this.old_y = touch.pageY; } else if (index == 2 || index == 3) { this.old_y = touch.pageY+ this.area_height; } // #endif // #ifndef APP-VUE if (index == 0 || index == 2) { this.old_x = touch.pageX - event.currentTarget.offsetLeft - this.posArray[index].x; } else if (index == 1 || index == 3) { this.old_x = touch.pageX - event.currentTarget.offsetLeft - this.posArray[index].x + this.area_width - this.pos_size; } if (index == 0 || index == 1) { this.old_y = touch.pageY - event.currentTarget.offsetTop - this.posArray[index].y; } else if (index == 2 || index == 3) { this.old_y = touch.pageY - event.currentTarget.offsetTop - this.posArray[index].y + this.area_height - this.pos_size; } // #endif this.endDrage = false; }, m_move(event, index) { if (this.endDrage) return; let t = this; event.preventDefault(); event.stopPropagation(); var touch; if (event.type.indexOf('mouse') == -1 && event.changedTouches.length == 1) { var touch = event.changedTouches[0]; } else { touch = { pageX:event.pageX, pageY:event.pageY } } // #ifdef APP-VUE let ch = touch.pageY - this.pos_size/2; let chx = touch.pageX - this.pos_size/2; // #endif // #ifndef APP-VUE let ch = touch.pageY - t.old_y; let chx = touch.pageX - t.old_x; // #endif let pos_size = this.pos_size; let x_cha_len = chx - t.posArray[index].x; let y_cha_len = ch - t.posArray[index].y; t.posArray.splice(index, 1, { x: chx, y: ch }); let w = 0; let h = 0; if (index == 0) { t.posArray.splice(1, 1, { x: t.posArray[1].x, y: ch }); t.posArray.splice(2, 1, { x: chx, y: t.posArray[2].y }); w = t.posArray[1].x + pos_size - t.posArray[0].x; h = t.posArray[2].y + pos_size - t.posArray[0].y; } else if (index == 1) { t.posArray.splice(0, 1, { x: t.posArray[0].x, y: ch }); t.posArray.splice(3, 1, { x: chx, y: t.posArray[3].y }); w = t.posArray[1].x + pos_size - t.posArray[0].x; h = t.posArray[2].y + pos_size - t.posArray[1].y; } else if (index == 2) { t.posArray.splice(0, 1, { x: chx, y: t.posArray[0].y }); t.posArray.splice(3, 1, { x: t.posArray[3].x, y: ch }); w = t.posArray[3].x + pos_size - t.posArray[2].x; h = t.posArray[2].y + pos_size - t.posArray[1].y; } if (index !== 3) { this.area_width = w < 30 ? 30 : w; this.area_height = h < 30 ? 30 : h; } else { let top_x = chx - this.area_width + pos_size; let top_y = ch - this.area_height + pos_size; t.posArray.splice(0, 1, { x: top_x, y: top_y }); t.posArray.splice(1, 1, { x: top_x + this.area_width - pos_size, y: top_y }); t.posArray.splice(2, 1, { x: top_x, y: top_y + this.area_height - pos_size }); } }, m_end(event, index) { if (this.disabled) return; let t = this; event.preventDefault(); event.stopPropagation(); this.endDrage = true; } } }; </script> <style lang="scss" scoped> .tm-avatarCrop { .tm-avatarCrop-bodywk { pointer-events: none; } .tm-avatarCrop-area { background-color: rgba(0, 0, 0, 0.3); border: 1px dotted rgba(255, 255, 255, 0.7); pointer-events: none; .tm-avatarCrop-pos { display: flex; justify-content: center; align-items: center; background-color: grey; pointer-events: auto; &.tm-avatarCrop-area-top-left { left: 0; top: 0; transform: rotate(90deg); } &.tm-avatarCrop-area-top-right { right: 0; top: 0; } &.tm-avatarCrop-area-bottom-right { right: 0; bottom: 0; } &.tm-avatarCrop-area-bottom-left { left: 0; bottom: 0; } } } } </style>