Search code examples
javascriptcanvastile

Tiles won't display correctly on canvas


I'm working on a minigame that needs a tiled background, tiles that I pick from a png tileSet (16x16 tiles). Even if the tiles are 16x16, they are displayed as 32x32 tiles on the canvas from a spriteSheet (see code below). The issue is it seem that when I enlarge artificially the size of the tile, it takes a tiny part of the tile that is next to it. An image might describe it better than words :

Zoom on the background

You can see in the water part, there is a pinkish line, that shouldn't be here (I checked that there were no separations on the tileset, and 0,0 is at the right place i believe). I also tried to use the native size of the tiles (16x16 instead of 32x32) for the background, no pink line to be seen and also the grey grid disappeared:

So i guess it comes from the 32x32, but i have no idea how to fix it ... Here is the code i use to load the tileset and display the background :

loadImage('/img/dungeon.png')
.then(image => {
  const sprites = new SpriteSheet(image, 16, 16);
  sprites.define('stone_floor', 1, 0, 32, 32);
  sprites.define('water', 2, 7, 32, 32);
  sprites.define('wall_top', 2, 0, 32, 32);
  sprites.define('void', 0, 0, 32, 32);
  sprites.define('stone_floor&water', 3, 7, 32, 32);

  loadLevel('1-2')
  .then(level => {
    drawBackground(level.background, context, sprites);
  })
});

SpriteSheet class :

class SpriteSheet {
  constructor(image, width, height, offset = 0){
    this.image = image;
    this.width = width;
    this.height = height;
    this.offset = offset;
    this.tiles = new Map();
  }
  define(name, x, y, imgw, imgh){
    const buffer = document.createElement('canvas');
    buffer.width = imgw;
    buffer.height = imgh;
    buffer
        .getContext('2d')
        .drawImage(this.image,
                  x * this.width + x*this.offset, y * this.height + y*this.offset,
                  this.width, this.height,
                  0, 0, imgw, imgh);
    this.tiles.set(name, buffer);
  }
  draw(name, context, x, y){
    const buffer = this.tiles.get(name);
    context.drawImage(buffer, x, y);
  }
  drawTile(name, context, x, y){
    const buffer = this.tiles.get(name);
    context.drawImage(buffer, x * buffer.width, y * buffer.height);
  }
}

PS : this project is highly based on MethMethMethod's tutorial : video


Solution

  • Based on @Blindman67's comment :

    The bleed from neighboring tiles is due to the bilinear filtering when you scale the tile up. The simple solution is to turn of bilinear filtering for the tiles. ctx.imageSmoothingEnabled = false; If you wish to keep the filtering then the only option is to increase the tile size to 18 by 18 adding a border around the tile to match the edge colour. You still only render the 16*16 tile but the filtering will sample from the border pixels rather than the neighboring tile.

    I was able to fix my problem, although the first option proposed didn't work (turning off image smoothing). I managed to modify my spriteSheet class so that it would first create a 18x18px buffer image by overlaying 1 18x18 tile by the same tile, 16x16 and centered :

    defineTile(name, x, y, imgw, imgh) {
        const img_buffer = document.createElement('canvas');
        img_buffer.width = this.width + 4;
        img_buffer.height = this.height + 4;
        img_buffer
          .getContext('2d')
          .drawImage(this.image,
            x * this.width + x * this.offset, y * this.height + y * this.offset,
            this.width, this.height,
            0, 0, this.width + 4, this.height + 4);
        img_buffer
          .getContext('2d')
          .drawImage(this.image,
            x * this.width + x * this.offset, y * this.height + y * this.offset,
            this.width, this.height,
            2, 2, this.width, this.height);
    
        const buffer = document.createElement('canvas');
        buffer.width = imgw;
        buffer.height = imgh;
        buffer
          .getContext('2d')
          .drawImage(img_buffer,
            2, 2,
            this.width, this.height,
            0, 0, imgw, imgh);
        this.tiles.set(name, buffer);
      }  
    

    Even though i don't know if this is the most optimized way to do it, it does the job well ! I am opened to any suggestion that could make this part even better of course, but i consider the problem solved.