svghtml5-canvasresizedrawimage

Unexpected cropping of SVG image when resizing canvas


const pieceSVG = document.getElementById("pieceSVG");

// Convert the SVG element to a string to be converted to blob
var svgString = new XMLSerializer().serializeToString(pieceSVG);


//create our canvas
const canvas = document.getElementById("dummyCanvas");
//and context
const ctx = canvas.getContext("2d");

//create our image in blob format and then create url for it
const imageBlob = new Blob([svgString], {
  type: "image/svg+xml"
});
const imageUrl = URL.createObjectURL(imageBlob);

const img = new Image();
img.height = 100;
img.width = 100;
img.src = imageUrl;

//set width, if we are narrow it should be horizontal
const setCanvasWidth = () => {
  const width =
    window.innerWidth < 720 ? window.innerWidth * 0.6 : window.innerWidth / 10;

  return width;
};

//if wide it should be vertical
const setCanvasHeight = () => {
  const height =
    window.innerWidth > 720 ?
    window.innerHeight * 0.6 :
    window.innerHeight / 10;

  return height;
};

const setSize = () => {
  canvas.width = setCanvasWidth();
  canvas.height = setCanvasHeight();
};
//draw piece tied to the height if horizontal and to the width if horizontal
//draw piece tied to the height if horizontal and to the width if horizontal
const drawPiece = () => {
  if (!ctx) return;
  if (window.innerWidth < 720) {
    ctx.drawImage(img, 0, 0, 45, 45, 0, 0, canvas.height, canvas.height);
  } else {
    ctx.drawImage(img, 0, 0, 45, 45, 0, 0, canvas.width, canvas.width);
  }
};
//set our canvas size
setSize();

window.addEventListener("resize", setSize);

//set up our event loop to draw the piece
const eventLoop = () => {
  requestAnimationFrame(() => {
    drawPiece();
    eventLoop();
  });
};

eventLoop();
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Document</title>

</head>

<body>
  <canvas id="dummyCanvas" style="background-color: blue"></canvas>
  <svg id="pieceSVG" xmlns="http://www.w3.org/2000/svg" version="1.1" width="45" height="45">
  <g style="fill:#ffffff;stroke:#000000;stroke-width:1.5;stroke-linejoin:round">
    <path d="M 9,26 C 17.5,24.5 30,24.5 36,26 L 38.5,13.5 L 31,25 L 30.7,10.9 L 25.5,24.5 L 22.5,10 L 19.5,24.5 L 14.3,10.9 L 14,25 L 6.5,13.5 L 9,26 z"/>
    <path d="M 9,26 C 9,28 10.5,28 11.5,30 C 12.5,31.5 12.5,31 12,33.5 C 10.5,34.5 11,36 11,36 C 9.5,37.5 11,38.5 11,38.5 C 17.5,39.5 27.5,39.5 34,38.5 C 34,38.5 35.5,37.5 34,36 C 34,36 34.5,34.5 33,33.5 C 32.5,31 32.5,31.5 33.5,30 C 34.5,28 36,28 36,26 C 27.5,24.5 17.5,24.5 9,26 z"/>
    <path d="M 11.5,30 C 15,29 30,29 33.5,30" style="fill:none"/>
    <path d="M 12,33.5 C 18,32.5 27,32.5 33,33.5" style="fill:none"/>
    <circle cx="6" cy="12" r="2" />
    <circle cx="14" cy="9" r="2" />
    <circle cx="22.5" cy="8" r="2" />
    <circle cx="31" cy="9" r="2" />
    <circle cx="39" cy="12" r="2" />
  </g>
</svg>

</body>

</html>

I have an SVG image that is being drawn to HTML5 canvas. I have set the canvas up to be responsive to screen width. I am aware that using css styling to set height and width distort the image so I have used canvas.height = screenWidth / 15... and canvas.width in the same way.

I can see that the canvas height and width is the size expected, however the SVG does not draw to the correct dimensions and cuts off unexpectedly.

//this is my draw pieces method for my piecesClass

 drawPieces() {
    drawTakenPieces(this.context, this.pieceArray, this.squareWidth);
  } 

which calls this function

const drawTakenPieces = (
  ctx: CanvasRenderingContext2D,
  pieceArray: PieceOffBoard[],
  squareWidth: number
) => {
  pieceArray.forEach((piece) => {
    drawSquareOffBoard(piece.coord, squareWidth, ctx, "beige");

    drawPieceByCoord(piece.coord, squareWidth, piece, ctx);
  });
};

which in turn calls this function

//draws the piece by coordinate - full piece cannot be plugged in
export const drawPieceByCoord = (
  coord: Coord,
  squareWidth: number,
  piece: PieceType | PieceOffBoard,
  ctx: CanvasRenderingContext2D
) => {
  if (!ctx) return;
  //set our parameters

  const img = piece.image;

  //we need to adjust our x and y values so that the center of the picture is drawn on the central coord

  let { x, y } = coord;
  x = x - squareWidth / 2;
  y = y - squareWidth / 2;

  //draw that image to the canvas params 2-5 are for source pic, last 4 are for canvas drawing
  ctx.drawImage(img, 0, 0, 45, 45, x, y, squareWidth, squareWidth);
};

As you can see I am using the long form of the drawImage api as I want to keep the ratio correct. Also my SVG is hardcoded to dimensions of 45 , 45 in the SVG file itself which is why those numbers have been selected. The draw function works perfectly with another ctx where the canvas width and height are the same even when resized.

I have tried tinkering with some scaling and with various combinations including the more basic form of the api ctx.drawImage(img,0,0). I have checked through stack-overflow and tried my luck with chatGPT but have not gotten anything that appears to work.

I have also console.logged the canvas width alongside my square width value to make sure that I was setting these correctly and they are the same minus some rounding differences which I presume is due to not working with fractions of pixels.

I hope someone can help with this because I have been stuck on what I presume is something quite simple and basic for the last day.

Please see below for minimal reproducible example. All it needs is an html doc with a canvas with id dummyCanvas

const pieceSVG = require("./assets/whitePieces/white_queen.svg");

//create our canvas
const canvas = document.getElementById("dummyCanvas") as HTMLCanvasElement;
//and context
const ctx = canvas.getContext("2d");

//create our image
const imageBlob = new Blob([pieceSVG], { type: "image/svg+xml" });
const imageUrl = URL.createObjectURL(imageBlob);

const img = new Image();
img.height = 100;
img.width = 100;
img.src = imageUrl;

//set width, if we are narrow it should be horizontal
const setCanvasWidth = (): number => {
  const width =
    window.innerWidth < 720 ? window.innerWidth * 0.6 : window.innerWidth / 10;

  return width;
};

//if wide it should be vertical
const setCanvasHeight = (): number => {
  const height =
    window.innerWidth > 720
      ? window.innerHeight * 0.6
      : window.innerHeight / 10;

  return height;
};

const setSize = () => {
  canvas.width = setCanvasWidth();
  canvas.height = setCanvasHeight();
};
//draw piece tied to the height if horizontal and to the width if horizontal
const drawPiece = () => {
  if (!ctx) return;
  if (window.innerWidth < 720) {
    ctx.drawImage(img, 0, 0, 45, 45, 0, 0, canvas.height, canvas.height);
  } else {
    ctx.drawImage(img, 0, 0, 45, 45, 0, 0, canvas.width, canvas.width);
  }
};
//set our canvas size
setSize();

window.addEventListener("resize", setSize);

//set up our event loop to draw the piece
const eventLoop = () => {
  requestAnimationFrame(() => {
    drawPiece();
    eventLoop();
  });
};

eventLoop();


Solution

  • So I identified the issue. The issue was not replicating when getting the element through document.getElementById but was still occurring when requiring the file through webpack.

    I was using svg-inline-loader to load my svgs which in turn caused the "width" and the "height" attributes to be stripped away. Without this width and height attributes, when scaling there was no absolute reference point for the ctx.drawImage to work from causing there to be a cut-off.

    This was fixed by editing my webpack.config.js in the rules section. This is the configuration which worked for me which allowed me to keep the svg height and width properties.

     {
            test: /\.svg$/,
            use: {
              loader: "svg-inline-loader",
              options: { removeSVGTagAttrs: false },
            },
          },