nicecode-v2/packages/meta/src/cropperImage/CropperImage.tsx

502 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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