This paper mainly introduces the function of picture zooming and dragging on Canvas, which is compatible with PC and mobile terminals

Event listeners

PC event monitoring

The first is all kinds of event listening. PC listening to zoom wheel events (mousewheel, wheel)

this.canvasRef.addEventListener('mousedown', this.startMouse.bind(this)) this.canvasRef.addEventListener('mousemove', this.moveMouse.bind(this)) this.canvasRef.addEventListener('mouseup', this.endMouse.bind(this)) this.canvasRef.addEventListener('mousewheel', This. The mouseWheel. Bind (this)) / / listening roller enclosing canvasRef. AddEventListener (' wheel 'this. The mouseWheel. Bind (this)) / / to monitor rollerCopy the code

Mobile event listening

Mobile zooming is listening for multi-finger touches

this.canvasRef.addEventListener('touchstart'.this.startTouch.bind(this))
this.canvasRef.addEventListener('touchmove'.this.moveTouch.bind(this))
this.canvasRef.addEventListener('touchend'.this.endMouse.bind(this))
Copy the code

Image to load

Image loading uses Promise to handle loading and crossOrigin addresses cross-domain.

private loadImage(url: string) {
   return new Promise((reject, resolve) = > {
       this.img = new Image();
       this.img.crossOrigin = 'Anonymous'
       this.img.onload = function() {
           reject(' ');
       }
       this.img.onerror = function(error) {
           console.error(error, 'error=====')
           resolve(error)
       }
       this.img.src = url
   })
}
Copy the code

Mobile location

Calculates the position of the mouse or touch relative to the Canvas container

/** * Handle mouse position *@private
* @param {number} startX
* @param {number} startY
* @returns {IPos}
* @memberof MapCanvas* /
private windowToCanvas(startX: number, startY: number): IPos {
   const { left, top, width, height} = this.canvasRef.getBoundingClientRect();
   return {
       x: startX - left - (width - this.canvasRef.width) / 2.y: startY - top - (height - this.canvasRef.height) / 2}}Copy the code

Moving picture

The PC and mobile end move position logic is similar, according to the current move position minus the last move position.

/** * drag to move *@private
* @param {(React.MouseEvent<HTMLElement> } e
* @memberof MapCanvas* /
private moveMouse(e: React.MouseEvent<HTMLElement> ) {
   if(!this.isMove) return false
   const { pageX, pageY } = e
   this.movePos = this.windowToCanvas(pageX, pageY)
   const x = this.movePos.x - this.startPos.x, y = this.movePos.y - this.startPos.y;
   this.imgX += x;
   this.imgY += y;
   this.startPos = {... this.movePos}// Update the latest location
   this.drawImage()
}
Copy the code

Draw pictures

Before drawing, you must first clear the previous drawing

/** * draw a picture *@private
* @memberof MapCanvas* /
private drawImage() {
   // Clear the previous frame draw
   this.ctx.clearRect(0.0.this.canvasRef.width, this.canvasRef.height)
   // Draw a picture
   this.ctx.drawImage(
       this.img,
       0.0.this.img.width, 
       this.img.height,
       this.imgX,
       this.imgY,
       this.img.width * this.imgScale,
       this.img.height * this.imgScale
   )
}
Copy the code

The zoom

The zoom events are different on PC and mobile

PC zoom

Whether to monitor scroll narrowing or zoom in on PC is judged according to the value of wheelDelta of the rolling event. If the value is greater than 0, it is zoom in; otherwise, it is zoom in.

/** **@private
* @param {(React.WheelEvent<HTMLElement> & { wheelDelta: number })} e
* @memberof MapCanvas* /
private mouseWheel(e: React.WheelEvent<HTMLElement> & { wheelDelta: number } ) {
   const { clientX, clientY, wheelDelta } = e
   const pos = this.windowToCanvas(clientX, clientY)
   // Calculate the position of the image
   const newPos = { x: Number(((pos.x - this.imgX)/this.imgScale).toFixed(2)), y: Number(((pos.y - this.imgY)/this.imgScale).toFixed(2))}// Determine whether to zoom in or out
   if(wheelDelta > 0) { Enlarge / /
       this.imgScale += 0.05
       if(this.imgScale >= this.MAX_SCALE) {
           this.imgScale = this.MAX_SCALE
       }
   } else { / / to narrow
       this.imgScale -= 0.05
       if(this.imgScale <= this.MINIMUM_SCALE) {
           this.imgScale = this.MINIMUM_SCALE
       }
   }
   // Calculate the position of the image, according to the current zoom, calculate the new position
   this.imgX = (1-this.imgScale)*newPos.x+(pos.x-newPos.x);
   this.imgY = (1-this.imgScale)*newPos.y+(pos.y-newPos.y);
   this.drawImage(); // Start drawing pictures
}
Copy the code

Mobile scaling

The zoom of the mobile image is judged by the size between the sliding end and the sliding end

/** * Pythagorean theorem, find the straight line distance between two points *@private
* @param {React.Touch} p1
* @param {React.Touch} p2
* @returns {number}
* @memberof MapCanvas* /
private getDistance(p1: React.Touch, p2: React.Touch): number {
   const x = p2.pageX - p1.pageX
   const y = p2.pageY - p1.pageY
   // Find the distance between two points
   return Math.sqrt((x * x) + (y * y))
}
Copy the code

Compare the distance before and after sliding to judge the scale;

/** * Drag to zoom on the move side *@private
 * @param {React.TouchEvent<HTMLElement>} e
 * @memberof MapCanvas* /
private moveTouch(e: React.TouchEvent
       ) {
    if(!this.isMove || ! e.touches)return false
    const { clientX, clientY } = e.touches[0]
    // If it is a single finger
    if(e.touches.length < 2) {
        this.movePos = this.windowToCanvas(clientX, clientY)
        const x = this.movePos.x - this.startPos.x, y = this.movePos.y - this.startPos.y;
        this.imgX += x;
        this.imgY += y;
        this.startPos = {... this.movePos}// Update the latest location
    } else {
        const now = e.touches
        // Process the position
        const pos = this.windowToCanvas(clientX, clientY)
        const newPos = {x: Number(((pos.x-this.imgX)/this.imgScale).toFixed(2)),y: Number(((pos.y-this.imgY)/this.imgScale).toFixed(2))};
        const curPos = this.getDistance(now[0],now[1]); // The current position
        const startPos = this.getDistance(this.touchs[0].this.touchs[1]); // The previous position
        // Determine whether the position is zoomed in or out
        if(curPos > startPos) { Enlarge / /
            this.imgScale += 0.03
            if(this.imgScale >= this.MAX_SCALE) {
                this.imgScale = this.MAX_SCALE
            }
        } else {
            this.imgScale -= 0.03
            if(this.imgScale <= this.MINIMUM_SCALE) {
                this.imgScale = this.MINIMUM_SCALE
            }
        }
        // Calculate the position of the image, with the current scale, calculate the new position
        this.imgX = (1-this.imgScale)*newPos.x+(pos.x-newPos.x);
        this.imgY = (1-this.imgScale)*newPos.y+(pos.y-newPos.y);
        this.touchs = now
    }
    this.drawImage()
}
Copy the code

The last

All the code

interface IPos {
   x: number
   y: number
}

class MapCanvas {
   private canvasRef:HTMLCanvasElement
   private ctx: CanvasRenderingContext2D
   private img: HTMLImageElement 
   private startPos: IPos = { x: 0.y: 0 } // Start coordinates
   private touchs: React.TouchList         // Store multi-finger position
   private movePos: IPos                   // Store the moving coordinate position
   private imgX: number = 0                // The image initializes the X position
   private imgY: number = 60               // The image initializes the Y position
   private isMove: boolean = false         // Whether to move
   private imgScale: number = 0.5          // Image scale
   private MINIMUM_SCALE: number = 0.2     // Minimum zoom
   private MAX_SCALE: number = 5           // Maximum zoom
   constructor(canvas: HTMLCanvasElement) {
       this.canvasRef = canvas
       const { width, height} = this.canvasRef.getBoundingClientRect();
       this.canvasRef.width = width
       this.canvasRef.height = height
       this.ctx = canvas.getContext('2d')
       this.initCavas();
   }

   /** * initialize *@memberof MapCanvas* /
   async initCavas() {
       await this.loadImage('https://img.qlchat.com/qlLive/activity/image/ICLAJAYZ-PZ19-M9WM-1620287616694-OQR1MJFKO67O.jpg')
       this.drawImage();
       // Event listener on PC
       this.canvasRef.addEventListener('mousedown'.this.startMouse.bind(this))
       this.canvasRef.addEventListener('mousemove'.this.moveMouse.bind(this))
       this.canvasRef.addEventListener('mouseup'.this.endMouse.bind(this))
       this.canvasRef.addEventListener('mousewheel'.this.mouseWheel.bind(this)) // Listen to the scroll wheel
       this.canvasRef.addEventListener('wheel'.this.mouseWheel.bind(this)) // Listen to the scroll wheel
       // Mobile event listener
       this.canvasRef.addEventListener('touchstart'.this.startTouch.bind(this))
       this.canvasRef.addEventListener('touchmove'.this.moveTouch.bind(this))
       this.canvasRef.addEventListener('touchend'.this.endMouse.bind(this))}/** * image loading *@private
    * @param {string} url
    * @returns
    * @memberof MapCanvas* /
   private loadImage(url: string) {
       return new Promise((reject, resolve) = > {
           this.img = new Image();
           this.img.crossOrigin = 'Anonymous'
           this.img.onload = function() {
               reject(' ');
           }
           this.img.onerror = function(error) {
               console.error(error, 'error=====')
               resolve(error)
           }
           this.img.src = url
       })
   }

   /** * draw a picture *@private
    * @memberof MapCanvas* /
   private drawImage() {
       // Clear the previous frame draw
       this.ctx.clearRect(0.0.this.canvasRef.width, this.canvasRef.height)
       // Draw a picture
       this.ctx.drawImage(
           this.img,
           0.0.this.img.width,
           this.img.height,
           this.imgX,
           this.imgY,
           this.img.width * this.imgScale,
           this.img.height * this.imgScale
       )
   }

   /** * start dragging *@private
    * @param {(React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement>)} e
    * @memberof MapCanvas* /
   private startMouse(e: React.MouseEvent<HTMLElement> ) {
       const { pageX, pageY } = e;
       this.isMove = true
       this.startPos = this.windowToCanvas(pageX, pageY)
   }

   /** * start touching *@private
    * @param {React.TouchEvent<HTMLElement>} e
    * @memberof MapCanvas* /
   private startTouch(e: React.TouchEvent<HTMLElement>) {
       const { touches } = e
       this.isMove = true;
       // Check if there are multiple fingers
       if(touches.length < 2) {
           const { clientX, clientY } = touches[0]
           this.startPos = this.windowToCanvas(clientX, clientY) // clientX: Position of touch point relative to browser window
       } else {
           this.touchs = touches
       }
   }

   /** * drag to move *@private
    * @param {(React.MouseEvent<HTMLElement> } e
    * @memberof MapCanvas* /
   private moveMouse(e: React.MouseEvent<HTMLElement> ) {
       if(!this.isMove) return false
       const { pageX, pageY } = e
       this.movePos = this.windowToCanvas(pageX, pageY)
       const x = this.movePos.x - this.startPos.x, y = this.movePos.y - this.startPos.y;
       this.imgX += x;
       this.imgY += y;
       this.startPos = {... this.movePos}// Update the latest location
       this.drawImage()
   }

   /** * Drag to zoom on the move side *@private
    * @param {React.TouchEvent<HTMLElement>} e
    * @memberof MapCanvas* /
   private moveTouch(e: React.TouchEvent
       ) {
       if(!this.isMove || ! e.touches)return false
       const { clientX, clientY } = e.touches[0]
       // If it is a single finger
       if(e.touches.length < 2) {
           this.movePos = this.windowToCanvas(clientX, clientY)
           const x = this.movePos.x - this.startPos.x, y = this.movePos.y - this.startPos.y;
           this.imgX += x;
           this.imgY += y;
           this.startPos = {... this.movePos}// Update the latest location
       } else {
           const now = e.touches
           // Process the position
           const pos = this.windowToCanvas(clientX, clientY)
           const newPos = {x: Number(((pos.x-this.imgX)/this.imgScale).toFixed(2)),y: Number(((pos.y-this.imgY)/this.imgScale).toFixed(2))};
           const curPos = this.getDistance(now[0],now[1]); // The current position
           const startPos = this.getDistance(this.touchs[0].this.touchs[1]); // The previous position
           // Determine whether the position is zoomed in or out
           if(curPos > startPos) { Enlarge / /
               this.imgScale += 0.03
               if(this.imgScale >= this.MAX_SCALE) {
                   this.imgScale = this.MAX_SCALE
               }
           } else {
               this.imgScale -= 0.03
               if(this.imgScale <= this.MINIMUM_SCALE) {
                   this.imgScale = this.MINIMUM_SCALE
               }
           }
           // Calculate the position of the image, with the current scale, calculate the new position
           this.imgX = (1-this.imgScale)*newPos.x+(pos.x-newPos.x);
           this.imgY = (1-this.imgScale)*newPos.y+(pos.y-newPos.y);
           this.touchs = now
       }
       this.drawImage()
   }
   
   /** * Drag end *@private
    * @param {(React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement>)} e
    * @memberof MapCanvas* /
   private endMouse(e: React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement>) {
      this.isMove = false
   }

   /** **@private
    * @param {(React.WheelEvent<HTMLElement> & { wheelDelta: number })} e
    * @memberof MapCanvas* /
   private mouseWheel(e: React.WheelEvent<HTMLElement> & { wheelDelta: number } ) {
       const { clientX, clientY, wheelDelta } = e
       const pos = this.windowToCanvas(clientX, clientY)
       // Calculate the position of the image
       const newPos = { x: Number(((pos.x - this.imgX)/this.imgScale).toFixed(2)), y: Number(((pos.y - this.imgY)/this.imgScale).toFixed(2))}// Determine whether to zoom in or out
       if(wheelDelta > 0) { Enlarge / /
           this.imgScale += 0.05
           if(this.imgScale >= this.MAX_SCALE) {
               this.imgScale = this.MAX_SCALE
           }
       } else { / / to narrow
           this.imgScale -= 0.05
           if(this.imgScale <= this.MINIMUM_SCALE) {
               this.imgScale = this.MINIMUM_SCALE
           }
       }
       // Calculate the position of the image, according to the current zoom, calculate the new position
       this.imgX = (1-this.imgScale)*newPos.x+(pos.x-newPos.x);
       this.imgY = (1-this.imgScale)*newPos.y+(pos.y-newPos.y);
       this.drawImage(); // Start drawing pictures
   }

   /** * Handle mouse position *@private
    * @param {number} startX
    * @param {number} startY
    * @returns {IPos}
    * @memberof MapCanvas* /
   private windowToCanvas(startX: number, startY: number): IPos {
       const { left, top, width, height} = this.canvasRef.getBoundingClientRect();
       return {
           x: startX - left - (width - this.canvasRef.width) / 2.y: startY - top - (height - this.canvasRef.height) / 2}}/** * Pythagorean theorem, find the straight line distance between two points *@private
    * @param {React.Touch} p1
    * @param {React.Touch} p2
    * @returns {number}
    * @memberof MapCanvas* /
   private getDistance(p1: React.Touch, p2: React.Touch): number {
       const x = p2.pageX - p1.pageX
       const y = p2.pageY - p1.pageY
       return Math.sqrt((x * x) + (y * y))
   }

}

const CanvasMap:React.FC<IP> = ({}: IP) = > {
   const canvasRef = React.useRef<HTMLCanvasElement>()
   React.useEffect(() = > {
       new MapCanvas(canvasRef.current);
   }, [])
   return (
       <div className={ styles['map-box'] }>
           <div>
               <canvas ref={canvasRef}></canvas>
           </div>
       </div>)}export default CanvasMap
Copy the code

That’s the end of this article. I hope it helps.

Xiaobian for the first time to write an article writing style is limited, untalented and shallow, the article if there is wrong, hope to inform.