画布:
const canvasRef = useRef<HTMLCanvasElement|null>(null);
return (
<div>
<canvas ref={canvasRef}/>
</div>
)
const canvas = useMemo(() => {
return new fabric.Canvas(
canvasRef.current,
{
backgroundColor: '#e5e5e5',
selection: false,
width: canvasWidth,
height: canvasHeight,
}
)
}, [canvasRef.current])
useEffect(() => {
if (!canvas || imageInfo.width === 0) {
return;
}
canvas?.setWidth(imageInfo.width)
canvas?.setHeight(imageInfo.height)
}, [imageInfo])
useEffect(() => {
if (!canvas) {
return;
}
const handleWheel = (handle: fabric.IEvent<WheelEvent>) => {
const { e } = handle
const isDown = e.deltaY > 0
e.preventDefault();
updateImageScale(isDown)
}
canvas.on('mouse:wheel', handleWheel)
return () => {
// @ts-ignore
canvas.off('mouse:wheel', handleWheel)
}
}, [canvas])
useEffect(() => {
const mouseDown = (e) => {
const target = e.target || {}
console.info(e.target)
}
canvas.on('mouse:down', mouseDown)
return () => {
canvas.off("mouse:down", mouseDown)
}
}, [imageInfo])
useEffect(() => {
const handleMouseDown = (handle: fabric.IEvent<MouseEvent>) => {
const { e } = handle
const point = {
x: e.pageX,
y: e.pageY,
}
}
canvas.on('mouse:down', handleMouseDown)
return () => {
canvas.off('mouse:down', handleMouseDown)
}
}, [canvas])
const updateImageScale = (isDown) => {
let off = 0.1;
if (isDown) {
off = -0.1;
}
let zoom: number = canvas.getZoom() + off;
if (zoom <= 0.1) {
zoom = 0.1;
}
if (canvasWheeled.current === 0) {
canvasWheeled.current = canvas.getZoom();
}
canvas.setZoom(zoom)
}
const handleMouseMove = (handle: fabric.IEvent<MouseEvent>) => {
const { e } = handle
const offset = {
x: -1 * (e.pageX - point.x) + imageOffset.current.x,
y: -1 * (e.pageY - point.y) + imageOffset.current.y,
}
canvas.absolutePan(offset)
}
useEffect(() => {
/**
正常行为
***/
return () => {
imageInfos.map(img => {
canvas.remove(img); //移除画布中的图片
});
}
}, [imageInfo])
画图:
fabric.Image.fromURL(url, img => {
canvas.add(img) // 这个img 就是上文中提到的 remove 用到的img,需要保存在某个地方然后做remove
}, {
selectable: false, // 是否可操作
hoverCursor: 'default', // 鼠标移动到图上时手势
top: 0, // 图片相对画布偏移量
left: 0, // 图片相对画布偏移量
})
fabric.Image.fromURL(url, img => {
img.scale(1.5) // 图片放大0.5倍
canvas.add(img)
}, {
selectable: false, // 是否可操作
hoverCursor: 'default', // 鼠标移动到图上时手势
top: 0, // 图片相对画布偏移量
left: 0, // 图片相对画布偏移量
})
fabric.Image.fromURL(
url, (img) => {
img.scale(scale)
canvas?.add(img)
img.sendToBack()
imageInfos.push(img)
}, {
left: images.region[0] * scale,
top: images.region[1] * scale,
selectable: false,
hoverCursor: 'default',
}
)
fabric.Image.fromURL(
props.url, (img) => {
img.cropX = region[0]
img.cropY = region[1]
img.width = region[4] - region[0]
img.height = region[5] - region[1]
img.scale(scale)
canvas?.add(img)
img.sendToBack()
imageInfos.push(img)
}, {
left: region[0] * scale, // 这个是基于画布的偏移,所以需要考虑缩放比例
top: region[1] * scale, // 这个是基于画布的偏移,所以需要考虑缩放比例
selectable: false,
hoverCursor: 'default',
}
)
//以一个矩形为例
const rotateImageRect = (regionInfo) => {
const imageInfo = props.imageInfo;
if (props.orientation == 90) {
return {
x: imageInfo.height - (regionInfo.y + regionInfo.h),
y: regionInfo.x,
w: regionInfo.h,
h: regionInfo.w,
}
} else if (props.orientation == 180) {
return {
x: imageInfo.width - (regionInfo.x + regionInfo.w),
y: imageInfo.height - (regionInfo.y + regionInfo.h),
w: regionInfo.w,
h: regionInfo.h
}
} else if (props.orientation == 270) {
return {
x: regionInfo.y,
y: imageInfo.width -( regionInfo.x + regionInfo.w),
w: regionInfo.h,
h: regionInfo.w,
}
} else {
return regionInfo;
}
}
fabric.Image.fromURL(
url, (img) => {
const angle = ((360 - props.orientation) || 0) % 360
img.cropX = newRegion.x
img.cropY = newRegion.y
img.width = newRegion.w
img.height = newRegion.h
let left = images.region[0]
let top = images.region[1]
if (angle == 90) {
left = left + newRegion.h;
} else if (angle == 270) {
top += newRegion.w
} else if (angle == 180) {
left = left + newRegion.w;
top += newRegion.h
}
img.rotate(angle)
.set("left", left * scale)
.set("top", top * scale)
img.scale(scale)
canvas?.add(img)
img.sendToBack()
}, {
selectable: false,
hoverCursor: 'default',
}
)
画字:
const createText = (detail, index, scale, type, editedCallback) => {
const options = {
left: detail.region[0] * scale,
top: detail.region[1] * scale,
hasControls: false, // 是否展示控制框,就旋转,放大缩小
editable: true, // 是否可编辑
lockMovementX: true, // 拖拽移动的时候固定x轴
lockMovementY: true, // 拖拽移动的时候固定y轴
hoverCursor: 'pointer', // 鼠标移动到文本块的时候,手势变化
textBackgroundColor: 'red', // 文本块背景
key: `${type}_${index}`, // 当前文本块的唯一标志,可有可无
}
let fontSize = (detail.region[5] - detail.region[1]) * scale * 0.9; // 字体大小
const text = new fabric.IText(detail.result, {...options, fontSize});
text.on("editing:exited", () => { //editing:exited 是编辑结束之后的事件回调
editedCallback(text.text)
})
return text;
}
Demo:
import React, { useEffect, useRef, useState, useMemo } from 'react';
import { fabric } from 'fabric'
export type DocumentReductionImageProps = {
width: number,
height: number,
orientation: number,
url: string,
activeBoxIndex: string,
regions: any[],
setImageInfo?: (imageInfo: any) => void
updateZoom?: (zoom: number) => void
}
export type ImageInfo = {
width: number | undefined,
height: number | undefined,
}
export type offset = {
x: number | undefined,
y: number | undefined,
}
const DrawFile: React.FC<DocumentReductionImageProps> = (props) => {
const [imageInfo, setImageInfo] = useState<ImageInfo>({width: 0, height: 0})
const canvasRef = useRef<HTMLCanvasElement|null>(null);
const imageOffset = useRef<offset>({x: 0, y: 0})
const canvasWheeled = useRef<number>(0)
const canvas = useMemo(() => {
return new fabric.Canvas(
canvasRef.current,
{
backgroundColor: '#e5e5e5',
selection: false,
width: props.width,
height: props.height,
}
)
}, [canvasRef.current])
const updateImageScale = (isDown) => {
let off = 0.1;
if (isDown) {
off = -0.1;
}
let zoom: number = canvas.getZoom() + off;
if (zoom <= 0.1) {
zoom = 0.1;
}
if (canvasWheeled.current === 0) {
canvasWheeled.current = canvas.getZoom();
}
canvas.setZoom(zoom)
props.updateZoom?.(zoom)
}
//drawImage
useEffect(() => {
if (!canvas) {
return ;
}
const drewPage: fabric.Image[] = []
fabric.Image.fromURL(
props.url,
oImg => {
drewPage.push(oImg)
let newImageInfo = {width: oImg.width, height: oImg.height};
const angle = ((360 - props.orientation) || 0) % 360
if ((angle / 90) % 2) {
newImageInfo = {width: oImg.height, height: oImg.width};
}
const scale = props.width / newImageInfo.width;
if (angle) {
const left = angle == 90 || angle == 180 ? props.width : 0;
const top = angle == 180 || angle == 270 ? scale * newImageInfo.height : 0;
oImg.rotate(angle)
.set('left', left)
.set('top', top)
} else {
oImg.set('top', 0)
}
oImg.scale(scale)
canvas.add(oImg)
setImageInfo(newImageInfo);
props.setImageInfo?.(newImageInfo)
},
{
selectable: false,
hoverCursor: 'default',
top: 0,
left: 0,
}
)
return () => {
drewPage.map(img => {
canvas.remove(img)
})
setImageInfo({width: 0, height: 0});
};
}, [props.url, canvas]);
// 拖拽
useEffect(() => {
// 拖拽
const handleMouseDown = (handle: fabric.IEvent<MouseEvent>) => {
const { e } = handle
const point = {
x: e.pageX,
y: e.pageY,
}
if (canvasWheeled.current != 0) {
const zoomRate = canvas.getZoom() / (canvasWheeled.current);
imageOffset.current = {x: imageOffset.current.x * zoomRate, y: imageOffset.current.y * zoomRate}
canvasWheeled.current = 0;
}
const handleMouseMove = (handle: fabric.IEvent<MouseEvent>) => {
const { e } = handle
const offset = {
x: -1 * (e.pageX - point.x) + imageOffset.current.x,
y: -1 * (e.pageY - point.y) + imageOffset.current.y,
}
canvas.absolutePan(offset)
}
const handleMouseLeave = (handle: fabric.IEvent<MouseEvent>) => {
const { e } = handle
// @ts-ignore
canvas.off('mouse:move', handleMouseMove)
// @ts-ignore
canvas.off('mouse:up', handleMouseLeave)
// @ts-ignore
canvas.off('mouse:out', handleMouseLeave)
imageOffset.current = {
x: -1 * (e.pageX - point.x) + imageOffset.current.x,
y: -1 * (e.pageY - point.y) + imageOffset.current.y,
}
}
canvas.on('mouse:move', handleMouseMove)
canvas.on('mouse:up', handleMouseLeave)
canvas.on('mouse:out', handleMouseLeave)
}
canvas.on('mouse:down', handleMouseDown)
return () => {
// @ts-ignore
canvas.off('mouse:down', handleMouseDown)
}
}, [canvas])
//滚动缩放
useEffect(() => {
if (!canvas) {
return;
}
const handleWheel = (handle: fabric.IEvent<WheelEvent>) => {
const { e } = handle
const isDown = e.deltaY > 0
e.preventDefault();
updateImageScale(isDown)
}
canvas.on('mouse:wheel', handleWheel)
return () => {
// @ts-ignore
canvas.off('mouse:wheel', handleWheel)
}
}, [canvas])
//画框
useEffect(() => {
if (!canvas || !(props.regions?.length > 0) || imageInfo.width === 0) {
return;
}
const scale = props.width / imageInfo.width;
const region = {
left: props.regions[0] * scale,
top: props.regions[1] * scale,
selectable: false,
hoverCursor: 'default',
stroke: '#8BC7FF',
strokeWidth: 1.8,
width: (props.regions[2] - props.regions[0]) * scale,
height: (props.regions[3] - props.regions[1]) * scale,
}
const rect = new fabric.Rect(region)
rect.set('opacity', 0.5)
rect.set('fill', 'transparent')
canvas.add(rect)
return () => {
canvas.remove(rect);
}
}, [props.regions, canvas, props.activeBoxIndex, imageInfo])
//定位activeBoxIndex
useEffect(() => {
if (!props.regions || props.regions.length != 4) {
return;
}
const scale = props.width / imageInfo.width;
const offset = {
x: props.regions[0] * scale * canvas.getZoom() - 100,
y: props.regions[1] * scale * canvas.getZoom() - 100,
}
imageOffset.current = offset
canvas.absolutePan(offset)
canvasWheeled.current = 0;
}, [props.regions])
return (
<div>
<canvas ref={canvasRef}/>
</div>
)
}
export default DrawFile;
官方文档: