接入画布

This commit is contained in:
X1627315083
2025-06-09 10:25:54 +08:00
parent 87a08f5f8f
commit c266967f16
157 changed files with 43833 additions and 1571 deletions

View File

@@ -0,0 +1,909 @@
import { TransformCommand } from "../../commands/StateCommands";
import { generateId } from "../../utils/helper";
import { OperationType, OperationTypes } from "../../utils/layerHelper";
export class CanvasEventManager {
constructor(canvas, options = {}) {
this.canvas = canvas;
this.toolManager = options.toolManager || null;
this.animationManager = options.animationManager;
this.thumbnailManager = options.thumbnailManager;
this.editorMode = options.editorMode || OperationType.SELECT;
this.activeElementId = options.activeElementId || { value: null };
this.layerManager = options.layerManager || null;
this.layers = options.layers || null;
// 事件处理的内部状态 - 优化设备检测
this.deviceInfo = this._detectDeviceType();
this.dragStartTime = 0;
this.lastMousePositions = [];
this.positionHistoryLimit = 5; // 追踪鼠标位置的历史记录,用于计算速度
this.longPressTimer = null;
this.longPressThreshold = 500;
// 初始化所有事件
this.initEvents();
}
initEvents() {
this.setupZoomEvents();
// 优化三端设备的事件处理逻辑
if (this.deviceInfo.isMobile || this.deviceInfo.isTablet) {
// 真正的移动设备和平板设备使用触摸事件
this.setupTouchEvents();
} else {
// PC 和 Mac 设备主要使用鼠标事件
this.setupMouseEvents();
}
// Mac 设备需要额外的触摸手势支持(用于特殊场景)
if (this.deviceInfo.isMac && this.deviceInfo.hasTouchSupport) {
this.setupMacTouchGestures();
}
// 共享事件
this.setupSelectionEvents();
this.setupObjectEvents();
this.setupDoubleClickEvents();
// this.setupHandlePathCreated();
}
setupZoomEvents() {
// 水平/垂直滚动相关状态
this._scrollWheelEvents = [];
this._scrollAccumulatedDelta = { x: 0, y: 0 };
this._scrollAccumulationTimeout = null;
this._scrollAccumulationTime = 100; // 降低滚轮累积时间窗口
this._lastScrollTime = 0; // 跟踪上次滚动时间
this._scrollThrottleDelay = 5; // 滚动节流延迟(毫秒)
// 缩放处理 - 使用动画管理器,针对 Mac 设备优化
this.canvas.on("mouse:wheel", (opt) => {
// Mac 设备双指滚动优化:确保滚动事件正确处理
if (this.deviceInfo.isMac) {
// Mac设备的简化处理逻辑减少不必要的动画中断
// 让动画管理器自行处理冲突,避免过度干预
} else {
// 非 Mac 设备的标准处理
if (
this.animationManager._panAnimation ||
this.animationManager._zoomAnimation
) {
this.animationManager._wasPanning =
!!this.animationManager._panAnimation;
this.animationManager._wasZooming =
!!this.animationManager._zoomAnimation;
this.animationManager.smoothStopAnimations({ duration: 0.1 });
}
}
// 按住 Ctrl 键时实现垂直滚动Mac 下是 Cmd 键)
const isCtrlOrCmd = this.deviceInfo.isMac ? opt.e.metaKey : opt.e.ctrlKey;
if (isCtrlOrCmd) {
this.handleScrollWheel(opt, "vertical");
opt.e.preventDefault();
return;
}
// 按住 Shift 键时实现水平滚动
if (opt.e.shiftKey) {
this.handleScrollWheel(opt, "horizontal");
opt.e.preventDefault();
return;
}
// 标准缩放行为 - 让 AnimationManager 处理平滑过渡
// Mac 设备下的双指滚动将直接进入这里进行缩放
this.animationManager.handleMouseWheel(opt);
});
}
/**
* 处理滚轮滚动事件
* @param {Object} opt 滚轮事件对象
* @param {String} direction 滚动方向: 'vertical' 或 'horizontal'
*/
handleScrollWheel(opt, direction) {
// 获取当前视图变换
const vpt = this.canvas.viewportTransform.slice(0); // 创建副本避免直接修改
const zoom = this.canvas.getZoom();
// 计算滚动量 - 根据方向决定是水平还是垂直滚动
let deltaX = 0;
let deltaY = 0;
// 设置滚动方向和距离
if (direction === "horizontal") {
deltaX = opt.e.deltaY; // 水平滚动
} else {
deltaY = opt.e.deltaY; // 垂直滚动
}
// 计算滚动因子,基于缩放级别和设备类型调整
let scrollFactor = Math.max(0.4, Math.min(1, 1 / zoom));
// Mac 设备优化:触控板滚动通常比鼠标滚轮更敏感
if (this.deviceInfo.isMac) {
const isMacTrackpadScroll =
Math.abs(opt.e.deltaY) < 100 && opt.e.deltaMode === 0;
if (isMacTrackpadScroll) {
// Mac 触控板滚动更细腻,需要调整滚动因子
scrollFactor *= 0.8; // 降低滚动敏感度
}
}
// 直接应用滚动变化,不使用累积和计时器
vpt[4] -= deltaX * scrollFactor;
vpt[5] -= deltaY * scrollFactor;
// 直接设置新的视图变换,不使用动画
this.canvas.setViewportTransform(vpt);
// 请求重新渲染画布
this.canvas.renderAll();
}
/**
* 处理累积的滚轮滚动事件并应用平移
* @private
* @param {String} direction 滚动方向
*/
_processAccumulatedScroll(direction) {
// 这个函数不再需要,但为了兼容性保留空实现
// 所有滚动逻辑已经移到 handleScrollWheel 中直接处理
return;
}
/**
* 停止所有惯性动画
* @param {boolean} smooth 是否平滑过渡,默认为 false立即停止
*/
stopInertiaAnimation(smooth = false) {
if (this.animationManager) {
if (this.animationManager._panAnimation && !smooth) {
this.animationManager._panAnimation.kill();
this.animationManager._panAnimation = null;
}
if (this.animationManager._zoomAnimation && !smooth) {
this.animationManager._zoomAnimation.kill();
this.animationManager._zoomAnimation = null;
}
}
}
/**
* 设置鼠标事件处理
*/
setupMouseEvents() {
// 鼠标按下事件
this.canvas.on("mouse:down", (opt) => {
// 平滑停止任何正在进行的惯性动画
this.stopInertiaAnimation(true);
if (
opt.e.altKey ||
opt.e.which === 2 ||
this.editorMode === OperationType.PAN
) {
this.canvas.isDragging = true;
this.canvas.lastPosX = opt.e.clientX;
this.canvas.lastPosY = opt.e.clientY;
this.canvas.defaultCursor = "grabbing";
// 记录拖动开始时间和位置,用于计算速度
this.dragStartTime = Date.now();
this.lastMousePositions = []; // 重置位置历史
if (this.canvas.isDragging) {
this.canvas.selection = false;
this.canvas.renderAll();
}
}
});
// 鼠标移动事件
this.canvas.on("mouse:move", (opt) => {
if (!this.canvas.isDragging) return;
const vpt = this.canvas.viewportTransform;
vpt[4] += opt.e.clientX - this.canvas.lastPosX;
vpt[5] += opt.e.clientY - this.canvas.lastPosY;
// 记录鼠标位置和时间,用于计算惯性
const now = Date.now();
this.lastMousePositions.push({
x: opt.e.clientX,
y: opt.e.clientY,
time: now,
});
// 保持历史记录在限定数量内
if (this.lastMousePositions.length > this.positionHistoryLimit) {
this.lastMousePositions.shift();
}
this.canvas.renderAll();
this.canvas.lastPosX = opt.e.clientX;
this.canvas.lastPosY = opt.e.clientY;
});
// 鼠标抬起事件
this.canvas.on("mouse:up", (opt) => {
this.handleDragEnd(opt);
});
}
/**
* 设置触摸事件处理
*/
setupTouchEvents() {
// 触摸开始事件
this.canvas.on("touch:gesture", (opt) => {
// 平滑停止任何正在进行的惯性动画
this.stopInertiaAnimation(true);
if (opt.e.touches && opt.e.touches.length === 2) {
this.canvas.isDragging = true;
this.canvas.lastPosX =
(opt.e.touches[0].clientX + opt.e.touches[1].clientX) / 2;
this.canvas.lastPosY =
(opt.e.touches[0].clientY + opt.e.touches[1].clientY) / 2;
// 重置触摸位置历史
this.dragStartTime = Date.now();
this.lastMousePositions = [];
if (this.canvas.isDragging) {
this.canvas.selection = false;
this.canvas.renderAll();
}
opt.e.preventDefault();
}
});
// 单指触摸开始 - 处理拖动
this.canvas.on("touch:drag", (opt) => {
// 平滑停止任何正在进行的惯性动画
this.stopInertiaAnimation(true);
if (this.editorMode === OperationType.PAN) {
this.canvas.isDragging = true;
this.canvas.lastPosX = opt.e.touches[0].clientX;
this.canvas.lastPosY = opt.e.touches[0].clientY;
this.dragStartTime = Date.now();
this.lastMousePositions = [];
if (this.canvas.isDragging) {
this.canvas.selection = false;
this.canvas.renderAll();
}
opt.e.preventDefault();
}
});
// 触摸移动事件
this.canvas.on("touch:gesture:update", (opt) => {
if (!this.canvas.isDragging) return;
if (opt.e.touches && opt.e.touches.length === 2) {
const currentX =
(opt.e.touches[0].clientX + opt.e.touches[1].clientX) / 2;
const currentY =
(opt.e.touches[0].clientY + opt.e.touches[1].clientY) / 2;
const vpt = this.canvas.viewportTransform;
vpt[4] += currentX - this.canvas.lastPosX;
vpt[5] += currentY - this.canvas.lastPosY;
// 记录触摸位置和时间
const now = Date.now();
this.lastMousePositions.push({
x: currentX,
y: currentY,
time: now,
});
// 保持历史记录在限定数量内
if (this.lastMousePositions.length > this.positionHistoryLimit) {
this.lastMousePositions.shift();
}
this.canvas.renderAll();
this.canvas.lastPosX = currentX;
this.canvas.lastPosY = currentY;
opt.e.preventDefault();
}
});
// 单指拖动更新
this.canvas.on("touch:drag:update", (opt) => {
if (!this.canvas.isDragging || this.editorMode !== OperationType.PAN)
return;
const currentX = opt.e.touches[0].clientX;
const currentY = opt.e.touches[0].clientY;
const vpt = this.canvas.viewportTransform;
vpt[4] += currentX - this.canvas.lastPosX;
vpt[5] += currentY - this.canvas.lastPosY;
// 记录触摸位置和时间
const now = Date.now();
this.lastMousePositions.push({
x: currentX,
y: currentY,
time: now,
});
if (this.lastMousePositions.length > this.positionHistoryLimit) {
this.lastMousePositions.shift();
}
this.canvas.renderAll();
this.canvas.lastPosX = currentX;
this.canvas.lastPosY = currentY;
opt.e.preventDefault();
});
// 触摸结束事件
this.canvas.on("touch:gesture:end", (opt) => {
this.handleDragEnd(opt, true);
});
// 单指拖动结束
this.canvas.on("touch:drag:end", (opt) => {
this.handleDragEnd(opt, true);
});
}
/**
* 处理拖动结束(鼠标抬起或触摸结束)
*/
handleDragEnd(opt, isTouch = false) {
if (this.canvas.isDragging) {
// 使用动画管理器处理惯性效果
if (this.lastMousePositions.length > 1 && opt && opt.e) {
this.animationManager.applyInertiaEffect(
this.lastMousePositions,
isTouch
);
}
}
this.canvas.isDragging = false;
if (this.toolManager) {
this.toolManager.restoreSelectionState(); // 恢复选择状态
}
this.canvas.renderAll();
}
setupSelectionEvents() {
// 监听对象选择事件
this.canvas.on("selection:created", (opt) => this.updateSelectedLayer(opt));
this.canvas.on("selection:updated", (opt) => this.updateSelectedLayer(opt));
// this.canvas.on("selection:cleared", () => this.clearSelectedElements());
}
setupObjectEvents() {
// 监听对象变化事件,用于更新缩略图
this.canvas.on("object:added", (e) => {
if (this.thumbnailManager && e.target && e.target.id) {
// 延迟更新以确保对象完全添加
setTimeout(() => {
// 现在图层就是元素本身,直接更新元素的缩略图
this.thumbnailManager.generateLayerThumbnail(
e.target.layerId,
e.target
);
}, 300);
}
});
// 添加对象开始变换时的状态捕获
this.canvas.on(
"object:moving",
this._captureInitialTransformState.bind(this)
);
this.canvas.on(
"object:scaling",
this._captureInitialTransformState.bind(this)
);
this.canvas.on(
"object:rotating",
this._captureInitialTransformState.bind(this)
);
this.canvas.on(
"object:skewing",
this._captureInitialTransformState.bind(this)
);
this.canvas.on("object:modified", (e) => {
// 移除调试日志
// console.log("object:modified", e);
const activeObj = e.target || this.canvas.getActiveObject();
if (activeObj && this.layerManager?.commandManager) {
// 使用新的轻量级 TransformCommand 替代完整状态保存
// 检查对象是否有初始变换状态记录
if (activeObj._initialTransformState) {
// 创建并执行 TransformCommand只记录变换属性的变化
const transformCmd = new TransformCommand({
canvas: this.canvas,
objectId: activeObj.id,
initialState: activeObj._initialTransformState,
finalState: TransformCommand.captureTransformState(activeObj),
objectType: activeObj.type,
name: `变换 ${activeObj.type || "对象"}`,
});
// 执行并将命令添加到历史栈
this.layerManager.commandManager.execute(transformCmd, {
name: "对象修改",
});
// 清除临时状态记录
delete activeObj._initialTransformState;
}
}
if (this.thumbnailManager && e.target) {
if (e.target.id) {
this.updateLayerThumbnail(e.target.id, e.target);
// 如果该元素是分组图层的一部分,也更新分组图层的缩略图
if (e.target.parentId) {
this.updateLayerThumbnail(e.target.parentId);
}
}
}
});
this.canvas.on("object:removed", (e) => {
if (this.thumbnailManager && e.target) {
if (e.target.id) {
this.thumbnailManager.clearElementThumbnail(e.target.id);
// 如果该元素是分组图层的一部分,也更新分组图层的缩略图
if (e.target.parentId) {
setTimeout(() => this.updateLayerThumbnail(e.target.parentId), 50);
}
}
}
});
// // 鼠标抬起时,检查是否需要保存状态
// this.canvas.on("mouse:up", (e) => {
// // 只在选择模式下处理对象变换的状态保存
// if (this.editorMode !== OperationType.SELECT) {
// // 绘画、擦除等模式通过各自的命令管理状态,不需要在这里保存
// return;
// }
// const activeObj = this.canvas.getActiveObject();
// if (
// activeObj &&
// activeObj._stateRecord &&
// activeObj._stateRecord.isModifying
// ) {
// const original = activeObj._stateRecord.originalState;
// // 检查是否是真正的变换操作(移动、缩放、旋转)
// const hasTransformChanged =
// original.left !== activeObj.left ||
// original.top !== activeObj.top ||
// original.scaleX !== activeObj.scaleX ||
// original.scaleY !== activeObj.scaleY ||
// original.angle !== activeObj.angle;
// // 只有在对象发生变换且不是命令执行过程中时才保存状态
// if (hasTransformChanged && this.layerManager) {
// // 立即保存状态,而不是延迟执行
// this.layerManager.saveCanvasState();
// delete activeObj._stateRecord;
// } else {
// // 清理状态记录,即使没有保存状态
// delete activeObj._stateRecord;
// }
// }
// });
}
setupDoubleClickEvents() {
// 双击处理
this.canvas.on("mouse:dblclick", (opt) => {
if (opt.target) {
// 双击对象的特殊处理
} else {
// 双击空白处重置缩放
if (this.animationManager) {
this.animationManager.resetZoom(true);
}
}
});
}
setupLongPress(callback) {
this.canvas.on("mouse:down", (opt) => {
if (!opt.target) return;
this.longPressTimer = setTimeout(() => {
callback(opt);
}, this.longPressThreshold);
});
this.canvas.on("mouse:up", () => {
clearTimeout(this.longPressTimer);
});
this.canvas.on("mouse:move", () => {
clearTimeout(this.longPressTimer);
});
}
// 设置路径创建事件
setupHandlePathCreated() {
// 在 CanvasEventManager 的构造函数或初始化方法中
// this.canvas.on("path:created", this._handlePathCreated.bind(this));
}
_handlePathCreated(e) {
// // 获取新创建的路径对象
// const path = e.path;
// // 设置路径的ID和其他属性
// path.id = generateId(); // 生成唯一ID
// // 获取当前活动图层
// const activeLayer = this.layerManager.getActiveLayer();
// // 将路径对象绑定到当前活动图层
// if (activeLayer) {
// // 设置路径的图层ID
// path.layerId = activeLayer.id;
// // 更新图层对象列表
// if (!activeLayer.fabricObjects) activeLayer.fabricObjects = [];
// activeLayer.fabricObjects.push(path);
// // 更新图层缩略图
// if (this.thumbnailManager) {
// this.thumbnailManager.generateLayerThumbnail(activeLayer.id);
// }
// }
}
/**
* 合并图层中的对象为图像以提高性能
* @param {Object} options 合并选项
* @param {fabric.Image} options.fabricImage 新的图像对象
* @param {Object} options.activeLayer 当前活动图层
* @private
*/
async mergeLayerObjectsForPerformance({ fabricImage, activeLayer }) {
// 确保有命令管理器
if (!this.layerManager || !this.layerManager.commandManager) {
console.warn("合并对象失败:没有命令管理器");
return;
}
// 确保有活动图层
if (!activeLayer) {
console.warn("合并对象失败:没有活动图层");
return;
}
// 验证是否需要合并
const hasExistingObjects =
Array.isArray(activeLayer.fabricObjects) &&
activeLayer.fabricObjects.length > 0;
const hasNewImage = !!fabricImage;
if (!hasExistingObjects && !hasNewImage) {
console.log("没有对象需要合并");
return;
}
// 如果只有一个新图像且图层为空,直接添加到图层
if (hasNewImage && !hasExistingObjects) {
this.layerManager.addObjectToLayer(fabricImage, activeLayer.id);
return;
}
// 执行高保真合并操作
try {
console.log(`开始合并图层 ${activeLayer.name} 中的对象为组...`);
const command = await this.layerManager.LayerObjectsToGroup(
activeLayer,
fabricImage
);
this.layerManager?.commandManager?.execute?.(command, {
name: `合并图层 ${activeLayer.name} 中的对象为组`,
});
} catch (error) {
console.error("合并图层对象时发生错误:", error);
// 降级处理:如果合并失败,至少保证新图像能添加到图层
if (fabricImage && this.layerManager) {
console.log("执行降级处理:直接添加图像到图层");
this.layerManager.addObjectToLayer(fabricImage, activeLayer.id);
}
}
}
updateSelectedLayer(opt) {
const selected = opt.selected[0];
if (selected) {
this.layerManager.activeLayerId.value = selected.layerId;
}
}
// clearSelectedElements() {
// this.activeElementId.value = null;
// }
// 更新图层缩略图
updateLayerThumbnail(layerId) {
if (!this.thumbnailManager || !layerId || !this.layers) return;
const layer = this.layers.value.find((l) => l.id === layerId);
if (layer) {
this.thumbnailManager.generateLayerThumbnail(layer);
}
}
// 更新子元素组合缩略图
updateLayerChidrenThumbnail(layerId, fabricObject) {
if (!this.thumbnailManager || !fabricObject || !this.layers) return;
// 查找对应的图层(现在元素就是图层)
const layer = this.layers.value.find(
(l) =>
l.id === elementId ||
(l.fabricObjects && l.fabricObjects?.[0]?.id === layerId)
);
if (layer) {
// 生成图层缩略图
this.thumbnailManager.generateLayerThumbnail(layer);
}
// 同时也维护元素缩略图,以保持向后兼容性
this.thumbnailManager.generateElementThumbnail(
{ id: elementId, type: fabricObject.type },
fabricObject
);
}
/**
* 设置编辑器模式
* @param {string} mode 编辑器模式
*/
setEditorMode(mode) {
if (!OperationTypes.includes(mode)) {
console.warn(`不支持的编辑器模式: ${mode}`);
return;
}
// 切换工具时,立即停止任何惯性动画,但使用平滑过渡
this.stopInertiaAnimation(true);
this.editorMode = mode;
// 如果切换到选择模式,还原鼠标指针
if (mode === OperationType.SELECT) {
this.canvas.defaultCursor = "default";
} else if (mode === OperationType.PAN) {
this.canvas.defaultCursor = "grab";
}
}
dispose() {
// 移除所有事件监听
this.canvas.off();
// 清理 Mac 专用的原生事件监听器
if (this.deviceInfo.isMac && this.canvas.upperCanvasEl) {
const upperCanvas = this.canvas.upperCanvasEl;
// 移除手势事件监听器
upperCanvas.removeEventListener("gesturestart", null);
upperCanvas.removeEventListener("gesturechange", null);
upperCanvas.removeEventListener("gestureend", null);
upperCanvas.removeEventListener("wheel", null);
}
// 清除计时器
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
// 停止所有动画
this.stopInertiaAnimation();
}
/**
* 捕获对象开始变换时的初始状态
* @private
* @param {Object} e 事件对象
*/
_captureInitialTransformState(e) {
const obj = e.target;
// 只在首次触发变换事件时记录初始状态
if (obj && !obj._initialTransformState && obj.id) {
// 捕获对象的初始变换状态
obj._initialTransformState = TransformCommand.captureTransformState(obj);
// 添加调试日志(可选)
// console.log(`捕获对象 ${obj.id} (${obj.type}) 的初始变换状态`);
}
}
/**
* 精确检测设备类型,区分 PC、Mac、平板和移动设备
* @private
* @returns {Object} 设备信息对象
*/
_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);
// 检测设备类型
const isMobile = /mobile|phone|android.*mobile|iphone/.test(userAgent);
const isTablet = /tablet|ipad|android(?!.*mobile)/.test(userAgent);
const isDesktop = !isMobile && !isTablet;
// 检测浏览器类型(用于特定优化)
const isSafari = /safari/.test(userAgent) && !/chrome/.test(userAgent);
const isChrome = /chrome/.test(userAgent);
const isFirefox = /firefox/.test(userAgent);
return {
isMac,
isWindows,
isLinux,
isMobile,
isTablet,
isDesktop,
isSafari,
isChrome,
isFirefox,
hasTouchSupport,
// 判断是否应该使用触摸事件作为主要交互方式
preferTouchEvents: (isMobile || isTablet) && !isDesktop,
// 判断是否需要特殊的 Mac 触控板处理
needsMacTrackpadOptimization: isMac && isDesktop && hasTouchSupport,
};
}
/**
* 设置 Mac 专用的触摸手势处理
* 主要用于处理触控板的多指手势,但不干扰双指滚动的缩放功能
*/
setupMacTouchGestures() {
// Mac 触控板专用:三指拖拽进行画布平移
let macGestureState = {
isThreeFingerDrag: false,
startX: 0,
startY: 0,
};
// 监听 Mac 专用的手势事件
this.canvas.upperCanvasEl.addEventListener(
"gesturestart",
(e) => {
// 阻止浏览器默认的手势行为,但保留双指缩放
if (e.scale !== 1) {
e.preventDefault();
}
},
{ passive: false }
);
this.canvas.upperCanvasEl.addEventListener(
"gesturechange",
(e) => {
// 只处理三指以上的手势,保留双指缩放给 mouse:wheel 事件
if (e.touches && e.touches.length >= 3) {
e.preventDefault();
if (!macGestureState.isThreeFingerDrag) {
macGestureState.isThreeFingerDrag = true;
macGestureState.startX = e.pageX;
macGestureState.startY = e.pageY;
this.canvas.isDragging = true;
this.canvas.lastPosX = e.pageX;
this.canvas.lastPosY = e.pageY;
this.stopInertiaAnimation(true);
} else {
// 执行三指拖拽平移
const vpt = this.canvas.viewportTransform;
vpt[4] += e.pageX - this.canvas.lastPosX;
vpt[5] += e.pageY - this.canvas.lastPosY;
this.canvas.renderAll();
this.canvas.lastPosX = e.pageX;
this.canvas.lastPosY = e.pageY;
}
}
},
{ passive: false }
);
this.canvas.upperCanvasEl.addEventListener(
"gestureend",
(e) => {
if (macGestureState.isThreeFingerDrag) {
macGestureState.isThreeFingerDrag = false;
this.canvas.isDragging = false;
if (this.toolManager) {
this.toolManager.restoreSelectionState();
}
this.canvas.renderAll();
}
},
{ passive: false }
);
// 添加 Mac 专用的鼠标滚轮优化,确保双指滚动正常工作
this.setupMacScrollOptimization();
}
/**
* Mac 滚轮优化:确保双指滚动正确触发缩放
*/
setupMacScrollOptimization() {
if (!this.deviceInfo.isMac) return;
// Mac 下的滚轮事件优化
let macScrollState = {
lastWheelTime: 0,
wheelTimeout: null,
};
// 监听原生滚轮事件,确保 Mac 双指滚动正确处理
this.canvas.upperCanvasEl.addEventListener(
"wheel",
(e) => {
const now = Date.now();
// Mac 双指滚动的特征:通常有较高的 deltaY 精度和连续性
const isMacTrackpadScroll =
this.deviceInfo.isMac &&
Math.abs(e.deltaY) < 100 && // 像素模式
e.deltaMode === 0; // 像素模式
if (isMacTrackpadScroll) {
// 清除之前的超时
if (macScrollState.wheelTimeout) {
clearTimeout(macScrollState.wheelTimeout);
}
// 确保这个事件会被 Fabric.js 的 mouse:wheel 正确处理
macScrollState.lastWheelTime = now;
// 设置短暂延迟,防止与触摸事件冲突
macScrollState.wheelTimeout = setTimeout(() => {
// 滚轮事件处理完成
}, 16); // 约一帧的时间
}
},
{ passive: true }
);
}
}

View File

@@ -0,0 +1,720 @@
/**
* 键盘管理器
* 负责处理编辑器中的键盘事件和快捷键
* 支持PC、Mac和iPad三端适配
*/
export class KeyboardManager {
/**
* 创建键盘管理器
* @param {Object} options 配置选项
* @param {Object} options.toolManager 工具管理器实例
* @param {Object} options.commandManager 命令管理器实例
* @param {Object} options.layerManager 图层管理器实例
* @param {HTMLElement} options.container 容器元素,用于添加事件监听
*/
constructor(options = {}) {
this.toolManager = options.toolManager;
this.commandManager = options.commandManager;
this.layerManager = options.layerManager;
this.container = options.container || document;
// 检测平台类型
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._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: "粘贴" },
[`${cmdOrCtrl}+x`]: { action: "cut", description: "剪切" },
// 删除
delete: { action: "delete", description: "删除" },
backspace: { action: "delete", 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: "重做" },
}),
};
}
/**
* 初始化并开始监听键盘事件
*/
init() {
// 添加键盘事件监听
this.container.addEventListener("keydown", this._handleKeyDown);
this.container.addEventListener("keyup", this._handleKeyUp);
// 如果是触摸设备,添加触摸事件监听
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}`
);
}
/**
* 处理键盘按下事件
* @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}+`)) {
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("复制当前选中图层");
this.layerManager.copyLayer(this.layerManager.activeLayerId.value);
break;
case "paste":
// 粘贴逻辑
console.log("粘贴");
this.layerManager.pasteLayer();
break;
case "cut":
// 剪切逻辑
console.log("剪切");
this.layerManager.cutLayer(this.layerManager.activeLayerId.value);
break;
case "delete":
// 删除逻辑
console.log("删除");
this.layerManager.removeLayer(this.layerManager.activeLayerId.value);
break;
case "selectAll":
// 全选逻辑
console.log("全选");
// 这里需要实现全选逻辑 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 "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.container.removeEventListener("keydown", this._handleKeyDown);
this.container.removeEventListener("keyup", this._handleKeyUp);
// 如果有触摸事件,也移除它们
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);
}
// 清除引用
this.toolManager = null;
this.commandManager = null;
this.layerManager = null;
this.container = null;
this.customHandlers = {};
this.tempToolState = { active: false, originalTool: null };
this.touchState = {};
}
}