fix(zhst/meta、zhst/biz、zhst/material): 修改图片标注组件

This commit is contained in:
NICE CODE BY DEV 2024-05-16 18:02:10 +08:00
parent 37c34eb785
commit 8dc81e1743
15 changed files with 611 additions and 360 deletions

View File

@ -1,5 +1,16 @@
# @zhst/biz
## 0.22.0
### Minor Changes
- fix: zhst/meta、zhst/biz、zhst/material-修改图片标注组件
### Patch Changes
- Updated dependencies
- @zhst/meta@0.21.0
## 0.21.5
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@zhst/biz",
"version": "0.21.5",
"version": "0.22.0",
"description": "业务库",
"keywords": [
"business",

View File

@ -1,5 +1,17 @@
# @zhst/material
## 0.18.0
### Minor Changes
- fix: zhst/meta、zhst/biz、zhst/material-修改图片标注组件
### Patch Changes
- Updated dependencies
- @zhst/meta@0.21.0
- @zhst/biz@0.22.0
## 0.17.4
### Patch Changes

View File

@ -1,6 +1,6 @@
{
"name": "@zhst/material",
"version": "0.17.4",
"version": "0.18.0",
"description": "物料库",
"keywords": [
"business",

View File

@ -1,5 +1,16 @@
# @zhst/utils
## 0.21.0
### Minor Changes
- fix: zhst/meta、zhst/biz、zhst/material-修改图片标注组件
### Patch Changes
- Updated dependencies
- @zhst/meta@0.21.0
## 0.20.3
### Patch Changes

View File

@ -176,6 +176,9 @@ export default {
h: this.image.height
};
},
getImage: function getImage() {
return this;
},
calcFitScreen: function calcFitScreen() {
if (!this.image) return;
var w = this.containerData.width;

View File

@ -1,6 +1,6 @@
{
"name": "@zhst/meta",
"version": "0.20.3",
"version": "0.21.0",
"description": "原子组件",
"keywords": [
"meta",

View File

@ -26,14 +26,16 @@ const props = {
objects: [
{
"bboxRatio": {
"id": "123",
"x": 0.5519352,
"y": 0.2965385,
"w": 0.05185461,
"h": 0.24698898
"h": 0.24698898,
},
},
{
"bboxRatio": {
"id": "456",
"x": 0.58543766,
"y": 0.3203356,
"w": 0.052037954,

View File

@ -153,6 +153,10 @@ export default {
return { w: this.image.width, h: this.image.height };
},
getImage() {
return this
},
calcFitScreen() {
if (!this.image) return;
const w = this.containerData.width;

View File

@ -1,19 +1,25 @@
import React, { useRef, useEffect, forwardRef, useImperativeHandle, useContext, useState, ReactNode, useMemo } from 'react'
import React, { useRef, useEffect, forwardRef, useImperativeHandle, useContext, useState, ReactNode } from 'react'
import classNames from 'classnames'
import { fabric } from 'fabric'
import { addEventListenerWrapper, getTransforms, pick } from '@zhst/func'
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 { Cropper, EVENT_CROP_END, EVENT_CROP_START, EVENT_SHAPE_SELECT } from '../ImageEditor';
import { EVENT_VIEWER_READY } from '../ImageEditor';
import { Rect } from '../ImageEditor/viewer/shape';
import { checkPointInRect, drawArrowLine, getImageDataByPosition, percentToLength } from './cropperImagehelper';
import Align from 'rc-align';
interface RectPro extends Rect {
imageRect?: string;
type?: 'line' | 'rect'; // line:线rect矩形
}
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;
@ -30,27 +36,22 @@ export interface CropperImageProps {
// 是否展示框选拓展框
showToast?: boolean;
// 自定义拓展框
customToast?: (data?: any) => React.JSX.Element
customToast?: (data: any) => React.JSX.Element;
toastStyle?: CSSStyleSheet
type?: 'line' | 'rect'; // 编辑类型
onMouseDown?: (data: { x: number; y: number }) => void;
onMouseUp?: (data: {
startX: number;
startY: number;
endX: number;
endY: number;
imageDom?: HTMLImageElement,
targetTransform?: {
translateX: number
translateY: number
scale: number
rotate: 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: RectPro) => void
onCropEnd?: (data: Partial<IRectData> & Partial<IArrowLinePosition> & {
type: RectPro['type'],
rectImageBase64?: string
targetTransform?: ITransform
targetData?: Rect
}) => void
children?: ReactNode
}
@ -75,6 +76,7 @@ const CropperImage = forwardRef<CropperImageRefProps, CropperImageProps>((props,
selectedItem,
onMouseDown,
onMouseUp,
onMouseMove,
onCropStart,
onCropEnd,
editAble,
@ -82,7 +84,8 @@ const CropperImage = forwardRef<CropperImageRefProps, CropperImageProps>((props,
selectAble = true,
showToast = false,
customToast = () => <div></div>,
type = 'ract',
toastStyle = {},
type = 'line',
scaleAble = false,
lineConfig = {
stroke: '#09f',
@ -96,202 +99,68 @@ const CropperImage = forwardRef<CropperImageRefProps, CropperImageProps>((props,
const [isDrawing, setIsDrawing] = useState(false) // 矩形是否在移动
const canvasRef = useRef<any>(null);
const currentShapeRef = useRef<any>(null)
const imageRef = useRef<HTMLDivElement>(null)
const viewerRef = useRef<any>(null)
const currentFabricRef = useRef<CanvasPro>(null)
// 自定义弹框
const alginContainerRef: any = useRef(null);
const alignRef: any = useRef(null);
const imageRectRef: any = useRef(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,
height: 600,
fitScaleAsMinScale: true,
dragAble: false,
});
// 监听形状选择事件
imageRectRef.current = addEventListenerWrapper(imageRef.current, EVENT_SHAPE_SELECT, (e: { detail: any; }) => {
// 选中的od
const id = e.detail;
if (id) {
const selectRectData = odList!.filter(_od => _od.id === id)?.[0]
const _data = percentToLength(selectRectData, viewerRef.current.canvas)
const imageRect = getImageDataByPosition(
{ x: _data.x, y: _data.y, w: _data.w, h: _data.h },
{ canvas: viewerRef.current.canvas }
)
id && onShapeSelected?.(id, { ..._data, imageRect, originData: selectRectData })
}
})
return () => {
// 再次加载,销毁原来的实例
viewerRef?.current?.destroy?.();
viewerRef.current = null;
imageRectRef.current?.remove();
viewerRef.current?.clearShape?.();
handleReady.remove?.()
}
}, [url])
const cropStartRef = useRef<any>(null)
const cropEndRef = useRef<any>(null)
/**
*
* @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 || {}
// 判断是否可编辑 - 非编辑态
if (!editAble) {
_viewer.clearShape()
// 判定是否存在od框
odList && odList.forEach(_od => {
_viewer?.addShape?.(_od);
})
return
} else {
// 编辑模式
_viewer?.clearShape?.();
if (type === 'rect') {
// 编辑模式 - 矩形绘制
currentShapeRef.current = initRect()
// cropEndRef.current = addEventListenerWrapper(imageRef.current, EVENT_CROP_END, (event: { detail: any; }) => {
// const data = event.detail;
// const targetPosition = { x: data.left, y: data.top, w: data.width, h: data.height }
// const imageRect = getImageDataByPosition(targetPosition, { canvas: viewerRef.current.canvas })
// const targetData = getTransformRect(_viewer.image, targetTransform, targetPosition)
// onCropEnd?.({ ...data , imageRect, targetTransform, targetData })
// setIsMove(false)
// })
} else {
// 编辑模式 - 线绘制
currentShapeRef.current = initLine()
}
}
return () => {
cropStartRef.current?.remove?.()
cropEndRef.current?.remove?.()
currentShapeRef.current?.destroy?.()
currentShapeRef.current?.dispose?.()
}
},[type, editAble])
// 初始化 - 矩形圈选工具
const initRect = () => {
const viewer = viewerRef?.current || {}
const { containerData = {}, targetTransform = {} } = viewer
let currentFabric: CanvasPro = new fabric.Canvas(
canvasRef.current,
{
backgroundColor: 'transparent',
width: containerData.width,
height: containerData.height,
selection: false,
}
)
let rect: fabric.Rect
let origX: number, origY: number
function addOrReplaceRect(newRect) {
// 移除画布上所有的矩形对象
const objects = currentFabric.getObjects();
for (let i = objects.length - 1; i >= 0; i--) {
if (objects[i].type === 'rect') {
currentFabric.remove(objects[i]);
}
}
// 添加新的矩形对象
currentFabric.add(newRect);
}
const checkPointInRect = (o: fabric.IEvent<MouseEvent>) => {
var pointer = currentFabric.getPointer(o.e);
var point = new fabric.Point(pointer.x, pointer.y);
let inRect = false
currentFabric.forEachObject(function(obj) {
if (obj.containsPoint(point) || obj._findTargetCorner(pointer)) {
inRect = true
} else {
inRect = false
}
});
return inRect
}
// 鼠标按下事件
currentFabric.on('mouse:down', function(o) {
currentFabric.startDraw = true
var pointer = currentFabric.getPointer(o.e);
origX = pointer.x;
origY = pointer.y;
// 创建一个矩形对象
rect = new fabric.Rect({
left: origX,
top: origY,
originX: 'left',
originY: 'top',
borderColor: '#09f',
cornerColor: '#09f',
cornerSize: 6,
width: pointer.x - origX,
height: pointer.y - origY,
angle: 0,
fill: 'transparent',
hasControls: true,
hasBorders: true,
lockRotation: true, // 锁定旋转
hasRotatingPoint: false // 隐藏旋转控制点
});
// 判断存在实例,并且鼠标点击在实例上
if (checkPointInRect(o) && rect) {
} else {
addOrReplaceRect(rect)
// 监听移动
rect.on('moving', o => console.log('o', o))
rect.on('resizing', o => console.log('o', o))
// 监听缩放
rect.on('scaling', o => console.log('o', o))
}
currentFabric.setActiveObject(currentFabric.item(0));
});
// 鼠标移动事件
currentFabric.on('mouse:move', function(o) {
if (!currentFabric.startDraw) return;
var pointer = currentFabric.getPointer(o.e);
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: Math.abs(origX - pointer.x) });
rect.set({ height: Math.abs(origY - pointer.y) });
currentFabric.renderAll();
});
// 鼠标松开事件
currentFabric.on('mouse:up', function(o) {
currentFabric.startDraw = false
});
return currentFabric
}
// 初始化线条
const initLine = () => {
const viewer = viewerRef?.current || {}
const { containerData = {}, targetTransform = {} } = viewer
const imageSize = viewer.getImgSize()
const { containerData = {}, targetTransform = {} } = _viewer
const _imgSize = _viewer.getImgSize() || {}
// @ts-ignore
currentFabricRef.current = new fabric.Canvas(
canvasRef.current,
@ -302,10 +171,206 @@ const CropperImage = forwardRef<CropperImageRefProps, CropperImageProps>((props,
selection: false,
}
)
const currentFabric = currentFabricRef.current
// 事件监听: 鼠标抬起事件
// 判断是否可编辑 - 非编辑态
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系统
@ -328,41 +393,13 @@ const CropperImage = forwardRef<CropperImageRefProps, CropperImageProps>((props,
x: pointer.x,
y: pointer.y
};
currentFabric.startDraw = true
currentFabric.renderAll();
onMouseDown?.(currentFabric.selectionStart)
});
// 事件监听:鼠标松开事件
currentFabric.on('mouse:up', async function(_options) {
currentFabric.clear()
let group = drawArrowLine(
currentFabric?.selectionStart?.x as number,
currentFabric.selectionStart?.y as number,
currentFabric.selectionEnd?.x as number,
currentFabric.selectionEnd?.y as number,
lineConfig
)
currentFabric.add(group)
// 停止绘制
currentFabric.startDraw = false
currentFabric.renderAll();
const _shapeData = {
startX: currentFabric.selectionStart?.x as number,
startY: currentFabric.selectionStart?.y as number,
endX: currentFabric.selectionEnd?.x as number,
endY: currentFabric.selectionEnd?.y as number
}
onMouseUp?.({ ..._shapeData, targetTransform })
});
// 事件监听:鼠标移动事件
currentFabric.on('mouse:move', function(options) {
// 存在起始点,开始绘制
if (currentFabric.selectionStart && currentFabric.startDraw) {
if (!currentFabric.startDraw) return;// 存在起始点,开始绘制
// 阻止默认行为
options.e.preventDefault();
var endPointer = options.pointer!!;
@ -373,28 +410,45 @@ const CropperImage = forwardRef<CropperImageRefProps, CropperImageProps>((props,
...imageSize
}
)) return
// 更新选区大小
currentFabric.selectionEnd = {
x: endPointer?.x || 0,
y: endPointer?.y || 0
};
// 清除之前的选择框
currentFabric.clear();
let group = drawArrowLine(
currentFabric.selectionStart.x,
currentFabric.selectionStart.y,
endPointer?.x || 0,
endPointer?.y || 0,
lineConfig
)
currentFabric.add(group)
}
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)
});
return currentFabric
}
useImperativeHandle(ref, () => ({
@ -409,6 +463,7 @@ const CropperImage = forwardRef<CropperImageRefProps, CropperImageProps>((props,
}
}));
const { left: shapeLeft, width: shapeWidth = 0, top: shapeTop, scaleX: shapeScaleX = 0 } = currentShapeRef.current || {}
return (
<div className={classNames(`${componentName}`)}>
{/* 图片 */}
@ -416,49 +471,27 @@ const CropperImage = forwardRef<CropperImageRefProps, CropperImageProps>((props,
ref={imageRef}
className={classNames(`${componentName}_img`)}
/>
{showToast && selectedItem && !isDrawing && (<>
<div
ref={alginContainerRef}
// @ts-ignore
style={Object.assign(
{
position: 'absolute',
left: 0,
top: 0,
width: selectedItem.w,
height: selectedItem.h,
userSelect: 'none',
pointerEvents: 'none'
},
getTransforms({
translateX: selectedItem.x,
translateY: selectedItem.y,
})
)}
>
</div>
<Align
ref={alignRef}
monitorWindowResize
align={{
points: ['bl', 'br'],
offset: [6, 0],
overflow: {
adjustX: true,
adjustY: true,
},
}}
target={function () {
return alginContainerRef.current;
}}
>
{customToast?.({
selectedItem
})}
</Align>
</>)}
<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>
);
})

View File

@ -1,7 +1,8 @@
import { isString } from "@zhst/func";
import { isString, omit } from "@zhst/func";
import { fabric } from 'fabric'
import { ILineOptions } from "fabric/fabric-impl";
import { Rect } from '../ImageEditor/viewer/shape';
import { IArrowLinePosition, IRectData, IShape, ITransform, RectPro } from "./type";
export const getImage = (propImage: HTMLImageElement | string) => {
return new Promise<HTMLElement>((resolve, reject) => {
@ -21,6 +22,44 @@ export const getImage = (propImage: HTMLImageElement | string) => {
});
}
// OD源数据转图形坐标
export const originPercentToShapeLength = (odData: Rect & { x2?: number; y2?: number }, opt: {
sourceImageWidth: number
sourceImageHeight: number
targetTransform?: ITransform
}) => {
const { targetTransform, sourceImageWidth = 0, sourceImageHeight = 0 } = opt || {}
const { translateX = 0, translateY = 0 } = targetTransform || {}
return {
left: odData.x * sourceImageWidth + translateX,
top: odData.y * sourceImageHeight + translateY,
width: odData.w * sourceImageWidth,
height: odData.h * sourceImageHeight,
endLeft: (odData.x2 || 0) * sourceImageWidth + translateX,
endTop: (odData.y2 || 0) * sourceImageHeight + translateY,
}
}
// 图形坐标转OD源数据
export const shapeLengthToPercent = (shapeData: IShape & { endLeft?: number, endTop?: number }, opt: {
sourceImageWidth: number
sourceImageHeight: number
targetTransform?: ITransform
}) => {
const { left = 0, width = 0, top = 0, height = 0, endLeft = 0, endTop = 0 } = shapeData
const { targetTransform, sourceImageWidth = 0, sourceImageHeight = 0 } = opt || {}
const { translateX = 0, translateY = 0 } = targetTransform || {}
return {
x: (left - translateX) / sourceImageWidth,
y: (top - translateY) / sourceImageHeight,
w: width / sourceImageWidth,
h: height / sourceImageHeight,
x2: (endLeft - translateX) / sourceImageWidth,
y2: (endTop - translateX) / sourceImageWidth,
}
}
/**
*
* @param o
@ -28,8 +67,8 @@ export const getImage = (propImage: HTMLImageElement | string) => {
* @returns Boolean
*/
export const checkMouseInRect = (o: fabric.IEvent<MouseEvent>, _fabricCanvas: fabric.Canvas) => {
var pointer = _fabricCanvas.getPointer(o.e);
var point = new fabric.Point(pointer.x, pointer.y);
let pointer = _fabricCanvas.getPointer(o.e);
let point = new fabric.Point(pointer.x, pointer.y);
let inRect = false
_fabricCanvas.forEachObject(function(obj) {
if (obj.containsPoint(point) || obj._findTargetCorner(pointer)) {
@ -47,7 +86,7 @@ export const checkMouseInRect = (o: fabric.IEvent<MouseEvent>, _fabricCanvas: fa
* @param rect whscaletranslateX,translateY
* @returns boolean
*/
export const checkPointInRect = (point: { x: number; y: number;}, rect: { w: number; h: number, translateX?: number; translateY?: number, scale: number }) => {
export const checkPointInRect = (point: Pick<Rect, 'x' | 'y'>, rect: { w: number; h: number, translateX?: number; translateY?: number, scale: number }) => {
const { w, h, translateX = 0, translateY = 0, scale = 1 } = rect;
const limitStartX = translateX
const limitEndX = translateX + (w * scale)
@ -62,61 +101,16 @@ export const checkPointInRect = (point: { x: number; y: number;}, rect: { w: num
return false
}
// 绘制带箭头的直线函数
export const drawArrowLine = (startX: number, startY: number, endX: number, endY: number, lineConfig: ILineOptions) => {
var angle = Math.atan2(endY - startY, endX - startX);
var line = new fabric.Line([startX, startY, endX, endY], lineConfig);
var arrowLength = 20;
var arrowWidth = 20;
var arrow = new fabric.Triangle({
left: endX - arrowLength / 2 * Math.cos(angle),
top: endY - arrowLength / 2 * Math.sin(angle),
width: arrowWidth,
height: arrowWidth,
originX: 'center',
originY: 'center',
fill: '#09f',
angle: angle * 180 / Math.PI + 90
});
return new fabric.Group([line, arrow], {
selectable: false,
});
}
// 百分比转长度
export const percentToLength = (originData: Rect, canvas: HTMLCanvasElement) => {
const { x = 0, y = 0, w = 0, h = 0 } = originData
const canvasW = canvas.width
const canvasH = canvas.height
return {
x: x * canvasW,
y: y * canvasH,
w: w * canvasW,
h: h * canvasH
}
}
// 通过位置截取图片
export const getImageDataByPosition = (position: {
w: number;
h: number;
x: number;
y: number;
}, opt: {
export const getImageDataByPosition = (position: IShape, opt: {
canvas: HTMLCanvasElement
fileType?: string
}) => {
const { x =0, y = 0, w = 0, h = 0 } = position
const { left = 0, top = 0, width = 0, height = 0 } = position
const { fileType = 'image/jpg', canvas } = opt
const _canvas = canvas
const ctx = _canvas.getContext('2d')
const imageData = ctx?.getImageData(x, y, w, h)
const imageData = ctx?.getImageData(left, top, width, height)
const newCanvas = document.createElement('canvas')
const newCtx = newCanvas.getContext('2d')
newCanvas.width = imageData?.width || 0
@ -124,3 +118,136 @@ export const getImageDataByPosition = (position: {
newCtx?.putImageData(imageData!!, 0, 0)
return newCanvas.toDataURL(fileType)
}
/**
*
* @param e fabric鼠标指针对象
* @param opt
*/
export const limitPointMove = (
e: fabric.IEvent<MouseEvent>,
opt: {
limitRect: IArrowLinePosition,
containerSize: Pick<IRectData, 'width' | 'height'>
}
) => {
const { limitRect, containerSize } = opt
const { startX, startY, endX, endY } = limitRect
// 阻止对象移动到画布外面
var obj = e.target!
var top = obj.top || 0
var left = obj.left || 0
var w = obj.width! * obj.scaleX!
var h = obj.height! * obj.scaleY!
var top_bound = startY;
var bottom_bound = endY - h;
var left_bound = startX;
var right_bound = endX - w;
if( w >= containerSize.width! ) {
obj.set('left', left_bound)
} else {
obj.set('left', (Math.min(Math.max(left, left_bound), right_bound)))
}
if( h >= containerSize.height! ) {
obj.set('top', top_bound)
} else {
obj.set('top', (Math.min(Math.max(top, top_bound), bottom_bound)))
}
}
// 绘制带箭头的直线函数
export const drawArrowLine = (arrowPosition: IArrowLinePosition, lineConfig: ILineOptions) => {
const { startX = 0, startY = 0, endX = 0, endY = 0 } = arrowPosition
let angle = Math.atan2(endY - startY, endX - startX);
let line = new fabric.Line([startX, startY, endX, endY], {
perPixelTargetFind: true, // 启用逐像素检测
selectable: true,
...lineConfig
});
let arrowLength = 20;
let arrowWidth = 20;
let arrow = new fabric.Triangle({
left: endX - arrowLength / 2 * Math.cos(angle),
top: endY - arrowLength / 2 * Math.sin(angle),
width: arrowWidth,
height: arrowWidth,
fill: '#FFF566',
originX: 'center',
originY: 'center',
angle: angle * 180 / Math.PI + 90,
perPixelTargetFind: true, // 启用逐像素检测
...lineConfig
});
return new fabric.Group([line, arrow], {
selectable: true,
evented: false,
});
}
/**
* fabric图形
* @param data
* @returns fabric
*/
export const createFabricShape = (data: Partial<RectPro> & IShape & { endLeft?: number; endTop?: number }, config: Partial<fabric.Object>) => {
let shape: any
let { type = 'rect', left = 0, width = 0, top = 0, height = 0, endLeft = 0, endTop = 0 } = data
const defaultConfig = {
borderColor: '#FFF566',
cornerColor: '#FFF566',
cornerSize: 8,
stroke: '#FFF566',
strokeWidth: 2,
hasControls: false,
hasBorders: false,
lockMovementX: true,
lockMovementY: true,
lockRotation: true, // 锁定旋转
hasRotatingPoint: false, // 隐藏旋转控制点
...config
}
switch (type) {
case 'line':
shape = drawArrowLine({
startX: left,
startY: top,
endX: endLeft,
endY: endTop
}, {
...defaultConfig
})
break;
case 'rect':
shape = new fabric.Rect({
left,
top,
width,
height,
originX: 'left',
originY: 'top',
angle: 0,
fill: 'transparent',
...defaultConfig
})
default:
break;
}
return shape
}
/**
* fabric图形颜色
* @param shapes
* @param color
*/
export const changeColor = (shapes: fabric.Object[], color: string) => {
shapes?.forEach(obj => {
obj.set('stroke', color)
})
}

View File

@ -23,6 +23,16 @@ export default () => {
"y": 0.3203356,
"w": 0.052037954,
"h": 0.2664015
},
{
"id": "46",
type: 'line',
"x": 0.18543766,
"y": 0.1203356,
"w": 0.62037954,
"h": 0.5864015,
"x2": 0.62037954,
"y2": 0.5864015
}
])
const [selectedItem, setSelectedItem] = useState<any>()
@ -64,23 +74,23 @@ export default () => {
url="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
onMouseUp={data => console.log('箭头绘制结束:', data)}
onShapeSelected={(id, shapeData) => {
console.log('矩形选择', id, shapeData)
console.log('shapeData', shapeData)
setImgUrl(shapeData?.imageRect as string)
}}
onCropStart={() => console.log('矩形开始绘制')}
onCropEnd={(data) => {
console.log('data', data)
console.log('绘制完成', data)
setSelectedItem({ x: data.left, y: data.top, h: data.height, w: data.width })
setImgUrl(data?.imageRect as string)
setImgUrl(data?.rectImageBase64 as string)
}}
selectedItem={selectedItem}
showToast={editAble}
customToast={() => (
<div style={{ marginLeft: '12px', color: '#fff', fontSize: '24px' }}></div>
<div style={{ marginLeft: '12px', color: '#fff', fontSize: '24px' }}><Button>DOM</Button></div>
)}
/>
</div>
<Image src={imgUrl} />
{imgUrl && <Image src={imgUrl} />}
</Space>
)
}

View File

@ -1,5 +1,6 @@
import CropperImage from "./CropperImage";
export type { CropperImageRefProps, CropperImageProps } from './CropperImage'
export * from './cropperImagehelper'
export default CropperImage

View File

@ -0,0 +1,34 @@
import { Rect } from '../ImageEditor/viewer/shape';
export type IShapeType = 'line' | 'rect'
export interface IShape {
top?: number;
left?: number
width: number;
height: number
}
export interface RectPro extends Rect {
imageRect?: string;
type?: IShapeType; // line:线rect矩形
}
export interface IArrowLinePosition {
startX: number
startY: number
endX: number
endY: number
}
export interface IRectData extends IShape {
scaleX?: number;
scaleY?: number;
}
export interface ITransform {
translateX: number
translateY: number
scale: number
rotate: number
}

View File

@ -27,7 +27,10 @@
"lib": ["dom", "es2017"],
"stripInternal": true,
"resolvePackageJsonExports": true,
"resolveJsonModule": true
"resolveJsonModule": true,
"types": [
// "@zhst/meta" 使 npm
]
},
"include": [".dumirc.ts", "src/**/*", "packages/**/*"],
"exclude": ["node_modules", "lib", "es", ".dumi"]