0
votes

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

1
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 tileBlindman67
Thank you very much for commenting @Blindman67 , I guess I’ll try both ways, even if the second one might be a bit more complex to implement ! Are there any noticeable downsides in turning off the bilinear filtering ?Alivanar
There is no downside apart from appearance, you will have to see to see if it will suit your game style. The worst FX of smoothing off is when you reduce the image size as you will start to get aliasing artifacts. On some (lower end) hardware smoothing off will give a bit of a performance increase.Blindman67
Actually, after trying the first method, no changement is visible I still have the "bleed" from the neighboring tiles... Any idea why ? I'll try to implement the second method later today.Alivanar
When logging the context, ctx.imageSmoothingEnabled is in fact set to falseAlivanar

1 Answers

0
votes

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.