javascriptjqueryhtml5-canvasgetimagedatadrawingcontext

Javascript ImageData Drawing Is Scaling Incorrectly


The idea of the program is to have an image of a map and to overlay a black canvas on that map. Then the user will click on some part of the canvas and, similar to a spray paint tool, the pixels near the mouse will become transparent on the canvas. Thus, the map will be shown (like a fog of war type feature). When I click near the top left of the canvas the spray paint works sort of as intended but as I click further right and down the canvas the pixels that get turned transparent are way further right and down... Any idea what is wrong here? Here is the code:

// On document ready function.
$(document).ready(function() {
  canvas = document.getElementById("myImageDisplayerCanvas");
  drawingContext = canvas.getContext("2d");
  drawingContext.fillStyle = "#000000";
  drawingContext.fillRect(0, 0, 800, 554);
});

// Spray paint logic.
var _intervalId, // used to track the current interval ID
  _center, // the current center to spray
  density = 10,
  radius = 10,
  drawingCxt,
  leftClickPressed,
  drawingContext,
  canvas;

function getRandomOffset() {
  var randomAngle = Math.random() * 360;
  var randomRadius = Math.random() * radius;

  return {
    x: Math.cos(randomAngle) * randomRadius,
    y: Math.sin(randomAngle) * randomRadius
  };
}

this.startDrawing = function(position) {
  _center = position;
  leftClickPressed = true;
  // spray once every 10 milliseconds
  _intervalId = setInterval(this.spray, 10);
};

this.moveDrawing = function(position) {
  if (leftClickPressed) {
    clearInterval(_intervalId);
    _center = position;
    // spray once every 10 milliseconds
    _intervalId = setInterval(this.spray, 10);
  }
}

this.finishDrawing = function(position) {
  clearInterval(_intervalId);
  leftClickPressed = false;
};

this.spray = function() {
  var centerX = parseInt(_center.offsetX),
    centerY =
    parseInt(_center.offsetY),
    i;

  for (i = 0; i < density; i++) {
    var offset = getRandomOffset();
    var x = centerX + parseInt(offset.x) - 1,
      y = centerY +
      parseInt(offset.y) - 1;
    var dy = y * 800 * 4;
    var pos = dy + x * 4;
    var imageData = drawingContext.getImageData(0, 0, 800, 554);
    imageData.data[pos++] = 0;
    imageData.data[pos++] = 0;
    imageData.data[pos++] = 0;
    imageData.data[pos++] = 0;
    drawingContext.putImageData(imageData, 0, 0);
  }
};
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<div id="myImageDisplayerDiv" style="position:relative; width:800px; height:554px">
  <img src="~/images/RedLarch.jpg" style="width:800px; height:554px; top: 0; left: 0; position: absolute; z-index: 0" />
  <canvas id="myImageDisplayerCanvas" onmousedown="startDrawing(event)" onmousemove="moveDrawing(event)" onmouseup="finishDrawing(event)" style="width:800px; height:554px; top: 0; left: 0; position: absolute; z-index: 1; opacity: 1; fill: black" />
</div>


Solution

  • Set canvas resolution.

    The reason the spray is offset is because you have not set the canvas resolution. You set the resolution via the canvas element width and height properties

    <canvas id="canvas" width="800" height="554"></canvas>
    

    Setting the style width and height sets the canvas display size not the resolution. If you don't set the canvas resolution it defaults to 300 by 150.

    There were many other problems with your code.

    Getting the whole canvas image data just to set a single pixel is way overkill. Just create a single pixel imageData object and place that pixel where needed

    Use requestAnimationFrame to sync with the display, never use setInterval or setTimeout as they are not synced to the display hardware and will cause you endless problems.

    Create a single mouse event function to handle all the mouse events and just record the mouse state. Use the main loop called by requestAnimationFrame to handle the drawing, never draw from a mouse event.

    You don't need to define functions with this.functionName = function(){} if the function is not part of an object.

    Trig function use radians not degrees. 360 deg in radians is 2 * Math.PI

    Below is a quick rewrite of you code using the above notes.

    const ctx = canvas.getContext("2d");
    requestAnimationFrame(mainLoop); // start main loop when code below has run
    ctx.fillStyle = "#000000";
    ctx.fillRect(0, 0, 800, 554);
    
    // create a pixel buffer for one pixel
    const imageData = ctx.getImageData(0, 0, 1, 1);
    const pixel32 = new Uint32Array(imageData.data.buffer);
    pixel32[0] = 0;
    
    // Spray paint logic.
    const density = 10;
    const  radius = 10;
    
    // mouse handler
    const mouse = {x : 0, y : 0, button : false};
    function mouseEvents(e){
    	const bounds = canvas.getBoundingClientRect();
    	mouse.x = e.pageX - bounds.left - scrollX;
    	mouse.y = e.pageY - bounds.top - scrollY;
    	mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
    }
    ["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
    
    function getRandomOffset() {
      const angle = Math.random() * Math.PI * 2;
      const rad = Math.random() * radius;
      return { x: Math.cos(angle) * rad, y: Math.sin(angle) * rad};
    }
    
    function spray(pos) {
      var i;
      for (i = 0; i < density; i++) {
        const offset = getRandomOffset();
        ctx.putImageData(imageData, pos.x + offset.x, pos.y + offset.y);
      }
    }
    
    // main loop called 60 times a second
    function mainLoop(){
      if (mouse.button) { spray(mouse) }
      requestAnimationFrame(mainLoop);
    }
    <canvas id="canvas" width="800" height="554"></canvas>