@felix.lsf/taro-cropper
Version:
Taro框架下的图片裁剪组件封装,开箱即用
490 lines • 23 kB
JavaScript
// import Taro, {CanvasContext, getImageInfo, getSystemInfoSync} from '@tarojs/taro';
import Taro from '@tarojs/taro';
import React, { PureComponent, Fragment } from 'react';
// import {BaseEventOrig, Canvas, CoverView, View} from '@tarojs/components';
import { Canvas, View } from '@tarojs/components';
import { easySetFillStyle, easySetLineWidth, easySetStrokeStyle } from "./canvas-util";
class TaroCropperComponent extends PureComponent {
constructor(props) {
super(props);
this.imageLeft = 0;
this.imageTop = 0;
this.imageLeftOrigin = 0;
this.imageTopOrigin = 0;
this.width = 0;
this.height = 0;
this.cropperWidth = 0;
this.cropperHeight = 0;
this.realImageWidth = 0;
this.realImageHeight = 0;
this.scaleImageWidth = 0;
this.scaleImageHeight = 0;
this.touch0X = 0;
this.touch0Y = 0;
this.oldDistance = 0;
this.oldScale = 1;
this.newScale = 1;
this.lastScaleImageWidth = 0;
this.lastScaleImageHeight = 0;
this.update = this.update.bind(this);
this.handleOnTouchMove = this.handleOnTouchMove.bind(this);
this.handleOnTouchStart = this.handleOnTouchStart.bind(this);
this.handleOnTouchEnd = this.handleOnTouchEnd.bind(this);
this._drawCropperCorner = this._drawCropperCorner.bind(this);
this._drawCropperContent = this._drawCropperContent.bind(this);
this.systemInfo = Taro.getSystemInfoSync();
this.state = {
scale: 1,
};
}
/**
* 根据props更新长等信息
*/
updateInfo(props) {
const { width, height, cropperWidth, cropperHeight, src, fullScreen } = props;
this.width = fullScreen ? this.systemInfo.windowWidth : this._getRealPx(width);
this.height = fullScreen ? this.systemInfo.windowHeight : this._getRealPx(height);
this.cropperWidth = this._getRealPx(cropperWidth);
this.cropperHeight = this._getRealPx(cropperHeight);
if (!src)
return Promise.reject();
return Taro.getImageInfo({
src: src
})
.then((res) => {
this.imageInfo = res;
const imageWidth = res.width;
const imageHeight = res.height;
if (imageWidth / imageHeight < this.cropperWidth / this.cropperHeight) { // 宽度充满
this.scaleImageWidth = this.realImageWidth = this.cropperWidth;
this.scaleImageHeight = this.realImageHeight = this.realImageWidth * imageHeight / imageWidth;
this.imageLeftOrigin = this.imageLeft = (this.width - this.cropperWidth) / 2;
this.imageTopOrigin = this.imageTop = (this.height - this.realImageHeight) / 2;
}
else {
this.scaleImageHeight = this.realImageHeight = this.cropperHeight;
this.scaleImageWidth = this.realImageWidth = this.realImageHeight * imageWidth / imageHeight;
this.imageLeftOrigin = this.imageLeft = (this.width - this.realImageWidth) / 2;
this.imageTopOrigin = this.imageTop = (this.height - this.cropperHeight) / 2;
}
// h5端返回的如果是blob对象,需要转成image对象才可以用Canvas绘制
if (process.env.TARO_ENV === 'h5' && src.startsWith('blob:')) {
return new Promise((resolve, reject) => {
this.image = new Image();
this.image.src = src;
this.image.id = `taro_cropper_${src}`;
this.image.style.display = 'none';
document.body.append(this.image);
this.image.onload = resolve;
this.image.onerror = reject;
});
}
else {
return Promise.resolve();
}
});
}
componentDidMount() {
const { cropperCanvasId, cropperCutCanvasId, cropperWidth, cropperHeight, width, height, fullScreen, } = this.props;
const loadImage = () => {
const src = process.env.TARO_ENV === 'h5' ? this.image : this.imageInfo.path;
return new Promise((resolve) => {
const image = this.cropperCanvas.createImage();
image.onload = () => resolve(image);
image.src = src;
});
};
const initCanvas = () => {
Taro.nextTick(() => {
// 使用 Taro.nextTick 模拟 setData 已结束,节点已完成渲染
Taro.createSelectorQuery()
.selectAll(`#${cropperCanvasId},#${cropperCutCanvasId}`)
.node(async (res) => {
const cropperRes = res[1];
const cropperCutRes = res[0];
this.cropperCanvas = cropperRes.node;
this.cropperCutCanvas = cropperCutRes.node;
// Canvas 绘制上下文
this.cropperCanvasContext = this.cropperCanvas.getContext('2d');
this.cropperCutCanvasContext = this.cropperCutCanvas.getContext('2d');
// 初始化画布大小
// const dprRel = this.systemInfo.pixelRatio;
const dprRel = 750 / this.systemInfo.windowWidth;
// Canvas 画布的实际绘制宽高
this.cropperCanvas.width = (fullScreen ? this.systemInfo.windowWidth : width) * dprRel;
this.cropperCanvas.height = (fullScreen ? this.systemInfo.windowHeight : height) * dprRel;
this.cropperCanvasContext.scale(dprRel, dprRel);
this.cropperCutCanvas.width = cropperWidth * dprRel;
this.cropperCutCanvas.height = cropperHeight * dprRel;
this.cropperCutCanvasContext.scale(dprRel, dprRel);
await this.updateInfo(this.props);
this.imageToDraw = await loadImage();
this.update();
})
.exec();
});
};
if (process.env.TARO_ENV == 'h5') {
setTimeout(() => {
initCanvas();
}, 500);
}
else {
initCanvas();
}
}
/**
* 单位转换
* @param value
* @private
*/
_getRealPx(value) {
return value / 750 * this.systemInfo.screenWidth;
}
/**
* 绘制裁剪框的四个角
* @private
*/
_drawCropperCorner() {
const { themeColor } = this.props;
const lineWidth = 2;
const lineLength = 10;
const cropperStartX = (this.width - this.cropperWidth) / 2;
const cropperStartY = (this.height - this.cropperHeight) / 2;
this.cropperCanvasContext.beginPath();
easySetStrokeStyle(this.systemInfo, this.cropperCanvasContext, themeColor);
easySetLineWidth(this.systemInfo, this.cropperCanvasContext, lineWidth);
// 左上角
this.cropperCanvasContext.moveTo(cropperStartX, cropperStartY);
this.cropperCanvasContext.lineTo(cropperStartX + lineLength, cropperStartY);
this.cropperCanvasContext.moveTo(cropperStartX, cropperStartY - lineWidth / 2);
this.cropperCanvasContext.lineTo(cropperStartX, cropperStartY + lineLength);
// 右上角
this.cropperCanvasContext.moveTo(cropperStartX + this.cropperWidth, cropperStartY);
this.cropperCanvasContext.lineTo(cropperStartX + this.cropperWidth - lineLength, cropperStartY);
this.cropperCanvasContext.moveTo(cropperStartX + this.cropperWidth, cropperStartY - lineWidth / 2);
this.cropperCanvasContext.lineTo(cropperStartX + this.cropperWidth, cropperStartY + lineLength);
// 左下角
this.cropperCanvasContext.moveTo(cropperStartX, cropperStartY + this.cropperHeight);
this.cropperCanvasContext.lineTo(cropperStartX + lineLength, cropperStartY + this.cropperHeight);
this.cropperCanvasContext.moveTo(cropperStartX, cropperStartY + this.cropperHeight + lineWidth / 2);
this.cropperCanvasContext.lineTo(cropperStartX, cropperStartY + this.cropperHeight - lineLength);
// 右下角
this.cropperCanvasContext.moveTo(cropperStartX + this.cropperWidth, cropperStartY + this.cropperHeight);
this.cropperCanvasContext.lineTo(cropperStartX + this.cropperWidth - lineLength, cropperStartY + this.cropperHeight);
this.cropperCanvasContext.moveTo(cropperStartX + this.cropperWidth, cropperStartY + this.cropperHeight + lineWidth / 2);
this.cropperCanvasContext.lineTo(cropperStartX + this.cropperWidth, cropperStartY + this.cropperHeight - lineLength);
this.cropperCanvasContext.closePath();
this.cropperCanvasContext.stroke();
}
/**
* 绘制裁剪框区域的图片
* @param props
* @param image 待绘制的图片对象
* @param deviationX 图片绘制x向偏移
* @param deviationY 图片绘制y向偏移
* @param imageWidth 图片的原始宽度
* @param imageHeight 图片的原始高度
* @param drawWidth 图片的绘制宽度
* @param drawHeight 图片的绘制高度
* @param reverse
* @private
*/
_drawCropperContent(
// props: TaroCropperComponentProps,
image, deviationX, deviationY, imageWidth, imageHeight, drawWidth, drawHeight) {
this._drawCropperCorner();
const cropperStartX = (this.width - this.cropperWidth) / 2;
const cropperStartY = (this.height - this.cropperHeight) / 2;
const cropperImageX = (cropperStartX - deviationX) / drawWidth * imageWidth;
const cropperImageY = (cropperStartY - deviationY) / drawHeight * imageHeight;
const cropperImageWidth = this.cropperWidth / drawWidth * imageWidth;
const cropperImageHeight = this.cropperHeight / drawHeight * imageHeight;
// 绘制裁剪框内裁剪的图片
// console.info('💢felix => update => content drawImage', {cropperImageX, cropperImageY, cropperImageWidth, cropperImageHeight,
// cropperStartX, cropperStartY, cropperWidth: this.cropperWidth, cropperHeight: this.cropperHeight})
this.cropperCanvasContext.drawImage(image, cropperImageX, cropperImageY, cropperImageWidth, cropperImageHeight, cropperStartX, cropperStartY, this.cropperWidth, this.cropperHeight);
this.cropperCutCanvasContext.drawImage(image, cropperImageX, cropperImageY, cropperImageWidth, cropperImageHeight, 0, 0, this.cropperWidth, this.cropperHeight);
}
update() {
if (!this.cropperCanvasContext || !this.cropperCutCanvasContext) {
return;
}
// 绘制前清空画布
this.cropperCanvasContext.clearRect(0, 0, this.cropperCanvas.width + 1, this.cropperCanvas.height + 1);
this.cropperCutCanvasContext.clearRect(0, 0, this.cropperCutCanvas.width + 1, this.cropperCutCanvas.height + 1);
if (!this.imageInfo || !this.imageToDraw) { // 图片资源无效则不执行更新操作
this._drawCropperCorner();
return;
}
// console.info('💢felix => update => CCC drawImage',
// {imageLeft: this.imageLeft, imageTop: this.imageTop, scaleImageWidth: this.scaleImageWidth, scaleImageHeight: this.scaleImageHeight})
this.cropperCanvasContext.drawImage(this.imageToDraw, 0, 0, this.imageInfo.width, this.imageInfo.height, this.imageLeft, this.imageTop, this.scaleImageWidth, this.scaleImageHeight);
// 绘制半透明层
this.cropperCanvasContext.beginPath();
easySetFillStyle(this.systemInfo, this.cropperCanvasContext, 'rgba(0, 0, 0, 0.3)');
this.cropperCanvasContext.fillRect(0, 0, this.width, this.height);
this.cropperCanvasContext.fill();
// 绘制裁剪框内部的区域
this._drawCropperContent(this.imageToDraw, this.imageLeft, this.imageTop, this.imageInfo.width, this.imageInfo.height, this.scaleImageWidth, this.scaleImageHeight);
}
/**
* 图片资源有更新则重新绘制
* @param nextProps
* @param nextContext
*/
componentWillReceiveProps(nextProps, nextContext) {
if (JSON.stringify(nextProps) != JSON.stringify(this.props)) {
// console.log('💢felix => componentWillReceiveProps => this.props', JSON.stringify(this.props));
// console.log('💢felix => componentWillReceiveProps => nextProps', JSON.stringify(nextProps));
this.updateInfo(nextProps)
.then(() => {
this.update();
});
}
return super.componentWillReceiveProps && super.componentWillReceiveProps(nextProps, nextContext);
}
/**
* 图片移动边界检测
* @param imageLeft
* @param imageTop
* @private
*/
_outsideBound(imageLeft, imageTop) {
this.imageLeft =
imageLeft > (this.width - this.cropperWidth) / 2
?
(this.width - this.cropperWidth) / 2
:
((imageLeft + this.scaleImageWidth) >= (this.width + this.cropperWidth) / 2
?
imageLeft
:
(this.width + this.cropperWidth) / 2 - this.scaleImageWidth);
this.imageTop =
imageTop > (this.height - this.cropperHeight) / 2
?
(this.height - this.cropperHeight) / 2
:
((imageTop + this.scaleImageHeight) >= (this.height + this.cropperHeight) / 2
?
imageTop
:
(this.height + this.cropperHeight) / 2 - this.scaleImageHeight);
}
_oneTouchStart(touch) {
this.touch0X = touch.x;
this.touch0Y = touch.y;
}
_twoTouchStart(touch0, touch1) {
const xMove = touch1.x - touch0.x;
const yMove = touch1.y - touch0.y;
this.lastScaleImageWidth = this.scaleImageWidth;
this.lastScaleImageHeight = this.scaleImageHeight;
// 计算得到初始时两指的距离
this.oldDistance = Math.sqrt(xMove * xMove + yMove * yMove);
}
_oneTouchMove(touch) {
const xMove = touch.x - this.touch0X;
const yMove = touch.y - this.touch0Y;
this._outsideBound(this.imageLeftOrigin + xMove, this.imageTopOrigin + yMove);
this.update();
}
_getNewScale(oldScale, oldDistance, touch0, touch1) {
const xMove = touch1.x - touch0.x;
const yMove = touch1.y - touch0.y;
const newDistance = Math.sqrt(xMove * xMove + yMove * yMove);
return oldScale + 0.02 * (newDistance - oldDistance);
}
_twoTouchMove(touch0, touch1) {
const { maxScale } = this.props;
const realMaxScale = maxScale >= 1 ? maxScale : 1;
const oldScale = this.oldScale;
const oldDistance = this.oldDistance;
this.newScale = this._getNewScale(oldScale, oldDistance, touch0, touch1);
// 限制缩放
this.newScale <= 1 && (this.newScale = 1);
this.newScale > realMaxScale && (this.newScale = realMaxScale);
this.scaleImageWidth = this.realImageWidth * this.newScale;
this.scaleImageHeight = this.realImageHeight * this.newScale;
const imageLeft = this.imageLeftOrigin - (this.scaleImageWidth - this.lastScaleImageWidth) / 2;
const imageTop = this.imageTopOrigin - (this.scaleImageHeight - this.lastScaleImageHeight) / 2;
this._outsideBound(imageLeft, imageTop);
this.update();
}
handleOnTouchEnd() {
this.oldScale = this.newScale;
this.imageLeftOrigin = this.imageLeft;
this.imageTopOrigin = this.imageTop;
}
handleOnTouchStart(e) {
const { src } = this.props;
if (!src)
return;
// @ts-ignore
const touch0 = e.touches[0];
// @ts-ignore
const touch1 = e.touches[1];
// 计算第一个触摸点的位置,并参照改点进行缩放
this._oneTouchStart(touch0);
// 两指手势触发
// @ts-ignore
if (e.touches.length >= 2) {
this._twoTouchStart(touch0, touch1);
}
}
handleOnTouchMove(e) {
const { src } = this.props;
if (!src)
return;
// 单指手势触发
// @ts-ignore
if (e.touches.length === 1) {
// @ts-ignore
this._oneTouchMove(e.touches[0]);
// @ts-ignore
}
else if (e.touches.length >= 2) { // 双指手势触发
// @ts-ignore
this._twoTouchMove(e.touches[0], e.touches[1]);
}
}
/**
* 将当前裁剪框区域的图片导出
*/
cut() {
const { fileType, quality } = this.props;
return new Promise((resolve, reject) => {
// const scope = process.env.TARO_ENV === 'h5' ? this : getCurrentInstance().page;
Taro.canvasToTempFilePath({
canvas: this.cropperCutCanvas,
x: 0,
y: 0,
width: this._getRealPx(this.cropperWidth) - 2,
height: this._getRealPx(this.cropperHeight) - 2,
destWidth: this.cropperWidth * this.systemInfo.pixelRatio,
destHeight: this.cropperHeight * this.systemInfo.pixelRatio,
fileType: fileType,
quality: quality,
success: res => {
switch (process.env.TARO_ENV) {
case 'alipay':
resolve({
errMsg: res.errMsg,
filePath: res.tempFilePath
});
break;
case 'weapp':
case 'qq':
case 'h5':
default:
resolve({
errMsg: res.errMsg,
filePath: res.tempFilePath
});
break;
}
},
fail: err => {
reject(err);
},
complete: () => {
}
}, this);
});
}
render() {
const { width, height, cropperCanvasId, fullScreen, fullScreenCss, themeColor, hideFinishText, cropperWidth, cropperHeight, cropperCutCanvasId, hideCancelText, onCancel, finishText, cancelText } = this.props;
const _width = fullScreen ? this.systemInfo.windowWidth : this._getRealPx(width);
const _height = fullScreen ? this.systemInfo.windowHeight : this._getRealPx(height);
const _cropperWidth = this._getRealPx(cropperWidth);
const _cropperHeight = this._getRealPx(cropperHeight);
const isFullScreenCss = fullScreen && fullScreenCss;
const cropperStyle = isFullScreenCss ? {} : {
position: 'relative'
};
const canvasStyle = {
background: 'rgba(0, 0, 0, 0.8)',
position: 'relative',
width: `${_width}px`,
height: `${_height}px`
};
const cutCanvasStyle = {
position: 'absolute',
left: `${(_width - _cropperWidth) / 2}px`,
top: `${(_height - _cropperHeight) / 2}px`,
width: `${_cropperWidth}px`,
height: `${_cropperHeight}px`,
};
let finish = null;
let cancel = null;
// const isH5 = process.env.TARO_ENV === 'h5';
if (!hideFinishText) {
const finishStyle = {
color: themeColor,
};
const onFinishClick = () => {
this.cut()
.then(res => {
this.props.onCut && this.props.onCut(res.filePath);
})
.catch(err => {
this.props.onFail && this.props.onFail(err);
});
};
// if (!isH5) {
finish = React.createElement(View, { className: 'btn finish', style: finishStyle, onTap: onFinishClick }, finishText || '确认');
// } else {
// finish = <View
// style={finishStyle}
// onClick={onFinishClick}
// >
// 完成
// </View>
// }
}
if (!hideCancelText) {
const cancelStyle = {
color: themeColor,
};
cancel = React.createElement(View, { className: 'btn cancel', style: cancelStyle, onTap: onCancel }, cancelText);
}
return (React.createElement(Fragment, null,
React.createElement(View, { className: `taro-cropper ${isFullScreenCss ? 'taro-cropper-fullscreen' : ''}`, style: cropperStyle },
React.createElement(Canvas, { type: '2d', id: cropperCutCanvasId, style: cutCanvasStyle, className: `cut-canvas-item ${isFullScreenCss ? 'cut-canvas-fullscreen' : ''}` }),
React.createElement(Canvas, { type: '2d', id: cropperCanvasId, onTouchStart: this.handleOnTouchStart, onTouchMove: this.handleOnTouchMove, onTouchEnd: this.handleOnTouchEnd, style: canvasStyle, className: `canvas-item ${isFullScreenCss ? 'canvas-fullscreen' : ''}`, disableScroll: true })),
React.createElement(View, { className: 'bottom-wapper' },
React.createElement(View, { className: 'bottom-area' },
!hideCancelText &&
cancel,
!hideFinishText &&
finish))));
}
}
TaroCropperComponent.defaultProps = {
width: 750,
height: 1200,
cropperWidth: 400,
cropperHeight: 400,
cropperCanvasId: 'cropperCanvasId',
cropperCutCanvasId: 'cropperCutCanvasId',
src: '',
themeColor: '#00ff00',
maxScale: 3,
fullScreen: false,
fullScreenCss: false,
hideFinishText: false,
hideCancelText: true,
finishText: '完成',
cancelText: '取消',
fileType: 'jpg',
quality: 1,
onCancel: () => {
},
onCut: () => {
},
onFail: () => {
},
};
export default TaroCropperComponent;
//# sourceMappingURL=index.js.map