使用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 的合法域名。

相关推荐
不是鱼1 天前
Canvas学习笔记(一)
前端·javascript·canvas
华仔啊2 天前
Vue3图片放大镜从原理到实现,电商级细节展示方案
前端·vue.js·canvas
xiaohe06014 天前
🧸 前端不是只会写管理后台,我用 400 行代码画了一个 LABUBU !
vue.js·typescript·canvas
风中凌乱的L14 天前
vue canvas标注
前端·vue.js·canvas
BetterChinglish14 天前
html5中canvas图形变换transform、setTransform原理(变换矩阵)
javascript·html5·canvas·变换矩阵
aiguangyuan15 天前
Taro 开发快速入门手册
taro·前端开发·移动端开发
吃饺子不吃馅16 天前
Web端PPT应用画布方案:Canvas 还是 DOM?
前端·架构·canvas
aiguangyuan17 天前
Taro多端适配技术解析
taro·前端开发·移动端开发
Airser18 天前
npm启动Taro框架报错
前端·npm·taro