You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 09:17:03