Files
FiDA_Front/src/components/Canvas/DepthCanvas/manager/CanvasManager.ts
2026-03-30 16:59:13 +08:00

402 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { fabric } from 'fabric-with-all'
import { ref, computed } from 'vue'
import { createCanvas } from '../tools/canvasFactory'
import { AnimationManager } from './AnimationManager'
import { detectDeviceType } from '../tools/index'
import { CanvasEventManager } from "./events/CanvasEventManager";
import { OperationType } from '../tools/layerHelper'
import { cloneObjects } from '../tools/canvasMethod'
import { createId } from '../../tools/tools'
import md5 from 'md5'
// 自定义画布转对象属性
fabric.Object.prototype.customProperties = ["top", "left", "width", "height", "scaleX", "scaleY", "info", "thumbnail", "absolutePositioned"];
fabric.Object.prototype.toObject_ = fabric.Object.prototype.toObject
fabric.Object.prototype.toObject = function () {
const args = [...arguments]
const arr = [...fabric.Object.prototype.customProperties]
args.forEach(v => (Array.isArray(v) ? arr.push(...v) : arr.push(v)))
if (this.fill?.source === null) {
let image = new Image()
image.crossOrigin = 'anonymous'
image.src = this.info?.fill?.source
this.fill.source = image
}
const object = this.toObject_(arr)
if (object.info) {
let lock = !!object.info.lock
if (object.type === "group") lock = true
object.evented = !lock
object.selectable = !lock
}
return object
}
interface CanvasInitOptions {
canvasRef: any
canvasViewWidth?: number
canvasViewHeight?: number
canvasWidth?: number
canvasHeight?: number
url?: string
}
export class CanvasManager {
stateManager: any
layerManager: any
animationManager: any
eventManager: any
deviceInfo: any
canvas: any
currentZoom: any
uniqueId: string
constructor(options) {
this.stateManager = options.stateManager;
this.uniqueId = md5(options.props.config.url || Date.now());
this.deviceInfo = detectDeviceType();
this.currentZoom = ref(100)
}
onMounted() { }
getCanvasSize() {
return {
canvasViewWidth: this.canvas.width,
canvasViewHeight: this.canvas.height,
canvasWidth: this.canvas.clipPath.width,
canvasHeight: this.canvas.clipPath.height,
}
}
setCanvasViewSize(options) {
var canvasViewWidth = options.canvasViewWidth || 1920
var canvasViewHeight = options.canvasViewHeight || 1080
this.canvas.setWidth(canvasViewWidth)
this.canvas.setHeight(canvasViewHeight)
}
/** 初始化画布 */
async initCanvas(options: CanvasInitOptions) {
this.layerManager = this.stateManager.layerManager
var canvasWidth = options.canvasWidth || 750
var canvasHeight = options.canvasHeight || 600
var image = null;
if (options.url) {
await new Promise((resolve) => {
fabric.Image.fromURL(options.url, async (img) => {
canvasWidth = img.width
canvasHeight = img.height
img.set({
left: 0,
top: 0,
scaleX: 1,
scaleY: 1,
evented: false,
selectable: false,
info: {
id: createId("image"),
name: "图片图层",
lock: true,
isOriginal: true,
}
})
image = img
resolve(img)
}, { crossOrigin: 'anonymous' })
})
}
this.canvas = createCanvas(options.canvasRef.value, {
preserveObjectStacking: true,
enableRetinaScaling: true,
backgroundColor: '#fff',
})
if (image) {
this.canvas.add(image)
await this.layerManager.updateLayerThumbnailsById(image.info.id)
}
this.setCanvasViewSize(options)
this.canvas.clipPath = new fabric.Rect({
left: 0,
top: 0,
width: canvasWidth,
height: canvasHeight
})
// 动画管理器
this.animationManager = new AnimationManager(this.canvas, {
currentZoom: this.currentZoom,
canvasManager: this,
wheelThrottleTime: 15, // 降低滚轮事件节流时间,提高响应性
defaultEase: "power2.lin",
defaultDuration: 0.3, // 缩短默认动画时间
});
this.setupCanvasEvents()
this.setupBrushEvents()
/** 测试-开始 */
// this.stateManager.setIsRecord(false)
// const groupObject = await this.layerManager.createGroupLayer()
// const parentId = groupObject.info.id
// const rect = await this.layerManager.createRectLayer({ left: 200, info: { parentId } })
// const star = await this.layerManager.createStarLayer({ left: 400, info: { parentId } })
// const arrow = await this.layerManager.createArrowLayer({ left: 600, info: { parentId } })
// await this.layerManager.createGroupLayer()
// this.layerManager.setActiveID(rect.info.id)
// this.stateManager.setIsRecord(true)
/** 测试-结束 */
this.resetZoom(false, true)// 画布居中
this.stateManager.toolManager.setTool(OperationType.SELECT)
this.layerManager.updateLayers()
this.stateManager.recordState()
}
/** 画布添加对象 */
async add(obj: any, isRecord = true) {
this.canvas.add(obj)
const id = obj?.info?.id || ""
if (id) {
this.sortActiveObject(obj)
await this.layerManager.updateLayers(!!obj.info.parentId)
this.renderAll()
await this.layerManager.updateLayerThumbnailsById(id)
}
if (isRecord) this.stateManager.recordState()
}
/** 画布移除对象 */
remove(obj: any, isUpdate = true) {
this.canvas.remove(obj)
if (isUpdate) {
this.layerManager.updateLayers()
this.renderAll()
}
}
/** 根据激活对象排序当前对象 */
async sortActiveObject(object: any) {
const activeID = this.layerManager.activeID.value;
if (!activeID) return
const activeObject = this.getObjectById(activeID)
if (!activeObject) return
const parentId = activeObject.info?.parentId
if (!object.info.parentId) object.info.parentId = parentId
const index = this.getObjects().indexOf(activeObject)
this.canvas.moveTo(object, index + 1)
// await this.sortObjectByIds(ids, isRecord)
}
/** 更新子图层裁剪区域 */
async updateSubLayerClipPath() {
const objects = this.getObjects().filter((v: any) => v.type !== "group" && !!v.info?.id);
for (let i = 0; i < objects.length; i++) {
let object = objects[i]
if (object.clipPath) object.set({ clipPath: null })
let group = this.getObjectById(object.info.parentId)
if (!group) continue
let path = group.clipPath
if (!path) continue
let clipPath = await cloneObjects([path]).then((v) => v[0])
clipPath.set({
absolutePositioned: true,
})
object.set({ clipPath })
}
this.renderAll()
}
/** 排序画布对象 */
async sortObjectByIds(ids: string[], isRecord?: boolean) {
ids.forEach((id, index) => {
this.canvas.moveTo(this.getObjectById(id), index)
})
await this.updateSubLayerClipPath()
this.renderAll()
if (isRecord) this.stateManager.recordState()
}
/** 设置画布事件 */
setupCanvasEvents() {
// 创建画布事件管理器
this.eventManager = new CanvasEventManager(this.canvas, {
canvasManager: this,
animationManager: this.animationManager,
toolManager: this.stateManager.toolManager,
layerManager: this.stateManager.layerManager,
stateManager: this.stateManager,
});
// 设置动画交互效果
this.animationManager.setupInteractionAnimations();
}
/** 设置激活对象 */
setActiveObjectById(id: string) {
this.discardActiveObject()
const obj = this.getObjectById(id)
if (!obj) return
if (obj.type === "group") {
obj._originPosition = {
top: obj.top,
left: obj.left,
cpTop: obj.clipPath.top,
cpLeft: obj.clipPath.left,
}
const objects = [obj];
this.getObjects().forEach((item: any) => {
if (item?.info?.parentId === id) objects.push(item)
})
if (objects.length > 0) this.canvas.setActiveObject(new fabric.ActiveSelection(objects, { canvas: this.canvas }));
} else {
if (obj.evented) this.canvas.setActiveObject(obj)
}
this.renderAll()
}
resetZoom(animated = true, adaptive = true) {
this.animationManager.resetZoom(animated, adaptive)
}
// 使用动画管理器的缩放方法
animateZoom(point, targetZoom, options = {}) {
this.animationManager.animateZoom(point, targetZoom, options);
}
zoomIn() {
const currentZoom = this.canvas.getZoom()
const newZoom = Math.min(currentZoom + 0.1, 20) // 增加20%最大20倍
// 使用画布中心作为缩放点
const centerPoint = {
x: this.canvas.width / 2,
y: this.canvas.height / 2
}
this.animateZoom(centerPoint, newZoom)
}
zoomOut() {
const currentZoom = this.canvas.getZoom()
const newZoom = Math.max(currentZoom - 0.1, 0.1) // 减少20%最小0.1倍
// 使用画布中心作为缩放点
const centerPoint = {
x: this.canvas.width / 2,
y: this.canvas.height / 2
}
this.animateZoom(centerPoint, newZoom)
}
getObjects() {
return this.canvas.getObjects() || []
}
getObjectById(id: string) {
return this.getObjects().find((item: any) => item?.info?.id === id)
}
getChildObjectsById(id: string) {
return this.getObjects().filter((item: any) => item?.info?.parentId === id)
}
/** 获取选中对象 */
getSelectedObject() {
return this.canvas.getActiveObject()
}
renderAll() {
this.canvas.renderAll()
}
deleteObjectById(id: string, isUpdate = true) {
const object = this.getObjectById(id)
if (object) {
this.canvas.remove(object)
if (isUpdate) this.layerManager.updateLayers()
this.renderAll()
}
}
/** 取消选中对象 */
discardActiveObject() {
this.canvas.discardActiveObject()
this.renderAll()
}
/** 画笔事件 */
setupBrushEvents() {
this.canvas.onBrushImageConverted = async (fabricImage) => {
const currentTool = this.stateManager.toolManager.currentTool.value;
if (currentTool === OperationType.DRAW) {
this.handleDrawImage(fabricImage)
} else if (currentTool === OperationType.AISELECT_DRAW) {
this.stateManager.aiSelectboxToolManager.handleDrawImage(fabricImage)
}
return true
};
}
/** 处理绘制图像 */
async handleDrawImage(fabricImage: fabric.Object) {
const activeID = this.stateManager.layerManager.activeID.value
const activeLayer = this.getObjectById(activeID)
if (activeLayer && activeLayer.fill?.repeat !== "repeat" && activeLayer.type !== "group") {
this.layerManager.imageMergeToLayer(activeLayer, fabricImage)
} else {
const emptyLayer = await this.layerManager.createEmptyLayer(false);
this.layerManager.setActiveID(emptyLayer.info.id, false)
this.layerManager.imageMergeToLayer(emptyLayer, fabricImage)
}
return true
}
/** 导出画布为JSON */
getCanvasJSON() {
const json = this.canvas.toJSON()
return JSON.stringify(json)
}
/** 加载画布JSON */
loadJSON(json: string, rerecord = true) {
return new Promise((resolve) => {
let jsonObj = null;
try {
jsonObj = JSON.parse(json)
} catch (error) {
console.error('JSON解析错误:', error)
}
if (!jsonObj) return resolve(false)
if (jsonObj.uniqueId) this.uniqueId = jsonObj.uniqueId
this.canvas.loadFromJSON(jsonObj, () => {
if (rerecord) this.stateManager.clearState()
this.resetZoom(false)
this.layerManager.updateLayers()
this.renderAll()
if (rerecord) this.stateManager.recordState()
resolve(true)
})
})
}
/** 导出画布为处理图片的JSON */
getCanvasDisUrlJSON() {
const canvas = this.canvas.toJSON()
const images = {};
const create = (url, key) => {
const key_ = `xxxxx_${this.uniqueId}_${md5(key)}_xxxxx`;
images[key_] = url
return key_
}
canvas.uniqueId = this.uniqueId
canvas.objects.forEach((object: any) => {
const id = object.info?.id
if (object.thumbnail) {
object.thumbnail = create(object.thumbnail, `thumbnail_${id}`)
}
if (object.src) {
object.src = create(object.src, `src_${id}`)
object.crossOrigin = 'anonymous'
}
if (object.fill?.source) {
object.fill.source = create(object.fill.source, `fillsource_${id}`)
}
if (object.info?.fill?.source) {
object.info.fill.source = create(object.info.fill.source, `infofillsource_${id}`)
}
})
return { canvas: JSON.stringify(canvas), images }
}
/** 处理JSON为正常画布 */
processCanvasDisUrlJSON(obj: { canvas: string, images: Object }) {
var json = obj.canvas;
const images = obj.images || {}
for (const key in images) {
json = json.replace(new RegExp(key), images[key])
}
return json
}
dispose() {
this.animationManager?.dispose()
this.eventManager?.dispose()
}
}