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

浙公网安备 33010602011771号