0

The x and y position of the grid on the canvas is being put 1 row to the left and 1 column above on some cells. I don't know what's causing this but it mostly happens in the middle to nearing the end of the grid.

'use strict';

// the canvas
const canvas = document.querySelector('.canvas');
const context = canvas.getContext('2d');

class Node {
  constructor(row, col) {
    // save current index (row, col) to know where in the canvas it is
    this.row = row;
    this.col = col;

    this.show = color => {
      context.beginPath();
      context.rect(this.row * cellDimension, this.col * cellDimension, cellDimension - 1, cellDimension - 1);
      context.fillStyle = color;
      context.fill();
    }
  }
}

// the counter from input field
const colRowCount = 60;
// will be determined from setup
let cellDimension;

let gridArray = new Array(colRowCount);
//start and finish
let startNode = new Node();
let endNode = new Node();
// array of the path nodes
let totalPath = new Array();


// setup all the neccessities
const setup = () => {
  // subtract the height of menu
  canvas.height = window.innerHeight;
  canvas.width = window.innerWidth;
  cellDimension = canvas.width / colRowCount;

  for (let i = 0; i < colRowCount; i++) {
    gridArray[i] = new Array(colRowCount);
    for (let j = 0; j < colRowCount; j++) {
      gridArray[i][j] = new Node(i, j);
    }
  }

  context.beginPath();
  for (let i = 0; i < canvas.width; i += cellDimension) {
    // horizontal divider
    context.moveTo(0, i);
    context.lineTo(canvas.width, i);

    // vertical divider
    context.moveTo(i, 0);
    context.lineTo(i, canvas.width);

    context.strokeStyle = '#ddd';
    context.stroke();
  }
}

const getMousePosition = event => {
  const rect = canvas.getBoundingClientRect();
  const x = event.clientX - rect.left;
  const y = event.clientY - rect.top;

  // top left
  let cellX = 0;
  let cellY = 0;
  // bounds of the square unit
  const bounds = Math.floor(Math.floor(canvas.width) / colRowCount);

  for (let i = 1; i < colRowCount; i++) {
    if (x > (bounds * i)) {
      cellX = i;
    }

    if (y > (bounds * i)) {
      cellY = i;
    }
  }

  return {
    x: cellX,
    y: cellY
  };
}

const placeStart = position => {
  startNode = gridArray[position.x][position.y];
  startNode.show('green');
}

window.addEventListener('load', () => {
  // setup the canvas
  setup();
  // event listener for canvas
  canvas.addEventListener('mousedown', event => {
    placeStart(getMousePosition(event));
  });
});
.container {
  height: calc(100% - 50px);
  width: 100%;
}


/*for all the grids with canvas*/

.canvas {
  height: 100%;
  width: 100%;
  border: 1px solid green;
}


/*for all the grids with canvas*/
<div class='container'>
  <canvas class='canvas'>
    </canvas>
</div>

The one in the jsfiddle is behaving much more unexpectedly.

Prosy Arceno
  • 2,616
  • 1
  • 8
  • 32
  • There is nothing to say that cellDimension is naturally an integer. Also when you floor things you lose accuracy (downwards). Are these causing the problem? – A Haworth Jun 01 '21 at 18:13
  • @AHaworth, I added after I noticed the problem so it's not the floor. But I do agree its gonna cause some more inaccuracy. I'll remove them on my local – Prosy Arceno Jun 01 '21 at 19:00

1 Answers1

1

Multiple issues here...

First, the canvas has two sizes, the one of its buffer, set by its width and height attributes, and its rendering one (ruled by CSS).
Here both sizes are different, so the actual canvas image is being stretched by the CSS renderer, causing antialiasing and rounding imprecisions. See more here.

Then, stroke() does overlap on both sides of the provided coordinates, so with a lineWidth of 1, you need to add (or remove) a 0.5px offset so that a vertical or horizontal line fits correctly in the boundaries of a single pixel, otherwise once again you'll have antialising kick in. See more here.

Finally, your coordinates are not even rounded, so you will fall in between two pixel coordinates, once more adding antialiasing...

Here is an attempt to fix all that:

'use strict';

// the canvas
const canvas = document.querySelector('.canvas');
const context = canvas.getContext('2d');
class Node {
  constructor(row, col) {
    // save current index (row, col) to know where in the canvas it is
    this.row = row;
    this.col = col;

    this.show = color => {
      const border_offset = context.lineWidth;
      context.beginPath();
      context.rect(this.row * cellDimension + border_offset, this.col * cellDimension + border_offset, cellDimension - border_offset, cellDimension - border_offset);
      context.fillStyle = color;
      context.fill();
      // remember we're on, so we can be redrawn on resize
      this.color = color;
    }
  }
}

// The counter from input field
const colRowCount = 60;
// Will be determined from setup
let cellDimension;

let gridArray = new Array(colRowCount);
//start and finish
let startNode = new Node();
let endNode = new Node();
// array of the path nodes
let totalPath = new Array();
// Initialize all the nodes directly, 
// we don't need to know the size of the cells
// or of the canvas to do so.
for (let i = 0; i < colRowCount; i++) {
  gridArray[i] = new Array(colRowCount);
  for (let j = 0; j < colRowCount; j++) {
    gridArray[i][j] = new Node(i, j);
  }
}

// called at init, and at every page resize
const redraw = () => {
  // get the size of the container measured by CSS
  const parent = canvas.parentNode;
  const parent_width = parent.offsetWidth;
  // remove 1 lineWidth for outer strokes
  const max_grid_size = parent_width - context.lineWidth;
  // cellDimension must be an integer
  cellDimension = Math.floor( max_grid_size / colRowCount );
  const grid_size = cellDimension * colRowCount;
  // now we know the size of our grid,
  // our canvas will be one lineWidth bigger to show the outer strokes
  canvas.width = canvas.height = grid_size + context.lineWidth;

  context.beginPath();
  const stroke_offset = context.lineWidth / 2;
  for (let i = 0; i <= grid_size; i += cellDimension) {
    // horizontal divider
    context.moveTo(stroke_offset, i + stroke_offset);
    context.lineTo(grid_size + stroke_offset, i + stroke_offset);

    // vertical divider
    context.moveTo(i + stroke_offset, stroke_offset);
    context.lineTo(i + stroke_offset, grid_size + stroke_offset );
  }
  // stroke only once
  context.strokeStyle = '#ddd';
  context.stroke();
  
  // redraw all the nodes that were already active
  gridArray.flat().filter( (node) => node.color )
    .forEach( (node) => node.show( node.color ) );
}

const getMousePosition = event => {
  const rect = canvas.getBoundingClientRect();
  const x = event.clientX - rect.left;
  const y = event.clientY - rect.top;

  // top left
  let cellX = 0;
  let cellY = 0;
  // bounds of the square unit
  const bounds = Math.floor(Math.floor(canvas.width) / colRowCount);

  for (let i = 1; i < colRowCount; i++) {
    if (x > (bounds * i)) {
      cellX = i;
    }

    if (y > (bounds * i)) {
      cellY = i;
    }
  }

  return {
    x: cellX,
    y: cellY
  };
}

const placeStart = position => {
  startNode = gridArray[position.x][position.y];
  startNode.show('green');
}

window.addEventListener('load', () => {
  redraw();
  // event listener for canvas
  canvas.addEventListener('mousedown', event => {
    placeStart(getMousePosition(event));
  });
  window.onresize = redraw;
});
.container {
  height: calc(100% - 50px);
  width: 100%;
}

.canvas {
  border: 1px solid green;
  /* we let the canvas buffer's size rule its presentation dimensions */
}
<div class='container'>
  <canvas class='canvas'></canvas>
</div>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Hi thanks for answering. I added your function to my code. I made an additional function to setup a grid, because I added a colCount which would depend on the height of the canvas. It's not drawing anything on the canvas somehow. May you take a look? Here is the [repo](https://github.com/pdarceno/pathfinding/blob/master/js/setup.js) – Prosy Arceno Jun 03 '21 at 04:46
  • Btw, I added your parent_height to the constants [here](https://github.com/pdarceno/pathfinding/blob/master/js/constants.js) – Prosy Arceno Jun 03 '21 at 04:47
  • parent_height should be recalculated at every resize event. Don't reinitialize the Nodes at each resize, you'll loose which were clicked on. `window.onresize = setup();` will set the return value of `setup()` (`undefined`) as the resize handler, I wrote `window.onresize = setup`. Not sure that's everything, I don't have much time to review your code sorry. – Kaiido Jun 03 '21 at 05:13
  • That's fine. I got it working, thanks very much I appreciate it – Prosy Arceno Jun 03 '21 at 05:25