From a4ba2604c442e83cb103fe6f2f8b0e6c449ead2f Mon Sep 17 00:00:00 2001 From: ByeWord <37115721+ByeWord@users.noreply.github.com> Date: Thu, 23 May 2024 10:28:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=A4=9A=E8=BE=B9=E5=BD=A2plugin=E5=8A=9F=E8=83=BD=20(#394)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: weicheng.liang --- .eslintrc-auto-import.json | 2 +- packages/core/Editor.ts | 4 + packages/core/index.ts | 1 + packages/core/plugin/PolygonModifyPlugin.ts | 171 ++++++++++++++++++++ packages/core/utils/utils.ts | 18 +++ src/components/edit.vue | 21 +++ src/language/zh.json | 5 +- src/views/home/index.vue | 4 + typings/auto-imports.d.ts | 1 + 9 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 packages/core/plugin/PolygonModifyPlugin.ts create mode 100644 src/components/edit.vue diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json index 61875e3b..b2abc8db 100644 --- a/.eslintrc-auto-import.json +++ b/.eslintrc-auto-import.json @@ -64,4 +64,4 @@ "watchPostEffect": true, "watchSyncEffect": true } -} \ No newline at end of file +} diff --git a/packages/core/Editor.ts b/packages/core/Editor.ts index 54c0a6ed..bfc4d72c 100644 --- a/packages/core/Editor.ts +++ b/packages/core/Editor.ts @@ -34,6 +34,10 @@ class Editor extends EventEmitter { this._initServersPlugin(); } + get fabricCanvas() { + return this.canvas; + } + // 引入组件 use(plugin: IPluginClass, options?: IPluginOption) { if (this._checkPlugin(plugin) && this.canvas) { diff --git a/packages/core/index.ts b/packages/core/index.ts index 10f1fcb3..8925506b 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -26,6 +26,7 @@ export { default as RulerPlugin } from './plugin/RulerPlugin'; export { default as MaterialPlugin } from './plugin/MaterialPlugin'; export { default as WaterMarkPlugin } from './plugin/WaterMarkPlugin'; export { default as FontPlugin } from './plugin/FontPlugin'; +export { default as PolygonModifyPlugin } from './plugin/PolygonModifyPlugin'; import EventType from './eventType'; import Utils from './utils/utils'; diff --git a/packages/core/plugin/PolygonModifyPlugin.ts b/packages/core/plugin/PolygonModifyPlugin.ts new file mode 100644 index 00000000..4eb663c0 --- /dev/null +++ b/packages/core/plugin/PolygonModifyPlugin.ts @@ -0,0 +1,171 @@ +import Editor from '../Editor'; +import { fabric } from 'fabric'; +import { drawImg } from '../utils/utils'; +import edgeImg from '../assets/edgecontrol.svg'; +import { noop } from 'lodash-es'; + +type IEditor = Editor; + +export type Options = { + fill: string; + style: fabric.IObjectOptions['cornerStyle']; +}; + +interface PointIndexPolygon extends fabric.Polygon { + pointIndex: number; + __corner: string; + _setPositionDimensions: (...args: any[]) => any; +} + +interface PointIndexControl extends fabric.Control { + pointIndex: number; +} + +const actionHandler: fabric.Control['actionHandler'] = function ( + eventData: MouseEvent, + transform: fabric.Transform, + x: number, + y: number +) { + const polygon = transform.target as PointIndexPolygon, + currentControl = polygon.controls[polygon.__corner] as PointIndexControl, + mouseLocalPosition = polygon.toLocalPoint(new fabric.Point(x, y), 'center', 'center'), + polygonBaseSize = getObjectSizeWithStroke(polygon), + size = polygon._getTransformedDimensions(0, 0); + if (polygon.points == null) return false; + polygon.points[currentControl.pointIndex] = new fabric.Point( + (mouseLocalPosition.x * polygonBaseSize.x) / size.x + polygon.pathOffset.x, + (mouseLocalPosition.y * polygonBaseSize.y) / size.y + polygon.pathOffset.y + ); + return true; +}; +const anchorWrapper = function (anchorIndex: number, fn: fabric.Control['actionHandler']) { + return function (eventData: MouseEvent, transform: fabric.Transform, x: number, y: number) { + const fabricObject = transform.target as PointIndexPolygon; + if (fabricObject.points == null) return false; + const absolutePoint = fabric.util.transformPoint( + new fabric.Point( + fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x, + fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y + ), + fabricObject.calcTransformMatrix() + ), + actionPerformed = fn(eventData, transform, x, y), + newDim = fabricObject._setPositionDimensions({}), + polygonBaseSize = getObjectSizeWithStroke(fabricObject), + newX = (fabricObject.points[anchorIndex].x - fabricObject.pathOffset.x) / polygonBaseSize.x, + newY = (fabricObject.points[anchorIndex].y - fabricObject.pathOffset.y) / polygonBaseSize.y; + const originX = (newX + 0.5) as any; + const originY = (newY + 0.5) as any; + fabricObject.setPositionByOrigin(absolutePoint, originX, originY); + return actionPerformed; + }; +}; +const getObjectSizeWithStroke = function (object: fabric.Object) { + const stroke = new fabric.Point( + object.strokeUniform ? 1 / object.scaleX! : 1, + object.strokeUniform ? 1 / object.scaleY! : 1 + ).multiply(object.strokeWidth!); + return new fabric.Point(object.width! + stroke.x, object.height! + stroke.y); +}; +const polygonPositionHandler = function ( + this: PointIndexControl, + dim: any, + finalMatrix: any, + fabricObject: any +) { + const x = fabricObject.points[this.pointIndex].x - fabricObject.pathOffset.x, + y = fabricObject.points[this.pointIndex].y - fabricObject.pathOffset.y; + // 求出在世界坐标系的位置 + return fabric.util.transformPoint( + new fabric.Point(x, y), // 物体坐标系下的位置 + fabric.util.multiplyTransformMatrices( + fabricObject.canvas.viewportTransform, + fabricObject.calcTransformMatrix() + ) + ); +}; +function renderIconEdge( + ctx: CanvasRenderingContext2D, + left: number, + top: number, + styleOverride: any, + fabricObject: fabric.Object +) { + const img = document.createElement('img'); + img.src = edgeImg; + drawImg(ctx, left, top, img, 25, 25, fabric.util.degreesToRadians(fabricObject.angle || 0)); +} + +class PolygonModifyPlugin { + public isEdit: boolean; + static pluginName = 'PolygonModifyPlugin'; + static events = []; + static apis = ['toggleEdit', 'activeEdit', 'inActiveEdit']; + + constructor(public canvas: fabric.Canvas, public editor: IEditor) { + this.isEdit = false; + this.init(); + } + init() { + console.info('[PolygonModifyPlugin]: init'); + } + _onDeselected: () => any = noop; + _ensureEvent(poly: fabric.Object) { + poly.off('deselected', this._onDeselected); + } + toggleEdit() { + this.isEdit ? this.inActiveEdit() : this.activeEdit(); + } + activeEdit() { + this.isEdit = true; + const poly = this.canvas.getActiveObject() as fabric.Polygon; + if (poly && poly.type === 'polygon') { + this._ensureEvent(poly); + if (poly.points == null) return; + const lastControl = poly.points.length - 1; + poly.controls = poly.points.reduce>(function ( + acc, + point, + index + ) { + acc['p' + index] = new fabric.Control({ + positionHandler: polygonPositionHandler, + actionHandler: anchorWrapper(index > 0 ? index - 1 : lastControl, actionHandler), + actionName: 'modifyPolygon', + render: renderIconEdge, + }); + Object.defineProperty(acc['p' + index], 'pointIndex', { value: index }); + return acc; + }, + {}); + poly.set({ + objectCaching: false, + }); + poly.hasBorders = !this.isEdit; + this.canvas.requestRenderAll(); + this._onDeselected = () => this.inActiveEdit(poly); + poly.on('deselected', this._onDeselected); + } + } + inActiveEdit(poly?: fabric.Polygon) { + this.isEdit = false; + poly = poly || (this.canvas.getActiveObject() as fabric.Polygon); + if (poly && poly.type === 'polygon') { + poly.cornerColor = 'blue'; + poly.cornerStyle = 'rect'; + poly.controls = fabric.Object.prototype.controls; + poly.hasBorders = !this.isEdit; + poly.set({ + objectCaching: true, + }); + if (this._onDeselected) { + poly.off('deselected', this._onDeselected); + this._onDeselected = noop; + } + } + this.canvas.requestRenderAll(); + } +} + +export default PolygonModifyPlugin; diff --git a/packages/core/utils/utils.ts b/packages/core/utils/utils.ts index 14199e82..e5690fb7 100644 --- a/packages/core/utils/utils.ts +++ b/packages/core/utils/utils.ts @@ -82,10 +82,28 @@ export function downFile(fileStr: string, fileType: string) { anchorEl.remove(); } +export function drawImg( + ctx: CanvasRenderingContext2D, + left: number, + top: number, + img: HTMLImageElement, + wSize: number, + hSize: number, + angle: number | undefined +) { + if (angle === undefined) return; + ctx.save(); + ctx.translate(left, top); + ctx.rotate(angle); + ctx.drawImage(img, -wSize / 2, -hSize / 2, wSize, hSize); + ctx.restore(); +} + export default { getImgStr, downFile, selectFiles, insertImgFile, clipboardText, + drawImg, }; diff --git a/src/components/edit.vue b/src/components/edit.vue new file mode 100644 index 00000000..f8266cb4 --- /dev/null +++ b/src/components/edit.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/src/language/zh.json b/src/language/zh.json index 4daea74e..35966d21 100644 --- a/src/language/zh.json +++ b/src/language/zh.json @@ -137,7 +137,8 @@ "del": "删除", "copy": "复制", "lock": "锁定", - "hide": "隐藏" + "hide": "隐藏", + "editPoly": "编辑多边形" }, "insertFile": { "insert": "插入", @@ -226,4 +227,4 @@ "myMaterial": { "uploadBtn": "上传素材" } -} \ No newline at end of file +} diff --git a/src/views/home/index.vue b/src/views/home/index.vue index d7ea5415..3031f3af 100644 --- a/src/views/home/index.vue +++ b/src/views/home/index.vue @@ -112,6 +112,7 @@ + @@ -231,7 +232,9 @@ import Editor, { MaterialPlugin, WaterMarkPlugin, FontPlugin, + PolygonModifyPlugin, } from '@kuaitu/core'; +import Edit from '@/components/edit.vue'; // 创建编辑器 const canvasEditor = new Editor(); @@ -307,6 +310,7 @@ onMounted(() => { // 初始化编辑器 canvasEditor.init(canvas); canvasEditor.use(DringPlugin); + canvasEditor.use(PolygonModifyPlugin); canvasEditor.use(AlignGuidLinePlugin); canvasEditor.use(ControlsPlugin); // canvasEditor.use(ControlsRotatePlugin); diff --git a/typings/auto-imports.d.ts b/typings/auto-imports.d.ts index 7807dd3e..58bd667a 100644 --- a/typings/auto-imports.d.ts +++ b/typings/auto-imports.d.ts @@ -1,6 +1,7 @@ /* eslint-disable */ /* prettier-ignore */ // @ts-nocheck +// noinspection JSUnusedGlobalSymbols // Generated by unplugin-auto-import export {} declare global {