skip to Main Content

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?

2

Answers


  1. 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

    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search