Create an image cropping component with taro starting from 0

Build a rotatable, scalable, draggable high performance clipping component based on Taro. “Like” friends get promoted and pay rise, marry Bai Fumei



github:Github.com/huzhiwu1/im…

If you find this component helpful, welcome start

This article will explain how to make a high performance picture cropping component in the following order

  1. Parameters that
  2. Interface layout
  3. Initialize the
  4. Image drag-and-drop function
  5. Zoom in and out of pictures
  6. Image rotation function
  7. Canvas canvas drawing
  8. Export the local address of the drawing picture

1. Parameter Description

To make things easy to start with, all parameters are internal, defined internally by the component, except the aspect ratio of the image and cropping box, which are passed in as external parameters

constructor(props) {
    super(props);
    this.state = {
     imgSrc: props.imgSrc,
     cut_ratio: props.cut_ratio, // Trim the width/height ratio of the box
 _img_height: 0.// Height of the image  _img_width: 0.// Width of the image  _img_ratio: 1.// Width/height ratio of the image  _img_left: 0.// Images can be used relative to the left margin of the window  _img_top: 0.// The image can be used relative to the top margin of the window  _window_height: 0.// The height of the available window  _window_width: 0.// The window width can be used  _canvas_width: 0./ / the width of the canvas  _canvas_height: 0./ / the height of the canvas  _canvas_left: 0.// Canvas is relative to the left margin of the usable window  _canvas_top: 0.// Canvas relative to the top margin of the usable window  _cut_width: 200.// Trim the width of the box  _cut_height: 200.// Trim the height of the box  _cut_left: 0.// Clipping box can be used relative to the left margin of the window  _cut_top: 0.// Clipping box can be used relative to the top margin of the window  scale: 1.// The default image magnification  angle: 0.// Image rotation Angle  max_scale: 2.// The maximum magnification of an image  min_scale: 0.5.// The smallest number of times the image can be shrunk  }; } Copy the code



2. Interface layout

The interface can be roughly divided into three parts:

  • Cropped box with grey overlay
  • The picture
  • canvas
<View className="image-cropper-wrapper">
  <View className="bg_container" />// Clipping box with grey overlay  <Image/>/ / picture  <Canvas/>/ / the canvas</View>
Copy the code

Clipping box and gray overlay will cover the image, and the image needs to respond to touch events, so we need to inbg_containerAdd a stylepointer-event:noneSo that it does not become the target of mouse events



The clipping frame and gray overlay can be divided into upper, middle and lower parts, and the middle part can be divided into left, middle and right parts:



Trim the box’s 8 white corners, which can be made up of<View>Absolute positioning to achieve

<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> Copy the code
.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;  } .} Copy the code

3. The initialization

At this stage, we need to initialize the following parameters:

  1. To obtaincanvascontext
  2. Gets the size of the window available to the device_window_height _window_width
  3. Depending on the size of the available window and the ratio of width to height of the clipped box passed in by the user_cut_ratio, calculate the width and height of the clipping frame_cut_height _cut_width
  4. Center the clipping box and calculate its relative distance to the usable window_cut_left _cut_right
  5. Get the information of the picture that the user passes in, get the aspect ratio of the picture_img_ratio
  6. Fill the cropped box with the long side of the image. Calculate the short side based on the width to height ratio of the image and get the width and height of the image_img_width _img_height
  7. Center the image and calculate its relative distance to the usable window_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(); } Copy the code

4. Drag and drop function of pictures

<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} /> Copy the code
  1. When touching, record the position of the touch point relative to the picture, so that the picture will move with the finger without bias,

E.touches [0] is the location information of the touch point of the first finger

_img_touch_start(e) {
  this._touch_end_flag = false; //_touch_end_flag is the end of touch flag, touchEnd is set to true
  if (e.touches.length === 1) {
    // One finger touch
    // Record the position of the touch point at the beginning
 this._img_touch_relative[0] = {  // Subtract the position of the image relative to the viewport to get the position of the finger relative to the upper left corner of the image x,y  x: e.touches[0].clientX - this.state._img_left,  y: e.touches[0].clientY - this.state._img_top,  };  } } Copy the code
  1. Record the location of the movement
_img_touch_move(e) {
  // If you end the touch, no more movement
  if (this._touch_end_flag) {
    console.log(End of "false");
    return;
 }   if (e.touches.length === 1) {  // Drag with one finger  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((a)= > {// setTimeout is added to trigger setState immediately  this.setState({  _img_left: left,  _img_top: top,  });  }, 0);  } } Copy the code
  1. The end of the movement,
_img_touch_end() {
 this._touch_end_flag = true;// Mark the end of the move
}
Copy the code

5. Zoom function of pictures

Zoom action on the mobile end, generallyA narrowing or widening of the distance between the points touched by two fingers, so we need to record the coordinates of two points in each response and calculate the distance between them, and then calculate the change of the distance relative to the last time

<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". }} /> Copy the code

In order for the zoom center of the image to be in the center of the image, the left margin and the top margin page need to respond to changes when the image is scaled

_img_height * (scale - 1)// Calculate how much higher the height is
_img_height * (scale - 1) / 2 // Calculate how far the top distance should move, divide by 2 so that both the top and bottom distances move half in order to center
_img_top - (_img_height * (scale - 1)) / 2 + "px".// The distance the top distance moves
Copy the code
  1. The touch begins and records the distance between two points
_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);
  // Calculate the distance between two points
  this._hypotenuse_length = Math.sqrt(
 Math.pow(width, 2) + Math.pow(height, 2)  ); } Copy the code
  1. Two finger zoom movement, record the distance between two points, and the last distance comparison, calculate the zoom multiple
_img_touch_move(e){
// Double finger zoom
    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)  );  // The distance between two points now/the distance between two points last time  let newScale =this.state.scale *(new_hypotenuse_length / this._hypotenuse_length);  // If the scale is larger than max_scale or min_scale, it does not change,  newScale =  newScale > this.state.max_scale ||  newScale < this.state.min_scale  ? this.state.scale  : newScale;  this._hypotenuse_length = new_hypotenuse_length;  setTimeout((a)= >{  this.setState({  scale:newScale  })  }) } Copy the code

6. Image rotation function

Let’s start with a utility function

**Math.atan2(**``**)**Returns the plane Angle (radian value) between the line segment from the origin (0,0) to (x,y) and the positive X-axis



So that the center of the rotation of the image is in the center of the image, so when I record the touch point,Record the position of the touch point relative to the center of the picture 



Position in the center of the image (X,Y)

x==_img_left+_img_width/2 y==_img_top+_img_height/2

  1. Start by touching and record the position of two points relative to the center of the picture
_img_touch_start(e){
  // Double finger rotation
  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. },  ]; } Copy the code
  1. Double refers to mobile
_img_touch_move(e){
// Rotate the two fingers and touch the two points relative to the center of the image
    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. },  ];  // Rotation Angle of the first finger  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;  // Rotation Angle of the second finger  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;  // The current rotation Angle  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((a)= > {  this.setState(  (prevState) = > ({  angle: prevState.angle + current_deg,  }), () = > { // console.log(this.state.angle, "angle");  }  );  }, 0); } Copy the code

7. Canvas painting

<Canvas
  canvasId="my-canvas"
  className="my-canvas-class"
  disableScroll={false}// Does not respond to scroll axis events on the canvas
 style={{  width: _canvas_width+"px",  height: _canvas_height+"px",  left: _canvas_left+"px",  top: _canvas_top+"px",  }} ></Canvas> Copy the code

Before drawing a picture, you need to determine the size and position of the canvas. In fact, the size and position of the canvas are consistent with the clipping box

  1. The position of the image relative to the cropping box so that you know which part of the image to draw
// The user moves and rotates the enlarged image to thu size
let img_width = _img_width * scale;
let img_height = _img_height * scale;
// The relative distance between the image and the clipping box
let distX = _img_left - (_img_width * (scale - 1)) / 2 - _cut_left;
let distY = _img_top - (_img_height * (scale - 1)) / 2 - _cut_top; Copy the code

Once you know the relative distance, move the axis of the canvas

  1. After the image is rotated, the canvas might rotate,ctx.rotate()The center of rotation is zerocanvasThe origin of the coordinate axes, we wantcanvasThe center of the rotation coincides with the center of the image so that the rotating image is perfectly drawn
// Rotate the axis of the canvas according to the rotation Angle of the image,
// To rotate the center of the image, move the canvas's axes first
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 ); Copy the code
  1. Draw pictures

drawImage(imageResource, dx, dy, dWidth, dHeight)

dx number The upper-left corner of the imageResource is on the X-axis of the target canvas
dy number The upper-left corner of the imageResource is positioned on the Y-axis of the target canvas
dWidth number The width of the imageResource drawn on the target canvas, allowing the drawn imageResource to be scaled
dHeight number The height at which the imageResource is drawn on the target canvas, allowing the drawn imageResource to be scaled
// Draw the image
this.ctx.drawImage(imgSrc, 0.0, img_width, img_height);
this.ctx.draw(false, () = > {  console.log("Cloud heart");

 callback && callback(); }); Copy the code

The complete code

_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,  }, () = > { // The user moves and rotates the enlarged image to thu size  let img_width = _img_width * scale;  let img_height = _img_height * scale;  // The relative distance between the image and the clipping box  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, "Before the CTX");   // Rotate the axis of the canvas according to the rotation Angle of the image,  // To rotate the center of the image, move the canvas's axes first  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");  // Move the origin of the canvas according to the relative distance  this.ctx.translate(distX, distY);   // Draw the image  this.ctx.drawImage(imgSrc, 0.0, img_width, img_height);  //draw(false) clears the last image and redraws it  this.ctx.draw(false, () = > { callback && callback();  });  }  ); } Copy the code

8. Export the local address of the image to be drawn

_getImg() {
    const { _cut_height, _cut_width, cut_ratio } = this.state;
    return new Promise((resolve, reject) = > {
        this._draw((a)= > {
            Taro.canvasToTempFilePath(
 {  width: _cut_width,  height: _cut_height,  destWidth: 400. destHeight: 400 / cut_ratio,  canvasId: "my-canvas". fileType: "png". success(res) {  console.log(res, "Success");  resolve(res);  },  fail(err) {  console.log(err, "err");  reject(err);  },  },  this.$scope // If you do not write this, you will get an error.  );  });  }); } Copy the code

conclusion

Time: 2020/07/10 If you think the article is good, please give a thumbs-up, thumbs-up are all handsome men and beautiful women, thumbs-up will be promoted and pay rise, ha ha ha, if needed to be reprinted, please indicate the source

This article is formatted using MDNICE