HTML Canvas 2D光线模拟器优化后碰撞异常问题排查
2D光线模拟器碰撞检测异常问题修复
我正在使用HTML Canvas开发一款2D光线模拟器,原本每次光线移动时创建1像素的ImageData对象,为优化性能改为仅在初始化阶段创建一次完整画布的ImageData后,碰撞检测功能出现异常。以下是可正常运行的旧版本代码,以及碰撞逻辑混乱的新版本代码,期望两者功能一致但新版本无法正常工作。
旧版本代码
HTML
<!DOCTYPE html> <html lang="en"> <body onmousemove="mousemove(event);"> <canvas id="canvas"></canvas> </body> </html>
CSS
* { box-sizing: border-box; } html,body { width: 100%; height: 100%; } body { margin: 0; overflow: hidden; } canvas { width: 100vw; height: 100vh; }
JavaScript
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); var mouseX = 0; var mouseY = 0; canvas.width = innerWidth; canvas.height = innerHeight; ctx.fillStyle = "rgb(10, 10, 10)"; ctx.strokeStyle = "rgba(255, 255, 0)"; ctx.lineWidth = 3; ImageData.prototype.getPixel = function(x, y, offset) { return this.data[(y * this.width + x) * 4 + offset]; }; function drawLine(first, second) { ctx.beginPath(); ctx.moveTo(first[0], first[1]); ctx.lineTo(second[0], second[1]); ctx.stroke(); }; function mousemove(e) { mouseX = e.clientX; mouseY = e.clientY; }; function drawScene() { ctx.fillStyle = "rgba(50, 50, 50, 1)"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "rgba(10, 10, 10, 1)"; ctx.fillRect(0, canvas.height-10, 200, 10); ctx.fillRect(0, canvas.height-210, 10, 200); ctx.fillRect(10, canvas.height-210, 200, 10); ctx.fillRect(200, canvas.height-190, 10, canvas.height-190); }; drawScene(); function render() { drawScene(); let rays = []; var data; for (let angle = 0; angle < 360; angle += 3) { // for every ray let xv = Math.sin(angle*(Math.PI/180))*5; let yv = Math.cos(angle*(Math.PI/180))*5; let x = mouseX; let y = mouseY; let rayinitx = x; let rayinity = y; let collisions = 0; let opacity = 1; while (x > 0 && x < canvas.width && y > 0 && y < canvas.height) { // for every movement of every ray x += xv; data = ctx.getImageData(x, y, 1, 1); let collide = false; if (data.getPixel(0, 0, 0) === 10 && data.getPixel(0, 0, 1) === 10 && data.getPixel(0, 0, 2) === 10 && data.getPixel(0, 0, 3) === 255) { xv *= -1; x += xv; collide = true; }; y += yv; data = ctx.getImageData(x, y, 1, 1); if (data.getPixel(0, 0, 0) === 10 && data.getPixel(0, 0, 1) == 10 && data.getPixel(0, 0, 2) === 10 && data.getPixel(0, 0, 3) === 255) { yv *= -1; y += yv; collide = true; }; if (collide) { rays.push([[rayinitx, rayinity], [x, y], opacity]); opacity -= 1/5; rayinitx = x; rayinity = y; if (opacity <= 0) { break; }; }; }; rays.push([[rayinitx, rayinity], [x, y], opacity]); rayinitx = x; rayinity = y; }; rays.forEach(ray => { ctx.strokeStyle = `rgba(255, 255, 0, ${ray[2]})`; drawLine(ray[0], ray[1]); }); }; document.body.addEventListener("keypress", function(e){ if (e.code == "Space") render(); });
新版本代码(碰撞逻辑异常)
HTML
<!DOCTYPE html> <html lang="en"> <body onmousemove="mousemove(event);"> <canvas id="canvas"></canvas> </body> </html>
CSS
* { box-sizing: border-box; } html,body { width: 100%; height: 100%; } body { margin: 0; overflow: hidden; } canvas { width: 100vw; height: 100vh; }
JavaScript
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); var mouseX = 0; var mouseY = 0; canvas.width = innerWidth; canvas.height = innerHeight; ctx.fillStyle = "rgb(10, 10, 10)"; ctx.strokeStyle = "rgba(255, 255, 0)"; ctx.lineWidth = 3; ImageData.prototype.getPixel = function(x, y, offset) { return this.data[(y * this.width + x) * 4 + offset]; }; function drawLine(first, second) { ctx.beginPath(); ctx.moveTo(first[0], first[1]); ctx.lineTo(second[0], second[1]); ctx.stroke(); }; function mousemove(e) { mouseX = e.clientX; mouseY = e.clientY; }; function drawScene() { ctx.fillStyle = "rgba(50, 50, 50, 1)"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "rgba(10, 10, 10, 1)"; ctx.fillRect(0, canvas.height-10, 200, 10); ctx.fillRect(0, canvas.height-210, 10, 200); ctx.fillRect(10, canvas.height-210, 200, 10); ctx.fillRect(200, canvas.height-190, 10, canvas.height-190); }; drawScene(); function render() { drawScene(); let rays = []; var data = ctx.getImageData(0, 0, canvas.width, canvas.height); for (let angle = 0; angle < 360; angle += 3) { // for every ray let xv = Math.sin(angle*(Math.PI/180))*5; let yv = Math.cos(angle*(Math.PI/180))*5; let x = mouseX; let y = mouseY; let rayinitx = x; let rayinity = y; let collisions = 0; let opacity = 1; while (x > 0 && x < canvas.width && y > 0 && y < canvas.height) { // for every movement of every ray x += xv; let collide = false; if (data.getPixel(x, y, 0) === 10 && data.getPixel(x, y, 1) === 10 && data.getPixel(x, y, 2) === 10 && data.getPixel(x, y, 3) === 255) { xv *= -1; x += xv; collide = true; }; y += yv; if (data.getPixel(x, y, 0) === 10 && data.getPixel(x, y, 1) == 10 && data.getPixel(x, y, 2) === 10 && data.getPixel(x, y, 3) === 255) { yv *= -1; y += yv; collide = true; }; if (collide) { rays.push([[rayinitx, rayinity], [x, y], opacity]); opacity -= 1/5; rayinitx = x; rayinity = y; if (opacity <= 0) { break; }; }; }; rays.push([[rayinitx, rayinity], [x, y], opacity]); rayinitx = x; rayinity = y; }; rays.forEach(ray => { ctx.strokeStyle = `rgba(255, 255, 0, ${ray[2]})`; drawLine(ray[0], ray[1]); }); }; document.body.addEventListener("keypress", function(e){ if (e.code == "Space") render(); });
问题原因
旧版本中调用ctx.getImageData(x, y, 1, 1)时,Canvas会自动将浮点数类型的x和y转换为整数(取整),因为像素是离散的,每个像素对应整数坐标。而新版本中直接使用浮点数x和y调用自定义的getPixel方法,计算出的数组索引不是整数,导致读取了错误的像素数据,最终碰撞检测逻辑混乱。
修复方案
在调用data.getPixel前,将x和y转换为整数,推荐使用Math.floor()或者Math.round()来取整,确保访问的是正确的像素位置。
修复后的新版本JavaScript代码
const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); var mouseX = 0; var mouseY = 0; canvas.width = innerWidth; canvas.height = innerHeight; ctx.fillStyle = "rgb(10, 10, 10)"; ctx.strokeStyle = "rgba(255, 255, 0)"; ctx.lineWidth = 3; ImageData.prototype.getPixel = function(x, y, offset) { return this.data[(y * this.width + x) * 4 + offset]; }; function drawLine(first, second) { ctx.beginPath(); ctx.moveTo(first[0], first[1]); ctx.lineTo(second[0], second[1]); ctx.stroke(); }; function mousemove(e) { mouseX = e.clientX; mouseY = e.clientY; }; function drawScene() { ctx.fillStyle = "rgba(50, 50, 50, 1)"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = "rgba(10, 10, 10, 1)"; ctx.fillRect(0, canvas.height-10, 200, 10); ctx.fillRect(0, canvas.height-210, 10, 200); ctx.fillRect(10, canvas.height-210, 200, 10); ctx.fillRect(200, canvas.height-190, 10, canvas.height-190); }; drawScene(); function render() { drawScene(); let rays = []; var data = ctx.getImageData(0, 0, canvas.width, canvas.height); for (let angle = 0; angle < 360; angle += 3) { // for every ray let xv = Math.sin(angle*(Math.PI/180))*5; let yv = Math.cos(angle*(Math.PI/180))*5; let x = mouseX; let y = mouseY; let rayinitx = x; let rayinity = y; let collisions = 0; let opacity = 1; while (x > 0 && x < canvas.width && y > 0 && y < canvas.height) { // for every movement of every ray x += xv; let collide = false; // 将x和y转为整数 const intX = Math.floor(x); const intY = Math.floor(y); if (data.getPixel(intX, intY, 0) === 10 && data.getPixel(intX, intY, 1) === 10 && data.getPixel(intX, intY, 2) === 10 && data.getPixel(intX, intY, 3) === 255) { xv *= -1; x += xv; collide = true; }; y += yv; // 再次转换x和y为整数 const newIntX = Math.floor(x); const newIntY = Math.floor(y); if (data.getPixel(newIntX, newIntY, 0) === 10 && data.getPixel(newIntX, newIntY, 1) == 10 && data.getPixel(newIntX, newIntY, 2) === 10 && data.getPixel(newIntX, newIntY, 3) === 255) { yv *= -1; y += yv; collide = true; }; if (collide) { rays.push([[rayinitx, rayinity], [x, y], opacity]); opacity -= 1/5; rayinitx = x; rayinity = y; if (opacity <= 0) { break; }; }; }; rays.push([[rayinitx, rayinity], [x, y], opacity]); rayinitx = x; rayinity = y; }; rays.forEach(ray => { ctx.strokeStyle = `rgba(255, 255, 0, ${ray[2]})`; drawLine(ray[0], ray[1]); }); }; document.body.addEventListener("keypress", function(e){ if (e.code == "Space") render(); });
内容的提问来源于stack exchange,提问作者GiantBooley
相关产品推荐
相关产品推荐

