从0开始用taro做一个图片裁剪组件

4,265 阅读8分钟

从0开始用taro做一个图片裁剪组件

基于Taro做一个可旋转,可缩放,可拖动的高性能裁剪组件。点赞的朋友升职加薪,迎娶白富美

1594377328306.gif
github:github.com/huzhiwu1/im…
如果觉得这个组件对你有帮助的话,欢迎start
本文会按以下顺序讲解如何做一个高性能的图片裁剪组件

  1. 参数说明
  2. 界面布局
  3. 初始化
  4. 图片的拖拽功能
  5. 图片的缩放功能
  6. 图片的旋转功能
  7. canvas的画布绘制
  8. 导出绘制图片的本地地址

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,// 图片可以缩小的最小倍数
    };
}

image.png

2. 界面布局

界面大概可以分成3个部分:

  • 裁剪框与灰色覆盖层
  • 图片
  • canvas
<View className="image-cropper-wrapper">
  <View className="bg_container" />//裁剪框与灰色覆盖层
  <Image/> //图片
  <Canvas/>//画布
</View>

裁剪框与灰色覆盖层会覆盖在图片上,而图片需要响应触摸事件,所以我们要在 bg_container 加个样式 pointer-event:none ,使其不会成为鼠标事件的目标

裁剪框与灰色覆盖层可分成上中下三部分,而中间部分又可分左中右三部分:
image.png
裁剪框的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. 初始化

在这一阶段,我们需要初始化以下参数:

  1. 获取 canvas 上下文
  2. 获取设备可使用窗口的大小 _window_height _window_width
  3. 根据可使用窗口的大小和用户传入的裁剪框的宽高比例 _cut_ratio ,计算裁剪框的宽高 _cut_height _cut_width
  4. 让裁剪框居中,计算其相对可使用窗口的相对距离 _cut_left _cut_right
  5. 获取用户传入的图片的信息,拿到图片的宽高比 _img_ratio
  6. 让图片的长边铺满裁剪框,根据图片的宽高比计算出短边,拿到图片的宽高 _img_width _img_height
  7. 让图片居中,计算其相对可使用窗口的相对距离 _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}
/>
  1. 触摸时,记录触摸点的位置,该触摸点的位置是相对图片的位置,这样图片才会随着手指无偏差移动,

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,
    };
  }
}
  1. 记录移动的位置信息
_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);
  }
}
  1. 移动结束,
_img_touch_end() {
 this._touch_end_flag = true;// 标记移动结束
}

5. 图片的缩放功能

缩放在移动端上的动作,一般都是 两根手指触摸点的距离的缩小或变大 ,所以我们需要在记录每次响应的两点的坐标,并计算他们之间的距离,再去计算相对上次的距离的变化
image.png

<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",//上边距移动的距离
  1. 触摸开始,记录两点之间的距离
_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)
  );
}
  1. 两手指的缩放移动,记录两点的距离,与上次的距离比较,计算缩放的倍数
_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轴正方向之间的平面角度(弧度值)
image.png
为了让图片旋转的中心在图片的中心,所以记录触摸点时, 要记录触摸点相对图片中心的位置 
image.png
图片中心的位置(X,Y)
x==_img_left+_img_width/2 y==_img_top+_img_height/2

  1. 触摸开始,记录两点相对图片中心的位置
_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,
    },
  ];
}
  1. 双指移动
_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>

绘制图片前,需要给画布确定大小和位置, 其实画布的大小和位置都和裁剪框一致 
需要解决的几个问题

  1. 图片和裁剪框的相对位置,这样才能知道绘制图片的哪一部分
// 用户移动旋转放大后的图像大小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;

知道相对距离后,即可移动画布的坐标轴
image.png

  1. 图片旋转后,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
);
  1. 绘制图片

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 排版