Таблица стилей неправильно обрезается на ‹холсте›

Я делаю игру с холстом, но у меня есть небольшая проблема с методом drawImage(...);, который должен обрезать лист спрайтов, чтобы получить правильный спрайт. Когда мы бежим и особенно когда прыгаем, мы можем видеть фрагменты соседнего спрайта.

(Примечание: если вы хотите запустить этот код, убедитесь, что вы используете 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: есть похожий пост здесь, но на самом деле это не помогает, потому что проверенный ответ использует метод setTransform(...); 2D-контекста, но даже если он работает для Safari и IE, id не работает (по крайней мере) для Firefox (см. мой вывод проверенного ответа). Это решение слишком «зависит от браузера», мне нужно решение, которое активно поддерживается.

Второй ответ в этом посте касается добавления пустой рамки в 1 пиксель вокруг каждого спрайта на листе спрайтов, чтобы избежать размытия краев. Для этого потребуется полностью переработать лист спрайтов, поэтому я бы принял этот ответ, только если нет более простого решения.


person JacopoStanchi    schedule 30.03.2018    source источник
comment
Кажется, у меня работает на моем дисплее без сетчатки. Где смещение .5? Я этого не вижу. Почему у вас смещение 0,5?   -  person Charlie    schedule 30.03.2018
comment
Это особенность холста HTML5, вместо того, чтобы измерять по линиям между пикселями, он измеряет от центра пикселей, и ctx.translate(0.5, 0.5); должен позволить отключить эту особенность. См. этот пост.   -  person JacopoStanchi    schedule 30.03.2018
comment
Можете ли вы прикрепить скриншот проблемы, с которой вы столкнулись? Вы пытались заполнить спрайты одной строкой прозрачных пикселей в качестве обходного пути?   -  person Charlie    schedule 30.03.2018
comment
@Charlie pic   -  person JacopoStanchi    schedule 30.03.2018
comment
ты уверен, что твоя математика верна? Firefox говорит, что лист спрайтов имеет ширину 246 пикселей, код говорит, что спрайты имеют ширину 41 пиксель, я вижу 6 в поперечнике, что составляет 252 пикселя. Вы уверены, что это соседний спрайт, а не тот же самый, обернутый из-за какой-то странной проблемы с текстурой? Я предлагаю раскрасить спрайты по-разному, чтобы вы могли определить, какие из них просачиваются во время тестирования.   -  person Charlie    schedule 30.03.2018
comment
Да, я почти уверен в себе, если бы я обрезал одну дополнительную строку пикселей, следующий спрайт был бы на один пиксель левее, а следующий еще на один пиксель левее и т. д. Кроме того, когда мы заставляем персонажа бежать, мы можем ясно видеть, что это разные спрайты. Я почти уверен, что это связано со смещением 0,5, потому что, как и в связанный ранее, артефакт верхнего спрайта сливается с фоном и не является чисто черным без прозрачности.   -  person JacopoStanchi    schedule 30.03.2018
comment
Вот мой лист спрайтов. Его размер 246 х 240. Размер спрайта 41 х 40. 246 = 6 х 41 и 240 = 6 х 40. Я не думаю, что здесь есть проблема.   -  person JacopoStanchi    schedule 30.03.2018
comment
Хех, я, должно быть, опечатался, мой плохой. Я по-прежнему предлагаю поменять местами некоторые цвета, чтобы убедиться, что это либо два разных спрайта, либо тот, который обернут.   -  person Charlie    schedule 30.03.2018
comment
Трюк со смещением 0,5 следует использовать только для метода stroke() и даже только для строк с нечетной шириной. Это связано с тем, что когда вы проводите линию в позиции x, линия будет нарисована с этим x в качестве центра вашей линии, поэтому линия шириной 1 пиксель должна распространяться от x-0,5 до x+0,5 => срабатывает сглаживание. все другие методы, или даже для широких линий, этот трюк произведет обратный эффект.   -  person Kaiido    schedule 30.03.2018
comment
@Kaiido У меня недостаточно репутации, чтобы прокомментировать ответ, который вы дали в другом сообщении, но setTransform(...) не решил проблему для Firefox Quantum: картинка. Решение setTransform(...) очень «зависит от браузера», поэтому вы предлагаете мне использовать метод прозрачной границы в 1 пиксель?   -  person JacopoStanchi    schedule 30.03.2018
comment
@JacopoStanchi фрагмент ответа должен был продемонстрировать ошибку, а не исправить ее. В исправлении говорилось, что не используются плавающие координаты, но я согласен, что это было не совсем ясно, поэтому я снова открыл ваш вопрос.   -  person Kaiido    schedule 31.03.2018
comment
Я знаю, но даже тех спрайтов, которые должны были быть хороши на твоем скриншоте, не было в моем фрагменте, как и в левом.   -  person JacopoStanchi    schedule 01.04.2018


Ответы (1)


Для пиксель-арта всегда рисуйте целыми числами, чтобы избежать размытия близких спрайтов.

Это означает, что матрица преобразования вашего контекста также должна быть установлена ​​на целочисленные значения, и что вы округляете все значения, которые вы передаете методу drawImage.

В вашем коде значения x и y вашего объекта являются плавающими значениями, когда вы перемещаетесь, потому что ваши значения gravity и speed являются плавающими.

Это не проблема сама по себе, вам просто нужно округлить ее на этапе рендеринга.

В приведенном ниже фрагменте я добавил условный 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>

person Kaiido    schedule 31.03.2018
comment
Спасибо, но теперь это делает меня еще одним препятствием, если я установлю гравитацию на 1 или любое другое положительное целое число, спрайт будет падать слишком быстро, и я не хочу замедлять requestAnimationFrame(...). - person JacopoStanchi; 01.04.2018
comment
О, я понял, мне нужно Math.floor(...) координаты в ctx.drawImage(...), но не обязательно сами координаты, верно? - person JacopoStanchi; 01.04.2018
comment
@JacopoStanchi да, в отредактированном фрагменте я обошел это, но вы можете захотеть... - person Kaiido; 02.04.2018