Reputation: 11
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:
Adjusting the crop rectangle coordinates.
Ensuring the crop
method's values align with the rectangle's bounds.
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