深度画布
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, computed } from 'vue'
|
||||
import { TOOLS } from '../manager/ToolManager'
|
||||
import { OperationType } from '../tools/layerHelper'
|
||||
const props = defineProps({
|
||||
zoom: { default: 1, type: Number },
|
||||
step: { default: 0.1, type: Number }
|
||||
@@ -32,29 +32,29 @@
|
||||
const emit = defineEmits(['export', 'import'])
|
||||
const stateManager = inject('stateManager') as any
|
||||
const toolManager = inject('toolManager') as any
|
||||
const tool = computed(() => stateManager.tool.value)
|
||||
const tool = computed(() => toolManager.currentTool.value)
|
||||
const historyIndex = computed(() => stateManager.historyIndex.value)
|
||||
const historyList = computed(() => stateManager.historyList.value)
|
||||
const isUndo = computed(() => !historyList.value[historyIndex.value - 1])
|
||||
const isRedo = computed(() => !historyList.value[historyIndex.value + 1])
|
||||
const tools = ref([
|
||||
{ name: TOOLS.SELECT, icon: 'dc-select', iconSize: 16, disabled: ref(false) },
|
||||
{ name: TOOLS.MOVE, icon: 'dc-move', iconSize: 18, disabled: ref(false) },
|
||||
{ name: TOOLS.BRUSH, icon: 'dc-brush', iconSize: 18, disabled: ref(false) },
|
||||
{ name: TOOLS.ERASER, icon: 'dc-eraser', iconSize: 18, disabled: ref(false) },
|
||||
{ name: TOOLS.IMAGE, icon: 'dc-image', iconSize: 17, disabled: ref(false) },
|
||||
{ name: TOOLS.SELECTBOX, icon: 'dc-selectbox', iconSize: 16, disabled: ref(false) },
|
||||
{ name: TOOLS.RECTANGLE, icon: 'dc-rectangle', iconSize: 16, disabled: ref(false) },
|
||||
{ name: OperationType.SELECT, icon: 'dc-select', iconSize: 16, disabled: ref(false) },
|
||||
{ name: OperationType.PAN, icon: 'dc-move', iconSize: 18, disabled: ref(false) },
|
||||
{ name: OperationType.DRAW, icon: 'dc-brush', iconSize: 18, disabled: ref(false) },
|
||||
{ name: OperationType.ERASER, icon: 'dc-eraser', iconSize: 18, disabled: ref(false) },
|
||||
{ name: OperationType.IMAGE, icon: 'dc-image', iconSize: 17, disabled: ref(false) },
|
||||
{ name: OperationType.SELECTBOX, icon: 'dc-selectbox', iconSize: 16, disabled: ref(false) },
|
||||
{ name: OperationType.RECTANGLE, icon: 'dc-rectangle', iconSize: 16, disabled: ref(false) },
|
||||
{ type: 'line' },
|
||||
{
|
||||
name: TOOLS.UNDO,
|
||||
name: OperationType.UNDO,
|
||||
icon: 'dc-undo',
|
||||
iconSize: 18,
|
||||
disabled: isUndo,
|
||||
on: () => stateManager.undoState()
|
||||
},
|
||||
{
|
||||
name: TOOLS.REDO,
|
||||
name: OperationType.REDO,
|
||||
icon: 'dc-redo',
|
||||
iconSize: 18,
|
||||
disabled: isRedo,
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
<layer-panel />
|
||||
<details-panel />
|
||||
<header-tools />
|
||||
<zoom :zoom="1" :step="0.1" is-home />
|
||||
<zoom
|
||||
:zoom="canvasManager.currentZoom.value / 100"
|
||||
:step="0.1"
|
||||
is-home
|
||||
@home="() => canvasManager.resetZoom()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -23,9 +28,8 @@
|
||||
// 管理器
|
||||
import { StateManager } from './manager/StateManager'
|
||||
import { EventManager } from './manager/EventManager'
|
||||
import { FlowManager } from './manager/FlowManager'
|
||||
import { NodeManager } from './manager/NodeManager'
|
||||
import { ToolManager, TOOLS } from './manager/ToolManager'
|
||||
import { CanvasManager } from './manager/CanvasManager'
|
||||
import { ToolManager } from './manager/ToolManager'
|
||||
|
||||
const canvasContainerRef = ref(null)
|
||||
const canvasRef = ref(null)
|
||||
@@ -41,77 +45,51 @@
|
||||
const stateManager = new StateManager({})
|
||||
provide('stateManager', stateManager)
|
||||
|
||||
// 画布管理器
|
||||
const canvasManager = new CanvasManager({ stateManager })
|
||||
stateManager.setManager({ canvasManager, canvasRef })
|
||||
provide('canvasManager', canvasManager)
|
||||
|
||||
// 事件管理器
|
||||
const eventManager = new EventManager({ stateManager })
|
||||
stateManager.setManager({ eventManager })
|
||||
provide('eventManager', eventManager)
|
||||
|
||||
// 流程管理器
|
||||
const flowManager = new FlowManager({ stateManager })
|
||||
stateManager.setManager({ flowManager })
|
||||
provide('flowManager', flowManager)
|
||||
|
||||
// 节点管理器
|
||||
const nodeManager = new NodeManager({ stateManager })
|
||||
stateManager.setManager({ nodeManager })
|
||||
provide('nodeManager', nodeManager)
|
||||
|
||||
// 工具管理器
|
||||
const toolManager = new ToolManager({ stateManager })
|
||||
const toolManager = new ToolManager({ stateManager, canvasManager })
|
||||
stateManager.setManager({ toolManager })
|
||||
provide('toolManager', toolManager)
|
||||
const initCanvas = () => {
|
||||
console.log('OverallCanvas: initCanvas')
|
||||
const canvasViewWidth = canvasContainerRef.value.clientWidth
|
||||
const canvasViewHeight = canvasContainerRef.value.clientHeight
|
||||
const canvasWidth = 750
|
||||
const canvasHeight = 600
|
||||
const canvas = new fabric.Canvas(canvasRef.value, {
|
||||
selection: true,
|
||||
width: canvasViewWidth,
|
||||
height: canvasViewHeight,
|
||||
imageSmoothingEnabled: true, // 启用图像平滑 - 抗锯齿
|
||||
imageSmoothingQuality: 'high', // 设置高质量图像平滑
|
||||
preserveObjectStacking: true,
|
||||
enableRetinaScaling: true,
|
||||
stopContextMenu: true,
|
||||
fireRightClick: true,
|
||||
backgroundColor: '#fff'
|
||||
})
|
||||
canvas.clipPath = new fabric.Rect({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: canvasWidth,
|
||||
height: canvasHeight
|
||||
})
|
||||
// 画布居中
|
||||
const canvasX = canvasViewWidth / 2 - canvasWidth / 2
|
||||
const canvasY = canvasViewHeight / 2 - canvasHeight / 2
|
||||
canvas.viewportTransform = [1, 0, 0, 1, canvasX, canvasY]
|
||||
// 创建矩形
|
||||
const rect = new fabric.Rect({
|
||||
left: 20,
|
||||
top: 20,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: '#f00'
|
||||
})
|
||||
canvas.add(rect)
|
||||
//创建圆形
|
||||
const circle = new fabric.Circle({
|
||||
left: 200,
|
||||
top: 200,
|
||||
radius: 50,
|
||||
fill: '#0f0'
|
||||
})
|
||||
canvas.add(circle)
|
||||
}
|
||||
|
||||
const observer = ref(null)
|
||||
onMounted(() => {
|
||||
initCanvas()
|
||||
canvasManager.initCanvas({
|
||||
canvasRef,
|
||||
canvasViewWidth: canvasContainerRef.value.clientWidth,
|
||||
canvasViewHeight: canvasContainerRef.value.clientHeight,
|
||||
canvasWidth: 750,
|
||||
canvasHeight: 600
|
||||
})
|
||||
|
||||
const trailingTimeout = ref(null)
|
||||
observer.value = new ResizeObserver((entries) => {
|
||||
clearTimeout(trailingTimeout.value)
|
||||
trailingTimeout.value = setTimeout(async () => {
|
||||
handleWindowResize()
|
||||
}, 100)
|
||||
})
|
||||
observer.value.observe(canvasContainerRef.value)
|
||||
})
|
||||
onBeforeMount(() => {
|
||||
// eventManager.removeEvents() // 移除事件
|
||||
})
|
||||
async function handleWindowResize() {
|
||||
console.log('==========画布窗口大小变化==========')
|
||||
canvasManager.setCanvasViewSize({
|
||||
canvasViewWidth: canvasContainerRef.value.clientWidth,
|
||||
canvasViewHeight: canvasContainerRef.value.clientHeight
|
||||
})
|
||||
canvasManager.resetZoom()
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import '@vue-flow/core/dist/style.css';
|
||||
|
||||
850
src/components/Canvas/DepthCanvas/manager/AnimationManager.js
Normal file
850
src/components/Canvas/DepthCanvas/manager/AnimationManager.js
Normal file
@@ -0,0 +1,850 @@
|
||||
import { gsap } from "gsap";
|
||||
|
||||
/**
|
||||
* 画布动画管理器
|
||||
* 负责处理画布平移、缩放等动画效果
|
||||
*/
|
||||
export class AnimationManager {
|
||||
/**
|
||||
* 创建动画管理器
|
||||
* @param {fabric.Canvas} canvas fabric.js画布实例
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
constructor(canvas, options = {}) {
|
||||
this.canvasManager = options.canvasManager;
|
||||
this.canvas = canvas;
|
||||
this.currentZoom = options.currentZoom || { value: 100 };
|
||||
|
||||
// 动画相关属性
|
||||
this._zoomAnimation = null;
|
||||
this._panAnimation = null;
|
||||
this._lastWheelTime = 0;
|
||||
this._lastWheelProcessTime = 0; // 上次处理wheel事件的时间
|
||||
this._wheelEvents = [];
|
||||
|
||||
// 检测设备类型,Mac设备使用更短的节流时间确保响应性
|
||||
this._isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
||||
this._wheelThrottleTime = this._isMac
|
||||
? options.wheelThrottleTime || 8 // Mac设备使用更短的节流时间
|
||||
: options.wheelThrottleTime || 30;
|
||||
|
||||
this._accumulatedWheelDelta = 0; // 累积滚轮增量
|
||||
this._wheelAccumulationTimeout = null; // 滚轮累积超时
|
||||
|
||||
// Mac设备使用更短的累积时间窗口,确保及时响应
|
||||
this._wheelAccumulationTime = this._isMac ? 60 : 120; // 滚轮累积时间窗口(毫秒)
|
||||
|
||||
// 添加新的状态跟踪变量
|
||||
this._wasPanning = false; // 是否有平移动画正在进行
|
||||
this._wasZooming = false; // 是否有缩放动画正在进行
|
||||
this._combinedAnimation = null; // 组合动画引用
|
||||
|
||||
// Mac特有的动画优化变量 - 使用最小防抖机制
|
||||
if (this._isMac) {
|
||||
this._lastMacAnimationTime = 0; // 上次Mac动画时间
|
||||
this._macAnimationCooldown = 2; // 最小的动画冷却时间,确保最大响应性
|
||||
}
|
||||
|
||||
// 初始化GSAP默认配置
|
||||
gsap.defaults({
|
||||
ease: options.defaultEase || (this._isMac ? "power2.out" : "power2.out"), // Mac使用简单高效的缓动
|
||||
duration: options.defaultDuration || (this._isMac ? 0.3 : 0.3), // Mac使用标准持续时间
|
||||
overwrite: "auto", // 自动覆盖同一对象上的动画
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 GSAP 实现平滑缩放动画
|
||||
* @param {Object} point 缩放中心点 {x, y}
|
||||
* @param {Number} targetZoom 目标缩放值
|
||||
* @param {Object} options 动画选项
|
||||
*/
|
||||
animateZoom(point, targetZoom, options = {}) {
|
||||
if (!this.canvas) return;
|
||||
|
||||
// 限制缩放范围
|
||||
targetZoom = Math.min(Math.max(targetZoom, 0.1), 20);
|
||||
|
||||
// 当前缩放值
|
||||
const currentZoom = this.canvas.getZoom();
|
||||
|
||||
// 如果变化太小,直接应用缩放
|
||||
if (Math.abs(targetZoom - currentZoom) < 0.01) {
|
||||
this._applyZoom(point, targetZoom);
|
||||
return;
|
||||
}
|
||||
|
||||
// 停止任何进行中的缩放动画
|
||||
if (this._zoomAnimation) {
|
||||
// 不是直接 kill,而是获取当前进度值作为新的起点
|
||||
const currentProgress = this._zoomAnimation.progress();
|
||||
const currentZoomValue = this._zoomAnimation.targets()[0].value;
|
||||
this._zoomAnimation.kill();
|
||||
this._zoomAnimation = null;
|
||||
|
||||
// 从当前过渡中的值开始新动画,而不是从最初的值
|
||||
const zoomObj = { value: currentZoomValue };
|
||||
const currentVpt = [...this.canvas.viewportTransform];
|
||||
|
||||
// 计算过渡动画持续时间 - 根据当前值到目标值的距离比例
|
||||
const progressRatio =
|
||||
Math.abs(targetZoom - currentZoomValue) / Math.abs(targetZoom - currentZoom);
|
||||
const duration = options.duration || 0.3 * progressRatio;
|
||||
|
||||
// 计算缩放后目标位置需要的修正,保持缩放点不变
|
||||
const animOptions = {
|
||||
value: targetZoom,
|
||||
duration: duration,
|
||||
ease: options.ease || "power2.out",
|
||||
onUpdate: () => {
|
||||
// 更新缩放值显示
|
||||
this.currentZoom.value = Math.round(zoomObj.value * 100);
|
||||
|
||||
// 计算过渡中的变换矩阵
|
||||
const zoom = zoomObj.value;
|
||||
const scale = zoom / currentZoomValue;
|
||||
const currentScaleFactor = scale;
|
||||
|
||||
// 应用变换
|
||||
const vpt = this.canvas.viewportTransform;
|
||||
vpt[0] = currentVpt[0] * scale;
|
||||
vpt[3] = currentVpt[3] * scale;
|
||||
|
||||
// 应用平移修正以保持缩放点
|
||||
const adjustX = (1 - currentScaleFactor) * point.x;
|
||||
const adjustY = (1 - currentScaleFactor) * point.y;
|
||||
vpt[4] = currentVpt[4] * scale + adjustX;
|
||||
vpt[5] = currentVpt[5] * scale + adjustY;
|
||||
|
||||
this.canvas.renderAll();
|
||||
},
|
||||
onComplete: () => {
|
||||
this._zoomAnimation = null;
|
||||
|
||||
// 确保最终状态准确
|
||||
this._applyZoom(point, targetZoom, true);
|
||||
},
|
||||
};
|
||||
|
||||
// 启动 GSAP 动画
|
||||
this._zoomAnimation = gsap.to(zoomObj, animOptions);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果没有正在进行的动画,创建新的缩放动画
|
||||
const zoomObj = { value: currentZoom };
|
||||
const currentVpt = [...this.canvas.viewportTransform];
|
||||
|
||||
// 计算缩放后目标位置需要的修正,保持缩放点不变
|
||||
const scaleFactor = targetZoom / currentZoom;
|
||||
const invertedScaleFactor = 1 / scaleFactor;
|
||||
|
||||
// 这个数学公式确保缩放点在屏幕上的位置保持不变
|
||||
const dx = point.x - point.x * invertedScaleFactor;
|
||||
const dy = point.y - point.y * invertedScaleFactor;
|
||||
|
||||
// 创建动画配置
|
||||
const animOptions = {
|
||||
value: targetZoom,
|
||||
duration: options.duration || 0.3,
|
||||
ease: options.ease || (this._isMac ? "expo.out" : "power2.out"), // Mac使用更平滑的缓动
|
||||
onUpdate: () => {
|
||||
// 更新缩放值显示
|
||||
this.currentZoom.value = Math.round(zoomObj.value * 100);
|
||||
|
||||
// 计算过渡中的变换矩阵
|
||||
const zoom = zoomObj.value;
|
||||
const scale = zoom / currentZoom;
|
||||
const currentScaleFactor = scale;
|
||||
|
||||
// 应用变换
|
||||
const vpt = this.canvas.viewportTransform;
|
||||
vpt[0] = currentVpt[0] * scale;
|
||||
vpt[3] = currentVpt[3] * scale;
|
||||
|
||||
// 应用平移修正以保持缩放点
|
||||
const adjustX = (1 - currentScaleFactor) * point.x;
|
||||
const adjustY = (1 - currentScaleFactor) * point.y;
|
||||
vpt[4] = currentVpt[4] * scale + adjustX;
|
||||
vpt[5] = currentVpt[5] * scale + adjustY;
|
||||
|
||||
this.canvas.renderAll();
|
||||
},
|
||||
onComplete: () => {
|
||||
this._zoomAnimation = null;
|
||||
|
||||
// 确保最终状态准确
|
||||
this._applyZoom(point, targetZoom, true);
|
||||
},
|
||||
};
|
||||
|
||||
// 启动 GSAP 动画
|
||||
this._zoomAnimation = gsap.to(zoomObj, animOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用缩放(内部使用)
|
||||
* @private
|
||||
*/
|
||||
_applyZoom(point, zoom, skipUpdate = false) {
|
||||
if (!skipUpdate) {
|
||||
this.currentZoom.value = Math.round(zoom * 100);
|
||||
}
|
||||
this.canvas.zoomToPoint(point, zoom);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 GSAP 实现平滑平移动画
|
||||
* @param {Object} targetPosition 目标位置 {x, y}
|
||||
* @param {Object} options 动画选项
|
||||
*/
|
||||
animatePan(targetPosition, options = {}) {
|
||||
if (!this.canvas) return;
|
||||
|
||||
// 停止任何进行中的平移动画
|
||||
if (this._panAnimation) {
|
||||
this._panAnimation.kill();
|
||||
}
|
||||
|
||||
const currentVpt = [...this.canvas.viewportTransform];
|
||||
const position = {
|
||||
x: -currentVpt[4],
|
||||
y: -currentVpt[5],
|
||||
};
|
||||
|
||||
// 计算平移距离
|
||||
const dx = targetPosition.x - position.x;
|
||||
const dy = targetPosition.y - position.y;
|
||||
|
||||
// 如果距离太小,直接应用平移
|
||||
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) {
|
||||
this._applyPan(targetPosition.x, targetPosition.y);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建动画配置
|
||||
const animOptions = {
|
||||
x: targetPosition.x,
|
||||
y: targetPosition.y,
|
||||
duration: options.duration || 0.3,
|
||||
ease: options.ease || (this._isMac ? "circ.out" : "power2.out"), // Mac使用更柔和的缓动
|
||||
onUpdate: () => {
|
||||
this._applyPan(position.x, position.y);
|
||||
},
|
||||
onComplete: () => {
|
||||
this._panAnimation = null;
|
||||
// 确保最终位置准确
|
||||
this._applyPan(targetPosition.x, targetPosition.y);
|
||||
},
|
||||
};
|
||||
|
||||
// 启动 GSAP 动画
|
||||
this._panAnimation = gsap.to(position, animOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用平移(内部使用)
|
||||
* @private
|
||||
*/
|
||||
_applyPan(x, y) {
|
||||
if (!this.canvas) return;
|
||||
|
||||
const vpt = this.canvas.viewportTransform;
|
||||
vpt[4] = -x;
|
||||
vpt[5] = -y;
|
||||
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用动画平移到指定元素
|
||||
* @param {Object} elementId 元素ID
|
||||
*/
|
||||
panToElement(elementId) {
|
||||
if (!this.canvas) return;
|
||||
|
||||
const obj = this.canvas.getObjects().find((obj) => obj.id === elementId);
|
||||
if (!obj) return;
|
||||
|
||||
const zoom = this.canvas.getZoom();
|
||||
const center = obj.getCenterPoint();
|
||||
|
||||
// 计算目标中心位置
|
||||
const targetX = center.x * zoom - this.canvas.width / 2;
|
||||
const targetY = center.y * zoom - this.canvas.height / 2;
|
||||
|
||||
// 动画平移
|
||||
this.animatePan(
|
||||
{ x: targetX, y: targetY },
|
||||
{
|
||||
duration: 0.6,
|
||||
ease: this._isMac ? "back.out(0.3)" : "power3.out", // Mac使用轻微回弹效果
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置缩放(带平滑动画)
|
||||
* @param {Boolean} animated 是否使用动画
|
||||
*/
|
||||
async resetZoom(animated = true) {
|
||||
const canvasViewWidth = this.canvasManager.canvasViewWidth;
|
||||
const canvasViewHeight = this.canvasManager.canvasViewHeight;
|
||||
const canvasWidth = this.canvasManager.canvasWidth;
|
||||
const canvasHeight = this.canvasManager.canvasHeight;
|
||||
const panX = canvasViewWidth / 2 - canvasWidth / 2
|
||||
const panY = canvasViewHeight / 2 - canvasHeight / 2
|
||||
return new Promise((resolve) => {
|
||||
if (animated) {
|
||||
// 停止任何进行中的动画
|
||||
if (this._zoomAnimation) {
|
||||
this._zoomAnimation.kill();
|
||||
}
|
||||
if (this._panAnimation) {
|
||||
this._panAnimation.kill();
|
||||
}
|
||||
|
||||
const center = {
|
||||
x: this.canvas.width / 2,
|
||||
y: this.canvas.height / 2,
|
||||
};
|
||||
|
||||
// 获取当前变换矩阵
|
||||
const currentVpt = [...this.canvas.viewportTransform];
|
||||
const currentZoom = this.canvas.getZoom();
|
||||
|
||||
// 创建一个对象来动画整个视图变换
|
||||
const viewTransform = {
|
||||
zoom: currentZoom,
|
||||
panX: currentVpt[4],
|
||||
panY: currentVpt[5],
|
||||
};
|
||||
|
||||
// 使用GSAP同时动画缩放和平移
|
||||
gsap.to(viewTransform, {
|
||||
zoom: 1,
|
||||
panX: panX,
|
||||
panY: panY,
|
||||
duration: 0.5,
|
||||
ease: this._isMac ? "back.out(0.2)" : "power3.out", // Mac使用轻微回弹效果
|
||||
onUpdate: () => {
|
||||
// 更新缩放显示值
|
||||
this.currentZoom.value = Math.round(viewTransform.zoom * 100);
|
||||
|
||||
// 应用新的变换
|
||||
const vpt = this.canvas.viewportTransform;
|
||||
vpt[0] = viewTransform.zoom;
|
||||
vpt[3] = viewTransform.zoom;
|
||||
vpt[4] = viewTransform.panX;
|
||||
vpt[5] = viewTransform.panY;
|
||||
|
||||
this.canvas.renderAll();
|
||||
},
|
||||
onComplete: () => {
|
||||
// 确保最终状态准确
|
||||
this.canvas.setViewportTransform([1, 0, 0, 1, panX, panY]);
|
||||
this.currentZoom.value = 100;
|
||||
this._zoomAnimation = null;
|
||||
this._panAnimation = null;
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.canvas.setViewportTransform([1, 0, 0, 1, panX, panY]);
|
||||
this.currentZoom.value = 100;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标滚轮缩放
|
||||
* @param {Object} opt 事件对象
|
||||
*/
|
||||
handleMouseWheel(opt) {
|
||||
const now = Date.now();
|
||||
let delta = opt.e.deltaY;
|
||||
|
||||
// 记录事件用于计算速度和惯性
|
||||
this._wheelEvents.push({
|
||||
delta: delta,
|
||||
point: { x: opt.e.offsetX, y: opt.e.offsetY },
|
||||
time: now,
|
||||
hasPanAnimation: this._wasPanning,
|
||||
hasZoomAnimation: this._wasZooming,
|
||||
});
|
||||
|
||||
// 保留最近的事件记录
|
||||
if (this._wheelEvents.length > 10) {
|
||||
this._wheelEvents.shift();
|
||||
}
|
||||
|
||||
// 检查是否是第一个事件或者距离上次处理已经过了足够时间
|
||||
const isFirstEvent = !this._wheelAccumulationTimeout;
|
||||
const timeSinceLastProcess = now - (this._lastWheelProcessTime || 0);
|
||||
|
||||
if (isFirstEvent || timeSinceLastProcess > this._wheelAccumulationTime) {
|
||||
// 立即处理第一个事件或长时间没有处理的事件,确保响应性
|
||||
this._processAccumulatedWheel(opt);
|
||||
this._lastWheelProcessTime = now;
|
||||
|
||||
// 清理之前的累积
|
||||
this._accumulatedWheelDelta = 0;
|
||||
|
||||
// 如果有pending的timeout,清除它
|
||||
if (this._wheelAccumulationTimeout) {
|
||||
clearTimeout(this._wheelAccumulationTimeout);
|
||||
this._wheelAccumulationTimeout = null;
|
||||
}
|
||||
} else {
|
||||
// 累积后续事件
|
||||
this._accumulatedWheelDelta += delta;
|
||||
|
||||
// 如果正在累积中,清除之前的定时器
|
||||
if (this._wheelAccumulationTimeout) {
|
||||
clearTimeout(this._wheelAccumulationTimeout);
|
||||
}
|
||||
|
||||
// 设置新的定时器,处理累积的事件
|
||||
this._wheelAccumulationTimeout = setTimeout(() => {
|
||||
this._processAccumulatedWheel(opt);
|
||||
this._lastWheelProcessTime = Date.now();
|
||||
|
||||
// 清理
|
||||
this._accumulatedWheelDelta = 0;
|
||||
this._wheelAccumulationTimeout = null;
|
||||
}, this._wheelThrottleTime);
|
||||
}
|
||||
|
||||
opt.e.preventDefault();
|
||||
opt.e.stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理累积的滚轮事件并应用缩放
|
||||
* @private
|
||||
* @param {Object} lastOpt 最后一个滚轮事件
|
||||
*/
|
||||
_processAccumulatedWheel(lastOpt) {
|
||||
if (!this._wheelEvents.length) return;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// Mac设备的轻量防抖检查 - 进一步减少冷却时间,确保响应性
|
||||
if (this._isMac && now - this._lastMacAnimationTime < this._macAnimationCooldown) {
|
||||
// 如果距离上次动画时间太短,只延迟很短时间,不阻塞太久
|
||||
if (this._wheelAccumulationTimeout) {
|
||||
clearTimeout(this._wheelAccumulationTimeout);
|
||||
}
|
||||
this._wheelAccumulationTimeout = setTimeout(
|
||||
() => {
|
||||
this._processAccumulatedWheel(lastOpt);
|
||||
},
|
||||
Math.min(this._macAnimationCooldown, 3)
|
||||
); // 最多延迟3ms
|
||||
return;
|
||||
}
|
||||
|
||||
const currentZoom = this.canvas.getZoom();
|
||||
|
||||
// 分析滚轮事件模式,计算平均增量、速度和加速度
|
||||
let sumDelta = 0;
|
||||
let count = 0;
|
||||
let earliestTime = now;
|
||||
let latestTime = 0;
|
||||
let point = {
|
||||
x: lastOpt.e.offsetX,
|
||||
y: lastOpt.e.offsetY,
|
||||
};
|
||||
|
||||
// 判断是否在事件收集期间有平移或缩放动画
|
||||
let hadPanAnimation = false;
|
||||
let hadZoomAnimation = false;
|
||||
|
||||
// 计算平均增量和速度
|
||||
this._wheelEvents.forEach((event) => {
|
||||
sumDelta += event.delta;
|
||||
count++;
|
||||
earliestTime = Math.min(earliestTime, event.time);
|
||||
latestTime = Math.max(latestTime, event.time);
|
||||
|
||||
// 使用最后记录的点作为缩放中心
|
||||
if (event.time > latestTime) {
|
||||
point = event.point;
|
||||
}
|
||||
|
||||
// 检查是否有动画状态
|
||||
if (event.hasPanAnimation) hadPanAnimation = true;
|
||||
if (event.hasZoomAnimation) hadZoomAnimation = true;
|
||||
});
|
||||
|
||||
// 计算平均增量
|
||||
const avgDelta = sumDelta / count;
|
||||
|
||||
// 计算滚动速度 - 基于事件频率和时间跨度
|
||||
const timeSpan = latestTime - earliestTime + 1; // 避免除以零
|
||||
const eventsPerSecond = (count / timeSpan) * 1000;
|
||||
|
||||
// 速度系数: 速度越快,缩放越敏感
|
||||
let speedFactor = Math.min(3, Math.max(0.5, eventsPerSecond / 10));
|
||||
|
||||
// 计算缩放因子,应用速度系数
|
||||
// 针对Mac设备优化:Mac触控板的deltaY值通常较小,需要适度增加敏感度
|
||||
let zoomFactorBase = 0.999;
|
||||
if (this._isMac) {
|
||||
// Mac设备的触控板需要适度的敏感度,避免过度反应
|
||||
zoomFactorBase = 0.995; // 适度降低基数,增加缩放敏感度
|
||||
|
||||
// 检测是否为触控板滚动(小幅度、高频次的特征)
|
||||
const avgAbsDelta = Math.abs(avgDelta);
|
||||
if (avgAbsDelta < 50 && count > 2) {
|
||||
// 触控板滚动,适度增加敏感度
|
||||
speedFactor *= 1.6; // 适度增加敏感度倍数
|
||||
zoomFactorBase = 0.993; // 进一步调整基数
|
||||
}
|
||||
}
|
||||
|
||||
const zoomFactor = zoomFactorBase ** (avgDelta * speedFactor);
|
||||
let targetZoom = currentZoom * zoomFactor;
|
||||
|
||||
// 限制缩放范围
|
||||
targetZoom = Math.min(Math.max(targetZoom, 0.1), 20);
|
||||
|
||||
// 根据滚动速度和缩放幅度计算动画持续时间
|
||||
// 速度快时缩短动画时间,缩放幅度大时延长动画时间
|
||||
const zoomRatio = Math.abs(targetZoom - currentZoom) / currentZoom;
|
||||
|
||||
let duration;
|
||||
if (this._isMac) {
|
||||
// Mac设备使用平衡的动画时间控制
|
||||
if (speedFactor > 2) {
|
||||
// 快速操作:快速但平滑
|
||||
duration = Math.min(0.18, Math.max(0.08, (zoomRatio * 0.3) / Math.sqrt(speedFactor)));
|
||||
} else if (speedFactor > 1.2) {
|
||||
// 中等速度:标准响应
|
||||
duration = Math.min(0.25, Math.max(0.1, (zoomRatio * 0.4) / Math.sqrt(speedFactor)));
|
||||
} else {
|
||||
// 慢速精确操作:确保平滑
|
||||
duration = Math.min(0.3, Math.max(0.12, (zoomRatio * 0.5) / Math.sqrt(speedFactor)));
|
||||
}
|
||||
} else {
|
||||
duration = Math.min(0.5, Math.max(0.15, (zoomRatio * 0.8) / Math.sqrt(speedFactor)));
|
||||
}
|
||||
|
||||
// 根据滚动速度选择不同的缓动效果
|
||||
let easeType;
|
||||
if (this._isMac) {
|
||||
// Mac设备使用更简单、性能更好的缓动函数
|
||||
// 避免复杂的指数和回弹效果,减少计算量
|
||||
if (speedFactor > 2) {
|
||||
// 快速滚动:使用简单的缓出效果
|
||||
easeType = "power2.out";
|
||||
} else if (speedFactor > 1.2) {
|
||||
// 中等速度:使用平滑的缓出
|
||||
easeType = "power1.out";
|
||||
} else {
|
||||
// 慢速精确操作:使用线性过渡
|
||||
easeType = "power1.out";
|
||||
}
|
||||
} else {
|
||||
// 非Mac设备保持原有的缓动
|
||||
easeType = speedFactor > 1.5 ? "power1.out" : "power2.out";
|
||||
}
|
||||
|
||||
// 根据是否有其他动画正在进行,选择合适的动画方法
|
||||
if (hadPanAnimation || this._wasPanning) {
|
||||
// 如果有平移动画,使用组合动画以保持平滑过渡
|
||||
this.animateCombinedTransform(point, targetZoom, {
|
||||
duration: duration,
|
||||
ease: easeType,
|
||||
});
|
||||
} else {
|
||||
// 如果没有其他动画,使用标准缩放动画
|
||||
this.animateZoom(point, targetZoom, {
|
||||
duration: duration,
|
||||
ease: easeType,
|
||||
});
|
||||
}
|
||||
|
||||
// 更新Mac设备的最后动画时间
|
||||
if (this._isMac) {
|
||||
this._lastMacAnimationTime = now;
|
||||
}
|
||||
|
||||
// 清理事件记录
|
||||
this._wheelEvents = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算并应用拖动结束后的惯性效果
|
||||
* @param {Array} positions 拖动过程中记录的位置数组
|
||||
* @param {Boolean} isTouchDevice 是否是触摸设备
|
||||
*/
|
||||
applyInertiaEffect(positions, isTouchDevice) {
|
||||
if (!positions || positions.length <= 1) return;
|
||||
|
||||
const lastPos = positions[positions.length - 1];
|
||||
const firstPos = positions[0];
|
||||
const deltaTime = lastPos.time - firstPos.time;
|
||||
|
||||
if (deltaTime <= 0) return;
|
||||
|
||||
// 计算速度向量 (像素/毫秒)
|
||||
const velocityX = (lastPos.x - firstPos.x) / deltaTime;
|
||||
const velocityY = (lastPos.y - firstPos.y) / deltaTime;
|
||||
const speed = Math.sqrt(velocityX * velocityX + velocityY * velocityY);
|
||||
|
||||
// 仅当速度足够大时应用惯性效果
|
||||
if (speed > 0.2) {
|
||||
// 计算惯性距离,基于速度和衰减因子
|
||||
const decayFactor = 300; // 调整此值以改变惯性效果的强度
|
||||
const inertiaDistanceX = velocityX * decayFactor;
|
||||
const inertiaDistanceY = velocityY * decayFactor;
|
||||
|
||||
// 计算目标位置
|
||||
const vpt = this.canvas.viewportTransform;
|
||||
const currentPos = {
|
||||
x: -vpt[4],
|
||||
y: -vpt[5],
|
||||
};
|
||||
|
||||
const targetPos = {
|
||||
x: currentPos.x - inertiaDistanceX,
|
||||
y: currentPos.y - inertiaDistanceY,
|
||||
};
|
||||
|
||||
// 应用惯性动画,速度越大,动画时间越长
|
||||
const animationDuration = Math.min(1.2, Math.max(0.6, speed * 2));
|
||||
|
||||
// 应用惯性动画
|
||||
this.animatePan(targetPos, {
|
||||
duration: animationDuration, // 动态计算持续时间
|
||||
ease: this._isMac ? "quart.out" : "power3.out", // Mac使用更自然的减速效果
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 平滑过渡停止所有动画
|
||||
* 用于在需要中断当前动画时提供更自然的过渡,而不是硬性中断
|
||||
* @param {Object} options 过渡选项
|
||||
*/
|
||||
smoothStopAnimations(options = {}) {
|
||||
const duration = options.duration || 0.15; // 默认短暂过渡时间
|
||||
|
||||
// 处理缩放动画
|
||||
if (this._zoomAnimation) {
|
||||
const zoomObj = this._zoomAnimation.targets()[0];
|
||||
const currentZoom = this.canvas.getZoom();
|
||||
|
||||
// 创建短暂的过渡动画到当前值
|
||||
gsap.to(zoomObj, {
|
||||
value: currentZoom,
|
||||
duration: duration,
|
||||
ease: this._isMac ? "circ.out" : "power1.out", // Mac使用更平滑的缓动
|
||||
onUpdate: () => {
|
||||
this.currentZoom.value = Math.round(zoomObj.value * 100);
|
||||
this.canvas.renderAll();
|
||||
},
|
||||
onComplete: () => {
|
||||
if (this._zoomAnimation) {
|
||||
this._zoomAnimation.kill();
|
||||
this._zoomAnimation = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 处理平移动画
|
||||
if (this._panAnimation) {
|
||||
const panObj = this._panAnimation.targets()[0];
|
||||
const vpt = this.canvas.viewportTransform;
|
||||
const currentPos = { x: -vpt[4], y: -vpt[5] };
|
||||
|
||||
// 创建短暂的过渡动画到当前位置
|
||||
gsap.to(panObj, {
|
||||
x: currentPos.x,
|
||||
y: currentPos.y,
|
||||
duration: duration,
|
||||
ease: this._isMac ? "circ.out" : "power1.out", // Mac使用更平滑的缓动
|
||||
onUpdate: () => {
|
||||
this._applyPan(panObj.x, panObj.y);
|
||||
},
|
||||
onComplete: () => {
|
||||
if (this._panAnimation) {
|
||||
this._panAnimation.kill();
|
||||
this._panAnimation = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置画布交互动画
|
||||
* 为对象交互添加流畅的动画效果
|
||||
*/
|
||||
setupInteractionAnimations() {
|
||||
if (!this.canvas) return;
|
||||
|
||||
// 启用对象旋转的流畅动画
|
||||
this._setupRotationAnimation();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置旋转动画
|
||||
* @private
|
||||
*/
|
||||
_setupRotationAnimation() {
|
||||
if (!fabric) return;
|
||||
|
||||
// 保存原始旋转方法
|
||||
const originalRotate = fabric.Object.prototype.rotate;
|
||||
const isMac = this._isMac; // 保存Mac检测结果
|
||||
|
||||
// 覆盖旋转方法以添加动画
|
||||
fabric.Object.prototype.rotate = function (angle) {
|
||||
const currentAngle = this.angle || 0;
|
||||
|
||||
if (Math.abs(angle - currentAngle) > 0.1) {
|
||||
gsap.to(this, {
|
||||
angle: angle,
|
||||
duration: 0.3,
|
||||
ease: isMac ? "back.out(0.3)" : "power2.out", // Mac使用轻微回弹
|
||||
onUpdate: () => {
|
||||
this.canvas && this.canvas.renderAll();
|
||||
},
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
// 如果角度差异很小,使用原始方法
|
||||
return originalRotate.call(this, angle);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理滚轮缩放,同时兼容正在进行的平移动画
|
||||
* @param {Object} point 缩放中心点
|
||||
* @param {Number} targetZoom 目标缩放值
|
||||
* @param {Object} options 动画选项
|
||||
*/
|
||||
animateCombinedTransform(point, targetZoom, options = {}) {
|
||||
if (!this.canvas) return;
|
||||
|
||||
// 限制缩放范围
|
||||
targetZoom = Math.min(Math.max(targetZoom, 0.1), 20);
|
||||
|
||||
// 当前状态
|
||||
const currentZoom = this.canvas.getZoom();
|
||||
const currentVpt = [...this.canvas.viewportTransform];
|
||||
const currentPos = { x: -currentVpt[4], y: -currentVpt[5] };
|
||||
|
||||
// 如果有正在进行的动画,先停止它们
|
||||
if (this._combinedAnimation) {
|
||||
this._combinedAnimation.kill();
|
||||
this._combinedAnimation = null;
|
||||
}
|
||||
|
||||
if (this._zoomAnimation) {
|
||||
this._zoomAnimation.kill();
|
||||
this._zoomAnimation = null;
|
||||
}
|
||||
|
||||
if (this._panAnimation) {
|
||||
this._panAnimation.kill();
|
||||
this._panAnimation = null;
|
||||
}
|
||||
|
||||
// 创建一个统一的变换对象来动画
|
||||
const transform = {
|
||||
zoom: currentZoom,
|
||||
panX: currentVpt[4],
|
||||
panY: currentVpt[5],
|
||||
progress: 0, // 用于动画进度跟踪
|
||||
};
|
||||
|
||||
// 获取平移目标位置(如果有的话)
|
||||
let panTarget = { x: currentPos.x, y: currentPos.y };
|
||||
if (this._wasPanning) {
|
||||
// 如果之前有平移动画,尝试获取平移的目标位置
|
||||
const vpt = this.canvas.viewportTransform;
|
||||
panTarget = {
|
||||
x: currentPos.x,
|
||||
y: currentPos.y,
|
||||
};
|
||||
}
|
||||
|
||||
// 计算新的变换矩阵,同时考虑平移和缩放
|
||||
const scaleFactor = targetZoom / currentZoom;
|
||||
|
||||
// 创建动画
|
||||
this._combinedAnimation = gsap.to(transform, {
|
||||
zoom: targetZoom,
|
||||
progress: 1,
|
||||
duration: options.duration || 0.3,
|
||||
ease: options.ease || (this._isMac ? "expo.out" : "power2.out"), // Mac使用更平滑的缓动
|
||||
onUpdate: () => {
|
||||
// 计算当前动画阶段的混合变换
|
||||
const currentScaleFactor = transform.zoom / currentZoom;
|
||||
|
||||
// 应用缩放
|
||||
const vpt = this.canvas.viewportTransform;
|
||||
vpt[0] = currentVpt[0] * (transform.zoom / currentZoom);
|
||||
vpt[3] = currentVpt[3] * (transform.zoom / currentZoom);
|
||||
|
||||
// 平滑混合平移和缩放调整
|
||||
const adjustX = (1 - currentScaleFactor) * point.x;
|
||||
const adjustY = (1 - currentScaleFactor) * point.y;
|
||||
|
||||
// 如果存在平移目标,进行插值
|
||||
if (this._wasPanning) {
|
||||
const t = transform.progress;
|
||||
const interpolatedX = currentPos.x * (1 - t) + panTarget.x * t;
|
||||
const interpolatedY = currentPos.y * (1 - t) + panTarget.y * t;
|
||||
|
||||
// 结合缩放和平移的调整
|
||||
vpt[4] = -interpolatedX * currentScaleFactor + adjustX;
|
||||
vpt[5] = -interpolatedY * currentScaleFactor + adjustY;
|
||||
} else {
|
||||
// 只有缩放,保持中心点
|
||||
vpt[4] = currentVpt[4] * currentScaleFactor + adjustX;
|
||||
vpt[5] = currentVpt[5] * currentScaleFactor + adjustY;
|
||||
}
|
||||
|
||||
// 更新缩放值显示
|
||||
this.currentZoom.value = Math.round(transform.zoom * 100);
|
||||
this.canvas.renderAll();
|
||||
},
|
||||
onComplete: () => {
|
||||
this._combinedAnimation = null;
|
||||
this._zoomAnimation = null;
|
||||
this._panAnimation = null;
|
||||
this._wasPanning = false;
|
||||
this._wasZooming = false;
|
||||
|
||||
// 确保最终状态准确
|
||||
this._applyZoom(point, targetZoom, true);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose() {
|
||||
if (this._zoomAnimation) {
|
||||
this._zoomAnimation.kill();
|
||||
this._zoomAnimation = null;
|
||||
}
|
||||
|
||||
if (this._panAnimation) {
|
||||
this._panAnimation.kill();
|
||||
this._panAnimation = null;
|
||||
}
|
||||
|
||||
this._wheelEvents = [];
|
||||
this.canvas = null;
|
||||
this.currentZoom = null;
|
||||
}
|
||||
}
|
||||
104
src/components/Canvas/DepthCanvas/manager/CanvasManager.ts
Normal file
104
src/components/Canvas/DepthCanvas/manager/CanvasManager.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { fabric } from 'fabric-with-all'
|
||||
import { ref } 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'
|
||||
|
||||
interface CanvasInitOptions {
|
||||
canvasRef: any
|
||||
canvasViewWidth?: number
|
||||
canvasViewHeight?: number
|
||||
canvasWidth?: number
|
||||
canvasHeight?: number
|
||||
}
|
||||
export class CanvasManager {
|
||||
stateManager: any
|
||||
deviceInfo: any
|
||||
canvas: any
|
||||
canvasViewWidth: number
|
||||
canvasViewHeight: number
|
||||
canvasWidth: number
|
||||
canvasHeight: number
|
||||
currentZoom: any
|
||||
animationManager: any
|
||||
eventManager: any
|
||||
constructor(options) {
|
||||
this.stateManager = options.stateManager;
|
||||
this.deviceInfo = detectDeviceType();
|
||||
this.currentZoom = ref(100)
|
||||
}
|
||||
setCanvasViewSize(options) {
|
||||
this.canvasViewWidth = options.canvasViewWidth || 1920
|
||||
this.canvasViewHeight = options.canvasViewHeight || 1080
|
||||
}
|
||||
initCanvas(options: CanvasInitOptions) {
|
||||
this.setCanvasViewSize(options)
|
||||
this.canvasWidth = options.canvasWidth || 750
|
||||
this.canvasHeight = options.canvasHeight || 600
|
||||
this.canvas = createCanvas(options.canvasRef.value, {
|
||||
width: this.canvasViewWidth,
|
||||
height: this.canvasViewHeight,
|
||||
preserveObjectStacking: true,
|
||||
enableRetinaScaling: true,
|
||||
stopContextMenu: true,
|
||||
fireRightClick: true,
|
||||
backgroundColor: '#fff',
|
||||
})
|
||||
this.canvas.clipPath = new fabric.Rect({
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: this.canvasWidth,
|
||||
height: this.canvasHeight
|
||||
})
|
||||
// 画布居中
|
||||
const canvasX = this.canvasViewWidth / 2 - this.canvasWidth / 2
|
||||
const canvasY = this.canvasViewHeight / 2 - this.canvasHeight / 2
|
||||
this.canvas.viewportTransform = [1, 0, 0, 1, canvasX, canvasY]
|
||||
// 创建矩形
|
||||
const rect = new fabric.Rect({
|
||||
left: 20,
|
||||
top: 20,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fill: '#f00'
|
||||
})
|
||||
this.canvas.add(rect)
|
||||
//创建圆形
|
||||
const circle = new fabric.Circle({
|
||||
left: 200,
|
||||
top: 200,
|
||||
radius: 50,
|
||||
fill: '#0f0'
|
||||
})
|
||||
this.canvas.add(circle)
|
||||
this.animationManager = new AnimationManager(this.canvas, {
|
||||
currentZoom: this.currentZoom,
|
||||
canvasManager: this,
|
||||
wheelThrottleTime: 15, // 降低滚轮事件节流时间,提高响应性
|
||||
defaultEase: "power2.lin",
|
||||
defaultDuration: 0.3, // 缩短默认动画时间
|
||||
});
|
||||
this.setupCanvasEvents()
|
||||
this.stateManager.toolManager.setTool(OperationType.PAN)
|
||||
|
||||
|
||||
}
|
||||
setupCanvasEvents() {
|
||||
// 创建画布事件管理器
|
||||
this.eventManager = new CanvasEventManager(this.canvas, {
|
||||
canvasManager: this,
|
||||
animationManager: this.animationManager,
|
||||
toolManager: this.stateManager.toolManager,
|
||||
});
|
||||
// 设置动画交互效果
|
||||
this.animationManager.setupInteractionAnimations();
|
||||
}
|
||||
resetZoom() {
|
||||
this.animationManager.resetZoom()
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TOOLS } from "./ToolManager"
|
||||
import { OperationType } from "../tools/layerHelper"
|
||||
export class EventManager {
|
||||
stateManager: any
|
||||
vueFlow: any
|
||||
@@ -30,14 +30,14 @@ export class EventManager {
|
||||
handleClick(event: any) {
|
||||
this.stateManager.setActiveNodeID("")
|
||||
const tool = this.stateManager.tool.value
|
||||
if (tool === TOOLS.TEXT) {
|
||||
if (tool === OperationType.TEXT) {
|
||||
const { x, y, zoom } = this.vueFlow.value.viewport
|
||||
const position = {
|
||||
x: (event.offsetX - x) / zoom,
|
||||
y: (event.offsetY - y) / zoom
|
||||
}
|
||||
this.stateManager.nodeManager.createTextNode({ position })
|
||||
this.stateManager.toolManager.setTool(TOOLS.SELECT)
|
||||
this.stateManager.toolManager.setTool(OperationType.SELECT)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
export class FlowManager {
|
||||
stateManager: any
|
||||
vueFlow: any
|
||||
constructor(options) {
|
||||
this.stateManager = options.stateManager;
|
||||
this.vueFlow = options.vueFlow
|
||||
}
|
||||
setZoom(zoom: number) {
|
||||
this.stateManager.zoom.value = zoom
|
||||
this.vueFlow.value.zoomTo(zoom)
|
||||
}
|
||||
getNodeById(id: string) {
|
||||
return this.vueFlow.value.getNode(id)
|
||||
}
|
||||
getLastNode() {
|
||||
const lastNode = this.stateManager.getLastNode()
|
||||
if (lastNode?.id) {
|
||||
return this.vueFlow.value.getNode(lastNode.id)
|
||||
}
|
||||
return null;
|
||||
}
|
||||
getSubordNodeByID(id: string) {
|
||||
return this.vueFlow.value.getNodes?.find((v) => v.data.superiorID === id)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import { createId } from '../../tools/tools'
|
||||
import { NODE_DATATYPE, NODE_COMPONENT, NODE_DATATIER } from '../tools/index.d'
|
||||
interface NodeData {
|
||||
type?: string
|
||||
component?: any// 节点组件
|
||||
data?: object// 节点数据
|
||||
tier?: string// 节点层级
|
||||
isHeader?: boolean// 是否显示头
|
||||
superiorID?: string// 上级节点ID
|
||||
disableDelete?: boolean// 是否禁用删除
|
||||
disableCopy?: boolean// 是否禁用复制
|
||||
}
|
||||
interface NodeOptions {
|
||||
id?: string
|
||||
position?: { x: number, y: number }
|
||||
positionX?: number
|
||||
positionY?: number
|
||||
component?: any
|
||||
data?: NodeData
|
||||
}// 不可传入type class (内部使用)
|
||||
|
||||
export class NodeManager {
|
||||
stateManager: any
|
||||
vueFlow: any
|
||||
nodesep = 100 // 节点间距
|
||||
ranksep = 100 // 层级间距
|
||||
constructor(options) {
|
||||
this.stateManager = options.stateManager;
|
||||
this.vueFlow = options.vueFlow
|
||||
}
|
||||
|
||||
/** 删除节点 */
|
||||
deleteNode(id: string) {
|
||||
this.stateManager.deleteNode(id)
|
||||
}
|
||||
/** 添加节点 */
|
||||
addNode(node: any) {
|
||||
this.stateManager.addNode(node)
|
||||
}
|
||||
|
||||
/** 创建节点 */
|
||||
createNode(options: NodeOptions) {
|
||||
const superiorID = options?.data?.superiorID
|
||||
const snode = superiorID ? this.stateManager.flowManager.getNodeById(superiorID) : this.stateManager.flowManager.getLastNode();
|
||||
const id = options.id || createId()
|
||||
const positionX = options.positionX || 0
|
||||
const positionY = options.positionY || 0
|
||||
const position = options.position ||
|
||||
(!snode ?
|
||||
{ x: positionX, y: positionY } :
|
||||
{
|
||||
x: snode.position.x + snode.dimensions.width + this.nodesep + positionX,
|
||||
y: snode.position.y + positionY
|
||||
})
|
||||
const data = options?.data || {}
|
||||
data['component'] = options.component
|
||||
const options_ = {
|
||||
id,
|
||||
position,
|
||||
data
|
||||
}
|
||||
this.addNode(options_)
|
||||
return options_;
|
||||
}
|
||||
/** 创建结果节点 */
|
||||
createResultNode(options?: NodeOptions) {
|
||||
const options_ = {
|
||||
...(options ? options : {}),
|
||||
component: NODE_COMPONENT.RESULT_IMAGE,
|
||||
data: {
|
||||
tier: NODE_DATATIER.RESULT_IMAGE,
|
||||
type: NODE_DATATYPE.RESULT_IMAGE,
|
||||
isHeader: true,
|
||||
...(options?.data || {}),
|
||||
},
|
||||
}
|
||||
return this.createNode(options_)
|
||||
}
|
||||
/** 创建卡片选择节点 */
|
||||
createCardsSelect(options?: NodeOptions) {
|
||||
const options_ = {
|
||||
...(options ? options : {}),
|
||||
component: NODE_COMPONENT.CARD,
|
||||
positionY: 50,
|
||||
data: {
|
||||
tier: NODE_DATATIER.CARDS_SELECT,
|
||||
type: NODE_DATATYPE.CARDS_SELECT,
|
||||
...(options?.data || {}),
|
||||
},
|
||||
}
|
||||
return this.createNode(options_)
|
||||
}
|
||||
/** 创建卡片节点 */
|
||||
createCardNode(options?: NodeOptions) {
|
||||
const options_ = {
|
||||
...(options ? options : {}),
|
||||
component: NODE_COMPONENT.CARD,
|
||||
data: {
|
||||
...(options?.data || {}),
|
||||
}
|
||||
}
|
||||
return this.createNode(options_)
|
||||
}
|
||||
/** 创建文本节点 */
|
||||
createTextNode(options?: NodeOptions) {
|
||||
const options_ = {
|
||||
...(options ? options : {}),
|
||||
component: NODE_COMPONENT.TEXT,
|
||||
data: {
|
||||
...(options?.data || {}),
|
||||
}
|
||||
}
|
||||
return this.createNode(options_)
|
||||
}
|
||||
|
||||
copyNodeById(id: string) {
|
||||
const node = this.stateManager.getNodeById(id)
|
||||
const flowNode = this.stateManager.flowManager.getNodeById(id)
|
||||
if (!node) return console.warn(`${id}找不到对应节点`)
|
||||
if (node.data?.disableCopy) return console.warn(`${id}节点已禁用复制`)
|
||||
const node_ = {
|
||||
...JSON.parse(JSON.stringify(node)),
|
||||
id: createId(),
|
||||
position: {
|
||||
x: node.position.x,
|
||||
y: node.position.y + (flowNode?.dimensions?.height || 0) + this.ranksep,
|
||||
}
|
||||
}
|
||||
delete node_.data?.superiorID
|
||||
delete node_.data?.disableDelete
|
||||
this.stateManager.addNode(node_)
|
||||
}
|
||||
}
|
||||
@@ -4,27 +4,8 @@ import { ElMessageBox } from 'element-plus'
|
||||
import i18n from '@/lang'
|
||||
const t = i18n.global.t
|
||||
|
||||
export interface NodesItem {
|
||||
id: string
|
||||
type: string
|
||||
class: string
|
||||
position: { x: number, y: number }
|
||||
data: { component: any, type: string, superiorID?: string }
|
||||
}
|
||||
export class StateManager {
|
||||
vueFlow: any
|
||||
activeNodeID: any
|
||||
nodes: any
|
||||
nodes_: any
|
||||
edges: any
|
||||
zoom: any
|
||||
tool: any
|
||||
cursor: any
|
||||
// 节点是否可拖动
|
||||
nodesDraggable: any
|
||||
// 拖动时是否可以平移画布
|
||||
panOnDrag: any
|
||||
|
||||
export class StateManager {
|
||||
// 历史记录-撤回/重做
|
||||
mxHistory: any
|
||||
historyList: any
|
||||
@@ -32,155 +13,56 @@ export class StateManager {
|
||||
|
||||
// 管理器
|
||||
eventManager: any
|
||||
flowManager: any
|
||||
nodeManager: any
|
||||
canvasManager: any
|
||||
toolManager: any
|
||||
// 设置管理器
|
||||
setManager(options) {
|
||||
options.eventManager && (this.eventManager = options.eventManager)
|
||||
options.flowManager && (this.flowManager = options.flowManager)
|
||||
options.nodeManager && (this.nodeManager = options.nodeManager)
|
||||
options.canvasManager && (this.canvasManager = options.canvasManager)
|
||||
options.toolManager && (this.toolManager = options.toolManager)
|
||||
}
|
||||
constructor(options) {
|
||||
this.vueFlow = options.vueFlow
|
||||
this.zoom = ref(1)
|
||||
this.tool = ref("")
|
||||
this.cursor = ref("")
|
||||
this.nodesDraggable = ref(false)
|
||||
this.panOnDrag = ref(false)
|
||||
this.mxHistory = ref(50)
|
||||
this.historyList = ref([])
|
||||
this.historyIndex = ref(0)
|
||||
|
||||
this.activeNodeID = ref("")
|
||||
this.nodes = ref<NodesItem[]>([]);
|
||||
this.nodes_ = computed(() => {
|
||||
return this.nodes.value.map((node, index) => {
|
||||
const obj = node;
|
||||
const superiorID = node.data.superiorID;
|
||||
const isSuperior = this.nodes.value.some((v) => v.id === superiorID)
|
||||
const isSubord = this.nodes.value.some((v) => v.data.superiorID === node.id)
|
||||
if (!isSuperior && isSubord) {// 没有上级 有下级
|
||||
obj.type = NODE_TYPE.INPUT;
|
||||
} else if (isSuperior && isSubord) {// 有上级 有下级
|
||||
obj.type = NODE_TYPE.SECONDARY;
|
||||
} else if (isSuperior && !isSubord) {// 有上级 没有下级
|
||||
obj.type = NODE_TYPE.OUTPUT;
|
||||
} else {// 其他情况-没有上级 没有下级
|
||||
obj.type = NODE_TYPE.ALONE;
|
||||
}
|
||||
return obj
|
||||
})
|
||||
})
|
||||
|
||||
this.edges = computed(() => {
|
||||
const arr = []
|
||||
this.nodes.value.forEach((node, index) => {
|
||||
const superiorID = node.data.superiorID;
|
||||
const isSuperior = this.nodes.value.some((v) => v.id === superiorID)
|
||||
if (superiorID && isSuperior) {
|
||||
const source = node.data.superiorID
|
||||
const target = node.id
|
||||
arr.push({
|
||||
id: `el-${source}-${target}`,
|
||||
source: source,
|
||||
target: target,
|
||||
selectable: false,
|
||||
type: 'default'
|
||||
})
|
||||
}
|
||||
})
|
||||
return arr
|
||||
})
|
||||
|
||||
}
|
||||
/** 设置激活节点 */
|
||||
setActiveNodeID(id: string) { this.activeNodeID.value = id }
|
||||
/** 添加节点 */
|
||||
addNode(node: NodesItem) {
|
||||
this.nodes.value.push(node);
|
||||
this.recordState()
|
||||
}
|
||||
/** 删除节点 */
|
||||
async deleteNode(id: string, { isElMessageBox } = { isElMessageBox: false }) {
|
||||
const node = this.getNodeById(id)
|
||||
if (!node) return console.warn(`没有找到指定id:${id}`)
|
||||
if (node.data.disableDelete) return console.warn('该节点禁用删除')
|
||||
let deletePromise: any = true
|
||||
if (isElMessageBox) {
|
||||
deletePromise = await new Promise<void>((resolve, reject) => {
|
||||
ElMessageBox.confirm(
|
||||
t('flowCanvas.deleteCardConfirm'),
|
||||
'',
|
||||
{
|
||||
confirmButtonText: t('flowCanvas.confirm'),
|
||||
cancelButtonText: t('flowCanvas.cancel'),
|
||||
}
|
||||
).then(() => {
|
||||
resolve(true)
|
||||
}).catch(() => {
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
if (!deletePromise) return console.log('删除操作被取消')
|
||||
this.nodes.value = this.nodes.value.filter((node: NodesItem) => node.id !== id)
|
||||
this.recordState()
|
||||
}
|
||||
/** 获取节点 */
|
||||
getNodeById(id: string) { return this.nodes.value.find((node: NodesItem) => node.id === id) }
|
||||
/** 获取下级节点 */
|
||||
getSubordNodeByID(id: string) { return this.nodes.value.find((node: NodesItem) => node.data.superiorID === id) }
|
||||
getLastNode() { return this.nodes.value[this.nodes.value.length - 1] }
|
||||
/** 设置工具 */
|
||||
setTool(tool: string) { this.tool.value = tool }
|
||||
/** 设置光标 */
|
||||
setCursor(v: string) { this.cursor.value = v }
|
||||
/** 设置节点是否可拖动 */
|
||||
setNodesDraggable(v: boolean) { this.nodesDraggable.value = v }
|
||||
/** 设置是否可以平移画布 */
|
||||
setPanOnDrag(v: boolean) { this.panOnDrag.value = v }
|
||||
/** 设置节点层级至最顶部 */
|
||||
bringToFont(id) {
|
||||
const fromIndex = this.nodes.value.findIndex(item => item.id === id)
|
||||
if (fromIndex === -1) return console.warn(`没有找到指定id:${id}`)
|
||||
this.nodes.value.splice(this.nodes.value.length - 1, 0, ...this.nodes.value.splice(fromIndex, 1))
|
||||
}
|
||||
/** 设置节点层级至最低部 */
|
||||
sendToBack(id) {
|
||||
const fromIndex = this.nodes.value.findIndex(item => item.id === id)
|
||||
if (fromIndex === -1) return console.warn(`没有找到指定id:${id}`)
|
||||
this.nodes.value.splice(0, 0, ...this.nodes.value.splice(fromIndex, 1))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/** 记录状态 */
|
||||
recordState() {
|
||||
if (this.historyIndex.value < this.historyList.value.length - 1) {
|
||||
this.historyList.value.splice(this.historyIndex.value + 1)
|
||||
}
|
||||
const state = {
|
||||
nodes: JSON.stringify(this.nodes.value)
|
||||
}
|
||||
this.historyList.value.push(state)
|
||||
const size = this.historyList.value.length - this.mxHistory.value
|
||||
if (size > 0) this.historyList.value.splice(0, size)
|
||||
this.historyIndex.value = this.historyList.value.length - 1
|
||||
// if (this.historyIndex.value < this.historyList.value.length - 1) {
|
||||
// this.historyList.value.splice(this.historyIndex.value + 1)
|
||||
// }
|
||||
// const state = {
|
||||
// nodes: JSON.stringify(this.nodes.value)
|
||||
// }
|
||||
// this.historyList.value.push(state)
|
||||
// const size = this.historyList.value.length - this.mxHistory.value
|
||||
// if (size > 0) this.historyList.value.splice(0, size)
|
||||
// this.historyIndex.value = this.historyList.value.length - 1
|
||||
}
|
||||
/** 撤回状态 */
|
||||
undoState() {
|
||||
var index = this.historyIndex.value - 1
|
||||
const state = this.historyList.value[index]
|
||||
if (!state) return
|
||||
this.historyIndex.value = index
|
||||
this.nodes.value = JSON.parse(state.nodes)
|
||||
// var index = this.historyIndex.value - 1
|
||||
// const state = this.historyList.value[index]
|
||||
// if (!state) return
|
||||
// this.historyIndex.value = index
|
||||
// this.nodes.value = JSON.parse(state.nodes)
|
||||
}
|
||||
/** 重做状态 */
|
||||
redoState() {
|
||||
var index = this.historyIndex.value + 1
|
||||
const state = this.historyList.value[index]
|
||||
if (!state) return
|
||||
this.historyIndex.value = index
|
||||
this.nodes.value = JSON.parse(state.nodes)
|
||||
// var index = this.historyIndex.value + 1
|
||||
// const state = this.historyList.value[index]
|
||||
// if (!state) return
|
||||
// this.historyIndex.value = index
|
||||
// this.nodes.value = JSON.parse(state.nodes)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,52 +1,69 @@
|
||||
export const TOOLS = {
|
||||
SELECT: "SELECT",
|
||||
MOVE: "MOVE",
|
||||
BRUSH: "BRUSH",
|
||||
ERASER: "ERASER",
|
||||
IMAGE: "IMAGE",
|
||||
SELECTBOX: "SELECTBOX",
|
||||
RECTANGLE: "RECTANGLE",
|
||||
TEXT: "TEXT",
|
||||
UNDO: "UNDO",
|
||||
REDO: "REDO",
|
||||
}
|
||||
export const tools = [
|
||||
/** 选择工具 */
|
||||
{
|
||||
name: TOOLS.SELECT,
|
||||
nodesDraggable: true,
|
||||
panOnDrag: false,
|
||||
},
|
||||
/** 移动工具 */
|
||||
{
|
||||
name: TOOLS.MOVE,
|
||||
nodesDraggable: false,
|
||||
panOnDrag: true,
|
||||
},
|
||||
/** 文本工具 */
|
||||
{
|
||||
name: TOOLS.TEXT,
|
||||
cursor: "text",
|
||||
nodesDraggable: false,
|
||||
panOnDrag: false,
|
||||
},
|
||||
|
||||
]
|
||||
import { ref } from 'vue'
|
||||
import { OperationType } from '../tools/layerHelper'
|
||||
export class ToolManager {
|
||||
stateManager: any
|
||||
vueFlow: any
|
||||
canvasManager: any
|
||||
currentTool: any
|
||||
tools: any[]
|
||||
constructor(options) {
|
||||
this.stateManager = options.stateManager;
|
||||
this.vueFlow = options.vueFlow
|
||||
this.setTool(TOOLS.SELECT)
|
||||
this.canvasManager = options.canvasManager;
|
||||
this.currentTool = ref(null)
|
||||
this.tools = [
|
||||
/** 选择工具 */
|
||||
{
|
||||
name: OperationType.SELECT,
|
||||
cursor: "default",
|
||||
setup: this.setupSelectTool.bind(this),
|
||||
selection: true,
|
||||
},
|
||||
/** 移动工具 */
|
||||
{
|
||||
name: OperationType.PAN,
|
||||
cursor: "grab",
|
||||
setup: this.setupMoveTool.bind(this),
|
||||
},
|
||||
/** 画笔工具 */
|
||||
{
|
||||
name: OperationType.DRAW,
|
||||
cursor: "crosshair",
|
||||
},
|
||||
/** 橡皮擦工具 */
|
||||
{
|
||||
name: OperationType.ERASER,
|
||||
cursor: "crosshair",
|
||||
},
|
||||
/** 智能选框工具 */
|
||||
{
|
||||
name: OperationType.SELECTBOX,
|
||||
cursor: "crosshair",
|
||||
},
|
||||
/** 矩形工具 */
|
||||
{
|
||||
name: OperationType.RECTANGLE,
|
||||
cursor: "crosshair",
|
||||
},
|
||||
]
|
||||
}
|
||||
setTool(value: string) {
|
||||
const tool = tools.find((t) => t.name === value)
|
||||
const tool = this.tools.find((t) => t.name === value)
|
||||
if (!tool) return console.warn(`工具${tool}不存在`)
|
||||
this.stateManager.tool.value = tool.name
|
||||
this.stateManager.setNodesDraggable(!!tool.nodesDraggable)
|
||||
this.stateManager.setPanOnDrag(!!tool.panOnDrag)
|
||||
this.stateManager.setCursor(tool.cursor || "")
|
||||
}
|
||||
this.currentTool.value = tool.name
|
||||
this.canvasManager.canvas.defaultCursor = tool.cursor
|
||||
this.setCanvasEvented(!!tool.selection)
|
||||
this.canvasManager.canvas.isDragging = !!tool.isDragging
|
||||
|
||||
if (tool.setup) tool.setup()
|
||||
}
|
||||
// 切换工具时,设置画布事件
|
||||
setCanvasEvented(value: boolean) {
|
||||
this.canvasManager.canvas.selection = value
|
||||
this.canvasManager.canvas.getObjects().forEach((v) => v.evented = value)
|
||||
}
|
||||
/** 选择工具 */
|
||||
setupSelectTool() {
|
||||
}
|
||||
/** 移动工具 */
|
||||
setupMoveTool() {
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,762 @@
|
||||
/**
|
||||
* 键盘管理器
|
||||
* 负责处理编辑器中的键盘事件和快捷键
|
||||
* 支持PC、Mac和iPad三端适配
|
||||
*/
|
||||
export class KeyboardManager {
|
||||
/**
|
||||
* 创建键盘管理器
|
||||
* @param {Object} options 配置选项
|
||||
* @param {Object} options.toolManager 工具管理器实例
|
||||
* @param {Object} options.commandManager 命令管理器实例
|
||||
* @param {Object} options.layerManager 图层管理器实例
|
||||
* @param {Object} options.canvasManager 画布管理器实例
|
||||
* @param {Function} options.pasteText 粘贴文本回调函数
|
||||
* @param {Function} options.pasteImage 粘贴图片回调函数
|
||||
* @param {Ref<Boolean>} options.isRedGreenMode 是否为红绿模式
|
||||
* @param {HTMLElement} options.container 容器元素,用于添加事件监听
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.toolManager = options.toolManager;
|
||||
this.commandManager = options.commandManager;
|
||||
this.layerManager = options.layerManager;
|
||||
this.canvasManager = options.canvasManager;
|
||||
this.container = options.container || document;
|
||||
this.pasteText = options.pasteText || (() => { });
|
||||
this.pasteImage = options.pasteImage || (() => { });
|
||||
this.isRedGreenMode = options.isRedGreenMode;
|
||||
|
||||
// 检测平台类型
|
||||
this.platform = this.detectPlatform();
|
||||
this.isTouchDevice = this.detectTouchDevice();
|
||||
|
||||
// 快捷键的平台特定键名
|
||||
this.modifierKeys = {
|
||||
ctrl: this.platform === "mac" ? "meta" : "ctrl",
|
||||
cmdOrCtrl: this.platform === "mac" ? "meta" : "ctrl",
|
||||
alt: "alt",
|
||||
shift: "shift",
|
||||
option: "alt", // Mac 特有,等同于 alt
|
||||
cmd: "meta", // Mac 特有,等同于 Command
|
||||
};
|
||||
|
||||
// 快捷键显示的平台特定符号
|
||||
this.keySymbols = {
|
||||
ctrl: this.platform === "mac" ? "⌃" : "Ctrl",
|
||||
meta: this.platform === "mac" ? "⌘" : "Win",
|
||||
alt: this.platform === "mac" ? "⌥" : "Alt",
|
||||
shift: this.platform === "mac" ? "⇧" : "Shift",
|
||||
escape: "Esc",
|
||||
space: "空格",
|
||||
};
|
||||
|
||||
// 快捷键映射表 - 可通过配置进行扩展
|
||||
this.shortcuts = this.initShortcuts();
|
||||
|
||||
// 触摸相关状态
|
||||
this.touchState = {
|
||||
pinchStartDistance: 0,
|
||||
pinchStartBrushSize: 0,
|
||||
touchStartX: 0,
|
||||
touchStartY: 0,
|
||||
isTwoFingerTouch: false,
|
||||
};
|
||||
|
||||
// 临时工具状态
|
||||
this.tempToolState = {
|
||||
active: false,
|
||||
originalTool: null,
|
||||
};
|
||||
|
||||
// 事件绑定
|
||||
this._handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this._handleKeyUp = this.handleKeyUp.bind(this);
|
||||
this._handlePaste = this.handlePaste.bind(this);
|
||||
this._handleTouchStart = this.handleTouchStart.bind(this);
|
||||
this._handleTouchMove = this.handleTouchMove.bind(this);
|
||||
this._handleTouchEnd = this.handleTouchEnd.bind(this);
|
||||
|
||||
// 已注册的自定义事件处理程序
|
||||
this.customHandlers = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测当前平台
|
||||
* @returns {'mac'|'windows'|'ios'|'android'|'other'} 平台类型
|
||||
*/
|
||||
detectPlatform() {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
|
||||
if (userAgent.indexOf("mac") !== -1) return "mac";
|
||||
if (userAgent.indexOf("win") !== -1) return "windows";
|
||||
if (/(iphone|ipad|ipod)/.test(userAgent)) return "ios";
|
||||
if (userAgent.indexOf("android") !== -1) return "android";
|
||||
|
||||
return "other";
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否为触摸设备
|
||||
* @returns {boolean} 是否为触摸设备
|
||||
*/
|
||||
detectTouchDevice() {
|
||||
return (
|
||||
"ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化快捷键配置
|
||||
* @returns {Object} 快捷键配置
|
||||
*/
|
||||
initShortcuts() {
|
||||
const cmdOrCtrl = this.modifierKeys.cmdOrCtrl;
|
||||
|
||||
// 基本快捷键映射,将在构建时根据平台类型自动调整
|
||||
return {
|
||||
// 撤销/重做
|
||||
[`${cmdOrCtrl}+z`]: { action: "undo", description: "撤销" },
|
||||
[`${cmdOrCtrl}+shift+z`]: { action: "redo", description: "重做" },
|
||||
[`${cmdOrCtrl}+y`]: { action: "redo", description: "重做" },
|
||||
|
||||
// 复制/粘贴
|
||||
[`${cmdOrCtrl}+c`]: { action: "copy", description: "复制" },
|
||||
[`${cmdOrCtrl}+v`]: { action: "paste", description: "粘贴", noStop: true },
|
||||
[`${cmdOrCtrl}+x`]: { action: "cut", description: "剪切" },
|
||||
|
||||
// 删除
|
||||
delete: { action: "delete", description: "删除" },
|
||||
backspace: { action: "delete", description: "删除" },
|
||||
up: { action: "up", description: "上" },
|
||||
down: { action: "down", description: "下" },
|
||||
left: { action: "left", description: "左" },
|
||||
right: { action: "right", description: "右" },
|
||||
|
||||
// 选择
|
||||
[`${cmdOrCtrl}+a`]: { action: "selectAll", description: "全选" },
|
||||
escape: { action: "clearSelection", description: "取消选择" },
|
||||
|
||||
// 保存
|
||||
[`${cmdOrCtrl}+s`]: { action: "save", description: "保存" },
|
||||
|
||||
// 工具切换 (这些会由工具管理器处理)
|
||||
v: { action: "selectTool", param: "select", description: "选择工具" },
|
||||
b: { action: "selectTool", param: "draw", description: "画笔工具" },
|
||||
e: { action: "selectTool", param: "eraser", description: "橡皮擦" },
|
||||
i: { action: "selectTool", param: "eyedropper", description: "吸色工具" },
|
||||
h: { action: "selectTool", param: "pan", description: "移动画布" },
|
||||
l: { action: "selectTool", param: "lasso", description: "套索工具" },
|
||||
m: {
|
||||
action: "selectTool",
|
||||
param: "area_custom",
|
||||
description: "自由选区工具",
|
||||
},
|
||||
w: { action: "selectTool", param: "wave", description: "波浪工具" },
|
||||
j: { action: "selectTool", param: "liquify", description: "液化工具" },
|
||||
|
||||
// 数值调整
|
||||
"shift+[": {
|
||||
action: "decreaseTextureScale",
|
||||
description: "减小材质图片大小",
|
||||
},
|
||||
"shift+]": {
|
||||
action: "increaseTextureScale",
|
||||
description: "增大材质图片大小",
|
||||
},
|
||||
"[": { action: "decreaseBrushSize", param: 1, description: "减小画笔" },
|
||||
"]": { action: "increaseBrushSize", param: 1, description: "增大画笔" },
|
||||
|
||||
",": {
|
||||
action: "decreaseBrushOpacity",
|
||||
param: 0.01,
|
||||
description: "减小透明度",
|
||||
},
|
||||
".": {
|
||||
action: "increaseBrushOpacity",
|
||||
param: 0.01,
|
||||
description: "增大透明度",
|
||||
},
|
||||
|
||||
// 空格 - 临时切换到手型工具
|
||||
space: {
|
||||
action: "toggleTempTool",
|
||||
param: "pan",
|
||||
description: "临时切换到手形工具",
|
||||
},
|
||||
|
||||
// 图层操作
|
||||
[`${cmdOrCtrl}+shift+n`]: { action: "newLayer", description: "新建图层" },
|
||||
[`${cmdOrCtrl}+g`]: { action: "groupLayers", description: "组合图层" },
|
||||
[`${cmdOrCtrl}+o`]: {
|
||||
action: "addImageToNewLayer",
|
||||
description: "上传图片到新图层",
|
||||
},
|
||||
[`${cmdOrCtrl}+shift+g`]: {
|
||||
action: "ungroupLayers",
|
||||
description: "取消组合",
|
||||
},
|
||||
[`${cmdOrCtrl}+j`]: { action: "mergeLayers", description: "合并图层" },
|
||||
|
||||
// iPad特有的快捷键(当无法使用键盘时)
|
||||
...(this.platform === "ios" && {
|
||||
two_finger_tap: {
|
||||
action: "contextMenu",
|
||||
description: "显示上下文菜单",
|
||||
},
|
||||
three_finger_swipe_left: { action: "undo", description: "撤销" },
|
||||
three_finger_swipe_right: { action: "redo", description: "重做" },
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理粘贴事件
|
||||
* @param {ClipboardEvent} event 粘贴事件
|
||||
*/
|
||||
handlePaste(event) {
|
||||
event.preventDefault(); // 阻止默认粘贴行为
|
||||
if (this.isRedGreenMode.value) return;
|
||||
const text = event.clipboardData?.getData("text/plain") || "";
|
||||
if (/^aida_copy_canvas_layer/.test(text)) return;
|
||||
const items = event.clipboardData?.items || [];
|
||||
// console.log(this);
|
||||
for (const item of items) {
|
||||
if (item.type.indexOf("text/plain") !== -1) {
|
||||
item.getAsString((text) => {
|
||||
this.pasteText(text);
|
||||
});
|
||||
} else if (item.type.indexOf("image") !== -1) {
|
||||
const blob = item.getAsFile();
|
||||
this.pasteImage(blob);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化并开始监听键盘事件
|
||||
*/
|
||||
init() {
|
||||
// 添加键盘事件监听
|
||||
this.container.addEventListener("keydown", this._handleKeyDown);
|
||||
this.container.addEventListener("keyup", this._handleKeyUp);
|
||||
this.container.addEventListener("paste", this._handlePaste);
|
||||
|
||||
// 如果是触摸设备,添加触摸事件监听
|
||||
if (this.isTouchDevice) {
|
||||
this.container.addEventListener("touchstart", this._handleTouchStart);
|
||||
this.container.addEventListener("touchmove", this._handleTouchMove);
|
||||
this.container.addEventListener("touchend", this._handleTouchEnd);
|
||||
this.container.addEventListener("touchcancel", this._handleTouchEnd);
|
||||
}
|
||||
|
||||
// console.log(`键盘管理器已初始化,平台: ${this.platform}, 触摸设备: ${this.isTouchDevice}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* hide模式下,关闭快捷键
|
||||
*/
|
||||
removeEvents() {
|
||||
// 移除键盘事件监听
|
||||
this.container.removeEventListener("keydown", this._handleKeyDown);
|
||||
this.container.removeEventListener("keyup", this._handleKeyUp);
|
||||
this.container.removeEventListener("paste", this._handlePaste);
|
||||
|
||||
// 如果是触摸设备,移除触摸事件监听
|
||||
if (this.isTouchDevice) {
|
||||
this.container.removeEventListener("touchstart", this._handleTouchStart);
|
||||
this.container.removeEventListener("touchmove", this._handleTouchMove);
|
||||
this.container.removeEventListener("touchend", this._handleTouchEnd);
|
||||
this.container.removeEventListener("touchcancel", this._handleTouchEnd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 处理键盘按下事件
|
||||
* @param {KeyboardEvent} event 键盘事件
|
||||
*/
|
||||
handleKeyDown(event) {
|
||||
// 如果当前焦点在输入框内,不处理大部分快捷键
|
||||
if (this.isInputActive() && !["Escape", "Tab"].includes(event.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建快捷键标识符
|
||||
const shortcutKey = this.buildShortcutKey(event);
|
||||
|
||||
// 查找并执行快捷键动作
|
||||
const shortcut = this.shortcuts[shortcutKey];
|
||||
if (shortcut) {
|
||||
// 阻止默认行为,例如浏览器的保存对话框等
|
||||
if (shortcutKey.includes(`${this.modifierKeys.cmdOrCtrl}+`) && !shortcut.noStop) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
this.executeAction(shortcut.action, shortcut.param, event);
|
||||
return;
|
||||
}
|
||||
|
||||
// 工具快捷键处理
|
||||
if (this.toolManager && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
||||
this.toolManager.handleKeyboardShortcut(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理键盘释放事件
|
||||
* @param {KeyboardEvent} event 键盘事件
|
||||
*/
|
||||
handleKeyUp(event) {
|
||||
// 当空格键释放时,如果是临时工具,切回原始工具
|
||||
if (event.key === " " && this.tempToolState.active) {
|
||||
this.restoreTempTool();
|
||||
}
|
||||
|
||||
// 调用自定义处理程序
|
||||
const key = event.key.toLowerCase();
|
||||
if (this.customHandlers[key] && typeof this.customHandlers[key].onKeyUp === "function") {
|
||||
this.customHandlers[key].onKeyUp(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理触摸开始事件
|
||||
* @param {TouchEvent} event 触摸事件
|
||||
*/
|
||||
handleTouchStart(event) {
|
||||
const touches = event.touches;
|
||||
|
||||
// 存储初始状态以便后续计算
|
||||
if (touches.length === 2) {
|
||||
// 双指触摸 - 可用于缩放或调整画笔大小
|
||||
this.touchState.isTwoFingerTouch = true;
|
||||
this.touchState.pinchStartDistance = this.getDistanceBetweenTouches(touches[0], touches[1]);
|
||||
|
||||
// 如果有画笔管理器,记录起始画笔大小
|
||||
if (this.toolManager && this.toolManager.brushManager) {
|
||||
this.touchState.pinchStartBrushSize = this.toolManager.brushManager.brushSize.value;
|
||||
}
|
||||
} else if (touches.length === 3) {
|
||||
// 三指触摸 - 可用于撤销/重做
|
||||
this.touchState.touchStartX = touches[0].clientX;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理触摸移动事件
|
||||
* @param {TouchEvent} event 触摸事件
|
||||
*/
|
||||
handleTouchMove(event) {
|
||||
const touches = event.touches;
|
||||
|
||||
// 阻止默认行为(例如滚动)
|
||||
if (touches.length >= 2) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// 双指缩放处理 - 调整画笔大小
|
||||
if (touches.length === 2 && this.touchState.isTwoFingerTouch) {
|
||||
const currentDistance = this.getDistanceBetweenTouches(touches[0], touches[1]);
|
||||
const scale = currentDistance / this.touchState.pinchStartDistance;
|
||||
|
||||
// 调整画笔大小
|
||||
if (this.toolManager && this.toolManager.brushManager && scale !== 1) {
|
||||
const newSize = this.touchState.pinchStartBrushSize * scale;
|
||||
this.toolManager.brushManager.setBrushSize(newSize);
|
||||
}
|
||||
}
|
||||
// 三指滑动处理 - 撤销/重做
|
||||
else if (touches.length === 3) {
|
||||
const deltaX = touches[0].clientX - this.touchState.touchStartX;
|
||||
|
||||
// 滑动超过50px认为是有效的手势
|
||||
if (Math.abs(deltaX) > 50) {
|
||||
if (deltaX < 0) {
|
||||
// 向左滑动 - 撤销
|
||||
this.executeAction("undo");
|
||||
} else {
|
||||
// 向右滑动 - 重做
|
||||
this.executeAction("redo");
|
||||
}
|
||||
|
||||
// 更新起始位置,防止连续触发
|
||||
this.touchState.touchStartX = touches[0].clientX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理触摸结束事件
|
||||
* @param {TouchEvent} event 触摸事件
|
||||
*/
|
||||
handleTouchEnd(event) {
|
||||
// 检测双指轻拍 (两个手指几乎同时按下,又几乎同时抬起)
|
||||
if (this.touchState.isTwoFingerTouch && event.touches.length === 0) {
|
||||
if (new Date().getTime() - this.touchState.touchStartTime < 300) {
|
||||
// 双指轻拍 - 可以触发上下文菜单
|
||||
this.executeAction("contextMenu");
|
||||
}
|
||||
}
|
||||
|
||||
// 重置触摸状态
|
||||
this.touchState.isTwoFingerTouch = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个触摸点之间的距离
|
||||
* @param {Touch} touch1 第一个触摸点
|
||||
* @param {Touch} touch2 第二个触摸点
|
||||
* @returns {number} 两点间距离
|
||||
*/
|
||||
getDistanceBetweenTouches(touch1, touch2) {
|
||||
const dx = touch1.clientX - touch2.clientX;
|
||||
const dy = touch1.clientY - touch2.clientY;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行快捷键对应的动作
|
||||
* @param {string} action 动作名称
|
||||
* @param {*} param 动作参数
|
||||
* @param {Event} event 原始事件
|
||||
*/
|
||||
executeAction(action, param, event) {
|
||||
switch (action) {
|
||||
case "undo":
|
||||
if (this.commandManager) {
|
||||
this.commandManager.undo();
|
||||
}
|
||||
break;
|
||||
|
||||
case "redo":
|
||||
if (this.commandManager) {
|
||||
this.commandManager.redo();
|
||||
}
|
||||
break;
|
||||
|
||||
case "copy":
|
||||
// 复制逻辑
|
||||
// console.log("复制当前选中图层");
|
||||
if (this.isRedGreenMode.value) return;
|
||||
this.layerManager.copyLayer(this.layerManager.activeLayerId.value);
|
||||
break;
|
||||
|
||||
case "paste":
|
||||
// 粘贴逻辑
|
||||
// console.log("粘贴");
|
||||
if (this.isRedGreenMode.value) return;
|
||||
this.layerManager.pasteLayer();
|
||||
break;
|
||||
|
||||
case "cut":
|
||||
// 剪切逻辑
|
||||
// console.log("剪切");
|
||||
if (this.isRedGreenMode.value) return;
|
||||
this.layerManager.cutLayer(this.layerManager.activeLayerId.value);
|
||||
break;
|
||||
|
||||
case "delete":
|
||||
// 删除逻辑
|
||||
// console.log("删除");
|
||||
if (this.isRedGreenMode.value) return;
|
||||
this.layerManager.removeLayer(this.layerManager.activeLayerId.value);
|
||||
break;
|
||||
|
||||
case "selectAll":
|
||||
// 全选逻辑
|
||||
// console.log("全选");
|
||||
if (this.isRedGreenMode.value) return;
|
||||
// 这里需要实现全选逻辑 TODO: 是否在选择模式下才可以全选?
|
||||
if (this.layerManager) {
|
||||
this.layerManager.selectAll();
|
||||
}
|
||||
break;
|
||||
|
||||
case "clearSelection":
|
||||
// 清除选择逻辑
|
||||
// console.log("清除选择");
|
||||
// 这里需要实现清除选择逻辑
|
||||
if (this.layerManager) {
|
||||
this.layerManager.clearSelection();
|
||||
}
|
||||
break;
|
||||
|
||||
case "save":
|
||||
// 保存逻辑
|
||||
// console.log("保存");
|
||||
break;
|
||||
|
||||
case "selectTool":
|
||||
// 选择工具
|
||||
if (this.toolManager && param) {
|
||||
this.toolManager.setToolWithCommand(param);
|
||||
}
|
||||
break;
|
||||
|
||||
case "up":
|
||||
case "down":
|
||||
case "left":
|
||||
case "right":
|
||||
// 方向键逻辑
|
||||
this.canvasManager.moveActiveObject(action);
|
||||
break;
|
||||
|
||||
case "increaseBrushSize":
|
||||
// 增大画笔尺寸
|
||||
if (this.toolManager && this.toolManager.brushManager) {
|
||||
const amount = param || 5;
|
||||
this.toolManager.brushManager.increaseBrushSize(amount);
|
||||
}
|
||||
break;
|
||||
|
||||
case "decreaseBrushSize":
|
||||
// 减小画笔尺寸
|
||||
if (this.toolManager && this.toolManager.brushManager) {
|
||||
const amount = param || 5;
|
||||
this.toolManager.brushManager.decreaseBrushSize(amount);
|
||||
}
|
||||
break;
|
||||
|
||||
case "increaseBrushOpacity":
|
||||
// 增大画笔透明度
|
||||
if (this.toolManager && this.toolManager.brushManager) {
|
||||
const amount = param || 0.01;
|
||||
this.toolManager.brushManager.increaseBrushOpacity(amount);
|
||||
}
|
||||
break;
|
||||
|
||||
case "decreaseTextureScale":
|
||||
// 减小画笔材质图片大小
|
||||
if (this.toolManager && this.toolManager.brushManager) {
|
||||
const amount = param || 5;
|
||||
this.toolManager.brushManager.decreaseBrushSize(amount);
|
||||
}
|
||||
break;
|
||||
|
||||
case "increaseTextureScale":
|
||||
// 增大画笔材质图片大小
|
||||
if (this.toolManager && this.toolManager.brushManager) {
|
||||
const amount = param || 0.01;
|
||||
this.toolManager.brushManager.increaseTextureScale(amount);
|
||||
}
|
||||
break;
|
||||
|
||||
case "decreaseBrushOpacity":
|
||||
// 减小画笔透明度
|
||||
if (this.toolManager && this.toolManager.brushManager) {
|
||||
const amount = param || 0.01;
|
||||
this.toolManager.brushManager.decreaseBrushOpacity(amount);
|
||||
}
|
||||
break;
|
||||
|
||||
case "toggleTempTool":
|
||||
// 临时切换工具
|
||||
if (param && this.toolManager) {
|
||||
this.setTempTool(param);
|
||||
}
|
||||
break;
|
||||
|
||||
case "newLayer":
|
||||
// 创建新图层
|
||||
if (this.layerManager) {
|
||||
this.layerManager.createNewLayer();
|
||||
}
|
||||
break;
|
||||
|
||||
case "addImageToNewLayer":
|
||||
this.toolManager?.openFile?.();
|
||||
break;
|
||||
|
||||
case "groupLayers":
|
||||
// 组合图层
|
||||
if (this.layerManager) {
|
||||
this.layerManager.groupSelectedLayers();
|
||||
}
|
||||
break;
|
||||
|
||||
case "ungroupLayers":
|
||||
// 解组图层
|
||||
if (this.layerManager) {
|
||||
this.layerManager.ungroupSelectedLayer();
|
||||
}
|
||||
break;
|
||||
|
||||
case "mergeLayers":
|
||||
// 合并图层
|
||||
if (this.layerManager) {
|
||||
this.layerManager.mergeSelectedLayers();
|
||||
}
|
||||
break;
|
||||
|
||||
case "contextMenu":
|
||||
// 上下文菜单(通常由右击或触控设备上的特定手势触发)
|
||||
// console.log("显示上下文菜单");
|
||||
// 这里需要实现显示上下文菜单的逻辑
|
||||
break;
|
||||
|
||||
default:
|
||||
// 调用自定义注册的动作处理
|
||||
if (this.customHandlers[action]) {
|
||||
this.customHandlers[action].execute(param, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置临时工具
|
||||
* @param {string} toolId 临时工具ID
|
||||
*/
|
||||
setTempTool(toolId) {
|
||||
if (!this.toolManager || this.tempToolState.active) return;
|
||||
|
||||
// 保存当前工具
|
||||
this.tempToolState.originalTool = this.toolManager.getCurrentTool();
|
||||
this.tempToolState.active = true;
|
||||
|
||||
// 切换到临时工具
|
||||
this.toolManager.setTool(toolId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复临时工具切换前的工具
|
||||
*/
|
||||
restoreTempTool() {
|
||||
if (!this.toolManager || !this.tempToolState.active) return;
|
||||
|
||||
// 恢复到原始工具
|
||||
if (this.tempToolState.originalTool) {
|
||||
this.toolManager.setTool(this.tempToolState.originalTool);
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
this.tempToolState.active = false;
|
||||
this.tempToolState.originalTool = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建快捷键标识符
|
||||
* @param {KeyboardEvent} event 键盘事件
|
||||
* @returns {string} 快捷键标识符
|
||||
*/
|
||||
buildShortcutKey(event) {
|
||||
let shortcutKey = "";
|
||||
|
||||
// 统一处理Mac和PC的修饰键
|
||||
if ((this.platform === "mac" && event.metaKey) || (this.platform !== "mac" && event.ctrlKey)) {
|
||||
shortcutKey += `${this.modifierKeys.cmdOrCtrl}+`;
|
||||
} else if (event.ctrlKey) {
|
||||
shortcutKey += "ctrl+";
|
||||
}
|
||||
|
||||
if (event.shiftKey) shortcutKey += "shift+";
|
||||
if (event.altKey) shortcutKey += "alt+";
|
||||
|
||||
const key = event.key.toLowerCase();
|
||||
// 特殊键处理
|
||||
switch (key) {
|
||||
case " ":
|
||||
shortcutKey += "space";
|
||||
break;
|
||||
case "arrowup":
|
||||
shortcutKey += "up";
|
||||
break;
|
||||
case "arrowdown":
|
||||
shortcutKey += "down";
|
||||
break;
|
||||
case "arrowleft":
|
||||
shortcutKey += "left";
|
||||
break;
|
||||
case "arrowright":
|
||||
shortcutKey += "right";
|
||||
break;
|
||||
default:
|
||||
shortcutKey += key;
|
||||
}
|
||||
|
||||
return shortcutKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查当前是否有输入框处于活动状态
|
||||
* @returns {boolean} 是否有输入框处于活动状态
|
||||
*/
|
||||
isInputActive() {
|
||||
const activeElement = document.activeElement;
|
||||
const tagName = activeElement.tagName.toLowerCase();
|
||||
return (
|
||||
tagName === "input" ||
|
||||
tagName === "textarea" ||
|
||||
activeElement.getAttribute("contenteditable") === "true"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的快捷键
|
||||
* @returns {Array} 快捷键列表
|
||||
*/
|
||||
getShortcuts() {
|
||||
return Object.entries(this.shortcuts).map(([key, value]) => ({
|
||||
key,
|
||||
displayKey: this.formatShortcutForDisplay(key),
|
||||
...value,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化快捷键以便显示
|
||||
* @param {string} shortcut 快捷键标识符
|
||||
* @returns {string} 格式化后的快捷键显示
|
||||
*/
|
||||
formatShortcutForDisplay(shortcut) {
|
||||
// 将快捷键格式化为适合当前平台显示的形式
|
||||
return shortcut
|
||||
.split("+")
|
||||
.map((key) => {
|
||||
// 将键名转换为显示符号
|
||||
return this.keySymbols[key.toLowerCase()] || key.toUpperCase();
|
||||
})
|
||||
.join("+");
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册自定义快捷键处理程序
|
||||
* @param {string} action 动作名称
|
||||
* @param {Object} handler 处理程序对象
|
||||
* @param {Function} handler.execute 执行函数
|
||||
* @param {Function} handler.onKeyUp 键释放处理函数(可选)
|
||||
* @param {string} description 描述
|
||||
*/
|
||||
registerCustomHandler(action, handler, description = "") {
|
||||
if (!action || typeof handler.execute !== "function") {
|
||||
console.error("无效的自定义处理程序");
|
||||
return;
|
||||
}
|
||||
|
||||
this.customHandlers[action] = handler;
|
||||
|
||||
// 如果提供了快捷键,添加到快捷键映射
|
||||
if (handler.shortcut) {
|
||||
this.shortcuts[handler.shortcut] = {
|
||||
action,
|
||||
description: description || handler.description || action,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose() {
|
||||
// 移除事件监听
|
||||
this.removeEvents();
|
||||
// 清除引用
|
||||
this.toolManager = null;
|
||||
this.commandManager = null;
|
||||
this.layerManager = null;
|
||||
this.container = null;
|
||||
this.customHandlers = {};
|
||||
this.tempToolState = { active: false, originalTool: null };
|
||||
this.touchState = {};
|
||||
}
|
||||
}
|
||||
34
src/components/Canvas/DepthCanvas/tools/canvasFactory.js
Normal file
34
src/components/Canvas/DepthCanvas/tools/canvasFactory.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { fabric } from "fabric-with-all";
|
||||
|
||||
/**
|
||||
* Factory for creating optimized fabric canvas instances
|
||||
*/
|
||||
export const createCanvas = (elementId, options = {}) => {
|
||||
// Create the canvas instance
|
||||
const canvas = new fabric.Canvas(elementId, {
|
||||
enableRetinaScaling: true,
|
||||
renderOnAddRemove: false,
|
||||
preserveObjectStacking: true, // 保持对象堆叠顺序
|
||||
// skipOffscreen: true, // 跳过离屏渲染
|
||||
imageSmoothingEnabled: true, // 启用图像平滑 - 抗锯齿
|
||||
imageSmoothingQuality: "high", // 设置高质量图像平滑
|
||||
...options,
|
||||
});
|
||||
|
||||
return canvas;
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility to create a static canvas (for improved performance when interaction is not needed)
|
||||
*/
|
||||
export const createStaticCanvas = (elementId, options = {}) => {
|
||||
const canvas = new fabric.StaticCanvas(elementId, {
|
||||
enableRetinaScaling: true,
|
||||
imageSmoothingEnabled: true, // 启用图像平滑 - 抗锯齿
|
||||
imageSmoothingQuality: "high", // 设置高质量图像平滑
|
||||
skipOffscreen: false, // 不跳过离屏渲染
|
||||
...options,
|
||||
});
|
||||
|
||||
return canvas;
|
||||
};
|
||||
61
src/components/Canvas/DepthCanvas/tools/index.ts
Normal file
61
src/components/Canvas/DepthCanvas/tools/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 精确检测设备类型,区分 PC、Mac、平板和移动设备
|
||||
* @private
|
||||
* @returns {Object} 设备信息对象
|
||||
*/
|
||||
export const detectDeviceType = () => {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
const platform = navigator.platform.toLowerCase();
|
||||
const hasTouchSupport =
|
||||
"ontouchstart" in window || navigator.maxTouchPoints > 0;
|
||||
|
||||
// 检测操作系统
|
||||
const isMac = /mac|darwin/.test(platform) || /macintosh/.test(userAgent);
|
||||
const isWindows = /win/.test(platform);
|
||||
const isLinux = /linux/.test(platform) && !/android/.test(userAgent);
|
||||
|
||||
// 检测设备类型 - 修复iPad检测逻辑
|
||||
const isMobile = /mobile|phone|android.*mobile|iphone/.test(userAgent);
|
||||
// 修复iPad检测:包括iOS iPad和Android平板
|
||||
const isTablet =
|
||||
/tablet|ipad|android(?!.*mobile)/.test(userAgent) ||
|
||||
/ipad/.test(userAgent) ||
|
||||
(navigator.maxTouchPoints &&
|
||||
navigator.maxTouchPoints > 1 &&
|
||||
/mac/.test(userAgent));
|
||||
const isDesktop = !isMobile && !isTablet;
|
||||
|
||||
// 检测浏览器类型(用于特定优化)
|
||||
const isSafari = /safari/.test(userAgent) && !/chrome/.test(userAgent);
|
||||
const isChrome = /chrome/.test(userAgent);
|
||||
const isFirefox = /firefox/.test(userAgent);
|
||||
|
||||
// 调试日志 - 仅在开发环境输出
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
// console.log("设备检测结果:", {
|
||||
// userAgent,
|
||||
// platform,
|
||||
// isMobile,
|
||||
// isTablet,
|
||||
// isDesktop,
|
||||
// hasTouchSupport,
|
||||
// maxTouchPoints: navigator.maxTouchPoints,
|
||||
// });
|
||||
}
|
||||
return {
|
||||
isMac,
|
||||
isWindows,
|
||||
isLinux,
|
||||
isMobile,
|
||||
isTablet,
|
||||
isDesktop,
|
||||
isSafari,
|
||||
isChrome,
|
||||
isFirefox,
|
||||
hasTouchSupport,
|
||||
// 判断是否应该使用触摸事件作为主要交互方式
|
||||
preferTouchEvents: (isMobile || isTablet) && !isDesktop,
|
||||
// 判断是否需要特殊的 Mac 触控板处理
|
||||
needsMacTrackpadOptimization: isMac && isDesktop && hasTouchSupport,
|
||||
};
|
||||
}
|
||||
66
src/components/Canvas/DepthCanvas/tools/layerHelper.js
Normal file
66
src/components/Canvas/DepthCanvas/tools/layerHelper.js
Normal file
@@ -0,0 +1,66 @@
|
||||
|
||||
/**
|
||||
* 图层类型枚举
|
||||
*/
|
||||
export const LayerType = {
|
||||
EMPTY: "empty", // 空图层
|
||||
BITMAP: "bitmap", // 位图图层
|
||||
VECTOR: "vector", // 矢量图层
|
||||
TEXT: "text", // 文字图层
|
||||
GROUP: "group", // 组图层
|
||||
ADJUSTMENT: "adjustment", // 调整图层
|
||||
SMART_OBJECT: "smartObject", // 智能对象
|
||||
SHAPE: "shape", // 形状图层
|
||||
VIDEO: "video", // 视频图层 (预留)
|
||||
AUDIO: "audio", // 音频图层 (预留)
|
||||
FIXED: "fixed", // 固定图层 - 位于背景图层之上,普通图层之下
|
||||
BACKGROUND: "background", // 背景图层 - 位于固定图层之、普通图层之下
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 画布操作模式枚举:draw(绘画)、select(选择)、pan(拖拽)....
|
||||
*/
|
||||
export const OperationType = {
|
||||
// 编辑器模式
|
||||
DISABLED: "disabled", // 禁用
|
||||
|
||||
SELECT: "select",// 选择工具模式
|
||||
PAN: "pan", // 拖拽模式
|
||||
DRAW: "draw", // 绘画模式
|
||||
ERASER: "eraser", // 橡皮擦模式
|
||||
IMAGE: "image",// 图片工具模式
|
||||
SELECTBOX: "selectbox",// 选择框工具模式
|
||||
RECTANGLE: "rectangle",// 矩形工具模式
|
||||
TEXT: "text",// 文字工具模式
|
||||
UNDO: "undo",// 撤销工具模式
|
||||
REDO: "redo",// 重做工具模式
|
||||
};
|
||||
|
||||
// 所有操作模式类型列表
|
||||
export const OperationTypes = Object.values(OperationType);
|
||||
|
||||
/**
|
||||
* 混合模式枚举
|
||||
* 与 fabricjs 和 CSS3 的 globalCompositeOperation 对应
|
||||
*/
|
||||
export const BlendMode = {
|
||||
NORMAL: "source-over", // 正常模式
|
||||
MULTIPLY: "multiply", // 正片叠底
|
||||
SCREEN: "screen", // 滤色
|
||||
OVERLAY: "overlay", // 叠加
|
||||
DARKEN: "darken", // 变暗
|
||||
LIGHTEN: "lighten", // 变亮
|
||||
COLOR_DODGE: "color-dodge", // 颜色减淡
|
||||
COLOR_BURN: "color-burn", // 颜色加深
|
||||
HARD_LIGHT: "hard-light", // 强光
|
||||
SOFT_LIGHT: "soft-light", // 柔光
|
||||
DIFFERENCE: "difference", // 差值
|
||||
EXCLUSION: "exclusion", // 排除
|
||||
HUE: "hue", // 色相
|
||||
SATURATION: "saturation", // 饱和度
|
||||
COLOR: "color", // 颜色
|
||||
LUMINOSITY: "luminosity", // 明度
|
||||
DESTINATION_IN: "destination-in", // 目标内
|
||||
DESTINATION_OUT: "destination-out", // 目标外
|
||||
};
|
||||
Reference in New Issue
Block a user