preface

With the proliferation of touch devices such as smartphones and tablets, interaction has changed. Touch devices can interact directly with fingers, as opposed to a computer that uses a mouse and keyboard, and almost all support multi-touch. The most common multi-touch operation is two-finger zooming. For example, double fingers zoom in and out the size of the web page, circle of friends double fingers zoom in and out the picture to view. So with such a common gesture, have you ever wondered how it works? Follow me to find out!

Scaling principle

The principle is actually very simple, double pointing outward expansion means magnification, inward contraction means narrowing, scaling ratio is obtained by calculating the current distance of double fingers/the distance of double fingers last time. See figure below:

After calculating the scale, scale in the following two ways.

  1. Scale by transform
  2. Zoom is achieved by modifying width and height

The mainstream method is to use transform to achieve, because the performance is better. This article will cover both approaches, so take your pick. But before we get there, we need to understand two mathematical formulas and the PointerEvent PointerEvent. Because it’s going to be used. If you are not familiar with PointerEvent pointer events, you can also check out this article.

Formula for distance between two points

Suppose two points A and B and their coordinates are respectively A(x1, y1) and B(x2, y2), then the distance between A and B is:

/** * get the distance between two points *@param {object} A the first point *@param {object} B The second point star@returns* /
function getDistance(a, b) {
    const x = a.x - b.x;
    const y = a.y - b.y;
    return Math.hypot(x, y); // Math.sqrt(x * x + y * y);
}
Copy the code

Midpoint coordinate formula

Let two points A and B and their coordinates be A(x1, y1) and B(x2, y2) respectively, then the coordinates of the midpoint P of A and B are:

/** * get the midpoint coordinates *@param {object} A the first point *@param {object} B The second point star@returns* /
function getCenter(a, b) {
    const x = (a.x + b.x) / 2;
    const y = (a.y + b.y) / 2;
    return { x: x, y: y };
}
Copy the code

Gets the image zoom size

<img id="image" alt="">
const image = document.getElementById('image');

let result, // Image zoom width and height
    x, // The x offset
    y, // The y offset
    scale = 1.// Scale
    maxScale,
    minScale = 0.5;

// Since images are loaded asynchronously, we need to get naturalWidth and naturalHeight in the load method
image.addEventListener('load'.function () {
    result = getImgSize(image.naturalWidth, image.naturalHeight, window.innerWidth, window.innerHeight);
    maxScale = Math.max(Math.round(image.naturalWidth / result.width), 3);
    // Image width and height
    image.style.width = result.width + 'px';
    image.style.height = result.height + 'px';
    // Vertical horizontal center display
    x = (window.innerWidth - result.width) * 0.5;
    y = (window.innerHeight - result.height) * 0.5;
    image.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0) scale(1)';
});

// The image assignment needs to be placed after the load callback, because the image cache reads quickly and the load callback may not be executed
image.src='.. /images/xxx.jpg';

/** * Get the image scale *@param {number} naturalWidth 
 * @param {number} naturalHeight 
 * @param {number} maxWidth 
 * @param {number} maxHeight 
 * @returns * /
function getImgSize(naturalWidth, naturalHeight, maxWidth, maxHeight) {
    const imgRatio = naturalWidth / naturalHeight;
    const maxRatio = maxWidth / maxHeight;
    let width, height;
    // If the actual image width to height ratio >= display width to height ratio
    if (imgRatio >= maxRatio) {
        if (naturalWidth > maxWidth) {
            width = maxWidth;
            height = maxWidth / naturalWidth * naturalHeight;
        } else{ width = naturalWidth; height = naturalHeight; }}else {
        if (naturalHeight > maxHeight) {
            width = maxHeight / naturalHeight * naturalWidth;
            height = maxHeight;
        } else{ width = naturalWidth; height = naturalHeight; }}return { width: width, height: height }
}
Copy the code

Two-finger scaling logic

// Global variables
let isPointerdown = false.// Press the icon
    pointers = [], // Touch points group
    point1 = { x: 0.y: 0 }, // The first point coordinates
    point2 = { x: 0.y: 0 }, // The second point coordinates
    diff = { x: 0.y: 0 }, // The difference from the last Pointermove
    lastPointermove = { x: 0.y: 0 }, // Used to calculate diff
    lastPoint1 = { x: 0.y: 0 }, // Last time the first touch point coordinates
    lastPoint2 = { x: 0.y: 0 }, // The coordinates of the last second touch point
    lastCenter; // Last central point coordinates
    
/ / bind pointerdown
image.addEventListener('pointerdown'.function (e) {
    pointers.push(e);
    point1 = { x: pointers[0].clientX, y: pointers[0].clientY };
    if (pointers.length === 1) {
        isPointerdown = true;
        image.setPointerCapture(e.pointerId);
        lastPointermove = { x: pointers[0].clientX, y: pointers[0].clientY };
    } else if (pointers.length === 2) {
        point2 = { x: pointers[1].clientX, y: pointers[1].clientY };
        lastPoint2 = { x: pointers[1].clientX, y: pointers[1].clientY };
        lastCenter = getCenter(point1, point2);
    }
    lastPoint1 = { x: pointers[0].clientX, y: pointers[0].clientY };
});

/ / bind pointermove
image.addEventListener('pointermove'.function (e) {
    if (isPointerdown) {
        handlePointers(e, 'update');
        const current1 = { x: pointers[0].clientX, y: pointers[0].clientY };
        if (pointers.length === 1) {
            // Drag with one finger to view an image
            diff.x = current1.x - lastPointermove.x;
            diff.y = current1.y - lastPointermove.y;
            lastPointermove = { x: current1.x, y: current1.y };
            x += diff.x;
            y += diff.y;
            image.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0) scale(' + scale + ') ';
        } else if (pointers.length === 2) {
            const current2 = { x: pointers[1].clientX, y: pointers[1].clientY };
            // Calculate the ratio ratio of the last moving distance > 1 to enlarge, ratio < 1 to shrink
            let ratio = getDistance(current1, current2) / getDistance(lastPoint1, lastPoint2);
            // Scale
            const _scale = scale * ratio;
            if (_scale > maxScale) {
                scale = maxScale;
                ratio = maxScale / scale;
            } else if (_scale < minScale) {
                scale = minScale;
                ratio = minScale / scale;
            } else {
                scale = _scale;
            }
            // Calculate the center coordinates of the current two fingers
            const center = getCenter(current1, current2);
            // Calculate the image center offset, default transform-origin: 50% 50%
            // if transform-origin: 30% 40%, then origon. x = (ratio -1) * result.width * 0.3
            // origin. Y = (ratio - 1) * result.height * 0.4
            // If the transform-origin is set to the upper left corner by changing the width and height or using transform scaling.
            // Origin can be omitted because (ratio - 1) * result.width * 0 = 0
            const origin = { 
                x: (ratio - 1) * result.width * 0.5.y: (ratio - 1) * result.height * 0.5 
            };
            // Calculate the offset. Think carefully about why you want to calculate it this way.
            x -= (ratio - 1) * (center.x - x) - origin.x - (center.x - lastCenter.x);
            y -= (ratio - 1) * (center.y - y) - origin.y - (center.y - lastCenter.y);
            image.style.transform = 'translate3d(' + x + 'px, ' + y + 'px, 0) scale(' + scale + ') ';
            lastCenter = { x: center.x, y: center.y };
            lastPoint1 = { x: current1.x, y: current1.y };
            lastPoint2 = { x: current2.x, y: current2.y };
        }
    }
    e.preventDefault();
});

/ / bind pointerup
image.addEventListener('pointerup'.function (e) {
    if (isPointerdown) {
        handlePointers(e, 'delete');
        if (pointers.length === 0) {
            isPointerdown = false;
        } else if (pointers.length === 1) {
            point1 = { x: pointers[0].clientX, y: pointers[0].clientY };
            lastPointermove = { x: pointers[0].clientX, y: pointers[0].clientY }; }}});/ / bind pointercancel
image.addEventListener('pointercancel'.function (e) {
    if (isPointerdown) {
        isPointerdown = false;
        pointers.length = 0; }});/** * Update or delete pointer *@param {PointerEvent} e
 * @param {string} type* /
function handlePointers(e, type) {
    for (let i = 0; i < pointers.length; i++) {
        if (pointers[i].pointerId === e.pointerId) {
            if (type === 'update') {
                pointers[i] = e;
            } else if (type === 'delete') {
                pointers.splice(i, 1); }}}}Copy the code

Matters needing attention

Transform: translateX(300px) scale(2); And the transform: scale (2) the translateX (300 px); It’s not equal. Please follow the corresponding writing sequence during development. See figure below:

Results demonstrate

Demo: jsdemo. Codeman. Top/HTML/pinch….

Write in the last

I’ve packaged common gestures like Click, double click, long press, swipe, drag, scroll to zoom, double finger to zoom, double finger to rotate, etc. into plugins. The project has been open source on Github, interested partners can go to see. Project address: github.com/18223781723…