javascripthtmlcanvasimagedata

ImageData is different when getting one pixel


I am making a 2d light ray simulator with html canvas and i was creating a 1 pixel ImageData every time a ray moved so when i made it so it only created one at the start, it stopped working normally. The top is the old version that works and the bottom is the new version that doesn't work. I expect it to do the same thing but in the new version the collisions are messed up.

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();
        });
* {
                box-sizing: border-box;
            }
            html,body {
                width: 100%;
                height: 100%;
            }
            body {
                margin: 0;
                overflow: hidden;
            }
            canvas {
                width: 100vw;
                height: 100vh;
            }
<!DOCTYPE html>
<html lang="en">
    <body onmousemove="mousemove(event);">
        <canvas id="canvas"></canvas>
    </body>
</html>

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();
        });
* {
                box-sizing: border-box;
            }
            html,body {
                width: 100%;
                height: 100%;
            }
            body {
                margin: 0;
                overflow: hidden;
            }
            canvas {
                width: 100vw;
                height: 100vh;
            }
<!DOCTYPE html>
<html lang="en">
    <body onmousemove="mousemove(event);">
        <canvas id="canvas"></canvas>
    </body>
</html>


Solution

  • The problem is that your x and y values are not integers. This was working ok with getImageData() because this function is defined as accepting integers (precisely [EnforceRange] long) and that the browser will do the conversion automagically when passed floats. However Array index access doesn't do that conversion, so when you try to access the e.g 1.3424123124 item of the ImageData's .data, you get undefined, which is not === 10.

    You can fix that by enforcing the conversion to integer in your getPixel method:

    ImageData.prototype.getPixel = function(x, y, offset) {
      return this.data[(Math.floor(y) * this.width + Math.floor(x)) * 4 + offset];
    };
    

    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[(Math.floor(y) * this.width + Math.floor(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();
    });
    * {
      box-sizing: border-box;
    }
    
    html,
    body {
      width: 100%;
      height: 100%;
    }
    
    body {
      margin: 0;
      overflow: hidden;
    }
    
    canvas {
      width: 100vw;
      height: 100vh;
    }
    <!DOCTYPE html>
    <html lang="en">
    
    <body onmousemove="mousemove(event);">
      <canvas id="canvas"></canvas>
    </body>
    
    </html>