import dataUtil from '../core/utils/data-util'; import eventUtil from '../core/utils/event-util'; import { determineDirection, calcImageOffset, calcImageScale, calcImageSize, calcPythagoreanTheorem, clipTouchMoveOfCalculate, imageTouchMoveOfCalcOffset } from './calculate'; const detail = true; // 模拟enum const IMAGE_TYPE = { base64: 'base64', url: 'url' }; Component({ externalClasses: ['l-class'], relations: { '../image-clipper-tools/index': { type: 'child' } }, options: { pureDataPattern: /^_/ }, /** * 组件的属性列表 */ properties: { // 组件显示隐藏 show: { type: Boolean, value: false }, // 组件层级 zIndex: { type: Number, value: 99 }, // 图片url imageUrl: { type: String }, // 生成的图片类型 type: { type: String, options: ['url', 'base64'], value: 'url' }, // 生成的图片质量 quality: { type: Number, value: 1 }, // 裁剪框宽度 width: { type: Number, value: 400 }, // 裁剪框高度 height: { type: Number, value: 400 }, // 裁剪框最小宽度 minWidth: { type: Number, value: 200 }, // 裁剪框最大宽度 maxWidth: { type: Number, value: 600 }, // 裁剪框最小高度 minHeight: { type: Number, value: 200 }, // 裁剪框最大高度 maxHeight: { type: Number, value: 600 }, // 是否锁定裁剪框宽度 lockWidth: { type: Boolean, value: false }, // 是否锁定裁剪框高度 lockHeight: { type: Boolean, value: false }, // 是否锁定裁剪框比例 lockRatio: { type: Boolean, value: true }, // 生成图片相对于裁剪框的比例 scaleRatio: { type: Number, value: 1 }, // 图片最小缩放比 minRatio: { type: Number, value: 0.5 }, // 图片最大缩放比 maxRatio: { type: Number, value: 2 }, // 是否禁止缩放 disableScale: { type: Number, value: false }, // 是否禁止旋转 disableRotate: { type: Number, value: false }, // 是否限制移动范围 limitMove: { type: Boolean, value: false }, // 是否显示选择图片按钮 checkImage: { type: Boolean, value: true }, checkImageIcon: { type: String, value: './images/photo.png' }, // 是否显示顺时针旋转按钮 rotateAlong: { type: Boolean, value: true }, rotateAlongIcon: { type: String, value: './images/rotate-along.png' }, // 是否显示逆时针旋转按钮 rotateInverse: { type: Boolean, value: true }, rotateInverseIcon: { type: String, value: './images/rotate-inverse.png' }, // 是否显示确定按钮 sure: { type: Boolean, value: true }, sureIcon: { type: String, value: './images/sure.png' }, // 是否显示关闭按钮 close: { type: Boolean, value: true }, closeIcon: { type: String, value: './images/close.png' }, // 旋转按钮每次旋转的角度 rotateAngle: { type: Number, value: 90 }, }, /** * 组件的初始数据 */ data: { CANVAS_WIDTH: 0, CANVAS_HEIGHT: 0, cutX: 0, // 裁剪框x轴起点,可用于裁剪框left区域宽度 cutY: 0, // 裁剪框y轴起点,可用于裁剪框top区域高度 clipWidth: 0, // 裁剪框宽度 clipHeight: 0, // 裁剪框高度 cutAnimation: false, //是否开启图片和裁剪框过渡 imageWidth: 0, // 图片宽度 imageHeight: 0, // 图片高度 imageTop: 0, // 图片上边距 imageLeft: 0, // 图片左边距 scale: 1, // 图片缩放比 angle: 0, // 图片旋转角度 _SYS_INFO: {}, _MOVE_THROTTLE: null, // 触摸移动节流setTimeout _MOVE_THROTTLE_FLAG: true, // 节流标识 _TIME_CUT_CENTER: null, // 自动居中节流函数 _flagCutTouch: false, // 是否开始拖动裁剪框 _flagEndTouch: false, // 是否结束拖动裁剪框 _CUT_START: {}, // 拖动裁剪框所需数据 _cutAnimationTime: null, _touchRelative: [{ x: 0, y: 0 }], // 手指或鼠标和图片中心的相对位置 _hypotenuseLength: 0, // 双指触摸时斜边长度 _ctx: null, // canvas上下文 }, /** * 组件监听器 */ observers: { // 监听图片url改变,获取图片信息 'imageUrl'(url) { if (!url) return; this.imageReset(); wx.showLoading({ title: '请稍候...', mask: true }); wx.getImageInfo({ src: url, success: res => { // 计算图片尺寸 this.imgComputeSize(res.width, res.height); if (this.properties.limitMove) { // 限制移动,不留空白处理 this.imgMarginDetectionScale(); eventUtil.emit(this, 'linimageready', res); } }, fail: () => { this.imgComputeSize(); if (this.properties.limitMove) { this.imgMarginDetectionScale(); } } }); }, 'clipWidth, clipHeight'(widthVal, heightVal) { let { minWidth, minHeight } = this.data; minWidth = minWidth / 2; minHeight = minHeight / 2; if (widthVal < minWidth) { dataUtil.setDiffData(this, { clipWidth: minWidth }); } if (heightVal < minHeight) { dataUtil.setDiffData(this, { clipHeight: minHeight }); } this.computeCutSize(); }, 'rotateAngle'(val) { dataUtil.setDiffData(this, { cutAnimation: true, angle: val }); }, 'angle'(val) { this.moveStop(); const { limitMove } = this.properties; if (limitMove && val % 90) { dataUtil.setDiffData(this, { angle: Math.round(val / 90) * 90 }); } this.imgMarginDetectionScale(); }, 'cutAnimation'(val) { // 开启过渡260毫秒之后自动关闭 clearTimeout(this.data._cutAnimationTime); if (val) { let _cutAnimationTime = setTimeout(() => { dataUtil.setDiffData(this, { cutAnimation: false }); }, 260); dataUtil.setDiffData(this, { _cutAnimationTime }); } }, 'limitMove'(val) { if (val) { if (this.data.angle % 90) { dataUtil.setDiffData(this, { angle: Math.round(this.data.angle / 90) * 90 }); } this.imgMarginDetectionScale(); } }, 'cutY, cutX'() { this.cutDetectionPosition(); }, 'width, height'(width, height) { if(width !== this.width) { dataUtil.setDiffData(this, {clipWidth: width / 2}); } if(height !== this.height) { dataUtil.setDiffData(this, {clipHeight: height / 2}); } } }, /** * 组件的方法列表 */ methods: { /** * 设置裁剪框的一些信息 */ setCutInfo() { const { width, height } = this.properties; const { _SYS_INFO } = this.data; // 本组件动态style默认单位为px,需将用户传入值/2 const clipWidth = width / 2; const clipHeight = height / 2; const cutY = (_SYS_INFO.windowHeight - clipHeight) / 2; const cutX = (_SYS_INFO.windowWidth - clipWidth) / 2; const imageLeft = _SYS_INFO.windowWidth / 2; const imageTop = _SYS_INFO.windowHeight / 2; const _ctx = wx.createCanvasContext('image-clipper', this); this.setData({ clipWidth, clipHeight, cutX, cutY, CANVAS_HEIGHT: clipHeight, CANVAS_WIDTH: clipWidth, _ctx, imageLeft, imageTop }); }, /** * 裁剪框居中 */ setCutCenter() { const { sysInfo, clipHeight, clipWidth, imageTop, imageLeft } = this.data; let sys = sysInfo || wx.getSystemInfoSync(); let cutY = (sys.windowHeight - clipHeight) * 0.5; let cutX = (sys.windowWidth - clipWidth) * 0.5; this.setData({ imageTop: imageTop - this.data.cutY + cutY, imageLeft: imageLeft - this.data.cutX + cutX, cutY, cutX }); }, /** * 开始拖动裁剪框 * 需在此处查找到是否拖动的裁剪框四角 */ clipTouchStart(event) { if (!this.properties.imageUrl) { wx.showToast({ title: '请选择图片', icon: 'none' }); return; } const currentX = event.touches[0].clientX; const currentY = event.touches[0].clientY; const { cutX, cutY, clipWidth, clipHeight } = this.data; const corner = determineDirection(cutX, cutY, clipWidth, clipHeight, currentX, currentY); this.moveDuring(); const _CUT_START = { width: clipWidth, height: clipHeight, x: currentX, y: currentY, cutY, cutX, corner }; this.setData({ _flagCutTouch: true, _flagEndTouch: true, _CUT_START }); }, /** * 拖动裁剪框 * 当拖动的裁剪框区域时处理数据 */ clipTouchMove(event) { if (!this.properties.imageUrl) { wx.showToast({ title: '请选择图片', icon: 'none' }); return; } // fix & optimize #1129 // 只针对单指点击做处理 if (event.touches.length !== 1) { return; } const { _flagCutTouch, _MOVE_THROTTLE_FLAG } = this.data; if (_flagCutTouch && _MOVE_THROTTLE_FLAG) { const { lockRatio, lockHeight, lockWidth } = this.properties; if (lockRatio && (lockWidth || lockHeight)) return; dataUtil.setDiffData(this, { _MOVE_THROTTLE_FLAG: false }); this.moveThrottle(); const clipData = clipTouchMoveOfCalculate(this.data, event); // fix #1129 // 原因:未做 clipTouchMoveOfCalculate 方法返回 undefined 值处理 if(clipData) { const { width, height, cutX, cutY } = clipData; if (!lockWidth && !lockHeight) { dataUtil.setDiffData(this, { clipWidth: width, clipHeight: height, cutX, cutY }); } else if (!lockWidth) { dataUtil.setDiffData(this, { clipWidth: width, cutX }); } else if (!lockHeight) { dataUtil.setDiffData(this, { clipHeight: height, cutY }); } this.imgMarginDetectionScale(); } } }, /** * 拖动裁剪框结束 * 当拖动的裁剪框区域时处理数据 */ clipTouchEnd() { this.moveStop(); this.setData({ _flagCutTouch: false }); }, /** * 清空之前的自动居中延迟函数 */ moveDuring() { clearTimeout(this.data._TIME_CUT_CENTER); }, /** * 停止移动时需要做的操作 * 清空之前的自动居中延迟函数并添加最新的 */ moveStop() { clearTimeout(this.data._TIME_CUT_CENTER); const _TIME_CUT_CENTER = setTimeout(() => { //动画启动 if (!this.data.cutAnimation) { dataUtil.setDiffData(this, { cutAnimation: true }); } this.setCutCenter(); }, 800); dataUtil.setDiffData(this, { _TIME_CUT_CENTER }); }, /** * 重置延迟函数 */ moveThrottle() { if (this.data._SYS_INFO.platform === 'android') { clearTimeout(this.data._MOVE_THROTTLE); const _MOVE_THROTTLE = setTimeout(() => { dataUtil.setDiffData(this, { _MOVE_THROTTLE_FLAG: true }); }, 800 / 40); dataUtil.setDiffData(this, { _MOVE_THROTTLE }); } else { dataUtil.setDiffData(this, { _MOVE_THROTTLE_FLAG: true }); } }, /** * 图片初始化 */ imageReset() { const sys = this.data._SYS_INFO || wx.getSystemInfoSync(); this.setData({ scale: 1, angle: 0, imageTop: sys.windowHeight / 2, imageLeft: sys.windowWidth / 2 }); }, /** * 图片加载完成 */ imageLoad() { this.imageReset(); wx.hideLoading(); eventUtil.emit(this, 'linimageload', detail); }, /** * 计算图片尺寸 */ imgComputeSize(width, height) { const { imageWidth, imageHeight } = calcImageSize(width, height, this.data); this.setData({ imageWidth, imageHeight }); }, /** * 图片边缘检测-缩放 */ imgMarginDetectionScale(scale) { if (!this.properties.limitMove) return; const currentScale = calcImageScale(this.data, scale); this.imgMarginDetectionPosition(currentScale); }, /** * 图片边缘检测-位置 */ imgMarginDetectionPosition(scale) { if (!this.properties.limitMove) return; const { scale: currentScale, left, top } = calcImageOffset(this.data, scale); dataUtil.setDiffData(this, { imageLeft: left, imageTop: top, scale: currentScale }); }, /** * 开始图片触摸 */ imageTouchStart(e) { this.setData({ _flagEndTouch: false }); const { imageLeft, imageTop } = this.data; // 双指左指坐标 const clientXForLeft = e.touches[0].clientX; const clientYForLeft = e.touches[0].clientY; let _touchRelative = []; if (e.touches.length === 1) { // 单指拖动 _touchRelative[0] = { x: clientXForLeft - imageLeft, y: clientYForLeft - imageTop }; this.setData({ _touchRelative }); } else { // 双指右指坐标 const clientXForRight = e.touches[1].clientX; const clientYForRight = e.touches[1].clientY; // 双指放大 let width = Math.abs(clientXForLeft - clientXForRight); let height = Math.abs(clientYForLeft - clientYForRight); // 勾股定理求出斜边长度 const _hypotenuseLength = calcPythagoreanTheorem(width, height); _touchRelative = [{ x: clientXForLeft - imageLeft, y: clientYForLeft - imageTop }, { x: clientXForRight - imageLeft, y: clientYForRight - imageTop } ]; this.setData({_touchRelative, _hypotenuseLength}); } }, /** * 图片放大旋转等操作 */ imageTouchMove(e) { const { _flagEndTouch, _MOVE_THROTTLE_FLAG } = this.data; if (_flagEndTouch || !_MOVE_THROTTLE_FLAG) return; // 双指左指坐标 const clientXForLeft = e.touches[0].clientX; const clientYForLeft = e.touches[0].clientY; dataUtil.setDiffData(this, { _MOVE_THROTTLE_FLAG: false }); this.moveThrottle(); this.moveDuring(); if (e.touches.length === 1) { //单指拖动 const { left, top } = imageTouchMoveOfCalcOffset(this.data, clientXForLeft, clientYForLeft); dataUtil.setDiffData(this, { imageLeft: left, imageTop: top}); // 图像边缘检测,防止截取到空白 this.imgMarginDetectionPosition(); } else { // 双指右指坐标 const clientXForRight = e.touches[1].clientX; const clientYForRight = e.touches[1].clientY; // 双指放大 let width = Math.abs(clientXForLeft - clientXForRight), height = Math.abs(clientYForLeft - clientYForRight), // 勾股定理求出斜边长度 hypotenuse = calcPythagoreanTheorem(width, height), // 斜边 scale = this.data.scale * (hypotenuse / this.data._hypotenuseLength); // 计算出真实缩放倍率 // 如果禁止缩放则倍率一直为1 if (this.properties.disableScale) { scale = 1; } else { scale = scale <= this.properties.minRatio ? this.properties.minRatio : scale; scale = scale >= this.properties.maxRatio ? this.properties.maxRatio : scale; eventUtil.emit(this, 'linsizechange', { imageWidth: this.data.imageWidth * scale, imageHeight: this.data.imageHeight * scale }); } this.imgMarginDetectionScale(scale); dataUtil.setDiffData(this, { _hypotenuseLength: Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)), scale }); } }, /** * 图片手指触摸结束 */ imageTouchEnd() { dataUtil.setDiffData(this, { _flagEndTouch: true }); this.moveStop(); }, /** * 检测剪裁框位置是否在允许的范围内(屏幕内) */ cutDetectionPosition() { const { cutX, cutY, _SYS_INFO, clipHeight, clipWidth } = this.data; let cutDetectionPositionTop = () => { //检测上边距是否在范围内 if (cutY < 0) { dataUtil.setDiffData(this, { cutY: 0 }); } if (cutY > _SYS_INFO.windowHeight - clipHeight) { dataUtil.setDiffData(this, { cutY: _SYS_INFO.windowHeight - clipHeight }); } }, cutDetectionPositionLeft = () => { //检测左边距是否在范围内 if (cutX < 0) { dataUtil.setDiffData(this, { cutX: 0 }); } if (cutX > _SYS_INFO.windowWidth - clipWidth) { dataUtil.setDiffData(this, { cutX: _SYS_INFO.windowWidth - clipWidth }); } }; //裁剪框坐标处理(如果只写一个参数则另一个默认为0,都不写默认居中) if (cutY === null && cutX === null) { let newCutY = (_SYS_INFO.windowHeight - clipHeight) * 0.5; let newCutX = (_SYS_INFO.windowWidth - clipWidth) * 0.5; dataUtil.setDiffData(this, { cutX: newCutX, // 截取的框上边距 cutY: newCutY // 截取的框左边距 }); } else if (cutY !== null && cutX !== null) { cutDetectionPositionTop(); cutDetectionPositionLeft(); } else if (cutY !== null && cutX === null) { cutDetectionPositionTop(); dataUtil.setDiffData(this, { cutX: (_SYS_INFO.windowWidth - clipWidth) / 2 }); } else if (cutY === null && cutX !== null) { cutDetectionPositionLeft(); dataUtil.setDiffData(this, { cutY: (_SYS_INFO.windowHeight - clipHeight) / 2 }); } }, /** * 改变截取框大小 */ computeCutSize() { const { clipHeight, clipWidth, _SYS_INFO, cutX, cutY } = this.data; if (clipWidth > _SYS_INFO.windowWidth) { // 设置裁剪框宽度 dataUtil.setDiffData(this, { clipWidth: _SYS_INFO.windowWidth }); } else if (clipWidth + cutX > _SYS_INFO.windowWidth) { dataUtil.setDiffData(this, { cutX: _SYS_INFO.windowWidth - cutX }); } if (clipHeight > _SYS_INFO.windowHeight) { // 设置裁剪框高度 dataUtil.setDiffData(this, { clipHeight: _SYS_INFO.windowHeight }); } else if (clipHeight + cutY > _SYS_INFO.windowHeight) { dataUtil.setDiffData(this, { cutY: _SYS_INFO.windowHeight - cutY }); } }, /** * 获取图片数据 */ getImageData() { if (!this.properties.imageUrl) { wx.showToast({ title: '请选择图片', icon: 'none' }); return; } wx.showLoading({ title: '加载中' }); const { clipHeight, clipWidth, _ctx, scale, imageLeft, imageTop, cutX, cutY, angle } = this.data; let { CANVAS_HEIGHT, CANVAS_WIDTH } = this.data; const { scaleRatio, imageUrl, quality, type: imageType } = this.properties; // 绘制函数 const draw = () => { // 图片真实大小 const imageWidth = this.data.imageWidth * scale * scaleRatio; const imageHeight = this.data.imageHeight * scale * scaleRatio; // canvas和图片的相对距离 const xpos = imageLeft - cutX; const ypos = imageTop - cutY; // 旋转画布 _ctx.translate(xpos * scaleRatio, ypos * scaleRatio); _ctx.rotate((angle * Math.PI) / 180); _ctx.drawImage(imageUrl, -imageWidth / 2, -imageHeight / 2, imageWidth, imageHeight); _ctx.draw(false, () => { let params = { width: clipWidth * scaleRatio, height: Math.round(clipHeight * scaleRatio), destWidth: clipWidth * scaleRatio, destHeight: Math.round(clipHeight) * scaleRatio, fileType: 'png', quality }; let data = { url: '', base64: '', width: clipWidth * scaleRatio, height: clipHeight * scaleRatio }; if (IMAGE_TYPE.base64 === imageType) { wx.canvasGetImageData({ canvasId: 'image-clipper', x: 0, y: 0, width: clipWidth * scaleRatio, height: Math.round(clipHeight * scaleRatio), success: res => { const arrayBuffer = new Uint8Array(res.data); const base64 = wx.arrayBufferToBase64(arrayBuffer); data.url = base64; data.base64 = base64; wx.hideLoading(); eventUtil.emit(this, 'linclip', data); } }); } else { wx.canvasToTempFilePath({ ...params, canvasId: 'image-clipper', success: res => { data.url = res.tempFilePath; data.base64 = res.tempFilePath; wx.hideLoading(); eventUtil.emit(this, 'linclip', data); }, fail(res) { throw res; } }, this ); } }); }; if (CANVAS_WIDTH !== clipWidth || CANVAS_HEIGHT !== clipHeight) { CANVAS_WIDTH = clipWidth; CANVAS_HEIGHT = clipHeight; _ctx.draw(); setTimeout(() => { draw(); }, 100); } else { draw(); } }, /** * 上传图片 */ uploadImage() { wx.chooseImage({ count: 1, sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], success: (res) => { const tempFilePaths = res.tempFilePaths; this.setData({ imageUrl: tempFilePaths }); } }); }, /** * 工具栏旋转 */ rotate(event) { if (this.properties.disableRotate) return; if (!this.properties.imageUrl) { wx.showToast({ title: '请选择图片', icon: 'none' }); return; } const { rotateAngle } = this.properties; const originAngle = this.data.angle; const type = event.currentTarget.dataset.type; if (type === 'along') { this.setData({ angle: originAngle + rotateAngle }); } else { this.setData({ angle: originAngle - rotateAngle}); } eventUtil.emit(this, 'linrotate', { currentDeg: this.data.angle }); }, /** * 关闭 */ close() { this.setData({ show: false }); }, /** * 空方法,占位用 */ doNothing() {} }, /** * 组件的生命周期 */ lifetimes: { ready() { const _SYS_INFO = wx.getSystemInfoSync(); this.setData({ _SYS_INFO }); this.setCutInfo(); this.setCutCenter(); this.computeCutSize(); this.cutDetectionPosition(); } } });