diff --git a/packages/biz/CHANGELOG.md b/packages/biz/CHANGELOG.md index dda3b31..a499c5d 100644 --- a/packages/biz/CHANGELOG.md +++ b/packages/biz/CHANGELOG.md @@ -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 diff --git a/packages/biz/package.json b/packages/biz/package.json index 47f25ff..5e3485b 100644 --- a/packages/biz/package.json +++ b/packages/biz/package.json @@ -1,6 +1,6 @@ { "name": "@zhst/biz", - "version": "0.21.5", + "version": "0.22.0", "description": "业务库", "keywords": [ "business", diff --git a/packages/material/CHANGELOG.md b/packages/material/CHANGELOG.md index f657bba..b7d4782 100644 --- a/packages/material/CHANGELOG.md +++ b/packages/material/CHANGELOG.md @@ -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 diff --git a/packages/material/package.json b/packages/material/package.json index 92bd58d..70cd047 100644 --- a/packages/material/package.json +++ b/packages/material/package.json @@ -1,6 +1,6 @@ { "name": "@zhst/material", - "version": "0.17.4", + "version": "0.18.0", "description": "物料库", "keywords": [ "business", diff --git a/packages/meta/CHANGELOG.md b/packages/meta/CHANGELOG.md index 2e371f6..c61c3cf 100644 --- a/packages/meta/CHANGELOG.md +++ b/packages/meta/CHANGELOG.md @@ -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 diff --git a/packages/meta/es/ImageEditor/viewer/render.js b/packages/meta/es/ImageEditor/viewer/render.js index aac7cbd..7ac936f 100644 --- a/packages/meta/es/ImageEditor/viewer/render.js +++ b/packages/meta/es/ImageEditor/viewer/render.js @@ -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; diff --git a/packages/meta/package.json b/packages/meta/package.json index 4d8dba5..6a60c95 100644 --- a/packages/meta/package.json +++ b/packages/meta/package.json @@ -1,6 +1,6 @@ { "name": "@zhst/meta", - "version": "0.20.3", + "version": "0.21.0", "description": "原子组件", "keywords": [ "meta", diff --git a/packages/meta/src/BigImagePreview/demo/base.tsx b/packages/meta/src/BigImagePreview/demo/base.tsx index 75197ad..db158c7 100644 --- a/packages/meta/src/BigImagePreview/demo/base.tsx +++ b/packages/meta/src/BigImagePreview/demo/base.tsx @@ -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, diff --git a/packages/meta/src/ImageEditor/viewer/render.ts b/packages/meta/src/ImageEditor/viewer/render.ts index 7a65f0b..4f06555 100644 --- a/packages/meta/src/ImageEditor/viewer/render.ts +++ b/packages/meta/src/ImageEditor/viewer/render.ts @@ -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; diff --git a/packages/meta/src/cropperImage/CropperImage.tsx b/packages/meta/src/cropperImage/CropperImage.tsx index ca8ae23..2b75756 100644 --- a/packages/meta/src/cropperImage/CropperImage.tsx +++ b/packages/meta/src/cropperImage/CropperImage.tsx @@ -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) => void; + onMouseMove?: (e?: fabric.IEvent) => void; onShapeSelected?: (id: string, shapeData?: RectPro & { originData: Rect }) => void onCropStart?: () => void - onCropEnd?: (data: RectPro) => void + onCropEnd?: (data: Partial & Partial & { + type: RectPro['type'], + rectImageBase64?: string + targetTransform?: ITransform + targetData?: Rect + }) => void children?: ReactNode } @@ -75,6 +76,7 @@ const CropperImage = forwardRef((props, selectedItem, onMouseDown, onMouseUp, + onMouseMove, onCropStart, onCropEnd, editAble, @@ -82,7 +84,8 @@ const CropperImage = forwardRef((props, selectAble = true, showToast = false, customToast = () =>
, - type = 'ract', + toastStyle = {}, + type = 'line', scaleAble = false, lineConfig = { stroke: '#09f', @@ -96,202 +99,68 @@ const CropperImage = forwardRef((props, const [isDrawing, setIsDrawing] = useState(false) // 矩形是否在移动 const canvasRef = useRef(null); - const currentShapeRef = useRef(null) const imageRef = useRef(null) const viewerRef = useRef(null) const currentFabricRef = useRef(null) - - // 自定义弹框 - const alginContainerRef: any = useRef(null); - const alignRef: any = useRef(null); - const imageRectRef: any = useRef(null) + const currentShapeRef = useRef(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(null) - const cropEndRef = useRef(null) + /** + * 监听图形选中事件 + * @param e + */ + const handleSelected = (e: fabric.IEvent) => { + 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) => { - 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((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) => { + // 阻止对象移动到画布外面 + 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,73 +393,62 @@ const CropperImage = forwardRef((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!!; - // 阻止默认行为 - 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(); - let group = drawArrowLine( - currentFabric.selectionStart.x, - currentFabric.selectionStart.y, - endPointer?.x || 0, - endPointer?.y || 0, - lineConfig - ) - - currentFabric.add(group) - } + // 限定绘制区域 + 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) }); - return currentFabric } useImperativeHandle(ref, () => ({ @@ -409,6 +463,7 @@ const CropperImage = forwardRef((props, } })); + const { left: shapeLeft, width: shapeWidth = 0, top: shapeTop, scaleX: shapeScaleX = 0 } = currentShapeRef.current || {} return (
{/* 图片 */} @@ -416,49 +471,27 @@ const CropperImage = forwardRef((props, ref={imageRef} className={classNames(`${componentName}_img`)} /> - {showToast && selectedItem && !isDrawing && (<> -
-
- - {customToast?.({ - selectedItem - })} - - )} {children} +
+ {/* @ts-ignore */} + {customToast?.({ + ...selectedItem, + }) || ( +
+ 测试 +
+ )} +
); }) diff --git a/packages/meta/src/cropperImage/cropperImagehelper.ts b/packages/meta/src/cropperImage/cropperImagehelper.ts index 4968675..749035b 100644 --- a/packages/meta/src/cropperImage/cropperImagehelper.ts +++ b/packages/meta/src/cropperImage/cropperImagehelper.ts @@ -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((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, _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, _fabricCanvas: fa * @param rect 原始画布宽高:w、h;缩放比例:scale;整体偏移的坐标:translateX,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: { 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, + opt: { + limitRect: IArrowLinePosition, + containerSize: Pick + } +) => { + 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 & IShape & { endLeft?: number; endTop?: number }, config: Partial) => { + 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) + }) +} diff --git a/packages/meta/src/cropperImage/demo/basic.tsx b/packages/meta/src/cropperImage/demo/basic.tsx index 74d40c1..7ff5c06 100644 --- a/packages/meta/src/cropperImage/demo/basic.tsx +++ b/packages/meta/src/cropperImage/demo/basic.tsx @@ -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() @@ -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={() => ( -
自定义框
+
)} /> - + {imgUrl && } ) } diff --git a/packages/meta/src/cropperImage/index.tsx b/packages/meta/src/cropperImage/index.tsx index d985aec..e383ca9 100644 --- a/packages/meta/src/cropperImage/index.tsx +++ b/packages/meta/src/cropperImage/index.tsx @@ -1,5 +1,6 @@ import CropperImage from "./CropperImage"; export type { CropperImageRefProps, CropperImageProps } from './CropperImage' +export * from './cropperImagehelper' export default CropperImage diff --git a/packages/meta/src/cropperImage/type.ts b/packages/meta/src/cropperImage/type.ts new file mode 100644 index 0000000..919f75b --- /dev/null +++ b/packages/meta/src/cropperImage/type.ts @@ -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 +} diff --git a/tsconfig.json b/tsconfig.json index 8aef6e6..28e0062 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"]