-1

I have a html5 canvas element that takes size of the parent div in JSX.

<canvas
 ref={canvasRef}
 width={canvasParentRef.current?.offsetWidth}
 height={canvasParentRef.current?.offsetHeight}
 className={style.canvas}
/>

In this canvas I always have a rectangle on the center position, and possibly some other rectangles which position is based on the coordinates of the center rectangle.

I have a function to calculate position of the main rectangle that is centered on canvas.

  const centerRectCoords = () => {
    const {width, height} = rect;
    const canvas = canvasRef.current;
    const startX = canvas.width / 2 - width / 2;
    const startY = canvas.height / 2 - height / 2;

    return {
      startX,
      startY,
      endX: startX + width,
      endY: startY + height,
    };
  };

But I have a problem, everything looks too blurry on higher resolution screens. To prevent that, I have found some solution on MDN to scale canvas up. For that I have a following function:

 const scaleCanvas = (
    canvas: HTMLCanvasElement,
    ctx: CanvasRenderingContext2D
  ): void => {
    const canvasParent = canvasParentRef.current;
    if (!canvasParent) return;

    const { devicePixelRatio } = window;
    const dimensions = canvasParent.getBoundingClientRect();

    canvas.width = dimensions.width * devicePixelRatio;
    canvas.height = dimensions.height * devicePixelRatio;

    ctx.scale(devicePixelRatio, devicePixelRatio);

    canvas.style.width = `${dimensions.width}px`;
    canvas.style.height = `${dimensions.height}px`;
  };

This function is called in useEffect on mount.

But now whenever I generate these rectangles on higher resolution screen, they are not centered, because width and height on canvas is now bigger than style.width and style.height. I thought to maybe use style.width and style.height in centerRectCoords function calculation, but those values are undefined because canvas is not visible in my app on load, it is another tab.

How can I make everything scale nicely on bigger and smallers screens, with correct sizing and without it being blurry?

lukas23
  • 107
  • 2
  • 10
  • I think you should avoid CSS altogether. Also, you may need to [translate the canvas by a half pixel](https://stackoverflow.com/a/13294650/1762224). Set the size of the canvas appropriate to the screen resolution. – Mr. Polywhirl Jun 14 '23 at 12:47
  • 1
    @Mr.Polywhirl that comment is all wrong. Using CSS to shrink the canvas on high-res monitors *is* how you can get the canvas at the appropriate size. And offsetting by half a pixel only concerns the stroking of 1px lineWidth vertical or horizontal lines. Nothing else. – Kaiido Jun 14 '23 at 12:54
  • 1
    The cleanest might be to use the DOMMatrix returned by `ctx.getTransform()`, `invert()` it and use that to transform your coords, but the easiest if it's the only time you do such a conversion is certainly to divide your canvs width and height by `devicePixelRatio`. – Kaiido Jun 14 '23 at 12:59

2 Answers2

0

It’s impossible to imagine what you’re doing there and you didn’t even post an image!

But since you mention that things screw up based on screen size, then maybe what you need is CSS Media Queries:

Screen media queries are CUSTOM element sizes and behavior, based on the user's screen size.

Example of how screen media queries are used:

ie: Smaller screens do this...

@media screen and (max-width:799px){

 #Elem_1 {height:58.5%; width:48.5%;}

 #Elem_2 {height:38.5%; width:48.5%;}

}

ie: Bigger screens do that...

@media screen and (max-width:1500px){

 #Elem_1 {height:68.5%; width:58.5%;}

 #Elem_2 {height:78.5%; width:48.5%;}

}

https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries

PassThru
  • 9
  • 2
0

The problem is that since you do scale your context, the size of your canvas doesn't map to your context's visible area. If we say that you're on a 2x monitor, then if the canvas is 500px wide, on the context you'd need to draw a 250(magic unit) wide rectangle for it covers the whole bitmap.

So what you need is to convert the coordinates and sizes from the outer world, into the ones in your context.

One quite cumbersome way, but which will work in most situations is to get the context's current transformation as a DOMMatrix, invert it and then transform your coordinates based on this inverted matrix:

/**
 * Transforms a "world" point to the context coordinates.
 */
const worldToContext = (ctx, x, y) => {
  return ctx.getTransform()
    .invertSelf()
    .transformPoint({ x, y });
}
const centerRectCoords = (ctx, rect) => {
  const {width, height} = rect; // rect is a context coord
  /** 
   * canvas.width & height need to be transformed
   * We can treat both as a single point to get their context equivalent.
   */
  const {
    x: contextWidth,
    y: contextHeight
  } = worldToContext(ctx, canvas.width, canvas.height);
  const startX = contextWidth / 2 - width / 2;
  const startY = contextHeight / 2 - height / 2;

  return {
    startX,
    startY,
    endX: startX + width,
    endY: startY + height,
  };
};

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
canvas.width = 300 * 5;
canvas.height = 150 * 5;
ctx.scale(5, 5);
const rect = centerRectCoords(ctx, { width: 50, height: 50 });
ctx.lineTo(rect.startX, rect.startY);
ctx.lineTo(rect.endX, rect.startY);
ctx.lineTo(rect.endX, rect.endY);
ctx.lineTo(rect.startX, rect.endY);
ctx.closePath();
ctx.stroke();
canvas { 
  width: 300px;
  height: 150px;
  border: 1px solid;
}
<canvas></canvas>

But for this exact case, it might be a lot simpler and clearer to store the contextWidth and contextHeight properties directly in your resize handler:

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
/**
 * Store the context's dimensions.
 * By default set it to the canvas size.
 * When we'll update the context's scale, we'll also update these.
 */
let contextWidth = canvas.width;
let contextHeight = canvas.height;

const centerRectCoords = (rect) => {
  const { width, height } = rect;
  const startX = contextWidth / 2 - width / 2;
  const startY = contextHeight / 2 - height / 2;
  return {
    startX,
    startY,
    endX: startX + width,
    endY: startY + height,
  };
};
const scaleCanvas = (canvas, ctx) => {
  const canvasParent = canvas.parentNode;
  if (!canvasParent) return;

  const { devicePixelRatio } = window;
  const dimensions = canvasParent.getBoundingClientRect();

  canvas.width = dimensions.width * devicePixelRatio;
  canvas.height = dimensions.height * devicePixelRatio;
  // Update the context's size variables
  contextWidth = dimensions.width;
  contextHeight = dimensions.height;
  ctx.scale(devicePixelRatio, devicePixelRatio);

  canvas.style.width = `${dimensions.width}px`;
  canvas.style.height = `${dimensions.height}px`;
};
const draw = () => {
  const rect = centerRectCoords({ width: 50, height: 50 });
  ctx.lineTo(rect.startX, rect.startY);
  ctx.lineTo(rect.endX, rect.startY);
  ctx.lineTo(rect.endX, rect.endY);
  ctx.lineTo(rect.startX, rect.endY);
  ctx.closePath();
  ctx.stroke();
}
const observer = new ResizeObserver(([entry]) => {
  scaleCanvas(canvas, ctx);
  draw();
});
observer.observe(canvas.parentNode);
.resizer {
  outline: 1px solid;
  resize: both;
  overflow: hidden;
  width: 400px;
  height: 200px;
}
<div class=resizer>
  <canvas></canvas>
</div>

I'm not really into JSX, so I'll let you figure out the best way to store these variables so it's accessible to your script.

Kaiido
  • 123,334
  • 13
  • 219
  • 285