Files
FiDA_Front/src/components/Canvas/DepthCanvas/manager/animationManager.ts
2026-03-09 14:06:59 +08:00

844 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}