深度画布
This commit is contained in:
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, inject, computed } from 'vue'
|
import { ref, inject, computed } from 'vue'
|
||||||
import { TOOLS } from '../manager/ToolManager'
|
import { OperationType } from '../tools/layerHelper'
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
zoom: { default: 1, type: Number },
|
zoom: { default: 1, type: Number },
|
||||||
step: { default: 0.1, type: Number }
|
step: { default: 0.1, type: Number }
|
||||||
@@ -32,29 +32,29 @@
|
|||||||
const emit = defineEmits(['export', 'import'])
|
const emit = defineEmits(['export', 'import'])
|
||||||
const stateManager = inject('stateManager') as any
|
const stateManager = inject('stateManager') as any
|
||||||
const toolManager = inject('toolManager') 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 historyIndex = computed(() => stateManager.historyIndex.value)
|
||||||
const historyList = computed(() => stateManager.historyList.value)
|
const historyList = computed(() => stateManager.historyList.value)
|
||||||
const isUndo = computed(() => !historyList.value[historyIndex.value - 1])
|
const isUndo = computed(() => !historyList.value[historyIndex.value - 1])
|
||||||
const isRedo = computed(() => !historyList.value[historyIndex.value + 1])
|
const isRedo = computed(() => !historyList.value[historyIndex.value + 1])
|
||||||
const tools = ref([
|
const tools = ref([
|
||||||
{ name: TOOLS.SELECT, icon: 'dc-select', iconSize: 16, disabled: ref(false) },
|
{ name: OperationType.SELECT, icon: 'dc-select', iconSize: 16, disabled: ref(false) },
|
||||||
{ name: TOOLS.MOVE, icon: 'dc-move', iconSize: 18, disabled: ref(false) },
|
{ name: OperationType.PAN, icon: 'dc-move', iconSize: 18, disabled: ref(false) },
|
||||||
{ name: TOOLS.BRUSH, icon: 'dc-brush', iconSize: 18, disabled: ref(false) },
|
{ name: OperationType.DRAW, icon: 'dc-brush', iconSize: 18, disabled: ref(false) },
|
||||||
{ name: TOOLS.ERASER, icon: 'dc-eraser', iconSize: 18, disabled: ref(false) },
|
{ name: OperationType.ERASER, icon: 'dc-eraser', iconSize: 18, disabled: ref(false) },
|
||||||
{ name: TOOLS.IMAGE, icon: 'dc-image', iconSize: 17, disabled: ref(false) },
|
{ name: OperationType.IMAGE, icon: 'dc-image', iconSize: 17, disabled: ref(false) },
|
||||||
{ name: TOOLS.SELECTBOX, icon: 'dc-selectbox', iconSize: 16, disabled: ref(false) },
|
{ name: OperationType.SELECTBOX, icon: 'dc-selectbox', iconSize: 16, disabled: ref(false) },
|
||||||
{ name: TOOLS.RECTANGLE, icon: 'dc-rectangle', iconSize: 16, disabled: ref(false) },
|
{ name: OperationType.RECTANGLE, icon: 'dc-rectangle', iconSize: 16, disabled: ref(false) },
|
||||||
{ type: 'line' },
|
{ type: 'line' },
|
||||||
{
|
{
|
||||||
name: TOOLS.UNDO,
|
name: OperationType.UNDO,
|
||||||
icon: 'dc-undo',
|
icon: 'dc-undo',
|
||||||
iconSize: 18,
|
iconSize: 18,
|
||||||
disabled: isUndo,
|
disabled: isUndo,
|
||||||
on: () => stateManager.undoState()
|
on: () => stateManager.undoState()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: TOOLS.REDO,
|
name: OperationType.REDO,
|
||||||
icon: 'dc-redo',
|
icon: 'dc-redo',
|
||||||
iconSize: 18,
|
iconSize: 18,
|
||||||
disabled: isRedo,
|
disabled: isRedo,
|
||||||
|
|||||||
@@ -6,7 +6,12 @@
|
|||||||
<layer-panel />
|
<layer-panel />
|
||||||
<details-panel />
|
<details-panel />
|
||||||
<header-tools />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -23,9 +28,8 @@
|
|||||||
// 管理器
|
// 管理器
|
||||||
import { StateManager } from './manager/StateManager'
|
import { StateManager } from './manager/StateManager'
|
||||||
import { EventManager } from './manager/EventManager'
|
import { EventManager } from './manager/EventManager'
|
||||||
import { FlowManager } from './manager/FlowManager'
|
import { CanvasManager } from './manager/CanvasManager'
|
||||||
import { NodeManager } from './manager/NodeManager'
|
import { ToolManager } from './manager/ToolManager'
|
||||||
import { ToolManager, TOOLS } from './manager/ToolManager'
|
|
||||||
|
|
||||||
const canvasContainerRef = ref(null)
|
const canvasContainerRef = ref(null)
|
||||||
const canvasRef = ref(null)
|
const canvasRef = ref(null)
|
||||||
@@ -41,77 +45,51 @@
|
|||||||
const stateManager = new StateManager({})
|
const stateManager = new StateManager({})
|
||||||
provide('stateManager', stateManager)
|
provide('stateManager', stateManager)
|
||||||
|
|
||||||
|
// 画布管理器
|
||||||
|
const canvasManager = new CanvasManager({ stateManager })
|
||||||
|
stateManager.setManager({ canvasManager, canvasRef })
|
||||||
|
provide('canvasManager', canvasManager)
|
||||||
|
|
||||||
// 事件管理器
|
// 事件管理器
|
||||||
const eventManager = new EventManager({ stateManager })
|
const eventManager = new EventManager({ stateManager })
|
||||||
stateManager.setManager({ eventManager })
|
stateManager.setManager({ eventManager })
|
||||||
provide('eventManager', 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 })
|
stateManager.setManager({ toolManager })
|
||||||
provide('toolManager', toolManager)
|
provide('toolManager', toolManager)
|
||||||
const initCanvas = () => {
|
|
||||||
console.log('OverallCanvas: initCanvas')
|
const observer = ref(null)
|
||||||
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)
|
|
||||||
}
|
|
||||||
onMounted(() => {
|
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(() => {
|
onBeforeMount(() => {
|
||||||
// eventManager.removeEvents() // 移除事件
|
// eventManager.removeEvents() // 移除事件
|
||||||
})
|
})
|
||||||
|
async function handleWindowResize() {
|
||||||
|
console.log('==========画布窗口大小变化==========')
|
||||||
|
canvasManager.setCanvasViewSize({
|
||||||
|
canvasViewWidth: canvasContainerRef.value.clientWidth,
|
||||||
|
canvasViewHeight: canvasContainerRef.value.clientHeight
|
||||||
|
})
|
||||||
|
canvasManager.resetZoom()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
@import '@vue-flow/core/dist/style.css';
|
@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 {
|
export class EventManager {
|
||||||
stateManager: any
|
stateManager: any
|
||||||
vueFlow: any
|
vueFlow: any
|
||||||
@@ -30,14 +30,14 @@ export class EventManager {
|
|||||||
handleClick(event: any) {
|
handleClick(event: any) {
|
||||||
this.stateManager.setActiveNodeID("")
|
this.stateManager.setActiveNodeID("")
|
||||||
const tool = this.stateManager.tool.value
|
const tool = this.stateManager.tool.value
|
||||||
if (tool === TOOLS.TEXT) {
|
if (tool === OperationType.TEXT) {
|
||||||
const { x, y, zoom } = this.vueFlow.value.viewport
|
const { x, y, zoom } = this.vueFlow.value.viewport
|
||||||
const position = {
|
const position = {
|
||||||
x: (event.offsetX - x) / zoom,
|
x: (event.offsetX - x) / zoom,
|
||||||
y: (event.offsetY - y) / zoom
|
y: (event.offsetY - y) / zoom
|
||||||
}
|
}
|
||||||
this.stateManager.nodeManager.createTextNode({ position })
|
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'
|
import i18n from '@/lang'
|
||||||
const t = i18n.global.t
|
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
|
mxHistory: any
|
||||||
historyList: any
|
historyList: any
|
||||||
@@ -32,155 +13,56 @@ export class StateManager {
|
|||||||
|
|
||||||
// 管理器
|
// 管理器
|
||||||
eventManager: any
|
eventManager: any
|
||||||
flowManager: any
|
canvasManager: any
|
||||||
nodeManager: any
|
|
||||||
toolManager: any
|
toolManager: any
|
||||||
// 设置管理器
|
// 设置管理器
|
||||||
setManager(options) {
|
setManager(options) {
|
||||||
options.eventManager && (this.eventManager = options.eventManager)
|
options.eventManager && (this.eventManager = options.eventManager)
|
||||||
options.flowManager && (this.flowManager = options.flowManager)
|
options.canvasManager && (this.canvasManager = options.canvasManager)
|
||||||
options.nodeManager && (this.nodeManager = options.nodeManager)
|
|
||||||
options.toolManager && (this.toolManager = options.toolManager)
|
options.toolManager && (this.toolManager = options.toolManager)
|
||||||
}
|
}
|
||||||
constructor(options) {
|
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.mxHistory = ref(50)
|
||||||
this.historyList = ref([])
|
this.historyList = ref([])
|
||||||
this.historyIndex = ref(0)
|
this.historyIndex = ref(0)
|
||||||
|
|
||||||
this.activeNodeID = ref("")
|
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 }
|
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() {
|
recordState() {
|
||||||
if (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)
|
// this.historyList.value.splice(this.historyIndex.value + 1)
|
||||||
}
|
// }
|
||||||
const state = {
|
// const state = {
|
||||||
nodes: JSON.stringify(this.nodes.value)
|
// nodes: JSON.stringify(this.nodes.value)
|
||||||
}
|
// }
|
||||||
this.historyList.value.push(state)
|
// this.historyList.value.push(state)
|
||||||
const size = this.historyList.value.length - this.mxHistory.value
|
// const size = this.historyList.value.length - this.mxHistory.value
|
||||||
if (size > 0) this.historyList.value.splice(0, size)
|
// if (size > 0) this.historyList.value.splice(0, size)
|
||||||
this.historyIndex.value = this.historyList.value.length - 1
|
// this.historyIndex.value = this.historyList.value.length - 1
|
||||||
}
|
}
|
||||||
/** 撤回状态 */
|
/** 撤回状态 */
|
||||||
undoState() {
|
undoState() {
|
||||||
var index = this.historyIndex.value - 1
|
// var index = this.historyIndex.value - 1
|
||||||
const state = this.historyList.value[index]
|
// const state = this.historyList.value[index]
|
||||||
if (!state) return
|
// if (!state) return
|
||||||
this.historyIndex.value = index
|
// this.historyIndex.value = index
|
||||||
this.nodes.value = JSON.parse(state.nodes)
|
// this.nodes.value = JSON.parse(state.nodes)
|
||||||
}
|
}
|
||||||
/** 重做状态 */
|
/** 重做状态 */
|
||||||
redoState() {
|
redoState() {
|
||||||
var index = this.historyIndex.value + 1
|
// var index = this.historyIndex.value + 1
|
||||||
const state = this.historyList.value[index]
|
// const state = this.historyList.value[index]
|
||||||
if (!state) return
|
// if (!state) return
|
||||||
this.historyIndex.value = index
|
// this.historyIndex.value = index
|
||||||
this.nodes.value = JSON.parse(state.nodes)
|
// this.nodes.value = JSON.parse(state.nodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,69 @@
|
|||||||
export const TOOLS = {
|
import { ref } from 'vue'
|
||||||
SELECT: "SELECT",
|
import { OperationType } from '../tools/layerHelper'
|
||||||
MOVE: "MOVE",
|
export class ToolManager {
|
||||||
BRUSH: "BRUSH",
|
stateManager: any
|
||||||
ERASER: "ERASER",
|
canvasManager: any
|
||||||
IMAGE: "IMAGE",
|
currentTool: any
|
||||||
SELECTBOX: "SELECTBOX",
|
tools: any[]
|
||||||
RECTANGLE: "RECTANGLE",
|
constructor(options) {
|
||||||
TEXT: "TEXT",
|
this.stateManager = options.stateManager;
|
||||||
UNDO: "UNDO",
|
this.canvasManager = options.canvasManager;
|
||||||
REDO: "REDO",
|
this.currentTool = ref(null)
|
||||||
}
|
this.tools = [
|
||||||
export const tools = [
|
|
||||||
/** 选择工具 */
|
/** 选择工具 */
|
||||||
{
|
{
|
||||||
name: TOOLS.SELECT,
|
name: OperationType.SELECT,
|
||||||
nodesDraggable: true,
|
cursor: "default",
|
||||||
panOnDrag: false,
|
setup: this.setupSelectTool.bind(this),
|
||||||
|
selection: true,
|
||||||
},
|
},
|
||||||
/** 移动工具 */
|
/** 移动工具 */
|
||||||
{
|
{
|
||||||
name: TOOLS.MOVE,
|
name: OperationType.PAN,
|
||||||
nodesDraggable: false,
|
cursor: "grab",
|
||||||
panOnDrag: true,
|
setup: this.setupMoveTool.bind(this),
|
||||||
},
|
},
|
||||||
/** 文本工具 */
|
/** 画笔工具 */
|
||||||
{
|
{
|
||||||
name: TOOLS.TEXT,
|
name: OperationType.DRAW,
|
||||||
cursor: "text",
|
cursor: "crosshair",
|
||||||
nodesDraggable: false,
|
|
||||||
panOnDrag: false,
|
|
||||||
},
|
},
|
||||||
|
/** 橡皮擦工具 */
|
||||||
]
|
{
|
||||||
export class ToolManager {
|
name: OperationType.ERASER,
|
||||||
stateManager: any
|
cursor: "crosshair",
|
||||||
vueFlow: any
|
},
|
||||||
constructor(options) {
|
/** 智能选框工具 */
|
||||||
this.stateManager = options.stateManager;
|
{
|
||||||
this.vueFlow = options.vueFlow
|
name: OperationType.SELECTBOX,
|
||||||
this.setTool(TOOLS.SELECT)
|
cursor: "crosshair",
|
||||||
|
},
|
||||||
|
/** 矩形工具 */
|
||||||
|
{
|
||||||
|
name: OperationType.RECTANGLE,
|
||||||
|
cursor: "crosshair",
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
setTool(value: string) {
|
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}不存在`)
|
if (!tool) return console.warn(`工具${tool}不存在`)
|
||||||
this.stateManager.tool.value = tool.name
|
this.currentTool.value = tool.name
|
||||||
this.stateManager.setNodesDraggable(!!tool.nodesDraggable)
|
this.canvasManager.canvas.defaultCursor = tool.cursor
|
||||||
this.stateManager.setPanOnDrag(!!tool.panOnDrag)
|
this.setCanvasEvented(!!tool.selection)
|
||||||
this.stateManager.setCursor(tool.cursor || "")
|
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