Search code examples
javascripthtmlgraphicssafarihtml5-canvas

Why there's white line appears in HTML canvas between two shape?


Why there's white line appears in JS canvas between two shape?

I'm making a game with JS / TS (I'm using MacBook Pro), with HTML5 canvas, and there's a unexpected white line appear between two shapes in safari browser: enter image description here

but I run exactly same code in chrome, everything is fine: enter image description here

So why this is happened? And how can I fix it?

code I'm using to render

CONTEXT.drawImage(
    CACHES.get(this.materialURL),
    (this.rect.x - camera.location.x) * GRID_W,
    (this.rect.y - camera.location.y) * GRID_H,
    GRID_W,
    GRID_H,
);

Solution

  • Render artifacts

    More info?

    There are many reasons this can happen. Most are the result of rounding errors. Sometimes the error is in JavaScript, other times it occurs in the rendering.

    There are subtle differences in the JS engines (resulting from hardware, OS, driver and or engine implementations) that can result in rendering artifact that differ across devices.

    There are major differences in rendering implementations even on the same browser, same OS, and using the same hardware, depending on setup (flags).

    Where your artifacts are coming from I can only guess at without a lot more information. Even how you captured the example images can change the solution.

    Things to try

    1. Try using nearest pixel lookup by setting 2D context smoothing off

      ctx.imageSmoothingEnabled = false;
      

      To turn back on use

      ctx.imageSmoothingEnabled = true;
      
    2. Use software rendering (CPU) by setting the willReadFrequently flag when getting the context.

      const ctx = canvas.getContext("2d", {willReadFrequently: true});
      

      Note this can slow things down a lot

    3. Turn off canvas alpha (to stop BG appearing at seams) using context option alpha

      const ctx = canvas.getContext("2d", {alpha: false});
      
    4. Ensure that the source image resolution matches the render size.

      In other words does

      const img = CACHES.get(this.materialURL);
      const isSameRes  = img.width === GRID_W && img.height === GRID_H;
      

      isSameRes should equal true?

      Note use naturalWidth and naturalHeight if img is an instance of Image

    5. Extend the source image by 1 px on each edge copying the edge pixels as shown in next image. This will prevent transparent edge pixels bleeding into rendering result.

      Expand tile image by 1px on each edge

      Then render the inner original image as shown below

      const img = CACHES.get(this.materialURL);
      ctx.drawImage(
          img,
          1, 1, img.width - 2, img.height - 2,
          (this.rect.x - camera.location.x) * GRID_W,
          (this.rect.y - camera.location.y) * GRID_H,
          GRID_W,
          GRID_H,
      );
      

      Note this will add in tiny bit of overhead.

    6. Ensure integer coordinates by flooring coordinates and forcing constants to be integers.

      // When defining GRID_W and GRID_H (assuming positive integer values) 
      // Force internal type to int32 by using bitwise operation on values
      // Note this may not do anything
      const GRID_W = 32 | 0;
      const GRID_H = 32 | 0;
      
      // Render using floored coordinates.
      ctx.drawImage(
          CACHES.get(this.materialURL),
          Math.floor((this.rect.x - camera.location.x) * GRID_W),
          Math.floor((this.rect.y - camera.location.y) * GRID_H),
          GRID_W,
          GRID_H,
      );
      

    More

    There are many more options but without the needed information I would be wasting your time.