preface

Some time ago, I did a small project of looking for a station in an internal competition of the company. Since looking for a station requires a map of the station, I originally thought to build the model of the station with a modeling tool and put it on the map plug-in, but I stopped thinking about it because of workload and performance.

Combing the demand does not need to implement a single point to drag, single click, double refers to the effect of scale, and then build a picture to serve as a map, and then record the current displacement and scaling, can calculate a click on the location, and then the coordinates of the location traversal can know which location the click.

At first I thought it was very simple to achieve, and then encountered a lot of problems in the process of the project, and then after the completion of the project I will drag and zoom out separately, here I talk about my ideas and problems encountered.

Brief idea

Listen for finger movements through touchStart and TouchMove events. Scaling and moving actually change the scale and translate of the element’s transform, using a global variable to store the current shift and scale, and then modifying the scaling and moving of the specified element with each data change

Single refers to mobile

The displacement difference between two fingers is calculated by touchmove, which is the current displacement

Double refers to zoom

The distance between the current two fingers minus the distance between the last two fingers is how much it’s going to scale this time, and then there’s a scaling center concept, which is a little bit more complicated and we’ll talk about it later in the implementation

The implementation process

Implementation of single finger movement

Single finger drag is relatively simple, so first to achieve. I started by defining a transformData to record the current displacement and scaling.

this.transformData = {
  x: option? .transformData? .x ||0.y: option? .transformData? .y ||0.scale: option? .transformData? .scale ||1
},
Copy the code

Then listen for the TouchStart event as the start of the move, recording the position of the first click.

The touch event returns an object with targetTouches, Touches, and changeTouches. ‘Touches’ is a list of all touch points on the current screen,’ targetTouches’ is a list of all touch points on the current object, and ‘changedTouches’ is a list of touch points that refer to the current (triggered) event. Because my element overlays other elements, I use Touches to get points.

We then listen for the TouchMove event to start the movement of the element, where store is the last recorded position, the current position minus the last position is the current displacement, and add this displacement to the previous one. The position is then recorded for next use, and the transform modification method is called. This is a complete movement.

move (x, y) {
  // Calculate the current offset
  this.transformData.x += x - this.store.x;
  this.transformData.y += y - this.store.y;
  // Record it for next use
  this.store.x = x;
  this.store.y = y;
  / / modify the CSS
  this.setTransform();
}
Copy the code

Double finger scaling implementation

Add a judgment to the touch event that ‘touches’ of length 1 is a move and’ touches’ of length 2 is a pinch.

There’s another problem here, the first finger touches the container of the operation, the second finger touches outside the container of the operation and it doesn’t trigger the second touchStart, it depends on your needs, if you want the second finger to be able to operate anywhere, Or ‘Touches’ in front requires allowing a second finger anywhere. At this time, check whether the scaleStart triggered by TouchStart is recorded before the start of the scale method triggered by TouchMove. If not, call the scaleStart method. This time move is regarded as start.

Also record the distance between the two fingers when start, the distance can be calculated by using the Pythagorean theorem, and then calculate the distance between the current two fingers when move, and then subtract the distance recorded last time, which is the scale of this scaling. This scale is multiplied by the previously recorded scale to get the current scaled value. Then write down the value for next use. Finally, you can scale by calling the modify Transform method.

scale(touchList) {
  // Calculate the distance between the two fingers
  const distance = Math.sqrt(
    (touchList[0].clientX - touchList[1].clientX) ** 2 +
      (touchList[0].clientY - touchList[1].clientY) ** 2
  );
  // Scale to the current two-finger distance minus the last two-finger distance
  const scale = distance / this.store.distance
  this.transform.scale *= scale
  // Record the distance between two fingers
  this.store.distance = distance;
  / / modify the CSS
  this.setTransform();
}
Copy the code

Set the transform

In fact, the setting is a CSS

this.transformDom.style.transform = `
  translate(${this.transformData.x || 0}px, ${this.transformData.y || 0}px)
  scale(${this.transformData.scale || 0}, ${this.transformData.scale || 0})
`
Copy the code

Also remember to set the origin of the change, it defaults to the center, because we’re always operating relative to the top left corner so we need to put the origin in the top left corner

transform-origin: 0px 0px;
Copy the code

But the order of Translate and scale affects how the screen looks. In both images, zoom in by 2 by moving 100 pixels.

  • Move and then scale is green in the image below. Scaling does not change the value of the move
  • Zooming and then moving is blue in the following image. Zooming will result in data zooming for subsequent moves

Click on the try

Since we record the displacement without considering the effect of scaling, the displacement will be shown in the end. Therefore, we adopt the green scheme, translate and scale first

In fact, this is relatively easy to understand, the next is not quite understood, I propose two concepts here, the operation container coordinate and render container coordinate, where the operation container refers to the container bound to the touch event, render container refers to the container after the operation displacement scaling.

The reason for these two concepts is that our zoom center (covered in the next section) needs to get the render container coordinates. And what we did later to determine where the user clicked was we needed to know the coordinates of the user clicked on the map at the bottom, which was our render container.

We use the first displacement and then scaling, so can we reverse the coordinate value of the first scale and then reverse displacement can be done. No, because its scaling will only affect the position and size of the scaling operation, and will not affect the value of the previous displacement, so its displacement is not affected and should be reversed first and then reverse scaling.

The order of displacement scaling is actually not very well understood, and I thought about it for a long time at first, but then I simulated the actual situation and got the answer and understood it a little bit better.

As shown below, we operate the outer square 400, the inner square 100, and the distance between each scale is 10. We shifted the inner square by 100 and scaled it by 2 to get the current shape. Then our user clicks on the position in the red circle above. For the position of the outer block, 200, and for the inner block, we can intuitively see that it is at the position of 50, so the question arises: how can 200 get 50 by operating 100 and operating 2? Obviously, (200-100) / 2 = 50, So we know that in order to transform, we have to reverse shift and then reverse scale.

Click to see

Center of zoom with two fingers

We are now at the upper left of the scale is relative to render container, in our actual operation, often zoom element was gone, experience is very poor, so we need to let our scaling is the center of the two of us click on the center of the finger, and because we scale and displacement are based on the upper left corner of the point of operation, So we need to change the displacement of the container appropriately when scaling, so that it looks like the center of the double finger scaling. The result should look something like the following.

You start with the blue box, you move your finger from the blue circle to the green circle, and you expect to get the green box, and in fact now we have the red box. So you need to modify some displacement to get the green box.

I understand the specific problem and it seems quite complicated. For the problem that I can’t come up with directly in my head (I’m too bad), I usually simulate an actual situation, get a set of methods, and then try some special points with the method. If there is no problem, I can directly implement it.

We use the technology of dimensionality reduction strike (dog head) to degrade the two-dimensional problem into a one-dimensional view will be a little bit easier, why can dimensionality reduction, because the transformation of x and y is the same, so we first take a look at x, estimate y copy is good.

We can think of transform-origin in terms of scale centers, so we can see how different transform-origin shifts the container.

Click on the try

As you can see from the above example, the displacement of the scaling center causes the container to be shifted proportioned to the left of the scaling center minus the length of the scaling center. For example, if the scale center is zero, the displacement is zero; When the scaling center is 50%, the displacement is half of the extra length; The displacement is all the extra length when the scaling center is on the far right.

So we need three things: the old length, the new length, and the ratio of the distance from the center to the left. The length is easy to get, and the proportion first needs to get the center point that the user clicks, and the center point can be obtained by clicking (x1, y1)(x2, y2)

const center = 
  [x1 + (x2 - x1) / 2, y1 + (y2 - y1) / 2]
Copy the code

This point is just our external container position, because we need to figure out the ratio of the point to the render container position, we need to convert this point to the coordinates above the render container, which we talked about above, subtract the displacement and divide by the ratio

const scaleCenter = [
  center[0] - this.transformData.x) / this.transformData.scale, 
  center[1] - this.transformData.y) / this.transformData.scale
]
Copy the code

And then we get the ratio is 1

this.scaleTranslateProportion = [
  scaleCenter[0] / this.transformDom.offsetWidth,
  scaleCenter[1] / this.transformDom.offsetHeight,
];
Copy the code

The old length is just scale before scaling times the length of the container, and the new length is just the old length times the scale of the change

  const oldSize = [
    this.transformDom.offsetWidth * this.transformData.scale,
    this.transformDom.offsetHeight * this.transformData.scale,
  ];
Copy the code

And then we just add the corresponding displacement as we scale

this.transformData.x +=
  (oldSize[0] - oldSize[0] * scale) *
  scaleTranslateProportion[0] | |0;
this.transformData.y +=
  (oldSize[1] - oldSize[1] * scale) *
  scaleTranslateProportion[1] | |0;
Copy the code

The result is a very nice double finger zoom experience

Write in the back

Our single here refers to the movement and dual scaling is basically achieved, but if you simply want this feature is very good, but do you find there are many need to dig back optimization, such as drag can limit range, can zoom limit proportion, can directly point to add and subtract Numbers to zoom in, That’s all you need to do to make a feature work.

I actually saw that there was a gesturechange event that could listen for finger movement and pinching, but it was only supported in Safari and I didn’t follow up.

Through this TIME I also learned a lot, from the beginning of the implementation of the scheme to determine the problem to later encountered problems to gradually break down the decomposition of the problem, all under a very big effort. I also put a lot of work into the illustrations for this article.

I ended up wrapping up the entire code and throwing it in here with an interesting example that you can try if you need an ES6 mobile.