Canvas has inconsistent pixel grid across browsers when using drawImage()

隐身守侯 提交于 2020-01-14 14:45:09

问题


I recognize that Canvas drawImage is inexplicably offset by 1 Pixel is a very similar issue, but I was already applying the advice given in that question's answer before I even came across this problem.

I'm implementing a sprite sheet system for an HTML5-based game. Individual frames are defined simply:

frame = new AnimationFrame(img, x, y, w, h);

Inside the AnimationFrame constructor, all of the numeric parameters are truncated to integers.

Then when I go to draw it on the canvas, I use what should also be simple code:

context.drawImage(frame.img,
  frame.x,
  frame.y,
  frame.w,
  frame.h,
  this.position.x | 0,
  this.position.y | 0,
  frame.w,
  frame.h,
);

Unfortunately, I get different results in different browsers.

In Chrome on Mac and Firefox on Mac, slicing the sprite sheet this way causes the individual frames to be offset by half a pixel, causing the top edge of the character's head to be drawn too thin and the top of the next sprite down to peek into the bottom of this one.

I can accommodate this problem with frame.x - 0.5 and frame.y - 0.5 but if I do that then I get the opposite problem in Safari on Mac and iOS and in Firefox on Windows.

I don't particularly WANT to do a browser detect to decide how to nudge the coordinate system, so I'm looking for suggestions for a way to either (1) force the various browsers to behave the same way, or (2) detect the issue at page load time with a test so I can just store the pixel grid offset in a variable.

NB: I'm going for a chunky pixel aesthetic, so my canvas is scaled by a factor of 2. This works fine without blurring, but it makes the half-pixel issue more clearly visible. Without scaling, the edges of the sprite still get distorted, so that's not the problem.


回答1:


I can't tell for IE, but at least for Safari, this sounds like a bug in their nearest-neighbor algorithm when the transformation matrix translate is set to exactly n.5.

onload = function() {

  var ctx = document.querySelector('canvas').getContext('2d');
  ctx.imageSmoothingEnabled = false;
  var img = document.querySelector('#hero');

  ctx.setTransform(2, 0, 0, 2, 99.49, 99.49);
  ctx.drawImage(img, 32, 32, 16, 16, 0, 0, 16, 16);

  ctx.setTransform(2, 0, 0, 2, 99.5, 99.5);
  ctx.drawImage(img, 32, 32, 16, 16, 16, 0, 16, 16);

  ctx.setTransform(2, 0, 0, 2, 99.51, 99.51);
  ctx.drawImage(img, 32, 32, 16, 16, 32, 0, 16, 16);

};
<img src='http://xmpps.greenmaw.com/~coda/html52d/hero.png' id='hero' style='display:none' />
<canvas width='200' height='200'></canvas>

Result in Safari 11.0.3

You may want to let them know about it from their bug-tracker.

The workaround would be to make sure that you never lay on floating coordinates, even in your transformation matrix.




回答2:


Sprites can not share edges!

What you are seeing is normal. It is not only the browsers that will give different results, but different hardware and device settings will also show this problem to different degrees.

The problem is in the hardware and these artifacts may appear even when you set all your render values to integers.

Why is this happening?

The reason is because you are trying drawing on two side of the same line. The way GPU's samples images means that there will always be a little bit of error. You can never get rid of it. If you draw at location y = 16 a little bit of pixel 15 will bleed in. If you are using nearest neighbor, a little bit no matter how small equate to a whole display pixel.

The image shows your original sprite sheet. The top sprite's bottom is where the next sprite sprite starts. They share the boundary represented by the red line. A shared boundary will always bleed across.

This is not a browser bug, it is inherent in the hardware. You can not fix it with half offsets. There is only one way to solve the problem.

Simple solution

The solution is very simple. No two sprites may share an edge, you need a 1 pixel transparent space between consecutive sprites.

Image shows sprites on left share edge and bleed across the line. Sprites on right fixed with 1 pixel transparent space.

Eg

If you have sprite 16, by 16 then the coords you are using

// Format x,y,w,h
0,0,16,16    // right edge
16,0,16,16   // touches left edge. BAD!
... 
// next row
0,16,16,16   // Top touch sprite above bottom
16,16,16,16  // Left edge touches right edge. BAD!

Add a single pixel between all, they can still touch the image edge

0,0,16,16    // spr 0
17,0,16,16   // spr 1  No shared edge between them
... 
// next row
0,17,16,16   // spr 8  no shared edge above
17,17,16,16  // spr 9

In the render function the source coordinates should always be floored. However the transform, and the destination coordinates can be floats.

ctx.drawImage(image,int,int,int,int,float,float,float,float);

The next image is your sprite sheet correctly spaced. Sprites are still 16 by 16 but they are spaced 17 pixels apart so no two sprites share a boundary.

To draw a sprite by index from top left to right and same for each row down.

var posx,posy; // location of sprite on display canvas
var sprCols = 6; // number of columns
var sprWidth = 16; 
var sprHeight = 16; 
var sprIndex = ?  // index of sprite 0 top left
var x = (sprIndex % sprCols) * (sprWidth + 1);
var y = (sprIndex / sprCols | 0) * (sprHeight + 1);
ctx.drawImage(spriteSheet, x, y, sprWidth, sprHeight, posx, posy, sprWidth, sprHeight);

Back to game creation.

Now you can forget that crazy half pixel stuff, its just nuts, as it does not work. This is how how its done, and its been done this way since the first GPU's hit the market in the mid 1990's.

Note All images sourced from OP's fiddle



来源:https://stackoverflow.com/questions/48998301/canvas-has-inconsistent-pixel-grid-across-browsers-when-using-drawimage

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!