502 lines
16 KiB
TypeScript
502 lines
16 KiB
TypeScript
import React, { useRef, useEffect, forwardRef, useImperativeHandle, useContext, useState, ReactNode } from 'react'
|
||
import classNames from 'classnames'
|
||
import { fabric } from 'fabric'
|
||
import { addEventListenerWrapper, pick } from '@zhst/func'
|
||
import { useDebounceFn } from '@zhst/hooks'
|
||
import Viewer from '../ImageEditor/viewer';
|
||
import './index.less'
|
||
import { ConfigContext } from '../config-provider';
|
||
import { EVENT_VIEWER_READY } from '../ImageEditor';
|
||
import { Rect } from '../ImageEditor/viewer/shape';
|
||
import {
|
||
checkPointInRect,
|
||
drawArrowLine,
|
||
getImageDataByPosition,
|
||
checkMouseInRect,
|
||
createFabricShape,
|
||
originPercentToShapeLength,
|
||
changeColor,
|
||
shapeLengthToPercent
|
||
} from './cropperImagehelper';
|
||
import { getTransformRect } from '../BigImagePreview/bigImagePreviewHelper';
|
||
import { IArrowLinePosition, IRectData, ITransform, RectPro } from './type'
|
||
|
||
export interface CropperImageProps {
|
||
prefixCls?: string;
|
||
url?: string;
|
||
width?: number;
|
||
height?: number;
|
||
odList?: RectPro[] // od框
|
||
lineConfig?: fabric.Line; // 线条配置
|
||
editAble?: boolean; // 是否可编辑
|
||
selectedItem?: RectPro
|
||
selectAble?: boolean;
|
||
// 是否可放大缩小
|
||
scaleAble?: boolean;
|
||
// 是否展示框选拓展框
|
||
showToast?: boolean;
|
||
// 自定义拓展框
|
||
customToast?: (data: any) => React.JSX.Element;
|
||
toastStyle?: CSSStyleSheet
|
||
type?: 'line' | 'rect'; // 编辑类型
|
||
onMouseDown?: (data: { x: number; y: number }) => void;
|
||
onMouseUp?: (e?: fabric.IEvent<MouseEvent>) => void;
|
||
onMouseMove?: (e?: fabric.IEvent<MouseEvent>) => void;
|
||
onShapeSelected?: (id: string, shapeData?: RectPro & {
|
||
originData: Rect
|
||
}) => void
|
||
onCropStart?: () => void
|
||
onCropEnd?: (data: Partial<IRectData> & Partial<IArrowLinePosition> & {
|
||
type: RectPro['type'],
|
||
rectImageBase64?: string
|
||
targetTransform?: ITransform
|
||
targetData?: Rect
|
||
}) => void
|
||
children?: ReactNode
|
||
}
|
||
|
||
export interface CanvasPro extends fabric.Canvas {
|
||
selectionStart?: { x: number; y: number }
|
||
selectionEnd?: { x: number; y: number }
|
||
startDraw?: boolean;
|
||
}
|
||
|
||
export interface CropperImageRefProps {
|
||
rotateTo?: (val: number) => void;
|
||
canvasRef?: React.MutableRefObject<any>
|
||
viewerRef?: React.MutableRefObject<any>
|
||
}
|
||
|
||
// 对比图组件
|
||
const CropperImage = forwardRef<CropperImageRefProps, CropperImageProps>((props, ref) => {
|
||
const {
|
||
prefixCls: customizePrefixCls,
|
||
url,
|
||
height = 0,
|
||
odList,
|
||
selectedItem,
|
||
onMouseDown,
|
||
onMouseUp,
|
||
onMouseMove,
|
||
onCropStart,
|
||
onCropEnd,
|
||
editAble,
|
||
onShapeSelected,
|
||
selectAble = true,
|
||
showToast = false,
|
||
customToast = () => <div>无</div>,
|
||
toastStyle = {},
|
||
type = 'line',
|
||
scaleAble = false,
|
||
lineConfig = {
|
||
stroke: '#09f',
|
||
strokeWidth: 3,
|
||
selectable: true // 避免线选中而不是箭头
|
||
},
|
||
children
|
||
} = props;
|
||
const { getPrefixCls } = useContext(ConfigContext);
|
||
const componentName = getPrefixCls('cropper-view', customizePrefixCls);
|
||
const [isDrawing, setIsDrawing] = useState(false) // 矩形是否在移动
|
||
|
||
const canvasRef = useRef<any>(null);
|
||
const imageRef = useRef<HTMLDivElement>(null)
|
||
const viewerRef = useRef<any>(null)
|
||
const currentFabricRef = useRef<CanvasPro>(null)
|
||
const currentShapeRef = useRef<fabric.Object>(null)
|
||
const [isImgReady, setIsImgReady] = useState(false);
|
||
|
||
// 初始化 - 图片
|
||
useEffect(() => {
|
||
const handleReady = addEventListenerWrapper(imageRef.current, EVENT_VIEWER_READY, () => {
|
||
setIsImgReady(true)
|
||
});
|
||
|
||
viewerRef.current = new Viewer(imageRef.current!!, {
|
||
image: url,
|
||
scaleAble,
|
||
selectAble,
|
||
// @ts-ignore
|
||
height: parseInt(height),
|
||
fitScaleAsMinScale: true,
|
||
dragAble: false,
|
||
});
|
||
|
||
return () => {
|
||
// 再次加载,销毁原来的实例
|
||
viewerRef?.current?.destroy?.();
|
||
viewerRef.current = null;
|
||
viewerRef.current?.clearShape?.();
|
||
handleReady.remove?.()
|
||
}
|
||
}, [url])
|
||
|
||
/**
|
||
* 监听图形选中事件
|
||
* @param e
|
||
*/
|
||
const handleSelected = (e: fabric.IEvent<MouseEvent>) => {
|
||
const _viewer = viewerRef?.current || {}
|
||
const { containerData = {}, targetTransform = {} } = _viewer
|
||
changeColor(e.selected!, 'rgba(255, 0, 0, 1)')
|
||
const selectedItem = e.selected?.[0] || 0
|
||
// @ts-ignore
|
||
const _originData = selectedItem?.originData || selectedItem
|
||
const _data = shapeLengthToPercent(_originData, {
|
||
sourceImageWidth: containerData.width,
|
||
sourceImageHeight: containerData.height,
|
||
targetTransform
|
||
})
|
||
let rectImageBase64 = ''
|
||
|
||
if (_data.w && _data.h) {
|
||
rectImageBase64 = getImageDataByPosition(
|
||
_originData,
|
||
{ canvas: viewerRef.current.canvas }
|
||
)
|
||
}
|
||
onShapeSelected?.(_originData?.id, { ..._data, imageRect: rectImageBase64, originData: _originData })
|
||
}
|
||
|
||
// 初始化 - 编辑器
|
||
useEffect(() => {
|
||
const _viewer = viewerRef?.current || {}
|
||
const { containerData = {}, targetTransform = {} } = _viewer
|
||
const _imgSize = _viewer.getImgSize() || {}
|
||
// @ts-ignore
|
||
currentFabricRef.current = new fabric.Canvas(
|
||
canvasRef.current,
|
||
{
|
||
backgroundColor: 'transparent',
|
||
width: containerData.width,
|
||
height: containerData.height,
|
||
selection: false,
|
||
}
|
||
)
|
||
|
||
// 判断是否可编辑 - 非编辑态
|
||
if (!editAble) {
|
||
currentFabricRef.current.clear()
|
||
let originWidth = (_imgSize.w * targetTransform.scale)
|
||
let originHeight = (_imgSize.h * targetTransform.scale)
|
||
odList && odList?.forEach(od => {
|
||
const _shapeData = originPercentToShapeLength(od, { sourceImageWidth: originWidth, sourceImageHeight: originHeight, targetTransform })
|
||
let _data = {
|
||
id: od.id,
|
||
type: od.type || 'rect',
|
||
originData: od,
|
||
..._shapeData
|
||
}
|
||
let item = createFabricShape(_data, {
|
||
})
|
||
currentFabricRef.current?.add(item)
|
||
})
|
||
currentFabricRef.current?.renderAll()
|
||
// 当矩形被选中时
|
||
currentFabricRef.current?.on('selection:created', e => handleSelected(e));
|
||
currentFabricRef.current?.on('selection:updated', function(e) {
|
||
handleSelected(e)
|
||
changeColor(e.deselected!, '#FFF566')
|
||
});
|
||
// 当取消选中时
|
||
currentFabricRef.current?.on('selection:cleared', function(e) {
|
||
changeColor(e.deselected!, '#FFF566')
|
||
});
|
||
} else {
|
||
if (type === 'rect') {
|
||
// 编辑模式 - 矩形绘制
|
||
initRect(currentFabricRef.current)
|
||
} else {
|
||
// 编辑模式 - 线绘制
|
||
initLine(currentFabricRef.current)
|
||
}
|
||
}
|
||
return () => {
|
||
currentFabricRef.current?.removeListeners?.()
|
||
currentFabricRef.current?.dispose?.()
|
||
}
|
||
// TODO: 监听odList 需要优化
|
||
},[type, editAble, isImgReady])
|
||
|
||
// 监听矩形拖动,防抖
|
||
const { run: handleRectChange } = useDebounceFn(
|
||
(data: IRectData) => {
|
||
const _viewer = viewerRef.current
|
||
const { targetTransform = {} } = _viewer || {}
|
||
const { left = 0, top = 0, width = 0, height = 0, scaleX = 1, scaleY = 1 } = data || {}
|
||
const targetPosition = { x: Math.abs(left), y: Math.abs(top), w: Math.abs(width * scaleX), h: Math.abs(height * scaleY) }
|
||
let rectImageBase64 = ''
|
||
|
||
if (width && height) {
|
||
rectImageBase64 = getImageDataByPosition(data, { canvas: _viewer.canvas })
|
||
}
|
||
const targetData = getTransformRect(_viewer.image, targetTransform, targetPosition)
|
||
onCropEnd?.({ type: 'rect', left, top, width, height, rectImageBase64, targetTransform, targetData })
|
||
},
|
||
{
|
||
wait: 500,
|
||
},
|
||
);
|
||
|
||
// 初始化 - 矩形圈选工具
|
||
const initRect = (_fabricCanvas: CanvasPro) => {
|
||
const viewer = viewerRef?.current || {}
|
||
const { targetTransform = {} } = viewer
|
||
const imageSize = viewer.getImgSize() || { x: 0, y: 0 }
|
||
let currentFabric = _fabricCanvas
|
||
let rect: fabric.Rect
|
||
let origX: number, origY: number
|
||
// 坐标限制
|
||
let limitStartX = targetTransform.translateX
|
||
let limitStartY = targetTransform.translateY
|
||
let limitEndX = limitStartX + (imageSize.w * targetTransform.scale)
|
||
let limitEndY = limitStartY + (imageSize.h * targetTransform.scale)
|
||
|
||
function _getLimitPointer(_pointer: { x: number; y: number; } = { x: 0, y: 0 }) {
|
||
return {
|
||
x: Math.min(Math.max(_pointer.x, limitStartX), limitEndX),
|
||
y: Math.min(Math.max(_pointer.y, limitStartY), limitEndY)
|
||
};
|
||
}
|
||
|
||
const _handleMove = (e: fabric.IEvent<MouseEvent>) => {
|
||
// 阻止对象移动到画布外面
|
||
var obj = e.target!
|
||
var top = obj.top || 0
|
||
var left = obj.left || 0
|
||
var w = obj.width! * (obj?.scaleX || 1)!
|
||
var h = obj.height! * (obj?.scaleY || 1)!
|
||
var top_bound = limitStartY;
|
||
var bottom_bound = limitEndY - h;
|
||
var left_bound = limitStartX;
|
||
var right_bound = limitEndX - w;
|
||
|
||
if( w > currentFabric.width! ) {
|
||
obj.set('left', left_bound)
|
||
} else {
|
||
obj.set('left', (Math.min(Math.max(left, left_bound), right_bound)))
|
||
}
|
||
|
||
if( h > currentFabric.height! ) {
|
||
obj.set('top', top_bound)
|
||
} else {
|
||
obj.set('top', (Math.min(Math.max(top, top_bound), bottom_bound)))
|
||
}
|
||
// @ts-ignore
|
||
handleRectChange(pick(obj, 'width', 'height', 'left', 'top', 'scaleX', 'scaleY'))
|
||
}
|
||
// 鼠标:按下事件
|
||
currentFabric.on('mouse:down', function(o) {
|
||
currentFabric.startDraw = true
|
||
setIsDrawing(true)
|
||
var pointer = currentFabric.getPointer(o.e);
|
||
|
||
if (pointer.x < limitStartX || pointer.x > limitEndX
|
||
|| pointer.y < limitStartY || pointer.y > limitEndY
|
||
) return
|
||
|
||
origX = pointer.x;
|
||
origY = pointer.y;
|
||
// 创建一个矩形对象
|
||
rect = createFabricShape({
|
||
left: origX,
|
||
top: origY,
|
||
width: pointer.x - origX,
|
||
height: pointer.y - origY,
|
||
}, {
|
||
borderColor: '#09f',
|
||
stroke: '#09f',
|
||
cornerColor: '#09f',
|
||
fill: 'transparent',
|
||
hasControls: true,
|
||
hasBorders: true,
|
||
lockMovementX: false,
|
||
lockMovementY: false
|
||
});
|
||
|
||
// 判断存在实例,并且鼠标点击在实例上
|
||
if (checkMouseInRect(o, currentFabric) && rect) {
|
||
} else {
|
||
currentFabric.clear()
|
||
currentFabric.setActiveObject(rect)
|
||
currentFabric.add(rect);
|
||
}
|
||
onCropStart?.()
|
||
});
|
||
// 鼠标:移动事件
|
||
currentFabric.on('mouse:move', function(o) {
|
||
if (!currentFabric.startDraw || !rect) return;
|
||
const pointer = currentFabric.getPointer(o.e);
|
||
const limitPointer = _getLimitPointer(pointer) // 限制绘制的图形大小
|
||
const width = limitPointer.x - (rect.left || 0)
|
||
const height = limitPointer.y - (rect.top || 0)
|
||
|
||
if(origX > pointer.x) {
|
||
rect.set({ left: Math.abs(pointer.x) });
|
||
}
|
||
|
||
if(origY > pointer.y){
|
||
rect.set({ top: Math.abs(pointer.y) });
|
||
}
|
||
rect.set({ width });
|
||
rect.set({ height });
|
||
currentFabric.renderAll();
|
||
onMouseMove?.(o)
|
||
});
|
||
// 鼠标:松开事件
|
||
currentFabric.on('mouse:up', function(e) {
|
||
currentFabric.startDraw = false
|
||
const currentRef = currentFabric.getActiveObject()
|
||
if (!currentRef) return
|
||
setIsDrawing(false)
|
||
// @ts-ignore
|
||
currentShapeRef.current = currentRef
|
||
// @ts-ignore
|
||
handleRectChange(pick(currentRef, 'width', 'height', 'left', 'top', 'scaleX', 'scaleY'))
|
||
onMouseUp?.(e)
|
||
});
|
||
currentFabric.on('object:moving', (e) => _handleMove(e));
|
||
currentFabric.on('object:scaling', (e) => _handleMove(e));
|
||
}
|
||
|
||
// 初始化线条
|
||
const initLine = (_fabricCanvas: CanvasPro) => {
|
||
if (!currentFabricRef.current) return
|
||
const viewer = viewerRef?.current || {}
|
||
const { targetTransform = {} } = viewer
|
||
const imageSize = viewer.getImgSize()
|
||
let arrowLine: fabric.Object
|
||
const currentFabric = _fabricCanvas
|
||
|
||
// 事件监听: 鼠标按下
|
||
currentFabric.on('mouse:down', function(options) {
|
||
currentFabric.startDraw = true
|
||
setIsDrawing(true)
|
||
currentFabric.clear()
|
||
var evt = options.e;
|
||
|
||
// 检查鼠标是否按下左键并且没有按住Ctrl键(Windows系统)
|
||
if (evt.button === 1 || (evt.button === 0 && evt.ctrlKey === true)) {
|
||
return;
|
||
}
|
||
|
||
// 阻止默认行为
|
||
evt.preventDefault();
|
||
|
||
// 记录起始点坐标
|
||
const pointer = currentFabric.getPointer(evt);
|
||
|
||
if (!checkPointInRect(pointer, {
|
||
...pick(targetTransform, 'scale', 'translateX', 'translateY'),
|
||
...imageSize
|
||
})) return
|
||
|
||
currentFabric.selectionStart = {
|
||
x: pointer.x,
|
||
y: pointer.y
|
||
};
|
||
currentFabric.renderAll();
|
||
onMouseDown?.(currentFabric.selectionStart)
|
||
});
|
||
|
||
// 事件监听:鼠标移动事件
|
||
currentFabric.on('mouse:move', function(options) {
|
||
if (!currentFabric.startDraw) return;// 存在起始点,开始绘制
|
||
// 阻止默认行为
|
||
options.e.preventDefault();
|
||
var endPointer = options.pointer!!;
|
||
|
||
// 限定绘制区域
|
||
if (!checkPointInRect(endPointer, {
|
||
...pick(targetTransform, 'scale', 'translateX', 'translateY'),
|
||
...imageSize
|
||
}
|
||
)) return
|
||
// 更新选区大小
|
||
currentFabric.selectionEnd = {
|
||
x: endPointer?.x || 0,
|
||
y: endPointer?.y || 0
|
||
};
|
||
currentFabric.clear()
|
||
arrowLine = drawArrowLine({
|
||
startX: currentFabric?.selectionStart?.x as number,
|
||
startY: currentFabric.selectionStart?.y as number,
|
||
endX: currentFabric.selectionEnd?.x as number,
|
||
endY: currentFabric.selectionEnd?.y as number,
|
||
}, {
|
||
fill: '#09f',
|
||
...lineConfig
|
||
})
|
||
currentFabric.add(arrowLine)
|
||
// @ts-ignore
|
||
currentShapeRef.current = arrowLine
|
||
// 停止绘制
|
||
currentFabric.renderAll();
|
||
onMouseMove?.(options)
|
||
});
|
||
|
||
// 事件监听:鼠标松开事件
|
||
currentFabric.on('mouse:up', async function(e) {
|
||
currentFabric.startDraw = false
|
||
setIsDrawing(false)
|
||
const targetTransform = viewerRef.current?.targetTransform || {}
|
||
// @ts-ignore
|
||
onCropEnd?.({
|
||
type: 'line',
|
||
startX: currentFabric.selectionStart?.x,
|
||
startY: currentFabric.selectionStart?.y,
|
||
endX: currentFabric.selectionEnd?.x,
|
||
endY: currentFabric.selectionEnd?.y,
|
||
targetTransform
|
||
})
|
||
onMouseUp?.(e)
|
||
});
|
||
}
|
||
|
||
useImperativeHandle(ref, () => ({
|
||
canvasRef,
|
||
viewerRef,
|
||
rotateTo: (val) => {
|
||
viewerRef.current.rotateTo((pre: any) => pre + val)
|
||
},
|
||
clearShape: () => {
|
||
viewerRef.current?.clearShape?.();
|
||
currentFabricRef.current?.clear()
|
||
}
|
||
}));
|
||
|
||
const { left: shapeLeft, width: shapeWidth = 0, top: shapeTop, scaleX: shapeScaleX = 0 } = currentShapeRef.current || {}
|
||
return (
|
||
<div className={classNames(`${componentName}`)}>
|
||
{/* 图片 */}
|
||
<div
|
||
ref={imageRef}
|
||
className={classNames(`${componentName}_img`)}
|
||
/>
|
||
<canvas ref={canvasRef} className={classNames(`${componentName}_draw`)}></canvas>
|
||
{children}
|
||
<div
|
||
// @ts-ignore
|
||
className={classNames(`${componentName}_toast`)}
|
||
style={{
|
||
display: (showToast && currentShapeRef.current && !isDrawing) ? 'block' : 'none',
|
||
left:currentShapeRef.current ? (shapeLeft || 0) + (shapeWidth * shapeScaleX || 0) : 0,
|
||
top: currentShapeRef.current ? shapeTop : 0,
|
||
...toastStyle
|
||
}}
|
||
>
|
||
{/* @ts-ignore */}
|
||
{customToast?.({
|
||
...selectedItem,
|
||
}) || (
|
||
<div>
|
||
测试
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
})
|
||
|
||
export default CropperImage
|