Arian Nurrahman
Arian Nurrahman

Reputation: 11

React-Konva Crop Feature: Excess Area Appearing on Top and Left After Crop

I'm implementing a crop feature using react-konva. However, after cropping, there is always an excess area on the top and left of the cropped image. I expect the cropped region to precisely match the defined crop rectangle, but it doesn't align as shown below:

enter image description hereWhat I've Tried:

  1. Adjusting the crop rectangle coordinates.

  2. Ensuring the crop method's values align with the rectangle's bounds.

  3. Debugging scale, position, and transformation.

Despite this, the cropped image still contains the extra area.

Expected Behavior:

The blue line (crop rectangle) should perfectly align with the cropped image without any excess on the top or left.

Actual Behavior:

There is always an excess area on the top and left of the image after cropping.

How can I ensure the cropped image matches the crop rectangle precisely?

const CropableImage = (props: CropableImageProps) => {
  const imageRef = useRef<Konva.Image>(null);
  const rectRef = useRef<Konva.Rect>(null);
  const rectRefBackground = useRef<Konva.Rect>(null);
  const transformerRef = useRef<Konva.Transformer>(null);
  const { handleUpdateElementById, cropImageId } = useProductEditorImageEditor();
  const isCroping = cropImageId === props?.id;
  const [image] = useImage(props.src, "anonymous");

  const transformProps = useMemo(() => convertSvgTransformToKonvaProps(props.transform), [props.transform]);

  const imageTransformProps = useMemo(() => {
    if (props.transform) {
      return transformProps; // SVG
    } else {
      return {
        rotation: props.rotation,
        scaleX: props.scaleX,
        scaleY: props.scaleY,
        x: props.x,
        y: props.y,
      };
    }
  }, [props.transform, props.rotation, props.scaleX, props.scaleY, props.x, props.y, transformProps]);

  const originDimension = useMemo(() => {
    return {
      height: props.height || 0,
      scaleX: props.scaleX || 1,
      scaleY: props.scaleY || 1,
      width: props.width || 0,
      x: props.x || 0,
      y: props.y || 0,
    };
  }, [props]);

  const absolutePositionDimension = useMemo(() => {
    if (!rectRef.current)
      return {
        x: 0,
        y: 0,
      };

    const absolutePosition = rectRef.current.getAbsolutePosition();

    return {
      x: absolutePosition.x,
      y: absolutePosition.y,
    };
  }, []);

  const handleTransform = useCallback(() => {
    const rectDom = rectRef.current;
    if (!rectDom) return;

    const scaleX = rectDom.scaleX();
    const scaleY = rectDom.scaleY();
    let x = rectDom.x();
    let y = rectDom.y();

    let width = rectDom.width() * scaleX;
    let height = rectDom.height() * scaleY;

    if (x < 0) {
      width += x;
      x = 0;
    }
    if (x + width > originDimension.width) {
      width = originDimension.width - x;
    }

    if (y < 0) {
      height += y;
      y = 0;
    }
    if (y + height > originDimension.height) {
      height = originDimension.height - y;
    }

    const croppedData = {
      height,
      scaleX: 1,
      scaleY: 1,
      width,
      x,
      y,
    };

    rectDom.setAttrs(croppedData);
  }, [originDimension]);

  const handleTransformEnd = () => {
    const clipRect = rectRef.current;
    const imageDom = imageRef.current;
    if (!clipRect || !imageDom) return;

    const scaleX = Number(clipRect.scaleX());
    const scaleY = Number(clipRect.scaleY());
    const x = Number(clipRect.x());
    const y = Number(clipRect.y());
    const width = clipRect.width();
    const height = clipRect.height();

    const croppedData = {
      height,
      scaleX,
      scaleY,
      width,
      x,
      y,
    };
    const croppedDataWithNoPosition = {
      height,
      scaleX,
      scaleY,
      width,
    };

    imageDom.crop({
      height,
      width,
      x,
      y,
    });
    imageDom.setAttrs(croppedDataWithNoPosition);
    clipRect.setAttrs(croppedDataWithNoPosition);

    handleUpdateElementById(props?.id as string, {
      cropped: { ...croppedData },
    });
  };

  // TODO: Turn on cache and make sure the roundering corner works
  // useEffect(() => {
  //   if (image && imageRef.current) {
  //     imageRef.current.cache();
  //     imageRef.current.getLayer()?.batchDraw();
  //   }
  // }, [image]);

  useEffect(() => {
    if (transformerRef.current && rectRef.current) {
      transformerRef.current?.nodes([rectRef.current]);
    }
  }, [isCroping, props?.cropped]);

  return (
    <>
      <Group
        draggable={props?.draggable}
        height={props.cropped?.height || originDimension.height}
        id={props?.id}
        name={props?.name}
        offset={props?.offset}
        opacity={props?.opacity}
        width={props.cropped?.width || originDimension.width}
        onDragEnd={props?.onDragEnd}
        onDragMove={props?.onDragMove}
        onTransformEnd={props?.onTransformEnd}
        {...imageTransformProps}
      >
        <Group>
          <Image
            ref={imageRef}
            alt={props?.id}
            draggable={false}
            height={props.cropped?.height || originDimension.height}
            image={image}
            width={props.cropped?.width || originDimension.width}
            {...(isCroping && { fill: "rgba(0, 0, 0, 0.5)" })}
          />

          <Rect
            ref={rectRef}
            draggable={false}
            fill={isCroping ? "rgba(255, 255, 255, 0.5)" : "rgba(255, 255, 255, 1)"}
            globalCompositeOperation={isCroping ? "source-over" : "destination-in"}
            height={props.cropped?.height || originDimension.height}
            name='croppable-image'
            width={props.cropped?.width || originDimension.width}
            onTransform={handleTransform}
            onTransformEnd={handleTransformEnd}
            {...(isCroping || props?.cropped
              ? {
                  scaleX: props?.cropped?.scaleX,
                  scaleY: props?.cropped?.scaleY,
                  x: props?.cropped?.x,
                  y: props?.cropped?.y,
                }
              : {})}
          />
        </Group>

        {/* Crop Rectangle - draggable and resizable */}
        {isCroping && (
          <>
            <Transformer
              ref={transformerRef}
              resizeEnabled
              anchorCornerRadius={8}
              anchorSize={10}
              height={props.cropped?.height || originDimension.height}
              keepRatio={true}
              rotateEnabled={false}
              width={props.cropped?.width || originDimension.width}
            />

            {/* Image crop reference */}
            <Rect
              ref={rectRefBackground}
              draggable={false}
              height={originDimension.height}
              listening={false}
              name='croppable-image-background'
              scaleX={originDimension.scaleX}
              scaleY={originDimension.scaleY}
              width={originDimension.width}
              x={absolutePositionDimension.x}
              y={absolutePositionDimension.y}
            />
          </>
        )}
      </Group>
    </>
  );
};

Upvotes: 0

Views: 62

Answers (0)

Related Questions