diff --git a/src/components/Canvas/DepthCanvas/manager/animationManager.ts b/src/components/Canvas/DepthCanvas/manager/animationManager.ts new file mode 100644 index 0000000..999dc99 --- /dev/null +++ b/src/components/Canvas/DepthCanvas/manager/animationManager.ts @@ -0,0 +1,843 @@ +import { gsap } from 'gsap' + +/** + * 画布动画管理器 + * 负责处理画布平移、缩放等动画效果 + */ +export class AnimationManager { + /** + * 创建动画管理器 + * @param {fabric.Canvas} canvas fabric.js画布实例 + * @param {Object} options 配置选项 + */ + constructor(canvas, options = {}) { + 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) { + 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: 0, + panY: 0, + 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, 0, 0]) + this.currentZoom.value = 100 + this._zoomAnimation = null + this._panAnimation = null + resolve() + } + }) + } else { + this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]) + 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 + } +}