0

I'm doing a game with canvas but I have a small problem with the the method drawImage(...); which is supposed to crop the sprite sheet to get the proper sprite. When we run and especially when we jump, we can see bits of the adjacent sprite.

(Note: if you want to run this code, make sure you run either Firefox or Chrome as the values given to image-rendering are only supported on these browsers).

var ctx;
var heightCanvas;
var widthCanvas;
var player;
var reqAnim;
var stopped;

left = false;
right = false;
up = false;

window.onload = function() {
 var canvas = document.getElementById('canvas');
 heightCanvas = canvas.height;
 widthCanvas = canvas.width;
 ctx = canvas.getContext('2d');
 ctx.imageSmoothingEnabled = false;
 
 //Detection of arrow keys
 document.onkeydown = function(e) {
  if (e.keyCode == 37) left = true;
  if (e.keyCode == 39) right = true;
  if (e.keyCode == 38) up = true;
 }
 document.onkeyup = function(e) {
  if (e.keyCode == 37) left = false;
  if (e.keyCode == 39) right = false;
  if (e.keyCode == 38) up = false;
 }
 
 //The animation begins when the sprite sheet is loaded
 img = new Image();
 img.onload = function() {
  player = new Player(img,10,50);
  reqAnim = requestAnimationFrame(updateCanvas);
  stopped = false;
 }
 img.src = "https://i.imgur.com/6eKrMOI.png";
}

function updateCanvas() {
 ctx.clearRect(0, 0, widthCanvas, heightCanvas);
 player.updatePos();
 player.updateStateDirection();
 player.updateSprite();
 player.updateDisplay()
 reqAnim = requestAnimationFrame(updateCanvas);
}

function startStop() {
 if (stopped) {
  reqAnim = requestAnimationFrame(updateCanvas);
  stopped = false;
 } else {
  cancelAnimationFrame(reqAnim);
  stopped = true;
 }
}

//----------------------------------//
//----------------------------------//
//----------Code of Player----------//
//----------------------------------//
//----------------------------------//

function Player(spritesheet, x, y) {
 this.spritesheet = spritesheet;
 this.x = x;
 this.y = y;
 
 //The direction of the player. false = left, true = right
 this.direction = true;
 //The state of the player. 0 = stand, 1 = run
 this.state = 0;
 //The dimensions of a sprite in the sprite sheet
 this.width = 41;
 this.height = 40;
 
 //All the attributes beginning with 'ss' are related with the sprite sheet.
 
 //The coordinates of the current sprite in the sprite sheet 
 this.ssX = 0;
 this.ssY = 200;
 //The number of times that we have repeated the current sprite
 this.ssRepeat = 0;
 
 this.speed = 2.5;
 this.gravity = 0.3;
 this.gravitySpeed = 0;
 this.jumping = false;
 
 //state: 0 = stand, 1 = run
 //direction: false = left, true = right
 this.updateStateDirection = function() {
  if (left) { //If left is pressed
   if (this.state != 1 || this.direction) { //If the player wasn't running
    this.state = 1;       //or if he was running in the opposite direction
    this.ssY = 0;
   }
   this.direction = false;
  } else if (right) { //If right is pressed
   if (this.state != 1 || !this.direction) { //If the player wasn't running
    this.state = 1;       //or if he was running in the opposite direction
    this.ssY = 80;
   }
   this.direction = true;
  } else if (this.state != 0) { //If neither right nor left are pressed and the state isn't stand
   this.state = 0;
   if (this.direction) this.ssY = 200;
   else this.ssY = 160;
  }
 }
 
 this.updateSprite = function() {
  if (this.state == 0) { //If the state is stand
   if (this.ssRepeat < 15) //We display the same sprite 15 times before passing to the next one
    this.ssRepeat++;
   else {
    this.ssRepeat = 0;
    if (this.ssX < 205) this.ssX += this.width; //If we didn't reach the end of the sprite sheet
    else this.ssX = 0;
   }
  } else if (this.state == 1) { //If the state is run
   if (this.ssRepeat < 5) //We display the same sprite 5 times before passing to the next one
    this.ssRepeat++;
   else {
    this.ssRepeat = 0;
    if (this.ssX < 205) this.ssX += this.width; //If we didn't reach the end of the sprite sheet
    else {
     this.ssX = 0;
     if (this.ssY < 40) this.ssY = 40; //If we reached the end of the first line of the SS
     else if (this.ssY < 80) this.ssY = 0; //the end of the second
     else if (this.ssY < 120) this.ssY = 120; //the third
     else this.ssY = 80; //the fourth
    }
   }
  }
 }
 
 //Display the proper sprite of the spritesheet
 this.updateDisplay = function() {
  ctx.drawImage(this.spritesheet, this.ssX, this.ssY,
   this.width, this.height, this.x, this.y, this.width, this.height);
 }
    
    //Updates the position of the sprite according to the user's inputs
 this.updatePos = function() {
  this.jump();
  this.gravitySpeed += this.gravity;
  this.y += this.gravitySpeed;
  this.hitBottom();
  this.move();
 }
 
 this.hitBottom = function() {
  var rockbottom = heightCanvas - this.height;
  if (this.y > rockbottom) {
   this.y = rockbottom;
   this.gravitySpeed = 0;
   this.jumping = false;
  }
 }
 
 this.move = function() {
  if (left) player.x -= this.speed;
  if (right) player.x += this.speed;
 }
 
 this.jump = function() {
  if (!this.jumping) {
   if (up) {
    this.gravitySpeed = -5.2;
    this.jumping = true;
   }
  }
 }
 
}
<!DOCTYPE html>
<html>
 <head>
  <title>Forto</title>
  <meta charset="UTF-8"> 
  <style>
  canvas {
   border: 1px solid black;
   background-color: #9e9eaf;
   image-rendering: optimizespeed; /*Firefox*/
   image-rendering: pixelated; /*Chrome*/
  }
  </style>
  <script src="forto.js"></script>
 </head>
 <body>
  <canvas id="canvas" width="300" height="100"></canvas>
  <br>
  <button onclick="startStop()">Start/Stop</button>
 </body>
</html>

If you can't see the edge bleeding, that's part of the problem, this is not supported similarly on each browser and I want a solution that is the same on every browser. Here is what I see:

edge bleeding

Thank you for your help.


EDIT1: There is a similar post here, but it doesn't really help because the validated answer uses the setTransform(...); method of the 2D context, but even if it works for Safari and IE, id doesn't (at least) for Firefox (see my output of the validated answer). This solution is too 'browser dependant', I want a solution that is strongly supported.

The second answer of this post is about adding an empty border of 1 pixel around each sprite in the sprite sheet to avoid edge bleeding. This would require to rework entirely the sprite sheet, so I would accept this answer only if there is no simpler solution.

JacopoStanchi
  • 421
  • 5
  • 16
  • Seems to be working for me on my non-retina display. Where is the .5 offset? I don't see it. Why do you have a .5 offset? – Charlie Mar 30 '18 at 01:31
  • That's a feature of HTML5 canvas, instead of measuring from the lines between the pixels, it measure from the center of the pixels, and `ctx.translate(0.5, 0.5);` should allow to disable this particularity. See [this post](https://stackoverflow.com/questions/8696631/canvas-drawings-like-lines-are-blurry). – JacopoStanchi Mar 30 '18 at 01:34
  • Can you attach a screenshot of the issue you are having? Have you tried padding your sprites with a single line of transparent pixels as sort of a workaround? – Charlie Mar 30 '18 at 01:37
  • @Charlie [pic](https://i.imgur.com/2HtzWd9.png) – JacopoStanchi Mar 30 '18 at 01:40
  • are you sure your math is right? Firefox says the sprite sheet is 246px wide, the code says 41px wide sprites, I see 6 across, that comes to 252px. Are you sure its the adjacent sprite and not the same one being wrapped through some strange texture issue? I suggest coloring the sprites differently so you can tell which is bleeding through while testing. – Charlie Mar 30 '18 at 01:45
  • Yes I'm pretty sure of myself, if I was cropping one additional line of pixels, the next sprite would be one pixel to the left, and the next another one pixel to the left etc. Besides, when we make the character run, we can clearly see that this is different sprites. I'm pretty sure that this is related to the 0.5 offset because, just as [the post I linked previously](https://stackoverflow.com/questions/8696631/canvas-drawings-like-lines-are-blurry), the artifact of the upper sprite blends into the background and is not pure black with no transparency. – JacopoStanchi Mar 30 '18 at 01:52
  • [Here is my sprite sheet](https://i.imgur.com/6eKrMOI.png). Its size is 246 x 240. The size of a sprite is 41 x 40. 246 = 6 x 41 and 240 = 6 x 40. I don't think there's a problem here. – JacopoStanchi Mar 30 '18 at 02:04
  • Heh, I must have mistyped, my bad. I still suggest swapping out some colors to make sure its either two different sprites or the one being wrapped. – Charlie Mar 30 '18 at 02:29
  • **The 0.5 offset trick should only be used for `stroke()` method**, and even only for odd lineWidths. This is because when you stroke a line on a x position, the line will be drawn with this x as the center of your line, so a 1px wide line should spread from x-0.5 to x+0.5 => antialiasing kicks in. But for all other methods, or for even wide lines, this trick will produce the inverse effect. – Kaiido Mar 30 '18 at 03:45
  • @Kaiido I don't have enough reputation to comment the answer you made on the other post but `setTransform(...)` didn't fix the problem for Firefox Quantum: [pic](https://i.imgur.com/fzmHLCr.png). The solution of `setTransform(...)` is very 'browser dependant', so do you suggest me to do the transparent 1 pixel border method? – JacopoStanchi Mar 30 '18 at 11:35
  • @JacopoStanchi the snippet on the answer there was to demonstrate the bug, not to fix it. The fix said to not use floating coordinates, but I agree it wasn't really clear so I did reopen your question. – Kaiido Mar 31 '18 at 04:07
  • I know but even the sprites which were supposed to be good in your screenshot were not in my snippet, like the left one. – JacopoStanchi Apr 01 '18 at 15:17

1 Answers1

1

For pixel-art, always draw at integer values, so that you avoid close sprites to bleed off.

This means, that your context must have its transformation matrix set to integer values too, and that you do round all the values you pass to drawImage method.

In your code, the x and y values of your object are floating values when you do move, because your gravity and speed values are floats.

This is not a problem per se, you just need to round it in the rendering phase.

In below snippet, I added a conditional fillRect, which gets triggered every time these values were not integers.

var ctx;
var heightCanvas;
var widthCanvas;
var player;
var reqAnim;
var stopped;

left = false;
right = false;
up = false;

window.onload = function() {
  var canvas = document.getElementById('canvas');
  heightCanvas = canvas.height;
  widthCanvas = canvas.width;
  ctx = canvas.getContext('2d');
  ctx.imageSmoothingEnabled = false;

  //Detection of arrow keys
  document.onkeydown = function(e) {
    e.preventDefault();
    if (e.keyCode == 37) left = true;
    if (e.keyCode == 39) right = true;
    if (e.keyCode == 38) up = true;
  }
  document.onkeyup = function(e) {
    if (e.keyCode == 37) left = false;
    if (e.keyCode == 39) right = false;
    if (e.keyCode == 38) up = false;
  }

  //The animation begins when the sprite sheet is loaded
  img = new Image();
  img.onload = function() {
    player = new Player(img, 10, 50);
    reqAnim = requestAnimationFrame(updateCanvas);
    stopped = false;
  }
  img.src = "https://i.imgur.com/6eKrMOI.png";
}

function updateCanvas() {
  ctx.clearRect(0, 0, widthCanvas, heightCanvas);
  player.updatePos();
  player.updateStateDirection();
  player.updateSprite();
  player.updateDisplay()
  reqAnim = requestAnimationFrame(updateCanvas);
}

function startStop() {
  if (stopped) {
    reqAnim = requestAnimationFrame(updateCanvas);
    stopped = false;
  } else {
    cancelAnimationFrame(reqAnim);
    stopped = true;
  }
}

//----------------------------------//
//----------------------------------//
//----------Code of Player----------//
//----------------------------------//
//----------------------------------//

function Player(spritesheet, x, y) {
  this.spritesheet = spritesheet;
  this.x = x;
  this.y = y;

  //The direction of the player. false = left, true = right
  this.direction = true;
  //The state of the player. 0 = stand, 1 = run
  this.state = 0;
  //The dimensions of a sprite in the sprite sheet
  this.width = 41;
  this.height = 40;

  //All the attributes beginning with 'ss' are related with the sprite sheet.

  //The coordinates of the current sprite in the sprite sheet 
  this.ssX = 0;
  this.ssY = 200;
  //The number of times that we have repeated the current sprite
  this.ssRepeat = 0;

  this.speed = 2.5;
  this.gravity = 0.3;
  this.gravitySpeed = 0;
  this.jumping = false;

  //state: 0 = stand, 1 = run
  //direction: false = left, true = right
  this.updateStateDirection = function() {
    if (left) { //If left is pressed
      if (this.state != 1 || this.direction) { //If the player wasn't running
        this.state = 1; //or if he was running in the opposite direction
        this.ssY = 0;
      }
      this.direction = false;
    } else if (right) { //If right is pressed
      if (this.state != 1 || !this.direction) { //If the player wasn't running
        this.state = 1; //or if he was running in the opposite direction
        this.ssY = 80;
      }
      this.direction = true;
    } else if (this.state != 0) { //If neither right nor left are pressed and the state isn't stand
      this.state = 0;
      if (this.direction) this.ssY = 200;
      else this.ssY = 160;
    }
  }

  this.updateSprite = function() {
    if (this.state == 0) { //If the state is stand
      if (this.ssRepeat < 15) //We display the same sprite 15 times before passing to the next one
        this.ssRepeat++;
      else {
        this.ssRepeat = 0;
        if (this.ssX < 205) this.ssX += this.width; //If we didn't reach the end of the sprite sheet
        else this.ssX = 0;
      }
    } else if (this.state == 1) { //If the state is run
      if (this.ssRepeat < 5) //We display the same sprite 5 times before passing to the next one
        this.ssRepeat++;
      else {
        this.ssRepeat = 0;
        if (this.ssX < 205) this.ssX += this.width; //If we didn't reach the end of the sprite sheet
        else {
          this.ssX = 0;
          if (this.ssY < 40) this.ssY = 40; //If we reached the end of the first line of the SS
          else if (this.ssY < 80) this.ssY = 0; //the end of the second
          else if (this.ssY < 120) this.ssY = 120; //the third
          else this.ssY = 80; //the fourth
        }
      }
    }
  }

  //Display the proper sprite of the spritesheet
  this.updateDisplay = function() {

    // since speed and gravity are floats, our coords also are: we need to round them
    var x = Math.round(this.x),
        y = Math.round(this.y);
    // simply to show these are floating values
    if(this.x !== x || this.y !== y) {
      ctx.fillRect(0,0,50,50);
    }
    
    ctx.drawImage(this.spritesheet, this.ssX, this.ssY,
      this.width, this.height, x, y, this.width, this.height);
  }

  //Updates the position of the sprite according to the user's inputs
  this.updatePos = function() {
    this.jump();
    this.gravitySpeed += this.gravity;
    this.y += this.gravitySpeed;
    this.hitBottom();
    this.move();
  }

  this.hitBottom = function() {
    var rockbottom = heightCanvas - this.height;
    if (this.y > rockbottom) {
      this.y = rockbottom;
      this.gravitySpeed = 0;
      this.jumping = false;
    }
  }

  this.move = function() {
    if (left) player.x -= this.speed;
    if (right) player.x += this.speed;
  }

  this.jump = function() {
    if (!this.jumping) {
      if (up) {
        this.gravitySpeed = -5.2;
        this.jumping = true;
      }
    }
  }

}
canvas {
  border: 1px solid black;
  background-color: #9e9eaf;
  image-rendering: optimizespeed;
  /*Firefox*/
  image-rendering: pixelated;
  /*Chrome*/
}
<canvas id="canvas" width="300" height="100"></canvas>
<button onclick="startStop()">Start/Stop</button>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Thank you, but now that makes me another impediment, if I set the gravity to 1 or any other positive integer, the sprite falls too fast, and I don't want to slow down the `requestAnimationFrame(...)`. – JacopoStanchi Apr 01 '18 at 15:22
  • Oh I got it, I need to `Math.floor(...)` the coordinates in `ctx.drawImage(...)` but not necessarily the coordinates themselves right? – JacopoStanchi Apr 01 '18 at 16:43
  • @JacopoStanchi yes, in the edited snippet I did round it, but you may want to floor... – Kaiido Apr 01 '18 at 22:57