深度画布 矩形工具

This commit is contained in:
lzp
2026-03-12 15:51:18 +08:00
parent 86f1efbf43
commit d115047563
9 changed files with 378 additions and 213 deletions

View File

@@ -15,10 +15,13 @@
<span class="icon"><svg-icon name="export" size="12" /></span> <span class="icon"><svg-icon name="export" size="12" /></span>
<span class="text">Export</span> <span class="text">Export</span>
</button> </button>
<button class="export" @click="emit('import')"> <button class="export" @click="emit('export-local')">
<span class="text">import</span> <span class="text">保存本地</span>
</button> </button>
<button class="workbench"> <button class="export" @click="emit('import-local')">
<span class="text">本地导入</span>
</button>
<button class="workbench" @click="emit('export-close')">
<span class="icon"><svg-icon name="dc-workbench" size="20" /></span> <span class="icon"><svg-icon name="dc-workbench" size="20" /></span>
<span class="text">Workbench</span> <span class="text">Workbench</span>
</button> </button>
@@ -32,7 +35,7 @@
zoom: { default: 1, type: Number }, zoom: { default: 1, type: Number },
step: { default: 0.1, type: Number } step: { default: 0.1, type: Number }
}) })
const emit = defineEmits(['export', 'import']) const emit = defineEmits(['export', 'export-local', 'import-local', 'export-close'])
const importLocalImage = inject('importLocalImage') as () => void const importLocalImage = inject('importLocalImage') as () => void
const stateManager = inject('stateManager') as any const stateManager = inject('stateManager') as any
const toolManager = inject('toolManager') as any const toolManager = inject('toolManager') as any

View File

@@ -61,7 +61,6 @@
} }
const onClickLayer = () => { const onClickLayer = () => {
layerManager.setActiveID(props.layer.info.id) layerManager.setActiveID(props.layer.info.id)
toolManager.setTool(OperationType.SELECT)
} }
</script> </script>

View File

@@ -6,7 +6,12 @@
<template v-if="isReady"> <template v-if="isReady">
<layer-panel /> <layer-panel />
<details-panel /> <details-panel />
<depth-header-tools @export="exportCanvas" @import="importCanvas" /> <depth-header-tools
@export="exportCanvas"
@export-local="exportCanvasToLocalStorage"
@import-local="importCanvasFromLocalStorage"
@export-close="exportCanvasAndClose"
/>
<brush-control-panel :currentTool="toolManager.currentTool.value" /> <brush-control-panel :currentTool="toolManager.currentTool.value" />
<zoom <zoom
:zoom="canvasManager.currentZoom.value / 100" :zoom="canvasManager.currentZoom.value / 100"
@@ -22,6 +27,7 @@
import { fabric } from 'fabric-with-all' import { fabric } from 'fabric-with-all'
import { computed, ref, markRaw, onMounted, nextTick, provide, onBeforeUnmount } from 'vue' import { computed, ref, markRaw, onMounted, nextTick, provide, onBeforeUnmount } from 'vue'
import { OperationType } from './tools/layerHelper' import { OperationType } from './tools/layerHelper'
import { exportCanvasToImage } from './tools/exportMethod'
// 组件 // 组件
import layerPanel from './components/layer-panel/index.vue' import layerPanel from './components/layer-panel/index.vue'
import detailsPanel from './components/details-panel/index.vue' import detailsPanel from './components/details-panel/index.vue'
@@ -36,11 +42,7 @@
import { ToolManager } from './manager/ToolManager' import { ToolManager } from './manager/ToolManager'
import { KeyEventManager } from './manager/events/KeyEventManager' import { KeyEventManager } from './manager/events/KeyEventManager'
// 准备就绪 const emit = defineEmits(['close'])
const isReady = ref(false)
const canvasContainerRef = ref(null)
const canvasRef = ref(null)
const props = defineProps({ const props = defineProps({
config: { config: {
type: Object, type: Object,
@@ -48,6 +50,11 @@
} }
}) })
// 准备就绪
const isReady = ref(false)
const canvasContainerRef = ref(null)
const canvasRef = ref(null)
// 状态管理器 // 状态管理器
const stateManager = new StateManager({}) const stateManager = new StateManager({})
provide('stateManager', stateManager) provide('stateManager', stateManager)
@@ -136,10 +143,21 @@
provide('importLocalImage', importLocalImage) provide('importLocalImage', importLocalImage)
const exportCanvas = () => { const exportCanvas = () => {
// 导出图片
exportCanvasToImage(canvasManager.canvas).then((url) => {
const a = document.createElement('a')
a.href = url
a.download = 'canvas.png'
a.click()
})
}
// 导出到本地存储
const exportCanvasToLocalStorage = () => {
const json = canvasManager.getCanvasJSON() const json = canvasManager.getCanvasJSON()
localStorage.setItem('canvasJSON', json) localStorage.setItem('canvasJSON', json)
} }
const importCanvas = () => { // 从本地存储导入
const importCanvasFromLocalStorage = () => {
const json = localStorage.getItem('canvasJSON') const json = localStorage.getItem('canvasJSON')
if (!json) return if (!json) return
canvasManager.loadJSON(json, (success) => { canvasManager.loadJSON(json, (success) => {
@@ -151,6 +169,10 @@
stateManager.clearState(true) stateManager.clearState(true)
}) })
} }
// 导出画布并关闭
const exportCanvasAndClose = () => {
emit('close')
}
</script> </script>
<style lang="less"> <style lang="less">
@import '@vue-flow/core/dist/style.css'; @import '@vue-flow/core/dist/style.css';

View File

@@ -1,7 +1,7 @@
<template> <template>
<fullscreen-dialog v-model="dialogVisible" hide-destroy> <fullscreen-dialog v-model="dialogVisible" hide-destroy>
<div class="canvas-box"> <div class="canvas-box">
<depth-canvas :config="config" /> <depth-canvas :config="config" @close="close" />
</div> </div>
</fullscreen-dialog> </fullscreen-dialog>
</template> </template>

View File

@@ -70,7 +70,6 @@ export class CanvasManager {
this.setupCanvasEvents() this.setupCanvasEvents()
this.setupBrushEvents() this.setupBrushEvents()
this.stateManager.toolManager.setTool(OperationType.SELECT)
// 创建矩形 // 创建矩形
const rect = this.layerManager.createRectLayer({ const rect = this.layerManager.createRectLayer({
left: 400, left: 400,
@@ -86,6 +85,7 @@ export class CanvasManager {
this.layerManager.updateLayers() this.layerManager.updateLayers()
this.layerManager.setActiveID(text.info.id) this.layerManager.setActiveID(text.info.id)
this.stateManager.recordState() this.stateManager.recordState()
// this.stateManager.toolManager.setTool(OperationType.RECTANGLE)
} }
/** 画布添加对象 */ /** 画布添加对象 */
add(obj: any, isUpdate = true) { add(obj: any, isUpdate = true) {
@@ -95,6 +95,14 @@ export class CanvasManager {
this.renderAll() this.renderAll()
} }
} }
/** 画布移除对象 */
remove(obj: any, isUpdate = true) {
this.canvas.remove(obj)
if (isUpdate) {
this.layerManager.updateLayers()
this.renderAll()
}
}
/** 设置画布事件 */ /** 设置画布事件 */
setupCanvasEvents() { setupCanvasEvents() {
@@ -112,6 +120,7 @@ export class CanvasManager {
/** 设置激活对象 */ /** 设置激活对象 */
setActiveObjectByID(id: string) { setActiveObjectByID(id: string) {
const obj = this.getObjectById(id) const obj = this.getObjectById(id)
console.log(obj)
if (obj) this.canvas.setActiveObject(obj) if (obj) this.canvas.setActiveObject(obj)
this.renderAll() this.renderAll()
} }
@@ -122,7 +131,7 @@ export class CanvasManager {
return this.canvas.getObjects() || [] return this.canvas.getObjects() || []
} }
getObjectById(id: string) { getObjectById(id: string) {
return this.getObjects().find((item: any) => item.info.id === id) return this.getObjects().find((item: any) => item?.info?.id === id)
} }
renderAll() { renderAll() {
this.canvas.renderAll() this.canvas.renderAll()

View File

@@ -2,6 +2,7 @@ import { ref } from 'vue'
import { fabric } from 'fabric-with-all' import { fabric } from 'fabric-with-all'
import { createId } from '../../tools/tools' import { createId } from '../../tools/tools'
import { exportObjectsToImage } from '../tools/exportMethod' import { exportObjectsToImage } from '../tools/exportMethod'
import { OperationType } from '../tools/layerHelper'
export class LayerManager { export class LayerManager {
stateManager: any stateManager: any
@@ -18,6 +19,7 @@ export class LayerManager {
this.activeID.value = id this.activeID.value = id
if (isActive) { if (isActive) {
this.canvasManager.setActiveObjectByID(id) this.canvasManager.setActiveObjectByID(id)
this.stateManager.toolManager.setTool(OperationType.SELECT)
} }
} }
getLayerByID(id) { getLayerByID(id) {
@@ -114,7 +116,7 @@ export class LayerManager {
return textLayer return textLayer
} }
/** 创建矩形图层 */ /** 创建矩形图层 */
createRectLayer(options?: any) { createRectLayer(options?: any, isActive = false) {
const rectLayer = new fabric.Rect({ const rectLayer = new fabric.Rect({
width: 100, width: 100,
height: 100, height: 100,
@@ -128,10 +130,11 @@ export class LayerManager {
}) })
this.setLayerPosition(rectLayer, options) this.setLayerPosition(rectLayer, options)
this.canvasManager.add(rectLayer) this.canvasManager.add(rectLayer)
if (isActive) this.setActiveID(rectLayer.info.id)
return rectLayer return rectLayer
} }
/** 创建圆形图层 */ /** 创建圆形图层 */
createCircleLayer(options?: any) { createCircleLayer(options?: any, isActive = false) {
const circleLayer = new fabric.Circle({ const circleLayer = new fabric.Circle({
radius: 50, radius: 50,
fill: '#000', fill: '#000',
@@ -144,6 +147,7 @@ export class LayerManager {
}) })
this.setLayerPosition(circleLayer, options) this.setLayerPosition(circleLayer, options)
this.canvasManager.add(circleLayer) this.canvasManager.add(circleLayer)
if (isActive) this.setActiveID(circleLayer.info.id)
return circleLayer return circleLayer
} }
/** 创建图片图层 */ /** 创建图片图层 */
@@ -215,5 +219,5 @@ export class LayerManager {
}) })
}) })
} }
dispose() {} dispose() { }
} }

View File

@@ -0,0 +1,56 @@
import { fabric } from 'fabric-with-all'
/** 矩形工具管理器 */
export class RectToolManager {
// 管理器
canvasManager: any
stateManager: any
layerManager: any
isDragging: boolean = false
startX: number = 0
startY: number = 0
demoObject: any
constructor(options) {
this.canvasManager = options.canvasManager
this.stateManager = options.stateManager
this.layerManager = options.layerManager
}
mouseDownEvent(e) {
this.isDragging = true
this.startX = e.absolutePointer.x
this.startY = e.absolutePointer.y
const rect = new fabric.Rect({
left: this.startX,
top: this.startY,
width: 0,
height: 0,
fill: '#000',
evented: false,
})
this.demoObject = rect
this.canvasManager.canvas.add(rect)
this.canvasManager.canvas.renderAll()
}
mouseMoveEvent(e) {
if (!this.isDragging) return;
const width = e.absolutePointer.x - this.startX
const height = e.absolutePointer.y - this.startY
this.demoObject.set({ width, height })
this.canvasManager.canvas.renderAll()
}
mouseUpEvent(e) {
if (!this.isDragging) return;
this.isDragging = false;
var width = e.absolutePointer.x - this.startX
var height = e.absolutePointer.y - this.startY
if(width === 0) width = 50
if(height === 0) height = 50
this.demoObject.set({ width, height })
const object = this.demoObject.toJSON("evented")
this.layerManager.createRectLayer(object, true)
this.canvasManager.canvas.remove(this.demoObject)
this.canvasManager.canvas.renderAll()
}
dispose() { }
}

View File

@@ -1,5 +1,6 @@
import { isBoolean } from "lodash-es"; import { isBoolean } from "lodash-es";
import { OperationType, OperationTypes } from "../../tools/layerHelper"; import { OperationType, OperationTypes } from "../../tools/layerHelper";
import { RectToolManager } from "../RectToolManager"
export class CanvasEventManager { export class CanvasEventManager {
constructor(canvas, options = {}) { constructor(canvas, options = {}) {
@@ -22,6 +23,15 @@ export class CanvasEventManager {
this.longPressTimer = null; this.longPressTimer = null;
this.longPressThreshold = 500; this.longPressThreshold = 500;
const managers = {
canvasManager: this.canvasManager,
stateManager: this.stateManager,
toolManager: this.toolManager,
layerManager: this.layerManager,
}
this.rectToolManager = new RectToolManager(managers)
// 初始化所有事件 // 初始化所有事件
this.initEvents(); this.initEvents();
} }
@@ -188,51 +198,65 @@ export class CanvasEventManager {
// console.log("==========选择模式鼠标右击画布对象") // console.log("==========选择模式鼠标右击画布对象")
// } else // } else
if ( const currentTool = this.toolManager.currentTool.value;
opt.e.altKey || if (currentTool === OperationType.DRAW) {
opt.e.which === 2 || // 绘画模式
this.toolManager.currentTool.value === OperationType.PAN } else if (currentTool === OperationType.ERASER) {
) { // 橡皮擦模式
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
} else if (currentTool === OperationType.RECTANGLE) {
// 矩形模式
this.rectToolManager.mouseDownEvent(opt);
} else if (opt.e.altKey || opt.e.which === 2 || currentTool === OperationType.PAN) {
this.canvas.isDragging = true; this.canvas.isDragging = true;
this.canvas.lastPosX = opt.e.clientX; this.canvas.lastPosX = opt.e.clientX;
this.canvas.lastPosY = opt.e.clientY; this.canvas.lastPosY = opt.e.clientY;
this.canvas.defaultCursor = "grabbing"; // this.canvas.defaultCursor = "grabbing";
// 记录拖动开始时间和位置,用于计算速度 // 记录拖动开始时间和位置,用于计算速度
this.dragStartTime = Date.now(); this.dragStartTime = Date.now();
this.lastMousePositions = []; // 重置位置历史 this.lastMousePositions = []; // 重置位置历史
if (this.canvas.isDragging) { this.canvas.selection = false;
this.canvas.selection = false; this.canvas.renderAll();
this.canvas.renderAll();
}
} }
}); });
// 鼠标移动事件 // 鼠标移动事件
this.canvas.on("mouse:move", (opt) => { this.canvas.on("mouse:move", (opt) => {
if (!this.canvas.isDragging) return; const currentTool = this.toolManager.currentTool.value;
if (currentTool === OperationType.DRAW) {
// 绘画模式
} else if (currentTool === OperationType.ERASER) {
// 橡皮擦模式
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
} else if (currentTool === OperationType.RECTANGLE) {
// 矩形模式
this.rectToolManager.mouseMoveEvent(opt);
} else if (this.canvas.isDragging) {
const vpt = this.canvas.viewportTransform;
vpt[4] += opt.e.clientX - this.canvas.lastPosX;
vpt[5] += opt.e.clientY - this.canvas.lastPosY;
const vpt = this.canvas.viewportTransform; // 记录鼠标位置和时间,用于计算惯性
vpt[4] += opt.e.clientX - this.canvas.lastPosX; const now = Date.now();
vpt[5] += opt.e.clientY - this.canvas.lastPosY; this.lastMousePositions.push({
x: opt.e.clientX,
y: opt.e.clientY,
time: now,
});
// 记录鼠标位置和时间,用于计算惯性 // 保持历史记录在限定数量内
const now = Date.now(); if (this.lastMousePositions.length > this.positionHistoryLimit) {
this.lastMousePositions.push({ this.lastMousePositions.shift();
x: opt.e.clientX, }
y: opt.e.clientY, this.canvas.setViewportTransform(vpt);
time: now, this.canvas.renderAll();
}); this.canvas.lastPosX = opt.e.clientX;
this.canvas.lastPosY = opt.e.clientY;
// 保持历史记录在限定数量内
if (this.lastMousePositions.length > this.positionHistoryLimit) {
this.lastMousePositions.shift();
} }
this.canvas.setViewportTransform(vpt);
this.canvas.renderAll();
this.canvas.lastPosX = opt.e.clientX;
this.canvas.lastPosY = opt.e.clientY;
}); });
// 鼠标抬起事件 // 鼠标抬起事件
@@ -284,171 +308,197 @@ export class CanvasEventManager {
// 使用标准的mouse事件Fabric.js会自动处理触摸转换 // 使用标准的mouse事件Fabric.js会自动处理触摸转换
this.canvas.on("mouse:down", (opt) => { this.canvas.on("mouse:down", (opt) => {
// 只在PAN模式下处理触摸事件 // 只在PAN模式下处理触摸事件
if (this.toolManager.currentTool.value !== OperationType.PAN) { const currentTool = this.toolManager.currentTool.value;
return; if (currentTool === OperationType.DRAW) {
} // 绘画模式
} else if (currentTool === OperationType.ERASER) {
// 橡皮擦模式
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
} else if (currentTool === OperationType.RECTANGLE) {
// 矩形模式
this.rectToolManager.mouseDownEvent(opt);
} else if (currentTool === OperationType.PAN) {
// 平滑停止任何正在进行的惯性动画 // 平滑停止任何正在进行的惯性动画
this.stopInertiaAnimation(true); this.stopInertiaAnimation(true);
// 检查是否是触摸事件 // 检查是否是触摸事件
const isTouch = opt.e.type && opt.e.type.includes("touch"); const isTouch = opt.e.type && opt.e.type.includes("touch");
const touches = const touches =
opt.e.touches || (opt.e.originalEvent && opt.e.originalEvent.touches); opt.e.touches || (opt.e.originalEvent && opt.e.originalEvent.touches);
if (isTouch && touches && touches.length === 2) { if (isTouch && touches && touches.length === 2) {
// 双指触摸 - 用于缩放 // 双指触摸 - 用于缩放
this.touchState.isZooming = true; this.touchState.isZooming = true;
this.touchState.initialDistance = this.getTouchDistance( this.touchState.initialDistance = this.getTouchDistance(
touches[0], touches[0],
touches[1] touches[1]
); );
this.touchState.initialZoom = this.canvas.getZoom(); this.touchState.initialZoom = this.canvas.getZoom();
// 计算缩放中心点 // 计算缩放中心点
const centerX = (touches[0].clientX + touches[1].clientX) / 2; const centerX = (touches[0].clientX + touches[1].clientX) / 2;
const centerY = (touches[0].clientY + touches[1].clientY) / 2; const centerY = (touches[0].clientY + touches[1].clientY) / 2;
this.touchState.zoomCenter = { x: centerX, y: centerY }; this.touchState.zoomCenter = { x: centerX, y: centerY };
opt.e.preventDefault(); opt.e.preventDefault();
} else if (isTouch && touches && touches.length === 1) { } else if (isTouch && touches && touches.length === 1) {
// 单指触摸 - 用于拖拽 // 单指触摸 - 用于拖拽
this.canvas.isDragging = true; this.canvas.isDragging = true;
this.canvas.lastPosX = touches[0].clientX; this.canvas.lastPosX = touches[0].clientX;
this.canvas.lastPosY = touches[0].clientY; this.canvas.lastPosY = touches[0].clientY;
this.dragStartTime = Date.now(); this.dragStartTime = Date.now();
this.lastMousePositions = []; this.lastMousePositions = [];
this.canvas.selection = false; this.canvas.selection = false;
opt.e.preventDefault(); opt.e.preventDefault();
} else if (!isTouch) { } else if (!isTouch) {
// 鼠标事件 - 用于拖拽 // 鼠标事件 - 用于拖拽
this.canvas.isDragging = true; this.canvas.isDragging = true;
this.canvas.lastPosX = opt.e.clientX; this.canvas.lastPosX = opt.e.clientX;
this.canvas.lastPosY = opt.e.clientY; this.canvas.lastPosY = opt.e.clientY;
this.dragStartTime = Date.now(); this.dragStartTime = Date.now();
this.lastMousePositions = []; this.lastMousePositions = [];
this.canvas.selection = false; this.canvas.selection = false;
opt.e.preventDefault(); opt.e.preventDefault();
}
} }
}); });
// 触摸移动事件 - 优化性能 // 触摸移动事件 - 优化性能
this.canvas.on("mouse:move", (opt) => { this.canvas.on("mouse:move", (opt) => {
// 只在PAN模式下处理
if (this.toolManager.currentTool.value !== OperationType.PAN) {
return;
}
// 检查是否是触摸事件 const currentTool = this.toolManager.currentTool.value;
const isTouch = opt.e.type && opt.e.type.includes("touch"); if (currentTool === OperationType.DRAW) {
const touches = // 绘画模式
opt.e.touches || (opt.e.originalEvent && opt.e.originalEvent.touches); } else if (currentTool === OperationType.ERASER) {
// 橡皮擦模式
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
} else if (currentTool === OperationType.RECTANGLE) {
// 矩形模式
this.rectToolManager.mouseMoveEvent(opt);
} else if (currentTool === OperationType.PAN) {
if ( // 检查是否是触摸事件
isTouch && const isTouch = opt.e.type && opt.e.type.includes("touch");
touches && const touches =
touches.length === 2 && opt.e.touches || (opt.e.originalEvent && opt.e.originalEvent.touches);
this.touchState.isZooming
) {
// 双指缩放处理 - 修复抖动问题
const currentDistance = this.getTouchDistance(touches[0], touches[1]);
// 防止除零和异常值 if (
if (this.touchState.initialDistance === 0 || currentDistance === 0) { isTouch &&
touches &&
touches.length === 2 &&
this.touchState.isZooming
) {
// 双指缩放处理 - 修复抖动问题
const currentDistance = this.getTouchDistance(touches[0], touches[1]);
// 防止除零和异常值
if (this.touchState.initialDistance === 0 || currentDistance === 0) {
return;
}
const scale = currentDistance / this.touchState.initialDistance;
// 防止抖动:忽略微小的变化
if (Math.abs(scale - 1) < 0.01) {
return;
}
const newZoom = this.touchState.initialZoom * scale;
// 限制缩放范围
const clampedZoom = Math.max(0.1, Math.min(5, newZoom));
// 使用缩放中心点进行缩放
const point = new fabric.Point(
this.touchState.zoomCenter.x,
this.touchState.zoomCenter.y
);
this.canvas.zoomToPoint(point, clampedZoom);
opt.e.preventDefault();
return; return;
} }
const scale = currentDistance / this.touchState.initialDistance; if (!this.canvas.isDragging) return;
// 防止抖动:忽略微小的变化 let currentX, currentY;
if (Math.abs(scale - 1) < 0.01) {
if (isTouch && touches && touches.length === 1) {
// 单指触摸移动
currentX = touches[0].clientX;
currentY = touches[0].clientY;
} else if (!isTouch) {
// 鼠标移动
currentX = opt.e.clientX;
currentY = opt.e.clientY;
} else {
return; // 忽略其他情况
}
// 优化减少频繁的DOM操作
const deltaX = currentX - this.canvas.lastPosX;
const deltaY = currentY - this.canvas.lastPosY;
// 只有移动距离足够大时才更新
if (Math.abs(deltaX) < 1 && Math.abs(deltaY) < 1) {
return; return;
} }
const newZoom = this.touchState.initialZoom * scale; const vpt = this.canvas.viewportTransform;
vpt[4] += deltaX;
vpt[5] += deltaY;
// 限制缩放范围 // 优化:减少历史记录频率
const clampedZoom = Math.max(0.1, Math.min(5, newZoom)); const now = Date.now();
if (now - this.touchState.lastTouchTime > 16) {
// 约60fps
this.lastMousePositions.push({
x: currentX,
y: currentY,
time: now,
});
// 使用缩放中心点进行缩放 if (this.lastMousePositions.length > this.positionHistoryLimit) {
const point = new fabric.Point( this.lastMousePositions.shift();
this.touchState.zoomCenter.x, }
this.touchState.zoomCenter.y
);
this.canvas.zoomToPoint(point, clampedZoom); this.touchState.lastTouchTime = now;
}
this.canvas.setViewportTransform(vpt);
this.canvas.requestRenderAll(); // 使用requestRenderAll代替renderAll
this.canvas.lastPosX = currentX;
this.canvas.lastPosY = currentY;
opt.e.preventDefault(); opt.e.preventDefault();
return;
} }
if (!this.canvas.isDragging) return;
let currentX, currentY;
if (isTouch && touches && touches.length === 1) {
// 单指触摸移动
currentX = touches[0].clientX;
currentY = touches[0].clientY;
} else if (!isTouch) {
// 鼠标移动
currentX = opt.e.clientX;
currentY = opt.e.clientY;
} else {
return; // 忽略其他情况
}
// 优化减少频繁的DOM操作
const deltaX = currentX - this.canvas.lastPosX;
const deltaY = currentY - this.canvas.lastPosY;
// 只有移动距离足够大时才更新
if (Math.abs(deltaX) < 1 && Math.abs(deltaY) < 1) {
return;
}
const vpt = this.canvas.viewportTransform;
vpt[4] += deltaX;
vpt[5] += deltaY;
// 优化:减少历史记录频率
const now = Date.now();
if (now - this.touchState.lastTouchTime > 16) {
// 约60fps
this.lastMousePositions.push({
x: currentX,
y: currentY,
time: now,
});
if (this.lastMousePositions.length > this.positionHistoryLimit) {
this.lastMousePositions.shift();
}
this.touchState.lastTouchTime = now;
}
this.canvas.setViewportTransform(vpt);
this.canvas.requestRenderAll(); // 使用requestRenderAll代替renderAll
this.canvas.lastPosX = currentX;
this.canvas.lastPosY = currentY;
opt.e.preventDefault();
}); });
// 触摸结束事件 // 触摸结束事件
this.canvas.on("mouse:up", (opt) => { this.canvas.on("mouse:up", (opt) => {
// 只在PAN模式下处理 const currentTool = this.toolManager.currentTool.value;
if (this.toolManager.currentTool.value !== OperationType.PAN) { if (currentTool === OperationType.DRAW) {
return; // 绘画模式
} else if (currentTool === OperationType.ERASER) {
// 橡皮擦模式
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
} else if (currentTool === OperationType.RECTANGLE) {
// 矩形模式
this.rectToolManager.mouseUpEvent(opt);
} else if (currentTool === OperationType.PAN) {
// 重置触摸状态
this.touchState.isZooming = false;
this.touchState.initialDistance = 0;
this.handleDragEnd(opt, true);
} }
// 重置触摸状态
this.touchState.isZooming = false;
this.touchState.initialDistance = 0;
this.handleDragEnd(opt, true);
}); });
// 添加原生触摸事件监听器作为备用方案 // 添加原生触摸事件监听器作为备用方案
@@ -602,24 +652,28 @@ export class CanvasEventManager {
* 处理拖动结束(鼠标抬起或触摸结束) * 处理拖动结束(鼠标抬起或触摸结束)
*/ */
handleDragEnd(opt, isTouch = false) { handleDragEnd(opt, isTouch = false) {
if (this.canvas.isDragging) { const currentTool = this.toolManager.currentTool.value;
// 使用动画管理器处理惯性效果 if (currentTool === OperationType.DRAW) {
// 绘画模式
} else if (currentTool === OperationType.ERASER) {
// 橡皮擦模式
} else if (currentTool === OperationType.SELECTBOX) {
// 选择框模式
} else if (currentTool === OperationType.RECTANGLE) {
// 矩形模式
this.rectToolManager.mouseUpEvent(opt);
} else if (this.canvas.isDragging) {
// if (this.lastMousePositions.length > 1 && opt && opt.e) { // if (this.lastMousePositions.length > 1 && opt && opt.e) {
// this.animationManager.applyInertiaEffect( // this.animationManager.applyInertiaEffect(
// this.lastMousePositions, // this.lastMousePositions,
// isTouch // isTouch
// ); // );
// } // }
this.canvas.isDragging = false;
const vpt = this.canvas.viewportTransform;
this.canvas.setViewportTransform(vpt);
this.canvas.renderAll();
} }
this.canvas.isDragging = false;
if (this.toolManager) {
// this.toolManager.restoreSelectionState(); // 恢复选择状态
}
const vpt = this.canvas.viewportTransform;
this.canvas.setViewportTransform(vpt);
this.canvas.renderAll();
} }
setupSelectionEvents() { setupSelectionEvents() {
@@ -827,30 +881,6 @@ export class CanvasEventManager {
} }
} }
dispose() {
// 移除所有事件监听
this.canvas.off();
// 清理 Mac 专用的原生事件监听器
if (this.deviceInfo.isMac && this.canvas.upperCanvasEl) {
const upperCanvas = this.canvas.upperCanvasEl;
// 移除手势事件监听器
upperCanvas.removeEventListener("gesturestart", null);
upperCanvas.removeEventListener("gesturechange", null);
upperCanvas.removeEventListener("gestureend", null);
upperCanvas.removeEventListener("wheel", null);
}
// 清除计时器
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
// 停止所有动画
this.stopInertiaAnimation();
}
/** /**
* 精确检测设备类型,区分 PC、Mac、平板和移动设备 * 精确检测设备类型,区分 PC、Mac、平板和移动设备
@@ -1032,4 +1062,30 @@ export class CanvasEventManager {
{ passive: true } { passive: true }
); );
} }
dispose() {
this.rectToolManager?.dispose()
// 移除所有事件监听
this.canvas.off();
// 清理 Mac 专用的原生事件监听器
if (this.deviceInfo.isMac && this.canvas.upperCanvasEl) {
const upperCanvas = this.canvas.upperCanvasEl;
// 移除手势事件监听器
upperCanvas.removeEventListener("gesturestart", null);
upperCanvas.removeEventListener("gesturechange", null);
upperCanvas.removeEventListener("gestureend", null);
upperCanvas.removeEventListener("wheel", null);
}
// 清除计时器
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
// 停止所有动画
this.stopInertiaAnimation();
}
} }

View File

@@ -1,5 +1,21 @@
import { createStaticCanvas } from './canvasFactory' import { createStaticCanvas } from './canvasFactory'
import { getObjectsBoundingBox, cloneObjects } from './canvasMethod' import { getObjectsBoundingBox, cloneObjects } from './canvasMethod'
/** 导出画布为图片 */
export async function exportCanvasToImage(canvas) {
const clonedObjects = await cloneObjects(canvas.getObjects())
const staticCanvas = createStaticCanvas(document.createElement('canvas'))
const width = canvas.clipPath.width
const height = canvas.clipPath.height
staticCanvas.setWidth(width)
staticCanvas.setHeight(height)
clonedObjects.forEach(obj => staticCanvas.add(obj))
// 导出图片
const dataURL = staticCanvas.toDataURL({
type: 'image/png',
quality: 1,
})
return dataURL
}
/** 导出指定对象 */ /** 导出指定对象 */
export async function exportObjectsToImage(objects = [], isDetails = false) { export async function exportObjectsToImage(objects = [], isDetails = false) {
const clonedObjects = await cloneObjects(objects) const clonedObjects = await cloneObjects(objects)