Files
FiDA_Front/src/components/Canvas/DepthCanvas/manager/AnimationManager.js

851 lines
24 KiB
JavaScript
Raw Normal View History

2026-03-09 13:44:32 +08:00
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;
2026-03-09 16:45:30 +08:00
this.currentZoom = options.currentZoom;
2026-03-09 13:44:32 +08:00
// 动画相关属性
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;
}
}