绘制形状

This commit is contained in:
lzp
2026-03-17 17:17:48 +08:00
parent de5a35060b
commit f12d6aec96
19 changed files with 606 additions and 37 deletions

View File

@@ -3,6 +3,7 @@ import { fabric } from 'fabric-with-all'
import { createId } from '../../tools/tools'
import { exportObjectsToImage, exportObjectToThumbnail } from '../tools/exportMethod'
import { OperationType } from '../tools/layerHelper'
import { getArrowPath, cloneObjects, getStarArr } from '../tools/canvasMethod'
export class LayerManager {
stateManager: any
@@ -53,6 +54,7 @@ export class LayerManager {
this.canvasManager.renderAll()
}
}
/** 删除指定图层 */
deleteLayerById(id, isActive = true) {
this.canvasManager.deleteObjectById(id)
if (id === this.activeID.value && isActive) {
@@ -60,6 +62,24 @@ export class LayerManager {
}
if (isActive) this.stateManager.recordState()
}
/** 复制指定图层 */
copyLayerById(id) {
const object = this.canvasManager.getObjectById(id)
if (!object) return console.warn('复制图层失败对象不存在ID:', id)
cloneObjects([object]).then(objects => {
const newObject = objects[0]
const info = JSON.parse(JSON.stringify(newObject.info))
info.id = createId("image")
// info.name = info.name
newObject.set({
top: newObject.top + 15,
left: newObject.left + 15,
info: info,
})
this.canvasManager.add(newObject)
this.setActiveID(newObject.info.id)
})
}
// 拖拽排序
dragSort(id, newIndex) {
const index = Math.abs(this.layers.value.length - newIndex - 1)
@@ -90,7 +110,7 @@ export class LayerManager {
}
}
/** 创建空图层 */
createEmptyLayer(isRecord = true) {
createEmptyLayer(isRecord = true, isActive = false) {
const emptyObject = new fabric.Rect({
width: 0,
height: 0,
@@ -102,6 +122,7 @@ export class LayerManager {
})
this.setLayerPosition(emptyObject)
this.canvasManager.add(emptyObject, isRecord)
if (isActive) this.setActiveID(emptyObject.info.id, false)
return emptyObject
}
/** 创建文本图层 */
@@ -138,23 +159,109 @@ export class LayerManager {
if (isActive) this.setActiveID(rectObject.info.id)
return rectObject
}
/** 创建圆形图层 */
async createCircleLayer(options?: any, isActive = false) {
const circleObject = new fabric.Circle({
/** 创建直线图层 */
async createLineLayer(options?: any, isActive = false) {
const line = [options?.x1 || 0, options?.y1 || 0, options?.x2 || 100, options?.y2 || 0]
delete options.x1
delete options.y1
delete options.x2
delete options.y2
const lineObject = new fabric.Line(line, {
stroke: 'black', // 线条颜色
strokeWidth: 2, // 线条粗细
...(options || {}),
info: {
id: createId("line"),
name: '直线图层',
...(options?.info || {}),
}
})
this.setLayerPosition(lineObject, options)
await this.canvasManager.add(lineObject)
if (isActive) this.setActiveID(lineObject.info.id)
return lineObject
}
/** 创建椭圆图层 */
async createEllipseLayer(options?: any, isActive = false) {
const ellipseObject = new fabric.Ellipse({
radius: 50,
fill: '#000',
...(options || {}),
info: {
id: createId("circle"),
name: '圆图层',
id: createId("ellipse"),
name: '圆图层',
...(options?.info || {}),
}
})
this.setLayerPosition(circleObject, options)
await this.canvasManager.add(circleObject)
if (isActive) this.setActiveID(circleObject.info.id)
return circleObject
this.setLayerPosition(ellipseObject, options)
await this.canvasManager.add(ellipseObject)
if (isActive) this.setActiveID(ellipseObject.info.id)
return ellipseObject
}
/** 创建三角形图层 */
async createTriangleLayer(options?: any, isActive = false) {
const triangleObject = new fabric.Triangle({
width: 100,
height: 100,
fill: '#000',
...(options || {}),
info: {
id: createId("triangle"),
name: '三角形图层',
...(options?.info || {}),
}
})
this.setLayerPosition(triangleObject, options)
await this.canvasManager.add(triangleObject)
if (isActive) this.setActiveID(triangleObject.info.id)
return triangleObject
}
/** 创建五角星图层 */
async createStarLayer(options?: any, isActive = false) {
const width = options?.width || 100
const height = options?.height || 100
delete options.points
const starObject = new fabric.Polygon(getStarArr(width, height), {
fill: '#000',
...(options || {}),
info: {
id: createId("star"),
name: '五角星图层',
...(options?.info || {}),
}
})
this.setLayerPosition(starObject, options)
await this.canvasManager.add(starObject)
if (isActive) this.setActiveID(starObject.info.id)
return starObject
}
/** 创建箭头图层 */
async createArrowLayer(options?: any, isActive = false) {
const width = options?.width || 100
const height = options?.height || 10
delete options.width
delete options.height
const arrowObject = new fabric.Path(getArrowPath(width, height), {
stroke: '#000', // 只设置边框颜色
strokeWidth: 3, // 边框宽度
fill: 'transparent', // 不填充
strokeLineCap: 'round',
strokeLineJoin: 'round',
...(options || {}),
info: {
id: createId("star"),
name: '箭头图层',
...(options?.info || {}),
}
});
this.setLayerPosition(arrowObject, options)
this.canvasManager.add(arrowObject)
if (isActive) this.setActiveID(arrowObject.info.id)
return arrowObject
}
/** 创建图片图层 */
async createImageLayer(imgOrUrl: string | HTMLImageElement, options?: any, isRecord = true) {
const canvasWidth = this.canvasManager.canvasWidth

View File

@@ -207,5 +207,24 @@ export class ObjectManager {
}
}
/** 修改透明度
* @param id 目标对象ID
* @param options 透明度参数
* @param options.opacity 透明度
* @param isRecord 是否记录
*/
async updateOpacity(id: string, options: any, isRecord: boolean) {
const object = this.getFillRepeatObject(id)
if (!object) return null
const opacity = options.opacity
object.set("opacity", opacity);
this.canvasManager.renderAll()
if (isRecord) {
this.stateManager.recordState()
this.layerManager.updateLayerThumbnailsById(id)
}
}
dispose() { }
}

View File

@@ -1,3 +1,4 @@
import { OperationType, OperationTypes } from "../tools/layerHelper";
import { fabric } from 'fabric-with-all'
/** 矩形工具管理器 */
export class RectToolManager {
@@ -10,6 +11,9 @@ export class RectToolManager {
startX: number = 0
startY: number = 0
demoObject: any
tools = [
OperationType.RECTANGLE
]
constructor(options) {
this.canvasManager = options.canvasManager
this.stateManager = options.stateManager

View File

@@ -0,0 +1,248 @@
import { OperationType, OperationTypes } from "../tools/layerHelper";
import { getStarArr, getArrowPath, distance, angleBetweenPointsDegrees } from "../tools/canvasMethod";
import { fabric } from 'fabric-with-all'
/** 形状管理器 */
export class ShapeToolManager {
// 管理器
canvasManager: any
stateManager: any
layerManager: any
toolManager: any
isDragging: boolean = false
startX: number = 0
startY: number = 0
demoObject: any
tools = [
OperationType.RECTANGLE,
OperationType.LINE,
OperationType.ARROW,
OperationType.ELLIPSE,
OperationType.TRIANGLE,
OperationType.STAR,
]
constructor(options) {
this.canvasManager = options.canvasManager
this.stateManager = options.stateManager
this.layerManager = options.layerManager
this.toolManager = options.toolManager
}
mouseDownEvent(e) {
this.isDragging = false
this.demoObject = null
this.startX = e.absolutePointer.x
this.startY = e.absolutePointer.y
const currentTool = this.toolManager.currentTool.value
if (currentTool === OperationType.RECTANGLE) {
this.demoObject = this.downRectangle()
} else if (currentTool === OperationType.LINE) {
this.demoObject = this.downLine()
} else if (currentTool === OperationType.ELLIPSE) {
this.demoObject = this.downEllipse()
} else if (currentTool === OperationType.TRIANGLE) {
this.demoObject = this.downTriangle()
} else if (currentTool === OperationType.STAR) {
this.demoObject = this.downStar()
} else if (currentTool === OperationType.ARROW) {
this.demoObject = this.downArrow()
}
if (!this.demoObject) return;
this.demoObject.set({
evented: false,
})
this.demoObject.set
this.isDragging = true
this.canvasManager.canvas.add(this.demoObject)
this.canvasManager.canvas.renderAll()
}
mouseMoveEvent(e) {
if (!this.isDragging) return;
var width = e.absolutePointer.x - this.startX
var height = e.absolutePointer.y - this.startY
var left = this.startX
var top = this.startY
if (width < 0) {
left += width
width = -width
}
if (height < 0) {
top += height
height = -height
}
const currentTool = this.toolManager.currentTool.value
if (currentTool === OperationType.RECTANGLE) {
this.moveRectangle({ width, height, left, top })
} else if (currentTool === OperationType.LINE) {
this.moveLine(e.absolutePointer)
} else if (currentTool === OperationType.ELLIPSE) {
this.moveEllipse({ width, height, left, top })
} else if (currentTool === OperationType.TRIANGLE) {
this.moveTriangle({ width, height, left, top })
} else if (currentTool === OperationType.STAR) {
this.moveStar({ width, height, left, top })
} else if (currentTool === OperationType.ARROW) {
this.moveArrow(e.absolutePointer)
}
this.demoObject.set({
evented: false,
})
this.canvasManager.canvas.renderAll()
}
mouseUpEvent(e) {
if (!this.isDragging) return;
this.isDragging = false;
const object = this.demoObject.toJSON("evented")
const currentTool = this.toolManager.currentTool.value
if (currentTool === OperationType.RECTANGLE) {
this.upRectangle(object)
} else if (currentTool === OperationType.LINE) {
this.upLine(object)
} else if (currentTool === OperationType.ELLIPSE) {
this.upEllipse(object)
} else if (currentTool === OperationType.TRIANGLE) {
this.upTriangle(object)
} else if (currentTool === OperationType.STAR) {
this.upStar(object)
} else if (currentTool === OperationType.ARROW) {
this.upArrow(object)
}
this.canvasManager.canvas.remove(this.demoObject)
this.demoObject = null
this.canvasManager.canvas.renderAll()
}
/** 绘制矩形 */
downRectangle() {
const rect = new fabric.Rect({
left: this.startX,
top: this.startY,
width: 0,
height: 0,
fill: '#000',
})
return rect
}
moveRectangle({ width, height, left, top }) {
this.demoObject.set({ width, height, left, top })
}
upRectangle(object) {
if (object.width === 0) object.width = 100
if (object.height === 0) object.height = 100
this.layerManager.createRectLayer(object, true)
}
/** 绘制直线 */
downLine() {
const line = new fabric.Line([this.startX, this.startY, this.startX, this.startY], {
stroke: 'black', // 线条颜色
strokeWidth: 2 // 线条粗细
})
return line
}
moveLine({ x, y }) {
this.demoObject.set({
x1: this.startX,
y1: this.startY,
x2: x,
y2: y,
})
}
upLine(object) {
this.layerManager.createLineLayer(object, true)
}
/** 绘制椭圆 */
downEllipse() {
const circle = new fabric.Ellipse({
left: this.startX,
top: this.startY,
fill: '#000',
})
return circle
}
moveEllipse({ width, height, left, top }) {
this.demoObject.set({ rx: width / 2, ry: height / 2, left, top })
}
upEllipse(object) {
if (object.rx === 0) object.rx = 50
if (object.ry === 0) object.ry = 50
this.layerManager.createEllipseLayer(object, true)
}
/** 绘制三角形 */
downTriangle() {
const triangle = new fabric.Triangle({
left: this.startX,
top: this.startY,
width: 0,
height: 0,
fill: '#000',
})
return triangle
}
moveTriangle({ width, height, left, top }) {
this.demoObject.set({ width, height, left, top })
}
upTriangle(object) {
if (object.width === 0) object.width = 100
if (object.height === 0) object.height = 100
this.layerManager.createTriangleLayer(object, true)
}
/** 绘制五角星 */
downStar() {
const star = new fabric.Polygon(getStarArr(0, 0), {
left: this.startX,
top: this.startY,
width: 0,
height: 0,
fill: '#000',
strokeLineJoin: 'round', // 圆角连接
strokeLineCap: 'round', // 圆角端点
});
return star
}
moveStar({ width, height, left, top }) {
this.demoObject.set({ left, top, width, height, points: getStarArr(width, height) })
}
upStar(object) {
if (object.width === 0) object.width = 100
if (object.height === 0) object.height = 100
this.layerManager.createStarLayer(object, true)
}
/** 绘制箭头 */
downArrow() {
return new fabric.Path();
}
moveArrow({ x, y }) {
const width = distance(this.startX, this.startY, x, y)
const angle = angleBetweenPointsDegrees(this.startX, this.startY, x, y)
this.canvasManager.canvas.remove(this.demoObject)
const arrow = new fabric.Path(getArrowPath(width, 10), {
left: this.startX,
top: this.startY,
stroke: '#000', // 只设置边框颜色
strokeWidth: 3, // 边框宽度
fill: 'transparent', // 不填充
strokeLineCap: 'round',
strokeLineJoin: 'round',
originY: 'center',
angle: angle,
});
this.canvasManager.canvas.add(arrow)
this.demoObject = arrow
}
upArrow(object) {
this.layerManager.createArrowLayer(object, true)
}
dispose() { }
}

View File

@@ -54,6 +54,32 @@ export class ToolManager {
name: OperationType.RECTANGLE,
cursor: "crosshair",
},
/** 直线工具 */
{
name: OperationType.LINE,
cursor: "crosshair",
},
/** 箭头工具 */
{
name: OperationType.ARROW,
cursor: "crosshair",
},
/** 椭圆工具 */
{
name: OperationType.ELLIPSE,
cursor: "crosshair",
},
/** 三角形工具 */
{
name: OperationType.TRIANGLE,
cursor: "crosshair",
},
/** 五角星工具 */
{
name: OperationType.STAR,
cursor: "crosshair",
},
]
}
onMounted() {

View File

@@ -1,6 +1,6 @@
import { isBoolean } from "lodash-es";
import { OperationType, OperationTypes } from "../../tools/layerHelper";
import { RectToolManager } from "../RectToolManager"
import { ShapeToolManager } from "../ShapeToolManager"
import { AISelectboxToolManager } from "../AISelectboxToolManager"
@@ -32,7 +32,7 @@ export class CanvasEventManager {
toolManager: this.toolManager,
layerManager: this.layerManager,
}
this.rectToolManager = new RectToolManager(managers)
this.shapeToolManager = new ShapeToolManager(managers)
this.aiSelectboxToolManager = new AISelectboxToolManager(managers)
// 初始化所有事件
@@ -209,9 +209,9 @@ export class CanvasEventManager {
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
this.aiSelectboxToolManager.mouseDownEvent(opt);
} else if (currentTool === OperationType.RECTANGLE) {
// 形模式
this.rectToolManager.mouseDownEvent(opt);
} else if (this.shapeToolManager.tools.includes(currentTool)) {
// 形模式
this.shapeToolManager.mouseDownEvent(opt);
} else if (opt.e.altKey || opt.e.which === 2 || currentTool === OperationType.PAN) {
this.canvas.isDragging = true;
this.canvas.lastPosX = opt.e.clientX;
@@ -237,9 +237,9 @@ export class CanvasEventManager {
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
this.aiSelectboxToolManager.mouseMoveEvent(opt);
} else if (currentTool === OperationType.RECTANGLE) {
// 形模式
this.rectToolManager.mouseMoveEvent(opt);
} else if (this.shapeToolManager.tools.includes(currentTool)) {
// 形模式
this.shapeToolManager.mouseMoveEvent(opt);
} else if (this.canvas.isDragging) {
const vpt = this.canvas.viewportTransform;
vpt[4] += opt.e.clientX - this.canvas.lastPosX;
@@ -321,9 +321,9 @@ export class CanvasEventManager {
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
this.aiSelectboxToolManager.mouseUpEvent(opt);
} else if (currentTool === OperationType.RECTANGLE) {
// 形模式
this.rectToolManager.mouseDownEvent(opt);
} else if (this.shapeToolManager.tools.includes(currentTool)) {
// 形模式
this.shapeToolManager.mouseDownEvent(opt);
} else if (currentTool === OperationType.PAN) {
// 平滑停止任何正在进行的惯性动画
@@ -386,9 +386,9 @@ export class CanvasEventManager {
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
this.aiSelectboxToolManager.mouseMoveEvent(opt);
} else if (currentTool === OperationType.RECTANGLE) {
// 形模式
this.rectToolManager.mouseMoveEvent(opt);
} else if (this.shapeToolManager.tools.includes(currentTool)) {
// 形模式
this.shapeToolManager.mouseMoveEvent(opt);
} else if (currentTool === OperationType.PAN) {
// 检查是否是触摸事件
@@ -496,9 +496,9 @@ export class CanvasEventManager {
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
this.aiSelectboxToolManager.mouseUpEvent(opt);
} else if (currentTool === OperationType.RECTANGLE) {
// 形模式
this.rectToolManager.mouseUpEvent(opt);
} else if (this.shapeToolManager.tools.includes(currentTool)) {
// 形模式
this.shapeToolManager.mouseUpEvent(opt);
} else if (currentTool === OperationType.PAN) {
// 重置触摸状态
@@ -668,9 +668,9 @@ export class CanvasEventManager {
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
this.aiSelectboxToolManager.mouseUpEvent(opt);
} else if (currentTool === OperationType.RECTANGLE) {
// 形模式
this.rectToolManager.mouseUpEvent(opt);
} else if (this.shapeToolManager.tools.includes(currentTool)) {
// 形模式
this.shapeToolManager.mouseUpEvent(opt);
} else if (this.canvas.isDragging) {
// if (this.lastMousePositions.length > 1 && opt && opt.e) {
// this.animationManager.applyInertiaEffect(
@@ -1074,7 +1074,7 @@ export class CanvasEventManager {
}
dispose() {
this.rectToolManager?.dispose()
this.shapeToolManager?.dispose()
this.aiSelectboxToolManager?.dispose()
// 移除所有事件监听
this.canvas.off();

View File

@@ -9,13 +9,14 @@ export class KeyEventManager {
/** 处理键盘事件 */
_handleKeyDown: any
handleKeyDown(event: any) {
const activeID = this.stateManager.layerManager.activeID.value
const ctrl = event.ctrlKey ? 'ctrl-' : "";
const shift = event.shiftKey ? 'shift-' : "";
const key = event.key;
const reg = new RegExp(`^${ctrl}${shift}${key}$`, 'i')
const list = [
// { key: "ctrl-c", handler: () => this.handleCopy(event) },
// { key: "delete", handler: () => this.handleDelete(event) },
{ key: "ctrl-c", handler: () => this.stateManager.layerManager.copyLayerById(activeID) },
{ key: "delete", handler: () => this.stateManager.layerManager.deleteLayerById(activeID) },
{ key: "ctrl-z", handler: () => this.stateManager.undoState() },
{ key: "ctrl-shift-z", handler: () => this.stateManager.redoState() },
]