<canvas> 上的样式表未正确裁剪

Stylesheet not cropped properly on <canvas>

我正在用 canvas 玩游戏,但我的方法 drawImage(...); 有一个小问题,该方法应该裁剪 sprite sheet 以获得正确的 sprite。当我们运行,尤其是当我们跳跃的时候,我们可以看到相邻的精灵。

(注意:如果您想 运行 此代码,请确保您 运行 Firefox 或 Chrome 因为给定 image-rendering 的值仅在这些浏览器)。

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>

如果您看不到边缘出血,那是问题的一部分,这在每个浏览器上都不受类似支持,我想要一个在每个浏览器上都相同的解决方案。这是我看到的:

感谢您的帮助。


EDIT1:有个类似的post, 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)。这个方案太'browser dependant',我想要一个强烈支持的方案

此 post 的第二个答案是关于在精灵 sheet 中的每个精灵周围添加一个 1 像素的空边框以避免边缘出血。这将需要完全返工 sprite sheet,因此只有在没有更简单的解决方案时我才会接受这个答案。

对于 pixel-art,始终以整数值绘制,以免靠近的精灵流出。

这意味着,您的上下文也必须将其转换矩阵设置为整数值,并且您对传递给 drawImage 方法的所有值进行四舍五入。

在您的代码中,移动时对象的 xy 值是浮动值,因为您的 gravityspeed 值是浮点数。

这本身不是问题,您只需要在渲染阶段对其进行舍入即可。

在下面的代码片段中,我添加了一个条件 fillRect,每次这些值不是整数时都会触发它。

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>