preface

These functions are used in a few days ago to write a small program, so this article is a small program for the main framework. But in fact, it doesn’t matter if you don’t know small programs, you just need to know JS. It will focus on how to use Canvas to realize these functions. It is the key to learn how to operate canvas coordinates flexibly. 😄

Update record

  • Updated the Vue version and released the Vue version online

The online preview

Click online preview to view the effect [Only supports mobile terminal, please switch to mobile terminal on PC, nuggets built-in browser cannot read pictures, please use another browser]

Effect of demonstration

Implementation approach

The idea is to get an image from the index, push it into an array, and pass the array into the Canvas-Drag component, which does drag-and-zoom and other functions

Realize the upload and storage of pictures

Wx. ChooseImage ({count: 1, sizeType: [) // this.js addImg(e){const this1 = this. wx."original"."compressed"].sourceType: ["album"], success(res) {const tempFilePaths = res.tempfilepaths [0] // API wx.getimageInfo ({SRC: TempFilePaths, success(res) {const {width,height} = res const scale = this. getScale(width,height) Const obj = {width:width/scale, height:height/scale, url: tempFilePaths } const imgArr = this1.data.imgArr imgArr.push(obj) this1.setData({ imgArr }) } }) } }) }Copy the code

Get canvas object

Through a small program in the drag and drop components apiwx. CreateCanvasContext (string canvasId, Object this) for canvas Object, because we are in the component within the call, so we have to put this in the past, Represents looking for the canvas with the corresponding ID inside the component.

//canvas-drag.js // call fetch in component lifecycle readyready(){
    this.data.ctx = wx.createCanvasContext("canvas",this);
  },
Copy the code

The class that defines the image

The constructor of this class takes two arguments, an object containing the image width and link, and a Canvas object.

Class dragImg {constructor(img, CTX){this.x = 100; this.y = 100; // this. W = img.width; this.h = img.height; this.url = img.url; this.ctx = ctx; this.rotate = 0; this.selected =true; }}Copy the code

Receives and listens to external arrays and draws pictures on the canvas

Introduction to API and definition of class methods

To draw an image on a Canvas, start with two apis

  • ctx.drawImageDescribe or paint an image to canvas.Note: in the applet version, describe first and then paint, while in the non-applet version, paint directly onto the canvas
  • ctx.drawDraw the previous description (path, deformation, style) in the drawing context to the canvas.Note: this API is only available in applets
  • ctx.clearRectClear the contents of a specified areaUse in non – applets for heavy painting cloth

We define a class method on the image class that describes the image to the Canvas

  paint() {this.centerx = this.x + this.w / 2; this.centerY = this.y + this.h / 2; DrawImage (this.url, this.x, this.y, this.w, this.h); drawImage(this.url, this.x, this.y, this.w, this.h); // If it is selected, draw select dotted box, and zoom icon, delete iconif(this.ctx.setlinedAsh ([10, 10]); // This.ctx.setlinedash ([10, 10]); // This.ctx.setlinedash ([10, 10]); this.ctx.setLineWidth(2); this.ctx.setStrokeStyle("red"); this.ctx.lineDashOffset = 10; this.ctx.strokeRect(this.x, this.y, this.w, this.h); this.ctx.drawImage(CloseIcon, this.x - 15, this.y - 15, 24, 24); this.ctx.drawImage(ScaleIcon, this.x + this.w - 15, this.y + this.h - 15, 24, 24); }}Copy the code

Small program version drawing and redrawing

Each time an image item is pushed into the external array, the component instantiates and executes the paint method inside the instance, and finally calls the Draw method on the Canvas object to draw on the canvas. However, since the description is empty after the draw method is called, this means that if a new image item is inserted, the next time we execute draw, the previously described image will be gone, which is not the desired effect. So we push each image instance object into the dragArr array to manage. Each time we have a new image item, we iterate over the dragArr array, execute the paint method for each image instance, and call CTX’s Draw method when the loop ends. Since this drawing code will be reused later, we wrap it into a method to call.

//canvas-drag.js
 properties: {
    imgArr: {
      type: Array,
      value: [],
      observer: "onArrChange"
    },
  },
  onArrChange(arr){
      ifSlice (-1)[0] const item = new dragImg(newImg, this.data.ctx) this.data.dragArr.push(item) this.draw() } },draw(){
    this.data.dragArr.forEach((item) => {
      item.paint()
    })
    this.data.ctx.draw()
  }
Copy the code

Non – applets version drawing and redrawing

In the non-applets version, if you don’t clear the artboard, you will keep drawing inside, so we need to clean the artboard every time we go through the set of redraws to get the same effect as in the applets version.

draw() {// Clear the artboard this.ctx.clearRect(0, 0, this.c.width, This.c.eight) this.dragarr.foreach ((item) => {item.paint()})Copy the code

Whether it’s drawing a new image to the canvas, or pan, rotate, scale or redraw the image, we finally update the contents of the artboard by calling draw method so that we can draw the image on the canvas, let’s look at the effect

Click position (first edition)

The problem comes, since we can only bind click events on canvas, how can we determine whether the current click is in the blank or the picture, if it is in the picture, where the picture is, whether to click delete, click drag and drop or click the picture itself. Let’s take a look at the value of the click event distributed after clicking on the canvas.

The new method isInWhere

In this case, we will add a new method isInWhere on the image instance, passing in the xy coordinate at the click point, and return the relationship between the xy coordinate and the image instance.

IsInWhere (x, y) {// Transform the coordinates of the upper-left corner of the region and the height width of the regionlet transformW = 24;
    let transformH = 24;
    let transformX = this.x + this.w ;
    lettransformY = this.y + this.h ; // Remove the coordinates in the upper left corner of the region and the height width of the regionlet delW = 24;
    let delH = 24;
    let delX = this.x ;
    letdelY = this.y ; Move the coordinates of the regionlet moveX = this.x;
    let moveY = this.y;
    if(x - transformX >= 0 && y - transformY >= 0 && transformX + transformW - x >= 0 && transformY + transformH - y >= 0) { // Scale the areareturn "transform";
    }
    else if(x-delx >= 0&&y-dely >= 0&&delx + delw-x >= 0&&dely + delh-y >= 0) {// Delete the regionreturn "del";
    }
    else if(x-movex >= 0 && y-movey >= 0 && moveX + this.w-x >= 0 && moveY + this.h-y >= 0) {// Move the regionreturn "move"; } // Not in the selected areareturn false;
  }
Copy the code

Called when clicked

Bind the click event to the canvas, traverse the dragArr array for each click, and call the isInWhere method for each instance

start(e){
      const {x,y} = e.touches[0]
      this.data.dragArr.forEach((item)=>{
        const place = item.isInWhere(x,y)
      })
    }
Copy the code

Console. log(place) prints the result to see the effect

Click on multiple layers

At this time there are careful children’s shoes to ask, if there are multiple layers in the click place, do not directly take off


Start (e) {// Initialize an array to hold all clicked image objects this.data.clickedKarr = [] const {x, y } = e.touches[0] this.data.dragArr.forEach((item) => { const place = item.isInWhere(x, Y) item.place = place flase item.selected = flase item.selected = flase item.selected = flase item.selected = flase item.selected = flase item.selected = flase item.selected = flase item.selected =false
        if (place==='move'&&'transform') {// If place is notfalseOr del is pushed into the array. This data. ClickedkArr. Push (item)}}) const length = this. Data. ClickedkArr. Lengthif(length) {// We know that cavans is drawing images at higher and higher levels, so we take the last item of the array, Const lastImg = this.data.clickedKarr [length-1] // Set the selected value of this instance totrue, the next redraw will draw the border lastimg.selected =true// Save the selected instance this.data.lastImg = lastImg // Save the initial value of the instance. LastImg. J y, initialH: lastImg. J h, initialW: lastImg. W, initialRotate: lastImg. Rotate}} / / redraw this. The draw () / / save click coordinates, StartTouch = {startX: x, startY: y}}Copy the code

All right, let’s see what happens

Achieve translation effect

Translation simply change the x and y coordinates of the selected instance according to the translation amount, and then redraw it.

move(e) {
      const { x, y } = e.touches[0]
      const { initialX, initialY } = this.data.initialXY
      const { startX, startY } = this.data.startTouch
      const lastImg = this.data.lastImg
      if (this.data.clickedkArr.length) {
        ifLastimg.x = initialX + (x-startx) lastimg.y = initialY + (y -) {// Calculate the difference between the xy coordinates after the move and the xy coordinates when clicked (i.e. the translation) and the initial coordinates of the image object StartY)} //transform will be completed laterif (this.data.lastImg.place === 'transform') {
        }        
        this.draw()
      }
    }
Copy the code

Kangkang effect

Achieve rotation effect

In order to implement rotation, we first introduce the key method functions that we need to use

  • Math.atan2Used to calculate the Angle of movement when the finger slides
  • ctx.rotateUsed to rotate canvas

Calculate the rotation Angle using math.atan2

Syntax: math.atan2 (y,x) Note: The first argument here is the y coordinate. According to MDN, the atan2 method returns a number between -pi and PI indicating the offset Angle corresponding to the point (x, y). B: well… No one understands that. We need pictures and the truth



Once we know how to use this function, we can calculate the Angle of the finger coordinates to the center of the image.

// Add transform to moveif (this.data.lastImg.place === 'transform'){const {centerX, centerY}= lastImg const {initialRotate}= this.data.initial Const angleBefore = math.atan2 (starty-centery, startx-centerx)/math.pi * 180; // Const angleAfter = math.atan2 (y-centery, x-centerx)/math.pi * 180; // Const angleAfter = math.atan2 (y-centery, x-centerx)/math.pi * 180; Rotate = initialRotate + angleafter-anglebefore; } this.draw()Copy the code

Rotate the image with ctx.rotate

Now that we know the rotation Angle, we can rotate it with ctx.rotate. Let’s look at the API first. Ctx. rotate is the axis that is used to rotate the canvas. Instead of rotating an image inside the canvas, we rotate the canvas at its origin, the top left corner (0,0) (of course the origin can be changed). We describe the image on the new axis so that it looks like the image has been rotated. From the above, we can see that our rotation is based on the center of the image, and the rotate defaults to the origin (0,0), which is obviously not in line with our wishes. So let’s see what happens if we rotate at the origin

ctx.translate()
We shift the origin of the canvas to the midpoint of the image, rotate the axes, and shift it back (because the image is still described as (0,0)).



So we usectx.save()To save the original rotate state, usectx.restore()Rotate the rotate state back to the original state

  paint() { this.ctx.save(); this.centerX = this.x + this.w / 2; this.centerY = this.y + this.h / 2; // Change the origin to the midpoint of the image this.ctx.translate(this.centerx, this.centery); Rotate the axis this.ctx.rotate(this.rotate * math.pi / 180); // Rotate the axis this.ctx.rotate(this.rotate * math.pi / 180); // Change back this.ctx.translate(- this.centerx, - this.centery); DrawImage (this.url, this.x, this.y, this.w, this.h); drawImage(this.url, this.x, this.y, this.w, this.h); // If it is selected, draw select dotted box, and zoom icon, delete iconif (this.selected) {
      this.ctx.setLineDash([10, 10]);
      this.ctx.setLineWidth(2);
      this.ctx.setStrokeStyle("red");
      this.ctx.lineDashOffset = 10;
      this.ctx.strokeRect(this.x, this.y, this.w, this.h);
      this.ctx.drawImage(CloseIcon, this.x - 15, this.y - 15, 24, 24);
      this.ctx.drawImage(ScaleIcon, this.x + this.w - 15, this.y + this.h - 15, 24, 24);
    }
    this.ctx.restore();
  }
Copy the code

All right, here we go again, Jay.

Solve problems after rotation (difficult and difficult points)

We saw that we implemented image rotation in the previous step, but this is the core of this article from now on. What is the problem after rotation? Let’s look at a picture.

Can see after rotation, image coordinates actually does not change when we calculate the finger press the coordinates of the relationship between the location of the icon, we are still calculated according to the rotation before, because we before calculating the location of the method is not used to calculate the rotate Angle, the equivalent of each calculation are according to rotate = 0 to calculate, As a result, the icon still needs to click the position of the original icon for the next rotation. So we have to use the Rotate property to figure out where the coordinates of the rotated icon are.






The initial point of
Rotation Angle
The Angle of rotation

IsInWhere (x, y) {// Transform the coordinates of the upper-left corner of the region and the height width of the regionlet transformW = 24,transformH = 24;
    let transformX = this.x + this.w ;
    lettransformY = this.y + this.h ; // Get the icon rotation Angle, equal to the initial Angle + image rotation AnglelettransformAngle = Math.atan2(transformY - this.centerY, TransformX - this.centerx)/math.pi * 180 + this.rotate // Obtain the xy coordinates of the subscript of the AnglelettransformXY = this.getTransform(transformX, transformY, transformAngle); TransformX = Transformxy. x, transformY = Transformxy. y // delete the coordinates in the upper left corner of the region and the height width of the region. Delete the coordinates as calculated abovelet delW = 24;
    let delH = 24;
    let delX = this.x;
    let delY = this.y;
    let delAngle = Math.atan2(delY - this.centerY, delX - this.centerX) / Math.PI * 180 + this.rotate
    letdelXY = this.getTransform(delX, delY, delAngle); DelX = delxy. x, delY = delxy. y // Coordinates of the moving regionlet moveX = this.x;
    let moveY = this.y;
    if(x - transformX >= 0 && y - transformY >= 0 && transformX + transformW - x >= 0 && transformY + transformH - y >= 0) { // Scale the areareturn "transform";
    }
    else if(x-delx >= 0&&y-dely >= 0&&delx + delw-x >= 0&&dely + delh-y >= 0) {// Delete the regionreturn "del";
    }
    else if(x-movex >= 0 && y-movey >= 0 && moveX + this.w-x >= 0 && moveY + this.h-y >= 0) {// Move the regionreturn "move"; } // Not in the selected areareturn false; Rotate @param {*} rotate @param {*} rotate @param {*} rotate @param {*} rotate @param {*} rotate @param {*} rotate @param {*} rotate Rotate) {var Angle = math.pi / 180 * rotate; // The length of the line formed by the initial coordinates and the midpoint will not change no matter how much rotation. Var r = math.sqrt (math.pow (x-this.centerx, 2) + math.pow (y-this.centery, 2)); Var a = math. sine (Angle) * r; var a = math. sine (Angle) * r; Var b = math. cos(Angle) * r; var b = math. cos(Angle) * r; // The current xy coordinate is the origin of the coordinate axis relative to the center point of the picture, while our main coordinate axis is the canvas coordinate axis, so the standard Canvas coordinate is to add the coordinate value of the center pointreturn {
      x: this.centerX + b-12,
      y: this.centerY + a-12
    };
  }
Copy the code

All of a sudden, the difficulty has increased. Let’s see if we have succeeded

Scaling effect

The zoom effect is also not difficult, as we can see, when we zoom, the center coordinates remain the same, that is to say, we change the coordinates of XY relative to the width and height of the image and keep the center coordinates unchanged. So this is a very simple math problem, x plus w over 2 is equal to centerX, and if WE increase w by 10, to keep centerX constant, we have to subtract 5 from x. We take the linear distance between the coordinate when the finger moves and the center coordinate minus the linear distance between the coordinate when the finger presses the first time and the center coordinate as increment or subtraction, and change the values of X, y, W and H to achieve scaling.

Const {initialH, initialW} = this.data.initial; // Calculate the distance using Pythagorean theoremlet lineA = Math.sqrt(Math.pow(centerX - startX, 2) + Math.pow(centerY - startY, 2));
      let lineB = Math.sqrt(Math.pow(centerX - x, 2) + Math.pow(centerY - y, 2));
      letw = initialW + (lineB - lineA); // Since the scale is proportional, multiply by a width to height ratio.leth = initialH + (lineB - lineA) * (initialH / initialW); // define the minimum width and height lastimg. w = w <= 5? 5 : w; lastImg.h = h <= 5 ? 5 : h;ifLastimg.x = initialX - (lineb-linea) / 2; (w > 5 &&h > 5) {// lastimg.x = initialX - (lineb-linea) / 2; lastImg.y = initialY - (lineB - lineA) / 2; }Copy the code

Effect:

Delete pictures

When an image is clicked, save its index on the image instance object, and then determine if the clicked place is del. In dragArr, find the instance by index and delete it. Then call Draw () to update the canvas.

if(lastImg.place ==='del'){this.data.dragarr.splice (lastimg.index,1) // Redraw this.draw()return
        }
Copy the code

conclusion

As we can see, the realization of these functions is basically dependent on the operation of coordinate indices, so the basic mathematical knowledge should not be too much for teachers (I honestly said that I had a high score of 63 in last semester). Of course, we also need to know some convenient API usage and have a certain basic understanding of canvas. In fact, the whole functional process down, there will be a lot of bottlenecks and pits, these are we need to overcome and challenges, and then summarize to avoid the next pit. I welcome you to criticize and correct my bad writing. It is the first time for me to write such a long article. Thank you for reading this, if you think it’s good, I hope you can give me a thumbs-up. (/ / del / /)

The complete code

Github (Vue version) If it is helpful to you, I hope you can get a Star of yours ~. “(” â–½”)