从0开始用taro做一个图片裁剪组件
基于Taro做一个可旋转,可缩放,可拖动的高性能裁剪组件。点赞的朋友升职加薪,迎娶白富美
github:github.com/huzhiwu1/im…
如果觉得这个组件对你有帮助的话,欢迎start
本文会按以下顺序讲解如何做一个高性能的图片裁剪组件
1. 参数说明
为了起步简单,除了 图片
和 裁剪框
的宽高比由外部参数传入,其他的参数皆为内部参数,由组件内部自定义
constructor(props) {
super(props);
this.state = {
imgSrc: props.imgSrc,
cut_ratio: props.cut_ratio, //裁剪框的 宽/高 比
_img_height: 0, //图片的高度
_img_width: 0, //图片的宽度
_img_ratio: 1, //图片的 宽/高 比
_img_left: 0, //图片相对可使用窗口的左边距
_img_top: 0, //图片相对可使用窗口的上边距
_window_height: 0, //可使用窗口的高度
_window_width: 0, //可使用窗口宽度
_canvas_width: 0, //canvas的宽度
_canvas_height: 0, //canvas的高度
_canvas_left: 0, //canvas相对可使用窗口的左边距
_canvas_top: 0, //canvas相对可使用窗口的上边距
_cut_width: 200, //裁剪框的宽度
_cut_height: 200, //裁剪框的高度
_cut_left: 0, //裁剪框相对可使用窗口的左边距
_cut_top: 0, //裁剪框相对可使用窗口的上边距
scale: 1, //默认图片的放大倍数
angle: 0, //图片旋转角度
max_scale: 2,//图片可以放大的最大倍数
min_scale: 0.5,// 图片可以缩小的最小倍数
};
}
2. 界面布局
界面大概可以分成3个部分:
- 裁剪框与灰色覆盖层
- 图片
- canvas
<View className="image-cropper-wrapper">
<View className="bg_container" />//裁剪框与灰色覆盖层
<Image/> //图片
<Canvas/>//画布
</View>
裁剪框与灰色覆盖层会覆盖在图片上,而图片需要响应触摸事件,所以我们要在 bg_container
加个样式 pointer-event:none
,使其不会成为鼠标事件的目标
裁剪框与灰色覆盖层可分成上中下三部分,而中间部分又可分左中右三部分:
裁剪框的8个白色的边角,可以由 <View>
绝对定位去实现
<View
className="cut_wrapper"
style={{
width: _cut_width + "px",
height: _cut_height + "px",
}}
>
<View className="border border-top-left"></View>
<View className="border border-top-right"></View>
<View className="border border-right-top"></View>
<View className="border border-bottom-right"></View>
<View className="border border-right-bottom"></View>
<View className="border border-bottom-left"></View>
<View className="border border-left-bottom"></View>
<View className="border border-left-top"></View>
</View>
.cut_wrapper {
position: relative;
.border {
background-color: rgba(255, 255, 255, 0.4);
position: absolute;
}
.border-top-left {
height: 4px;
top: -4px;
left: -4px;
width: 30rpx;
}
...
}
3. 初始化
在这一阶段,我们需要初始化以下参数:
- 获取
canvas
上下文 - 获取设备可使用窗口的大小
_window_height
_window_width
- 根据可使用窗口的大小和用户传入的裁剪框的宽高比例
_cut_ratio
,计算裁剪框的宽高_cut_height
_cut_width
- 让裁剪框居中,计算其相对可使用窗口的相对距离
_cut_left
_cut_right
- 获取用户传入的图片的信息,拿到图片的宽高比
_img_ratio
- 让图片的长边铺满裁剪框,根据图片的宽高比计算出短边,拿到图片的宽高
_img_width
_img_height
- 让图片居中,计算其相对可使用窗口的相对距离
_img_left
_img_top
async componentWillMount() {
this.initCanvas();
await this.getDeviceInfo();
await this.computedCutSize();
await this.computedCutDistance();
await this.initImageInfo();
await this.computedImageSize();
await this.computedImageDistance();
}
4. 图片的拖拽功能
<Image
className="img"
src={imgSrc}
style={{
top: _img_top + "px",
left: _img_left + "px",
}}
onTouchStart={this._img_touch_start}
onTouchMove={this._img_touch_move}
onTouchEnd={this._img_touch_end}
/>
- 触摸时,记录触摸点的位置,该触摸点的位置是相对图片的位置,这样图片才会随着手指无偏差移动,
e.touches[0]
是第一个手指触摸点的位置信息
_img_touch_start(e) {
this._touch_end_flag = false; //_touch_end_flag是结束触摸的标志,touchEnd中会赋值为true
if (e.touches.length === 1) {
// 单指触摸
// 记录下开始时的触摸点的位置
this._img_touch_relative[0] = {
//减去图片相对视口的位置,得到手指相对图片的左上角的位置x,y
x: e.touches[0].clientX - this.state._img_left,
y: e.touches[0].clientY - this.state._img_top,
};
}
}
- 记录移动的位置信息
_img_touch_move(e) {
//如果结束触摸,则不再移动
if (this._touch_end_flag) {
console.log("结束false");
return;
}
if (e.touches.length === 1) {
// 单指拖动
let left = e.touches[0].clientX - this._img_touch_relative[0].x;
let top = e.touches[0].clientY - this._img_touch_relative[0].y;
setTimeout(() => {// 加上setTimeout是为了立即触发setState
this.setState({
_img_left: left,
_img_top: top,
});
}, 0);
}
}
- 移动结束,
_img_touch_end() {
this._touch_end_flag = true;// 标记移动结束
}
5. 图片的缩放功能
缩放在移动端上的动作,一般都是 两根手指触摸点的距离的缩小或变大 ,所以我们需要在记录每次响应的两点的坐标,并计算他们之间的距离,再去计算相对上次的距离的变化
<Image
style={{
width: _img_width * scale + "px",
height: _img_height * scale + "px",
top: _img_top - (_img_height * (scale - 1)) / 2 + "px",
left: _img_left - (_img_width * (scale - 1)) / 2 + "px",
}}
/>
为了让图片的缩放中心在 图片的中心 ,图片缩放时,其左边距和上边距页需要响应的变化
_img_height * (scale - 1)// 计算高度 变高了 多少
_img_height * (scale - 1) / 2 //计算上边距应移动的距离,除以2是为了让上下边距都移动一半,才能居中
_img_top - (_img_height * (scale - 1)) / 2 + "px",//上边距移动的距离
- 触摸开始,记录两点之间的距离
_img_touch_start(e){
let width = Math.abs(e.touches[0].clientX - e.touches[1].clientX);
let height = Math.abs(e.touches[0].clientY - e.touches[1].clientY);
//计算两点之间的距离
this._hypotenuse_length = Math.sqrt(
Math.pow(width, 2) + Math.pow(height, 2)
);
}
- 两手指的缩放移动,记录两点的距离,与上次的距离比较,计算缩放的倍数
_img_touch_move(e){
//双指放大
let width = Math.abs(e.touches[0].clientX - e.touches[1].clientX);
let height = Math.abs(e.touches[0].clientY - e.touches[1].clientY);
let new_hypotenuse_length = Math.sqrt(
Math.pow(width, 2) + Math.pow(height, 2)
);
//放大的倍数,等于现在的倍数*(现在两点的距离/上次两点间的距离)
let newScale =this.state.scale *(new_hypotenuse_length / this._hypotenuse_length);
//如果缩放倍数超过max_scale或是min_scale,则不变化,
newScale =
newScale > this.state.max_scale ||
newScale < this.state.min_scale
? this.state.scale
: newScale;
this._hypotenuse_length = new_hypotenuse_length;
setTimeout(()=>{
this.setState({
scale:newScale
})
})
}
6. 图片的旋转功能
先介绍个工具函数 **Math.atan2(**``**)**
返回从原点(0,0)到(x,y)点的线段与x轴正方向之间的平面角度(弧度值)
为了让图片旋转的中心在图片的中心,所以记录触摸点时, 要记录触摸点相对图片中心的位置
图片中心的位置(X,Y)x==_img_left+_img_width/2
y==_img_top+_img_height/2
- 触摸开始,记录两点相对图片中心的位置
_img_touch_start(e){
//双指旋转
this._img_touch_relative = [
{
x:
e.touches[0].clientX -
this.state._img_left -
this.state._img_width / 2,
y:
e.touches[0].clientY -
this.state._img_top -
this.state._img_height / 2,
},
{
x:
e.touches[1].clientX -
this.state._img_left -
this.state._img_width / 2,
y:
e.touches[1].clientY -
this.state._img_top -
this.state._img_height / 2,
},
];
}
- 双指移动
_img_touch_move(e){
// 双指旋转,新的触摸两点相对图片中心的位置
let _new_img_touch_relative = [
{
x:
e.touches[0].clientX -
this.state._img_left -
this.state._img_width / 2,
y:
e.touches[0].clientY -
this.state._img_top -
this.state._img_height / 2,
},
{
x:
e.touches[1].clientX -
this.state._img_left -
this.state._img_width / 2,
y:
e.touches[1].clientY -
this.state._img_top -
this.state._img_height / 2,
},
];
// 第一根手指的旋转角度
let first_atan_old =
(180 / Math.PI) *
Math.atan2(
this._img_touch_relative[0].y,
this._img_touch_relative[0].x
);
let first_atan =
(180 / Math.PI) *
Math.atan2(
_new_img_touch_relative[0].y,
_new_img_touch_relative[0].x
);
let first_deg = first_atan - first_atan_old;
// 第二根手指的旋转角度
let second_atan_old =
(180 / Math.PI) *
Math.atan2(
this._img_touch_relative[1].y,
this._img_touch_relative[1].x
);
let second_atan =
(180 / Math.PI) *
Math.atan2(
_new_img_touch_relative[1].y,
_new_img_touch_relative[1].x
);
let second_deg = second_atan - second_atan_old;
// 当前的旋转角度
let current_deg = 0;
if (Math.abs(first_deg) > Math.abs(second_deg)) {
current_deg = first_deg;
} else {
current_deg = second_deg;
}
// console.log(this._img_touch_relative[1], "img_touch_relative");
this._img_touch_relative = _new_img_touch_relative;
setTimeout(() => {
this.setState(
(prevState) => ({
angle: prevState.angle + current_deg,
}),
() => {
// console.log(this.state.angle, "angle");
}
);
}, 0);
}
7. canvas的画布绘制
<Canvas
canvasId="my-canvas"
className="my-canvas-class"
disableScroll={false}//在画布上不会响应滚动轴事件
style={{
width: _canvas_width + "px",
height: _canvas_height + "px",
left: _canvas_left + "px",
top: _canvas_top + "px",
}}
></Canvas>
绘制图片前,需要给画布确定大小和位置, 其实画布的大小和位置都和裁剪框一致
需要解决的几个问题
- 图片和裁剪框的相对位置,这样才能知道绘制图片的哪一部分
// 用户移动旋转放大后的图像大小thu
let img_width = _img_width * scale;
let img_height = _img_height * scale;
// 图片和裁剪框的相对距离
let distX = _img_left - (_img_width * (scale - 1)) / 2 - _cut_left;
let distY = _img_top - (_img_height * (scale - 1)) / 2 - _cut_top;
知道相对距离后,即可移动画布的坐标轴
- 图片旋转后,canvas也许要旋转,
ctx.rotate()
旋转的中心是canvas
的坐标轴的原点,我们希望canvas
旋转的中心和图片的中心重合,这样才能完美绘制出旋转的图片
// 根据图像的旋转角度,旋转画布的坐标轴,
//为了旋转中心在图片的中心,需要先移动下画布的坐标轴
this.ctx.translate(
distX + img_width / 2,
distY + img_height / 2
);
this.ctx.rotate((angle * Math.PI) / 180);
this.ctx.translate(
-distX - img_width / 2,
-distY - img_height / 2
);
- 绘制图片
drawImage(imageResource, dx, dy, dWidth, dHeight)
dx | number | imageResource的左上角在目标 canvas 上 x 轴的位置 |
---|---|---|
dy | number | imageResource的左上角在目标 canvas 上 y 轴的位置 |
dWidth | number | 在目标画布上绘制imageResource的宽度,允许对绘制的imageResource进行缩放 |
dHeight | number | 在目标画布上绘制imageResource的高度,允许对绘制的imageResource进行缩放 |
// 绘制图像
this.ctx.drawImage(imgSrc, 0, 0, img_width, img_height);
this.ctx.draw(false, () => {
console.log("云心");
callback && callback();
});
完整代码
_draw(callback) {
const {
_cut_height,
_cut_width,
_cut_left,
_cut_top,
angle,
scale,
_img_width,
_img_height,
_img_left,
_img_top,
imgSrc,
} = this.state;
this.setState(
{
_canvas_height: _cut_height,
_canvas_width: _cut_width,
_canvas_left: _cut_left,
_canvas_top: _cut_top,
},
() => {
// 用户移动旋转放大后的图像大小thu
let img_width = _img_width * scale;
let img_height = _img_height * scale;
// 图片和裁剪框的相对距离
let distX =
_img_left - (_img_width * (scale - 1)) / 2 - _cut_left;
let distY =
_img_top - (_img_height * (scale - 1)) / 2 - _cut_top;
console.log(this.ctx, "ctx前");
// 根据图像的旋转角度,旋转画布的坐标轴,
//为了旋转中心在图片的中心,需要先移动下画布的坐标轴
this.ctx.translate(
distX + img_width / 2,
distY + img_height / 2
);
this.ctx.rotate((angle * Math.PI) / 180);
this.ctx.translate(
-distX - img_width / 2,
-distY - img_height / 2
);
console.log(this.ctx, "ctx");
//根据相对距离移动画布的原点
this.ctx.translate(distX, distY);
// 绘制图像
this.ctx.drawImage(imgSrc, 0, 0, img_width, img_height);
//draw(false)会清空上次的图像,重新绘制
this.ctx.draw(false, () => {
callback && callback();
});
}
);
}
8. 导出绘制图片的本地地址
_getImg() {
const { _cut_height, _cut_width, cut_ratio } = this.state;
return new Promise((resolve, reject) => {
this._draw(() => {
Taro.canvasToTempFilePath(
{
width: _cut_width,
height: _cut_height,
destWidth: 400,
destHeight: 400 / cut_ratio,
canvasId: "my-canvas",
fileType: "png",
success(res) {
console.log(res, "成功");
resolve(res);
},
fail(err) {
console.log(err, "err");
reject(err);
},
},
this.$scope //不这样写会报错,
);
});
});
}
结语
作者:胡志武
时间:2020/07/10
如果觉得文章写得不错的话,请点个赞吧,点赞的都是帅哥美女,点赞的都会升职加薪,哈哈哈,
如需转载,请注明出处
本文使用 mdnice 排版