使用canvas处理图片,实现放大缩小增加标注功能

需求背景:需要在Taro小程序中给图片根据坐标增加标注信息,并实现图片的放大缩小,切换下一页功能。

实现技术:Taro、Canvas

实现代码

const [swiperCurrent, setSwiperCurrent] = useState(0) // 图片页码
const [imgList, setImgList] = useState([]) // 图片数据
const [isMoving, setIsMoving] = useState(false) // 图片是否移动
const [lastTouch, setLastTouch] = useState({ x: 0, y: 0 }) // 最后触摸的坐标
const [lastDistance, setLastDistance] = useState(0) // 最后缩放
const scale = useRef(1) // 放大倍数
const position = useRef({ x: 0, y: 0 }) // 图片移动的位置
const canvasRef = useRef(null)
const ctxRef = useRef(null)
const currentImageRef = useRef(null)
const imageCacheRef = useRef(new Map()) // 缓存图片,不然放大缩小时每次渲染图片会导致图片闪烁
const currentImgObjRef = useRef(null) // 当前图片对象
const lastClickTimeRef = useRef(0)

useEffect(() => {
    geImgtList()
}, [])

const geImgtList = async () => {
  const img = [{ url: 'http://e.hiphotos.baidu.com/image/pic/item/a1ec08fa513d2697e542494057fbb2fb4316d81e.jpg',coord:[ [10, 10], [ 20, 20 ] ], suspectCode: '标注的文字' }] 
  setImgList(img)
  if(!!img.length) {
     const currentImage = img[0];
     currentImageRef.current = currentImage
     try {
          const localImagePath = await loadBgImg(currentImage.url)
          await initCanvas(localImagePath, currentImage.coord, currentImage .suspectCode)
        } catch (error) {
          console.error('图片加载失败:', error)
        }
   }
}

 const loadBgImg = async (imgUrl) => {
    if (imageCacheRef.current.has(imgUrl)) {
      return imageCacheRef.current.get(imgUrl)
    }

    try {
      const res = await Taro.downloadFile({
        url: imgUrl
      })
      if (res.statusCode === 200) {
        imageCacheRef.current.set(imgUrl, res.tempFilePath)
        return res.tempFilePath
      } else {
        throw new Error(`下载失败: ${res.statusCode}`)
      }
    } catch (error) {
      console.error('图片加载失败:', error)
      throw error
    }
  }

 const initCanvas = (imagePath, coordinates = null, suspectCode) => {
    return new Promise((resolve, reject) => {
      Taro.createSelectorQuery()
      .select('#myCanvas')
      .fields({ node: true, size: true })
      .exec((res) => {
        if (res[0] && res[0].node) {
          const canvas = res[0].node
          canvasRef.current = canvas
          const ctx = canvas.getContext('2d')
          ctxRef.current = ctx
          const canvasWidth = res[0].width
          const canvasHeight = res[0].height
          
          const dpr = Taro.getSystemInfoSync().pixelRatio
          canvas.width = canvasWidth * dpr
          canvas.height = canvasHeight * dpr
          ctx.scale(dpr, dpr)
          
          // 创建图片对象并保存引用
          const img = canvas.createImage()
          currentImgObjRef.current = img
          
          img.onload = () => {
            try {
              drawImage(img, coordinates, suspectCode)
              resolve()
            } catch (error) {
              console.error('绘制图片时出错:', error)
              reject(error)
            }
          }
          
          img.onerror = (error) => {
            console.error('图片加载失败:', error)
            reject(error)
          }
          
          img.src = imagePath
        } else {
          console.error('无法获取 canvas 节点')
          reject(new Error('无法获取 canvas 节点'))
        }
      })
    })
  }

    
  const drawImage = (img, coordinates = null, suspectCode) => {
    if (!canvasRef.current || !ctxRef.current) return

    const canvas = canvasRef.current
    const ctx = ctxRef.current
    const canvasWidth = canvas.width / (Taro.getSystemInfoSync().pixelRatio || 1)
    const canvasHeight = canvas.height / (Taro.getSystemInfoSync().pixelRatio || 1)
    
    // 计算自适应尺寸
    const imgWidth = img.width
    const imgHeight = img.height
    
    const scaleX = canvasWidth / imgWidth
    const scaleY = canvasHeight / imgHeight
    const baseScale = Math.min(scaleX, scaleY, 1)
    
    // 应用当前缩放
    const currentScale = baseScale * scale.current
    
    // 计算绘制尺寸
    const drawWidth = imgWidth * currentScale
    const drawHeight = imgHeight * currentScale
    // 计算位置 - 垂直居中并应用偏移
    const baseX = (canvasWidth - drawWidth) / 2
    const baseY = (canvasHeight - drawHeight) / 2
    const x = baseX + position.current.x
    const y = baseY + position.current.y

    // 清空画布
    ctx.clearRect(0, 0, canvasWidth, canvasHeight)
    
    // 绘制图片
    ctx.drawImage(img, x, y, drawWidth, drawHeight)
    
    // 如果有坐标信息,绘制边框
    if (coordinates && coordinates.length >= 2) {
      const [x1, y1] = coordinates[0];
      const [x2, y2] = coordinates[1];
      
      const scaledX1 = x + (x1 / imgWidth) * drawWidth;
      const scaledY1 = y + (y1 / imgHeight) * drawHeight;
      const scaledX2 = x + (x2 / imgWidth) * drawWidth;
      const scaledY2 = y + (y2 / imgHeight) * drawHeight;
      
      // 绘制矩形边框
      ctx.strokeStyle = '#2e9cef';
      ctx.lineWidth = 1;
      ctx.strokeRect(
        scaledX1,
        scaledY1,
        scaledX2 - scaledX1,
        scaledY2 - scaledY1
      );
      
      // 绘制 suspectCode 文本
      let text = suspectCode
      if (text) {
        ctx.font = '14px Microsoft YaHei';
        ctx.fillStyle = '#2e9cef';
        ctx.textAlign = 'left';
        ctx.textBaseline = 'bottom';
        
        const textX = scaledX1;
        const textY = scaledY1 - 5;
        
        ctx.fillText(text, textX, textY);
      }
    }
  }

   // 单指触摸 - 移动
  const handleTouchStart = (e) => {
    if (e.touches.length === 1) {
      const touch = e.touches[0]
      setLastTouch({ x: touch.clientX, y: touch.clientY })
      setIsMoving(true)
    } else if (e.touches.length === 2) {
      // 双指触摸 - 缩放
      const touch1 = e.touches[0]
      const touch2 = e.touches[1]
      const distance = Math.sqrt(
        Math.pow(touch2.clientX - touch1.clientX, 2) + 
        Math.pow(touch2.clientY - touch1.clientY, 2)
      )
      setLastDistance(distance)
    }
  }

  // 移动
  const handleTouchMove = (e) => {
    if (e.touches.length === 1 && isMoving) {
      // 单指移动
      const touch = e.touches[0]
      const deltaX = touch.clientX - lastTouch.x
      const deltaY = touch.clientY - lastTouch.y
      
      position.current = { x: position.current.x + deltaX, y: position.current.y + deltaY }
      
      setLastTouch({ x: touch.clientX, y: touch.clientY })
      
      // 立即重绘
      if (currentImgObjRef.current) {
        const currentImage = currentImageRef.current
        drawImage(currentImgObjRef.current, currentImage?.coord, currentImage.suspectCode)
      }
    } else if (e.touches.length === 2) {
      // 双指缩放
      const touch1 = e.touches[0]
      const touch2 = e.touches[1]
      const distance = Math.sqrt(
        Math.pow(touch2.clientX - touch1.clientX, 2) + 
        Math.pow(touch2.clientY - touch1.clientY, 2)
      )
      
      if (lastDistance > 0) {
        const scaleFactor = distance / lastDistance
        const newScale = Math.max(0.5, Math.min(3, scale.current * scaleFactor))
        
        if (newScale !== scale.current) {
          scale.current = newScale
          
          // 立即重绘
          if (currentImgObjRef.current) {
            const currentImage = currentImageRef.current
            drawImage(currentImgObjRef.current, currentImage?.coord, currentImage.suspectCode)
          }
        }
      }
      
      setLastDistance(distance)
    }
  }

  const handleTouchEnd = () => {
    setIsMoving(false)
    setLastDistance(0)
  }
  
  // 双击图片回到图片初始大小
  const handleClick = () => {
    const currentTime = new Date().getTime();
    const timeDiff = currentTime - lastClickTimeRef.current;
// 点击时长
if(timeDiff < 200) { resetTransform() } lastClickTimeRef.current = currentTime; } // 重置缩放和位置 const resetTransform = () => { if (!currentImgObjRef.current) return scale.current = 1 position.current = { x: 0, y: 0 } // 立即重绘 const currentImage = currentImageRef.current drawImage(currentImgObjRef.current, currentImage?.coord, currentImage .suspectCode) } // 处理图片切换 const handleSwiperChange = async (idx) => { // 切换图片时重置缩放和位置 scale.current = 1 position.current = { x: 0, y: 0 } const currentImage = imgData.imgList[idx]; currentImageRef.current = currentImage try { const localImagePath = await loadBgImg(currentImage.url) const img = canvasRef.current.createImage() currentImgObjRef.current = img img.onload = () => { drawImage(img, currentImage.coord, currentImage.suspectCode) } img.onerror = () => { console.error('切换图片加载失败') } img.src = localImagePath } catch (error) { console.error('切换图片失败:', error) } } // 左滑 const canvasLeftClick = () => { let idx = 0 if(!swiperCurrent) { idx = imgList.length - 1 } else { idx = swiperCurrent - 1 } setSwiperCurrent(idx) handleSwiperChange(idx) } // 右滑 const canvasRightClick = () => { let idx = 0 if(swiperCurrent === imgList.length - 1) { idx = 0 } else { idx = swiperCurrent + 1 } setSwiperCurrent(idx) handleSwiperChange(idx) } return<View> <Canvas id="myCanvas" type="2d" onTouchStart={handleTouchStart} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd} onTouchCancel={handleTouchEnd} onClick={handleClick} /> <View className='canvas-left' onClick={canvasLeftClick}> <Image src='/img/left.png' className='canvas-arrow-icon'></Image> </View> <View className='canvas-right' onClick={canvasRightClick}> <Image src='/img/right.png' className='canvas-arrow-icon'></Image> </View> </View> ) #myCanvas { width: 100%; height: 100vh; background: rgba(0, 0, 0, 0.8); } .canvas-left { width: 150px; height: 150px; position: fixed; top: 50%; left: 10px; transform: translateY(-50%); z-index: 10; line-height: 150px; text-align: left; .canvas-arrow-icon { width: 80px; height: 80px; } } .canvas-right { width: 150px; height: 150px; position: fixed; top: 50%; right: 10px; transform: translateY(-50%); z-index: 10; line-height: 150px; text-align: right; .canvas-arrow-icon { width: 80px; height: 80px; } }

以上就是全部代码

项目上线后还遇到一个问题:在微信小程序上看不到图片,经过排查是因为后端使用的图片的域名是新加的,需要在小程序后台设置downloadFile 的合法域名。

 

posted @ 2025-11-19 14:08  凹润之之之  阅读(55)  评论(0)    收藏  举报