Canvas:如何修改自定义路径范围内的ImageData像素值
问题原因
- 像素坐标计算错误:
imageData.data数组的索引i对应的是RGBA通道的位置,每4个连续索引对应1个像素的完整信息。原代码直接用通道索引i计算坐标,没有先转换为像素索引,导致坐标完全错位,出现多区域同时被修改的问题。 - 全局
imageData持续复用:仅在图片加载时获取了一次原始画布的像素数据,之后所有修改都会永久保存在这个变量里,光标移动过的区域都会被持续修改,无法恢复原图状态。 - 背景绘制逻辑缺陷:原缩放逻辑会导致图片拉伸变形甚至超出画布范围,也会间接影响像素坐标匹配。
修复方案
- 修正坐标计算逻辑:先将通道索引转换为像素索引,再计算对应x/y坐标
- 保存原始像素数据副本,每次修改都基于原始数据操作,避免修改痕迹累加;如果需要保留光标划过的修改痕迹,可去掉副本逻辑直接修改全局像素数据
- 优化遍历逻辑:仅遍历光标覆盖的正方形区域,不需要遍历全画布,大幅提升性能,同时去掉性能较低的
isPointInPath判断,直接用距离公式判断点是否在圆形光标内 - 修正背景图片缩放逻辑,按比例居中绘制避免变形
修复后完整代码
JavaScript
let image; // 保存原始像素数据,避免修改累加 let originalImageData; const cursorCanvas = document.createElement("canvas"); const cursorCanvasContext = cursorCanvas.getContext("2d"); cursorCanvas.width = 100; cursorCanvas.height = 100; drawCursor(); const imageCanvas = document.querySelector("canvas"); imageCanvas.width = window.innerWidth; imageCanvas.height = window.innerHeight; const imageCanvasContext = imageCanvas.getContext("2d"); // 光标半径 const cursorRadius = cursorCanvas.width / 2; document.querySelector("img").addEventListener("load", (event) => { image = event.target; image.crossOrigin = "Anonymous"; drawBackground(); // 保存原始像素数据 originalImageData = imageCanvasContext.getImageData( 0, 0, imageCanvas.width, imageCanvas.height ); }); window.addEventListener("mousemove", (event) => { requestAnimationFrame(() => { if (originalImageData) { // 复制原始数据,在副本上修改,不需要保留划过痕迹可去掉此逻辑直接修改全局数据 const currentImageData = new ImageData( new Uint8ClampedArray(originalImageData.data), originalImageData.width, originalImageData.height ); const { clientX: cx, clientY: cy } = event; // 仅遍历光标覆盖的正方形区域,不需要遍历全画布 const startX = Math.max(0, Math.floor(cx - cursorRadius)); const endX = Math.min(imageCanvas.width, Math.ceil(cx + cursorRadius)); const startY = Math.max(0, Math.floor(cy - cursorRadius)); const endY = Math.min(imageCanvas.height, Math.ceil(cy + cursorRadius)); for (let y = startY; y < endY; y++) { for (let x = startX; x < endX; x++) { // 判断当前点是否在光标圆形范围内 const dx = x - cx; const dy = y - cy; if (dx * dx + dy * dy <= cursorRadius * cursorRadius) { const index = (y * imageCanvas.width + x) * 4; currentImageData.data[index] = 40; currentImageData.data[index + 1] = 40; currentImageData.data[index + 2] = 40; } } } imageCanvasContext.putImageData(currentImageData, 0, 0); } // 绘制自定义光标 imageCanvasContext.drawImage( cursorCanvas, event.clientX - cursorRadius, event.clientY - cursorRadius ); }); }); function drawBackground() { imageCanvasContext.clearRect(0, 0, imageCanvas.width, imageCanvas.height); if (image) { // 修正原图缩放逻辑,按最小比例适配画布,居中绘制不变形 const ratio = Math.min( imageCanvas.width / image.width, imageCanvas.height / image.height ); const drawWidth = image.width * ratio; const drawHeight = image.height * ratio; const offsetX = (imageCanvas.width - drawWidth) / 2; const offsetY = (imageCanvas.height - drawHeight) / 2; imageCanvasContext.drawImage( image, 0, 0, image.width, image.height, offsetX, offsetY, drawWidth, drawHeight ); } } function drawCursor() { cursorCanvasContext.beginPath(); cursorCanvasContext.arc( cursorCanvas.width / 2, cursorCanvas.width / 2, cursorCanvas.width / 2, 0, 2 * Math.PI ); cursorCanvasContext.stroke(); cursorCanvasContext.closePath(); }
CSS
html, body { width: 100vw; height: 100vh; overflow: hidden; } body { font-family: sans-serif; margin: 0; } img { visibility: hidden; position: absolute; } canvas { cursor: none; }
HTML
<img src="https://i.imgur.com/vzGhITt.jpeg" /> <canvas />
内容的提问来源于stack exchange,提问作者Raphael Rafatpanah
相关产品推荐
相关产品推荐

