Search code examples
cssreactjskonvajs

How to center items inside konvajs?


I am currently building some sort of meme-editor in react and for that I am using konvajs to function similar to a Canvas. My problem is that I am unable to center the items inside the canva stage, as there seems to be some property that just overrides my styling. This is (part of) the return statement in my react-component:

<div className="mycanvas">
    <Stage width={500} height={500} className="stage">
        <Layer>
            <Image image={image} className="meme" />
            {textFields.map((text) => (
                <Text
                    text={text}
                    draggable
                    fontFamily="Impact"
                    fontSize="30"
                    stroke="white"
                />
            ))}
        </Layer>
    </Stage>
</div>

And this is how the output gets rendered.

current rendering output

I have coloured the background of the wrapper blue, to show in which box the image should be centered.

I have already tried CSS on the classes "mycanvas", "stage" and "meme" and also on "konvajs-content" (as that showed up in my inspector for some reason). I have used align-items: center, margin: auto and a couple others, but I think normal CSS does not really apply here. I think it is an issue regarding the generaly styling of konvajs components, but unfortunately I could not find any solution on stackoverflow or the konva documentation.


Solution

  • This is an instance where CSS can't help. When the image is applied to the canvas using its height and width at the x and y coordinates you supply, the pixels of the image become part of the rasterized canvas. In other words, the image doesn't exist independent of the canvas.

    Therefore, if you want to center the image inside of your canvas, you need to do a little math to calculate the x and y coordinates that will place the image centered inside the canvas.

    Demo

    For example, if your canvas size is 500px tall and your image has a height of 350px, then you need to set the y position to 75px (i.e., (500 - 350) / 2).

    The demo code below shows how to replicate the behavior of CSS object-fit: contain. This will adjust the image to fill the canvas in one direction, and then center the image in the other direction.

    import { useState, useEffect } from "react";
    import { Stage, Layer, Image, Text } from "react-konva";
    
    function Example() {
      const w = window.innerWidth;
      const h = window.innerHeight;
      const src = "https://konvajs.org/assets/yoda.jpg";
    
      const [image, setImage] = useState(null);
      const [pos, setPos] = useState({ x: 0, y: 0 });
    
      useEffect(() => {
        const image = new window.Image();
        image.src = src;
        image.addEventListener("load", handleLoad);
    
        function handleLoad(event) {
          const image = event.currentTarget;
          /* after the image is loaded, you can get it's dimensions */
          const imgNaturalWidth = image.width;
          const imgNaturalHeight = image.height;
    
          /* 
            calculate the horizontal and vertical ratio of the 
            image dimensions versus the canvas dimensions
          */
          const hRatio = w / imgNaturalWidth;
          const vRatio = h / imgNaturalHeight;
    
          /*
            to replicate the CSS Object-Fit "contain" behavior,
            choose the smaller of the horizontal and vertical 
            ratios
    
            if you want a "cover" behavior, use Math.max to 
            choose the larger of the two ratios instead
          */
          const ratio = Math.min(hRatio, vRatio);
          /* 
            scale the image to fit the canvas 
          */
          image.width = imgNaturalWidth * ratio;
          image.height = imgNaturalHeight * ratio;
    
          /* 
            calculate the offsets so the image is centered inside
            the canvas
          */
          const xOffset = (w - image.width) / 2;
          const yOffset = (h - image.height) / 2;
    
          setPos({
            x: xOffset,
            y: yOffset
          });
          setImage(image);
        }
    
        return () => {
          image.removeEventListener("load", handleLoad);
        };
      }, [src, h, w]);
    
      return (
        <Stage width={w} height={h} style={{ background: "black" }}>
          <Layer>
            <Image x={pos.x} y={pos.y} image={image} />
            <Text
              text="I am centered"
              fontFamily="Impact"
              fontSize={50}
              stroke="white"
              strokeWidth={1}
              x={pos.x}
              y={pos.y}
            />
          </Layer>
        </Stage>
      );
    }