Skip to content

Commit

Permalink
feat: 添加绘制多边形的功能和完善编辑多边形的判定 (#398)
Browse files Browse the repository at this point in the history
* feat: 添加编辑多边形plugin功能

* refactor: 使用mixinState.mSelectOneType判定编辑多边形按钮的显示与否

* feat: 添加动态创建多边形插件

* feat: 添加绘制多边形DrawPolygonPlugin

* refactor: 抽离shiftAngle方法用于shiftKey的时候纠正角度

* refactor: 修改绘制polygon的交互逻辑

* feat: 添加shiftKey纠正点位和onEnd监听,并完善交互逻辑

* feat: 添加自由绘制功能

* refactor: 完善历史记录和绘制多边形结合交互

* refactor: 完善判定开启和关闭特殊tool时的副作用

* feat: 添加pathTextPlugin用户绘制路径文字

---------

Co-authored-by: weicheng.liang <[email protected]>
  • Loading branch information
ByeWord and weicheng.liang authored May 26, 2024
1 parent 197e1b0 commit 5cb3b3a
Show file tree
Hide file tree
Showing 11 changed files with 565 additions and 6 deletions.
3 changes: 3 additions & 0 deletions packages/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ 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';
export { default as DrawPolygonPlugin } from './plugin/DrawPolygonPlugin';
export { default as FreeDrawPlugin } from './plugin/FreeDrawPlugin';
export { default as PathTextPlugin } from './plugin/PathTextPlugin';
import EventType from './eventType';
import Utils from './utils/utils';

Expand Down
186 changes: 186 additions & 0 deletions packages/core/plugin/DrawPolygonPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { fabric } from 'fabric';
import Editor from '../Editor';
import { v4 as uuid } from 'uuid';
import { shiftAngle } from '../utils/utils';

type IEditor = Editor;

type LineCoords = [fabric.Point, fabric.Point];
type OffListener = (ev: fabric.IEvent) => void;
type OnEnd = (...args: any[]) => void;
class DrawPolygonPlugin {
isDrawingPolygon = false;
points: fabric.Point[] = [];
lines: fabric.Line[] = [];
anchors: fabric.Circle[] = [];
tempPoint: fabric.Point | undefined;
tempLine: fabric.Line | undefined;
lastPoint: fabric.Point | undefined;
onEnd?: OnEnd;
// 最后一点和第一点的距离为<=delta即闭合
delta = 5;
static pluginName = 'DrawPolygonPlugin';
static apis = ['beginDrawPolygon', 'endDrawPolygon', 'discardPolygon'];
constructor(public canvas: fabric.Canvas, public editor: IEditor) {}

_isUsingHistory() {
return this.canvas.offHistory && typeof this.canvas.offHistory === 'function';
}
_bindEvent() {
window.addEventListener('keydown', this._escListener);
this.canvas.on('mouse:down', this._downHandler);
this.canvas.on('mouse:move', this._moveHandler);
}
_escListener = (evt: KeyboardEvent) => {
if (evt.key === 'Escape' || evt['keyCode'] === 27) {
this._confirmBuildPolygon();
}
};
_downHandler = (ev: fabric.IEvent<MouseEvent>): void => {
if (!this.isDrawingPolygon) return;
const absPointer = ev.absolutePointer!;
const confirmPoint = new fabric.Point(absPointer.x, absPointer.y);
const anchor = this._mackAnchor(absPointer);
this.anchors.push(anchor);
if (this.tempLine == null) {
const tempPoint = new fabric.Point(absPointer.x, absPointer.y);
this.tempLine = this._makeLine([tempPoint, tempPoint]);
this.canvas.add(this.tempLine);
} else {
ev.e.shiftKey && confirmPoint.setXY(this.tempLine.x2!, this.tempLine.y2!);
anchor.set({ left: confirmPoint.x, top: confirmPoint.y });
this.tempLine.set({
x1: confirmPoint.x,
y1: confirmPoint.y,
x2: confirmPoint.x,
y2: confirmPoint.y,
});
}
if (this.lastPoint) {
const line = this._makeLine([this.lastPoint, confirmPoint]);
this.lines.push(line);
this.canvas.add(line);
if (this.points[0].distanceFrom(confirmPoint) / this.canvas.getZoom() <= this.delta) {
this._confirmBuildPolygon();
return;
}
}
this.canvas.add(anchor);
this.lastPoint = confirmPoint;
this.points.push(confirmPoint);
this._ensureAnchorsForward();
};
_moveHandler = (ev: fabric.IEvent<MouseEvent>): void => {
if (!this.isDrawingPolygon || !this.tempLine) return;
const absPoint = ev.absolutePointer!;
if (ev.e.shiftKey && this.lastPoint) {
const point = shiftAngle(this.lastPoint, absPoint);
this.tempLine.set({
x2: point.x,
y2: point.y,
});
} else {
this.tempLine.set({
x2: absPoint.x,
y2: absPoint.y,
});
}
this.canvas.renderAll();
};
_ensureAnchorsForward() {
this.anchors.forEach((item) => {
item.bringForward();
});
}
_unbindEvent() {
window.removeEventListener('keydown', this._escListener);
this.canvas.off('mouse:down', this._downHandler as OffListener);
this.canvas.off('mouse:move', this._moveHandler as OffListener);
}
_createPolygon(points: fabric.Point[]) {
return new fabric.Polygon(points, {
fill: '#ccc',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
id: uuid(),
});
}
_makeLine(coors: LineCoords) {
const [p1, p2] = coors;
return new fabric.Line([p1.x, p1.y, p2.x, p2.y], {
fill: '#000',
stroke: '#000',
strokeWidth: 1,
selectable: false,
evented: false,
});
}
_mackAnchor(position: fabric.Point) {
return new fabric.Circle({
radius: 5,
left: position.x,
top: position.y,
fill: 'rgb(178, 53, 84)',
scaleX: 1 / this.canvas.getZoom(),
scaleY: 1 / this.canvas.getZoom(),
strokeWidth: 1 / this.canvas.getZoom(),
originX: 'center',
originY: 'center',
evented: false,
selectable: false,
});
}
_confirmBuildPolygon() {
const points = this.points;
this.discardPolygon();
if (this._isUsingHistory()) {
this.canvas.historyProcessing = false;
}
if (points.length >= 3) {
const poly = this._createPolygon(points);
this.canvas.add(poly);
}
}
_prepare() {
this.canvas.discardActiveObject();
this.canvas.getObjects().forEach((obj) => {
obj.selectable = false;
obj.hasControls = false;
});
}
beginDrawPolygon(onEnd?: OnEnd) {
this._prepare();
this.onEnd = onEnd;
if (this._isUsingHistory()) {
this.canvas.historyProcessing = true;
}
this.canvas.requestRenderAll();
this.isDrawingPolygon = true;
this._bindEvent();
}
endDrawPolygon() {
this.canvas.discardActiveObject();
this.isDrawingPolygon = false;
this.lastPoint = undefined;
this.tempPoint = undefined;
this._unbindEvent();
this.onEnd && this.onEnd();
this.onEnd = undefined;
}
discardPolygon() {
this.lines.forEach((item) => {
this.canvas.remove(item);
});
this.anchors.forEach((item) => {
this.canvas.remove(item);
});
this.tempLine && this.canvas.remove(this.tempLine);
this.tempLine = undefined;
this.anchors = [];
this.lines = [];
this.points = [];
this.endDrawPolygon();
}
}

export default DrawPolygonPlugin;
26 changes: 26 additions & 0 deletions packages/core/plugin/FreeDrawPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { fabric } from 'fabric';
import Editor from '../Editor';

type IEditor = Editor;

type DrawOptions = {
width: number;
};

export default class FreeDrawPlugin {
static pluginName = 'FreeDrawPlugin';
static apis = ['startDraw', 'endDraw'];
constructor(public canvas: fabric.Canvas, public editor: IEditor) {}

startDraw(options: DrawOptions) {
this.canvas.isDrawingMode = true;
this.canvas.freeDrawingBrush = new fabric.PencilBrush(this.canvas);
this.canvas.freeDrawingBrush.width = options.width;
}
endDraw() {
if (this.canvas.isDrawingMode) {
this.canvas.isDrawingMode = false;
return;
}
}
}
72 changes: 72 additions & 0 deletions packages/core/plugin/PathTextPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { fabric } from 'fabric';
import Editor from '../Editor';

type IEditor = Editor;
type DrawOptions = {
decimate: number;
width: number;
defaultText: string;
color: string;
lineColor: string;
defaultFontSize: number;
};

export default class PathTextPlugin {
static pluginName = 'PathTextPlugin';
static apis = ['startTextPathDraw', 'endTextPathDraw'];
private options?: DrawOptions;
constructor(public canvas: fabric.Canvas, public editor: IEditor) {}

_beforeHandler = (opt: any) => {
if (this.options == null) return;
const path = opt.path as any;
const getPathSegmentsInfo = (fabric.util as any).getPathSegmentsInfo;
path.segmentsInfo = getPathSegmentsInfo(path.path);
path.set({ stroke: this.options.lineColor });
const text = this.options.defaultText;
const fontSize = this.options.defaultFontSize;
const textObject = new fabric.Text(text, {
fontSize: fontSize,
top: path.top,
left: path.left,
fill: this.options.color,
path: path,
});
this.canvas.add(textObject);
};
_createdHandler = (opt: any) => {
this.canvas.remove(opt.path);
};
_bindEvent() {
this.canvas.on('before:path:created', this._beforeHandler);
this.canvas.on('path:created', this._createdHandler);
}
_unbindEvent() {
this.canvas.off('before:path:created', this._beforeHandler);
this.canvas.off('path:created', this._createdHandler);
}
startTextPathDraw(options: Partial<DrawOptions> = {}) {
const defaultOptions = {
decimate: 8,
width: 2,
defaultText: '诸事顺遂 万事大吉',
color: '#000000',
lineColor: '#000000',
defaultFontSize: 20,
};
this.options = {
...defaultOptions,
...options,
};
this.canvas.isDrawingMode = true;
const brush = (this.canvas.freeDrawingBrush = new fabric.PencilBrush(this.canvas));
brush.decimate = this.options.decimate;
brush.width = this.options.width;
brush.color = this.options.color;
this._bindEvent();
}
endTextPathDraw() {
this.canvas.isDrawingMode = false;
this._unbindEvent();
}
}
5 changes: 3 additions & 2 deletions packages/core/plugin/PolygonModifyPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,10 @@ function renderIconEdge(
left: number,
top: number,
styleOverride: any,
fabricObject: fabric.Object,
img: HTMLImageElement
fabricObject: fabric.Object
) {
const img = document.createElement('img');
img.src = edgeImg;
drawImg(ctx, left, top, img, 25, 25, fabric.util.degreesToRadians(fabricObject.angle || 0));
}

Expand Down
18 changes: 18 additions & 0 deletions packages/core/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,21 @@ export default {
clipboardText,
drawImg,
};

export function shiftAngle(start: fabric.Point, end: fabric.Point) {
const startX = start.x;
const startY = start.y;
const x2 = end.x - startX;
const y2 = end.y - startY;
const r = Math.sqrt(x2 * x2 + y2 * y2);
let angle = (Math.atan2(y2, x2) / Math.PI) * 180;
angle = ~~(((angle + 7.5) % 360) / 15) * 15;

const cosx = r * Math.cos((angle * Math.PI) / 180);
const sinx = r * Math.sin((angle * Math.PI) / 180);

return {
x: cosx + startX,
y: sinx + startY,
};
}
Loading

0 comments on commit 5cb3b3a

Please sign in to comment.