// #ifdef APP-ANDROID import Choreographer from "android.view.Choreographer"; // Android 帧同步器,提供垂直同步信号 import FrameCallback from "android.view.Choreographer.FrameCallback"; // 帧回调接口 import Long from "kotlin.Long"; // Kotlin Long 类型 // #endif /** * 缓动函数类型定义 */ export type EasingFunction = (progress: number) => number; /** * 动画属性配置 */ export type AnimationAttribute = { /** 起始值 */ fromValue: string; /** 结束值 */ toValue: string; /** 单位 (px, %, deg等) */ unit: string; /** 当前值 */ currentValue: string; /** 当前进度 (0-1) */ progress: number; /** 属性名称 */ propertyName: string; }; /** * 动画配置选项 */ export type AnimationOptions = { /** 动画持续时间(毫秒) */ duration?: number; /** 循环次数 (-1为无限循环) */ loop?: number; /** 是否往返播放 */ alternate?: boolean; /** 是否按属性顺序依次执行动画 */ sequential?: boolean; /** 缓动函数名称 */ timingFunction?: string; /** 自定义贝塞尔曲线参数 */ bezier?: number[]; /** 动画完成回调 */ complete?: () => void; /** 动画开始回调 */ start?: () => void; /** 每帧回调 */ frame?: (progress: number) => void; }; // 贝塞尔曲线计算常量 const BEZIER_SPLINE_SIZE = 11; // 样本点数量,用于预计算优化 const BEZIER_SAMPLE_STEP = 1.0 / (BEZIER_SPLINE_SIZE - 1.0); // 样本步长 /** * 贝塞尔曲线系数A * 三次贝塞尔曲线的三次项系数 */ function getBezierCoefficientA(x1: number, x2: number): number { return 1.0 - 3.0 * x2 + 3.0 * x1; // B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃ 中的 t³ 系数 } /** * 贝塞尔曲线系数B * 三次贝塞尔曲线的二次项系数 */ function getBezierCoefficientB(x1: number, x2: number): number { return 3.0 * x2 - 6.0 * x1; // 二次项系数 } /** * 贝塞尔曲线系数C * 三次贝塞尔曲线的一次项系数 */ function getBezierCoefficientC(x1: number): number { return 3.0 * x1; // 一次项系数 } /** * 计算贝塞尔曲线值 * 使用霍纳法则提高计算效率 * @param t 时间参数 (0-1) * @param x1 控制点1的x坐标 * @param x2 控制点2的x坐标 */ function calculateBezierValue(t: number, x1: number, x2: number): number { const a = getBezierCoefficientA(x1, x2); // 获取三次项系数 const b = getBezierCoefficientB(x1, x2); // 获取二次项系数 const c = getBezierCoefficientC(x1); // 获取一次项系数 return ((a * t + b) * t + c) * t; // 霍纳法则:((at + b)t + c)t,减少乘法运算 } /** * 计算贝塞尔曲线斜率 * 对贝塞尔曲线求导得到斜率函数 * @param t 时间参数 (0-1) * @param x1 控制点1的x坐标 * @param x2 控制点2的x坐标 */ function getBezierSlope(t: number, x1: number, x2: number): number { const a = getBezierCoefficientA(x1, x2); // 三次项系数 const b = getBezierCoefficientB(x1, x2); // 二次项系数 const c = getBezierCoefficientC(x1); // 一次项系数 return 3.0 * a * t * t + 2.0 * b * t + c; // 导数:3at² + 2bt + c } /** * 二分法求解贝塞尔曲线参数 * 用于根据x值反推t参数,适用于斜率较小的情况 * @param targetX 目标x值 * @param startT 起始t值 * @param endT 结束t值 * @param x1 控制点1的x坐标 * @param x2 控制点2的x坐标 */ function binarySearchBezierT( targetX: number, startT: number, endT: number, x1: number, x2: number ): number { let currentX: number; // 当前计算的x值 let currentT: number; // 当前的t参数 let iterations = 0; // 迭代次数计数器 const maxIterations = 10; // 最大迭代次数,避免无限循环 const precision = 0.0000001; // 精度要求 do { currentT = startT + (endT - startT) / 2.0; // 取中点 currentX = calculateBezierValue(currentT, x1, x2) - targetX; // 计算误差 if (currentX > 0.0) { // 如果当前x值大于目标值 endT = currentT; // 缩小右边界 } else { // 如果当前x值小于目标值 startT = currentT; // 缩小左边界 } iterations++; // 增加迭代计数 } while (Math.abs(currentX) > precision && iterations < maxIterations); // 直到精度满足或达到最大迭代次数 return currentT; // 返回找到的t参数 } /** * 牛顿-拉夫逊法求解贝塞尔曲线参数 * 适用于斜率较大的情况,收敛速度快 * @param targetX 目标x值 * @param initialGuess 初始猜测值 * @param x1 控制点1的x坐标 * @param x2 控制点2的x坐标 */ function newtonRaphsonBezierT( targetX: number, initialGuess: number, x1: number, x2: number ): number { let t = initialGuess; // 当前t值,从初始猜测开始 const maxIterations = 4; // 最大迭代次数,牛顿法收敛快 for (let i = 0; i < maxIterations; i++) { const slope = getBezierSlope(t, x1, x2); // 计算当前点的斜率 if (slope == 0.0) { // 如果斜率为0,避免除零错误 return t; } const currentX = calculateBezierValue(t, x1, x2) - targetX; // 计算当前误差 t = t - currentX / slope; // 牛顿法迭代公式:t_new = t - f(t)/f'(t) } return t; // 返回收敛后的t值 } /** * 创建贝塞尔缓动函数 * 根据四个控制点坐标生成缓动函数,类似CSS的cubic-bezier * @param x1 控制点1的x坐标 (0-1) * @param y1 控制点1的y坐标 (0-1) * @param x2 控制点2的x坐标 (0-1) * @param y2 控制点2的y坐标 (0-1) */ function createBezierEasing(x1: number, y1: number, x2: number, y2: number): EasingFunction | null { // 验证控制点坐标范围,x坐标必须在0-1之间 if (!(0 <= x1 && x1 <= 1 && 0 <= x2 && x2 <= 1)) { return null; // 参数无效时返回null } const sampleValues: number[] = []; // 预计算的样本值数组 // 预计算样本值以提高性能,仅对非线性曲线进行预计算 if (x1 != y1 || x2 != y2) { // 如果不是线性函数 for (let i = 0; i < BEZIER_SPLINE_SIZE; i++) { // 计算等间距的样本点,用于快速查找 sampleValues.push(calculateBezierValue(i * BEZIER_SAMPLE_STEP, x1, x2)); } } /** * 根据x值获取对应的t参数 * 使用预计算样本进行快速查找和插值 * @param x 输入的x值 (0-1) */ function getTParameterForX(x: number): number { let intervalStart = 0.0; // 区间起始位置 let currentSample = 1; // 当前样本索引 const lastSample = BEZIER_SPLINE_SIZE - 1; // 最后一个样本索引 // 找到x值所在的区间,线性搜索预计算的样本 for (; currentSample != lastSample && sampleValues[currentSample] <= x; currentSample++) { intervalStart += BEZIER_SAMPLE_STEP; // 移动区间起始位置 } currentSample--; // 回退到正确的区间 // 线性插值获得初始猜测值,提高后续求解精度 const dist = (x - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]); // 计算在区间内的相对位置 const initialGuess = intervalStart + dist * BEZIER_SAMPLE_STEP; // 计算初始猜测的t值 const initialSlope = getBezierSlope(initialGuess, x1, x2); // 计算初始点的斜率 // 根据斜率选择合适的求解方法 if (initialSlope >= 0.001) { // 斜率足够大时使用牛顿法 return newtonRaphsonBezierT(x, initialGuess, x1, x2); } else if (initialSlope == 0.0) { // 斜率为0时直接返回 return initialGuess; } // 斜率太小时使用二分法,更稳定 return binarySearchBezierT(x, intervalStart, intervalStart + BEZIER_SAMPLE_STEP, x1, x2); } // 返回缓动函数,这是最终的缓动函数接口 return function (progress: number): number { // 线性情况直接返回,优化性能 if (x1 == y1 && x2 == y2) { return progress; } // 边界情况处理,避免计算误差 if (progress == 0.0 || progress == 1.0) { return progress; } // 计算贝塞尔曲线值:先根据progress(x)找到对应的t,再计算y值 return calculateBezierValue(getTParameterForX(progress), y1, y2); }; } /** * 颜色工具函数:标准化颜色值格式 * 处理不同格式的颜色输入,确保返回有效的颜色值 */ function getDefaultColor(colorValue: string): string { // 简化的颜色处理,实际项目中可能需要更完整的颜色转换 if (colorValue.startsWith("#")) { // 十六进制颜色格式 return colorValue; } if (colorValue.startsWith("rgb")) { // RGB或RGBA颜色格式 return colorValue; } // 默认返回黑色,作为兜底处理 return "#000000"; } /** * 十六进制颜色转RGB对象 * 将#RRGGBB格式的颜色转换为{r,g,b,a}对象,用于颜色动画插值 * @param hex 十六进制颜色值,如"#FF0000" * @returns 包含r,g,b,a属性的颜色对象 */ function hexToRgb(hex: string): UTSJSONObject { // 使用正则表达式解析十六进制颜色,支持带#和不带#的格式 const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); if (result != null) { // 解析成功 return { r: parseInt(result[1] ?? "0", 16), // 红色分量,16进制转10进制 g: parseInt(result[2] ?? "0", 16), // 绿色分量 b: parseInt(result[3] ?? "0", 16), // 蓝色分量 a: 1.0 // 透明度,默认不透明 } as UTSJSONObject; } // 解析失败时返回黑色 return { r: 0, g: 0, b: 0, a: 1.0 } as UTSJSONObject; } /** * 高性能动画引擎类 * 支持多平台的流畅动画效果,提供丰富的缓动函数和动画控制 */ export class AnimationEngine { /** 预定义缓动函数映射,存储常用的贝塞尔曲线参数 */ private readonly easingPresets = new Map([ ["linear", [0.0, 0.0, 1.0, 1.0]], // 线性缓动 ["ease", [0.25, 0.1, 0.25, 1.0]], // 默认缓动 ["easeIn", [0.42, 0.0, 1.0, 1.0]], // 加速进入 ["easeOut", [0.0, 0.0, 0.58, 1.0]], // 减速退出 ["easeInOut", [0.42, 0.0, 0.58, 1.0]], // 先加速后减速 ["easeInQuad", [0.55, 0.085, 0.68, 0.53]], // 二次方加速 ["easeOutQuad", [0.25, 0.46, 0.45, 0.94]], // 二次方减速 ["easeInOutQuad", [0.455, 0.03, 0.515, 0.955]], // 二次方先加速后减速 ["easeInCubic", [0.55, 0.055, 0.675, 0.19]], // 三次方加速 ["easeOutCubic", [0.215, 0.61, 0.355, 1.0]], // 三次方减速 ["easeInOutCubic", [0.645, 0.045, 0.355, 1.0]], // 三次方先加速后减速 ["easeInQuart", [0.895, 0.03, 0.685, 0.22]], // 四次方加速 ["easeOutQuart", [0.165, 0.84, 0.44, 1.0]], // 四次方减速 ["easeInOutQuart", [0.77, 0.0, 0.175, 1.0]], // 四次方先加速后减速 ["easeInQuint", [0.755, 0.05, 0.855, 0.06]], // 五次方加速 ["easeOutQuint", [0.23, 1.0, 0.32, 1.0]], // 五次方减速 ["easeInOutQuint", [0.86, 0.0, 0.07, 1.0]], // 五次方先加速后减速 ["easeInSine", [0.47, 0.0, 0.745, 0.715]], // 正弦加速 ["easeOutSine", [0.39, 0.575, 0.565, 1.0]], // 正弦减速 ["easeInOutSine", [0.445, 0.05, 0.55, 0.95]], // 正弦先加速后减速 ["easeInExpo", [0.95, 0.05, 0.795, 0.035]], // 指数加速 ["easeOutExpo", [0.19, 1.0, 0.22, 1.0]], // 指数减速 ["easeInOutExpo", [1.0, 0.0, 0.0, 1.0]], // 指数先加速后减速 ["easeInCirc", [0.6, 0.04, 0.98, 0.335]], // 圆形加速 ["easeOutCirc", [0.075, 0.82, 0.165, 1.0]], // 圆形减速 ["easeInOutBack", [0.68, -0.55, 0.265, 1.55]] // 回弹效果 ]); /** 目标DOM元素,动画作用的对象 */ private targetElement: UniElement | null = null; /** 动画持续时间(毫秒),默认500ms */ private animationDuration: number = 500; /** 动画是否正在运行,用于控制动画循环 */ private isRunning: boolean = false; /** 动画是否暂停,暂停时保留当前进度 */ private isPaused: boolean = false; /** 当前动画进度 (0-1),用于恢复暂停的动画 */ private currentProgress: number = 0; /** 是否反向播放,影响动画方向 */ private isReversed: boolean = false; /** 是否往返播放模式,控制动画是否来回播放 */ private isAlternate: boolean = false; /** 往返播放时是否处于反向状态 */ private isAlternateReversed: boolean = false; /** 循环播放次数 (-1为无限循环) */ private loopCount: number = 1; /** 当前已完成的循环次数 */ private currentLoop: number = 0; /** 动画是否正在停止,用于提前终止动画 */ private isStopping: boolean = true; /** 当前执行的属性索引(顺序执行模式),用于控制属性依次动画 */ private currentAttributeIndex: number = 0; /** 回调函数,提供动画生命周期钩子 */ private onComplete: () => void = () => {}; // 动画完成回调 private onStart: () => void = () => {}; // 动画开始回调 private onFrame: (progress: number) => void = () => {}; // 每帧回调 /** 动画属性列表,存储所有要动画的CSS属性 */ private animationAttributes: AnimationAttribute[] = []; /** 动画开始时间戳,用于计算动画进度 */ private startTimestamp: number = 0; /** 当前使用的缓动函数,将线性进度转换为缓动进度 */ private currentEasingFunction: EasingFunction | null = null; /** 是否按属性顺序依次执行动画,而非并行执行 */ private isSequentialMode: boolean = false; // 平台相关的动画控制器 // Android平台使用Choreographer提供高性能动画 // #ifdef APP-ANDROID private choreographer: Choreographer | null = null; // Android系统帧同步器 private frameCallback: FrameCallback | null = null; // 帧回调处理器 // #endif // iOS/小程序平台使用定时器 // #ifdef APP-IOS private displayLinkTimer: number = 0; // iOS定时器ID // #endif // Web平台使用requestAnimationFrame private animationFrameId: number | null = null; // 动画帧ID /** * 创建动画引擎实例 * 初始化动画引擎,设置目标元素和动画配置 * @param element 目标DOM元素,null时仅做计算不应用样式 * @param options 动画配置选项,包含持续时间、缓动函数等 */ constructor(element: UniElement | null, options: AnimationOptions) { this.targetElement = element; // 保存目标元素引用 // 设置动画参数,使用选项值或默认值 this.animationDuration = options.duration != null ? options.duration : this.animationDuration; // 设置动画持续时间 this.loopCount = options.loop != null ? options.loop : this.loopCount; // 设置循环次数 this.isAlternate = options.alternate != null ? options.alternate : this.isAlternate; // 设置往返播放 this.isSequentialMode = options.sequential != null ? options.sequential : this.isSequentialMode; // 设置顺序执行模式 // 设置缓动函数,优先使用预定义函数 if (options.timingFunction != null) { const easingParams = this.easingPresets.get(options.timingFunction); // 查找预定义缓动参数 if (easingParams != null) { // 根据贝塞尔参数创建缓动函数 this.currentEasingFunction = createBezierEasing( easingParams[0], // x1坐标 easingParams[1], // y1坐标 easingParams[2], // x2坐标 easingParams[3] // y2坐标 ); } } // 自定义贝塞尔曲线,会覆盖预定义函数 if (options.bezier != null && options.bezier.length == 4) { this.currentEasingFunction = createBezierEasing( options.bezier[0], // 自定义x1坐标 options.bezier[1], // 自定义y1坐标 options.bezier[2], // 自定义x2坐标 options.bezier[3] // 自定义y2坐标 ); } // 设置回调函数,提供动画生命周期钩子 if (options.complete != null) { this.onComplete = options.complete; // 动画完成回调 } if (options.start != null) { this.onStart = options.start; // 动画开始回调 } if (options.frame != null) { this.onFrame = options.frame; // 每帧更新回调 } } /** * 从样式值中提取单位 * 解析CSS值中的单位部分,用于动画计算 * @param value 样式值,如 "100px", "50%" * @param propertyName CSS属性名称,用于判断是否需要默认单位 * @returns 单位字符串 */ private extractUnit(value?: string, propertyName?: string): string { if (value == null) return "px"; // 默认单位为px const unit = value.replace(/[\d|\-|\+|\.]/g, ""); // 移除数字、负号、正号、小数点,保留单位 // opacity、z-index等属性无需单位 if (propertyName == "opacity" || propertyName == "z-index") { return ""; // 返回空字符串表示无单位 } return unit == "" ? "px" : unit; // 如果没有单位则默认为px } /** * 添加自定义缓动函数 * 向引擎注册新的缓动函数,可在后续动画中使用 * @param name 缓动函数名称 * @param bezierParams 贝塞尔曲线参数 [x1, y1, x2, y2] */ addCustomEasing(name: string, bezierParams: number[]): AnimationEngine { if (bezierParams.length == 4) { // 验证参数数量 this.easingPresets.set(name, bezierParams); // 添加到预设映射中 } return this; // 返回自身支持链式调用 } /** * 设置动画反向播放 * 控制动画从结束值向起始值播放 * @param reverse 是否反向播放,null表示切换当前状态 */ setReverse(reverse: boolean | null = null): AnimationEngine { if (reverse != null) { this.isReversed = reverse; // 设置指定状态 } else { this.isReversed = !this.isReversed; // 切换当前状态 } return this; // 支持链式调用 } /** * 设置循环播放次数 * 控制动画重复执行的次数 * @param count 循环次数,-1表示无限循环 */ setLoopCount(count: number): AnimationEngine { this.loopCount = count; // 设置循环次数 return this; // 支持链式调用 } /** * 设置动画持续时间 * 控制动画从开始到结束的总时长 * @param duration 持续时间(毫秒) */ setDuration(duration: number): AnimationEngine { this.animationDuration = duration; // 设置动画持续时间 return this; // 支持链式调用 } /** * 设置往返播放模式 * 控制动画是否在每次循环时反向播放 * @param alternate 是否往返播放 */ setAlternate(alternate: boolean): AnimationEngine { this.isAlternate = alternate; // 设置往返播放标志 return this; // 支持链式调用 } /** * 设置顺序执行模式 * 控制多个属性是同时动画还是依次动画 * @param sequential 是否按属性顺序依次执行 */ setSequential(sequential: boolean): AnimationEngine { this.isSequentialMode = sequential; // 设置执行模式 return this; // 支持链式调用 } /** * 添加动画属性 * 向动画引擎添加一个CSS属性的动画配置 * @param propertyName CSS属性名称 * @param fromValue 起始值(支持数字+单位,如"100px"、"50%") * @param toValue 结束值(单位必须与起始值一致) * @param unique 是否唯一,true时同名属性会被替换 */ addAttribute( propertyName: string, fromValue: string, toValue: string, unique: boolean = true ): AnimationEngine { const isColor = this.isColorProperty(propertyName); // 检测是否为颜色属性 const unit = isColor ? "" : this.extractUnit(fromValue, propertyName); // 提取单位 // 根据属性类型处理值 const processedFromValue = isColor ? getDefaultColor(fromValue) // 颜色属性标准化 : parseFloat(fromValue).toString(); // 数值属性提取数字 const processedToValue = isColor ? getDefaultColor(toValue) // 颜色属性标准化 : parseFloat(toValue).toString(); // 数值属性提取数字 // 查找是否已存在同名属性,用于决定是替换还是新增 let existingIndex = this.animationAttributes.findIndex( (attr: AnimationAttribute): boolean => attr.propertyName == propertyName ); if (!unique) { existingIndex = -1; // 强制添加新属性,不替换 } // 创建新的动画属性对象 const newAttribute: AnimationAttribute = { fromValue: processedFromValue, // 处理后的起始值 toValue: processedToValue, // 处理后的结束值 unit: unit, // 单位 progress: 0, // 初始进度为0 currentValue: processedFromValue, // 当前值初始化为起始值 propertyName: propertyName // 属性名称 }; if (existingIndex == -1) { this.animationAttributes.push(newAttribute); // 添加新属性 } else { this.animationAttributes[existingIndex] = newAttribute; // 替换现有属性 } return this; // 支持链式调用 } /** * 快捷方法:添加变换属性 */ transform(property: string, fromValue: string, toValue: string): AnimationEngine { return this.addAttribute(property, fromValue, toValue); } /** * 快捷方法:添加位移动画 */ translate(fromX: string, fromY: string, toX: string, toY: string): AnimationEngine { this.addAttribute("translateX", fromX, toX); this.addAttribute("translateY", fromY, toY); return this; } /** * 添加X轴位移动画 * @param fromX 起始X位置,可以使用"current"表示当前位置 * @param toX 结束X位置 * @returns */ translateX(fromX: string, toX: string): AnimationEngine { return this.addAttribute("translateX", fromX, toX); } /** * 添加Y轴位移动画 * @param fromY 起始Y位置,可以使用"current"表示当前位置 * @param toY 结束Y位置 * @returns */ translateY(fromY: string, toY: string): AnimationEngine { return this.addAttribute("translateY", fromY, toY); } /** * 快捷方法:添加缩放动画 */ scale(fromScale: string, toScale: string): AnimationEngine { return this.addAttribute("scale", fromScale, toScale); } /** * 快捷方法:添加旋转动画 */ rotate(fromDegree: string, toDegree: string): AnimationEngine { return this.addAttribute("rotate", fromDegree, toDegree); } /** * 快捷方法:添加透明度动画 */ opacity(fromOpacity: string, toOpacity: string): AnimationEngine { return this.addAttribute("opacity", fromOpacity, toOpacity); } /** * 线性插值计算 * 根据进度在两个数值之间进行插值,用于计算动画中间值 * @param startValue 起始值 * @param endValue 结束值 * @param progress 进度 (0-1) */ private interpolateValue(startValue: number, endValue: number, progress: number): number { return startValue + (endValue - startValue) * progress; // 线性插值公式:start + (end - start) * progress } /** * 判断是否为颜色相关属性 * 检测CSS属性名是否与颜色相关,用于特殊的颜色动画处理 * @param propertyName 属性名称 */ private isColorProperty(propertyName: string): boolean { return ( propertyName.indexOf("background") > -1 || // 背景颜色相关 propertyName.indexOf("color") > -1 || // 文字颜色相关 propertyName.indexOf("border-color") > -1 || // 边框颜色相关 propertyName.indexOf("shadow") > -1 // 阴影颜色相关 ); } /** * 判断是否为Transform相关属性 * 检测属性名是否为transform相关的CSS属性 * @param propertyName CSS属性名称 * @returns 是否为transform属性 */ private isTransformProperty(propertyName: string): boolean { return ( propertyName == "scaleX" || // X轴缩放 propertyName == "scaleY" || // Y轴缩放 propertyName == "scale" || // 等比缩放 propertyName == "rotateX" || // X轴旋转 propertyName == "rotateY" || // Y轴旋转 propertyName == "rotate" || // Z轴旋转 propertyName == "translateX" || // X轴位移 propertyName == "translateY" || // Y轴位移 propertyName == "translate" // 双轴位移 ); } /** * 设置元素样式属性 * 根据属性类型应用相应的样式值,支持transform、颜色、普通数值属性 * @param propertyName 属性名称 * @param currentValue 当前值 * @param unit 单位 * @param progress 动画进度 * @param attribute 动画属性对象 */ private setElementProperty( propertyName: string, currentValue: number, unit: string, progress: number, attribute: AnimationAttribute ): void { if (this.targetElement == null) return; // 没有目标元素时直接返回 const element = this.targetElement; // 获取目标元素引用 const valueStr = currentValue.toFixed(2); // 数值保留两位小数 // #ifdef MP if (element.style == null) { return; } // #endif // Transform 相关属性处理,使用CSS transform属性 switch (propertyName) { case "scaleX": // X轴缩放 element.style!.setProperty("transform", `scaleX(${currentValue})`); break; case "scaleY": // Y轴缩放 element.style!.setProperty("transform", `scaleY(${currentValue})`); break; case "scale": // 等比缩放 element.style!.setProperty("transform", `scale(${currentValue})`); break; case "rotateX": // X轴旋转 element.style!.setProperty("transform", `rotateX(${valueStr + unit})`); break; case "rotateY": // Y轴旋转 element.style!.setProperty("transform", `rotateY(${valueStr + unit})`); break; case "rotate": // Z轴旋转 element.style!.setProperty("transform", `rotate(${valueStr + unit})`); break; case "translateX": // X轴位移 element.style!.setProperty("transform", `translateX(${valueStr + unit})`); break; case "translateY": // Y轴位移 element.style!.setProperty("transform", `translateY(${valueStr + unit})`); break; case "translate": // 双轴位移 element.style!.setProperty( "transform", `translate(${valueStr + unit},${valueStr + unit})` ); break; default: // 颜色属性处理,需要进行RGBA插值 if (this.isColorProperty(propertyName)) { const startColor = hexToRgb(attribute.fromValue); // 解析起始颜色 const endColor = hexToRgb(attribute.toValue); // 解析结束颜色 // 提取起始颜色的RGBA分量,兼容不同的JSON对象访问方式 const startR = startColor.getNumber != null ? startColor.getNumber("r") : (startColor["r"] as number); const startG = startColor.getNumber != null ? startColor.getNumber("g") : (startColor["g"] as number); const startB = startColor.getNumber != null ? startColor.getNumber("b") : (startColor["b"] as number); const startA = startColor.getNumber != null ? startColor.getNumber("a") : (startColor["a"] as number); // 提取结束颜色的RGBA分量 const endR = endColor.getNumber != null ? endColor.getNumber("r") : (endColor["r"] as number); const endG = endColor.getNumber != null ? endColor.getNumber("g") : (endColor["g"] as number); const endB = endColor.getNumber != null ? endColor.getNumber("b") : (endColor["b"] as number); const endA = endColor.getNumber != null ? endColor.getNumber("a") : (endColor["a"] as number); // 对每个颜色分量进行插值计算 const r = this.interpolateValue( startR != null ? startR : 0, endR != null ? endR : 0, progress ); const g = this.interpolateValue( startG != null ? startG : 0, endG != null ? endG : 0, progress ); const b = this.interpolateValue( startB != null ? startB : 0, endB != null ? endB : 0, progress ); const a = this.interpolateValue( startA != null ? startA : 1, endA != null ? endA : 1, progress ); // 设置RGBA颜色值 element.style!.setProperty( propertyName, `rgba(${r.toFixed(0)},${g.toFixed(0)},${b.toFixed(0)},${a.toFixed(1)})` ); } else { // 普通数值属性处理,直接设置数值和单位 element.style!.setProperty(propertyName, valueStr + unit); } break; } } /** * Web平台动画运行方法 (H5/iOS/Harmony) * 使用requestAnimationFrame实现流畅的动画循环 */ private runWebAnimation(): void { // #ifdef H5 || APP-IOS || APP-HARMONY const self = this; // 保存this引用,避免在内部函数中this指向改变 self.startTimestamp = 0; // 重置开始时间戳 // 取消之前的动画帧,避免重复执行 if (self.animationFrameId != null) { cancelAnimationFrame(self.animationFrameId); } function animationLoop(): void { // 初始化开始时间,首次执行时记录时间戳 if (self.startTimestamp <= 0) { self.startTimestamp = Date.now(); } // 计算当前进度:(已用时间 / 总时间) + 暂停前的进度 const elapsed = Date.now() - self.startTimestamp; // 已经过的时间 const progress = Math.min(elapsed / self.animationDuration + self.currentProgress, 1.0); // 限制进度不超过1 // 执行动画更新,应用当前进度到所有属性 self.updateAnimationFrame(progress); // 检查暂停状态 if (self.isPaused) { self.isRunning = false; // 停止运行标志 self.currentProgress = progress; // 保存当前进度,用于恢复 console.log("动画已暂停"); return; // 退出动画循环 } // 检查动画完成或停止 if (progress >= 1.0 || self.isStopping) { self.handleAnimationComplete(); // 处理动画完成逻辑 return; // 退出动画循环 } // 继续下一帧,动画未完成且仍在运行 if (progress < 1.0 && self.isRunning) { self.onFrame(progress); // 触发每帧回调 self.animationFrameId = requestAnimationFrame(animationLoop); // 请求下一帧 } } // 开始动画,触发开始回调并启动动画循环 self.onStart(); animationLoop(); // #endif } /** * 更新动画帧 * 根据执行模式更新所有或当前属性的动画值 * @param progress 当前进度 (0-1) */ private updateAnimationFrame(progress: number): void { if (this.targetElement == null) return; // 没有目标元素时直接返回 if (!this.isSequentialMode) { // 并行执行所有属性动画,所有属性同时进行动画 for (let i = 0; i < this.animationAttributes.length; i++) { this.updateSingleAttribute(this.animationAttributes[i], progress); } } else { // 顺序执行属性动画,一个接一个地执行属性动画 if (this.currentAttributeIndex < this.animationAttributes.length) { this.updateSingleAttribute( this.animationAttributes[this.currentAttributeIndex], progress ); } } } /** * 更新单个属性的动画 * 计算属性的当前值并应用到元素上 * @param attribute 动画属性 * @param progress 进度 */ private updateSingleAttribute(attribute: AnimationAttribute, progress: number): void { attribute.progress = progress; // 更新属性的进度记录 if (!this.isColorProperty(attribute.propertyName)) { // 数值属性处理 const fromValue = parseFloat(attribute.fromValue); // 起始数值 const toValue = parseFloat(attribute.toValue); // 结束数值 // 应用缓动函数,将线性进度转换为缓动进度 let easedProgress = progress; if (this.currentEasingFunction != null) { easedProgress = this.currentEasingFunction(progress); } // 计算当前值,使用缓动进度进行插值 let currentValue = this.interpolateValue(fromValue, toValue, easedProgress); // 处理反向和往返播放,交换起始和结束值 if (this.isReversed || this.isAlternateReversed) { currentValue = this.interpolateValue(toValue, fromValue, easedProgress); } // 应用计算出的值到元素属性 this.setElementProperty( attribute.propertyName, currentValue, attribute.unit, progress, attribute ); } else { // 颜色属性处理,progress参数会在setElementProperty中用于颜色插值 this.setElementProperty(attribute.propertyName, 0, attribute.unit, progress, attribute); } } /** * 处理动画完成 */ private handleAnimationComplete(): void { // 顺序模式下检查是否还有未执行的属性 if ( this.isSequentialMode && this.currentAttributeIndex < this.animationAttributes.length - 1 ) { this.currentAttributeIndex++; this.currentProgress = 0; this.restartAnimation(); return; } // 重置状态 // #ifdef H5 || APP-IOS || APP-HARMONY if (this.animationFrameId != null) { cancelAnimationFrame(this.animationFrameId); } // #endif this.currentAttributeIndex = 0; this.currentProgress = 0; // 处理往返播放 if (this.isAlternate) { this.isAlternateReversed = !this.isAlternateReversed; } // 处理循环播放 if (this.loopCount == -1) { // 无限循环 this.restartAnimation(); return; } else { this.currentLoop++; if (this.currentLoop < this.loopCount) { this.restartAnimation(); return; } } // 动画完成 this.isRunning = false; this.onComplete(); } /** * 根据平台重新启动动画 */ private restartAnimation(): void { // 重置开始时间戳,确保循环动画正确计时 this.startTimestamp = 0; // 根据平台选择合适的动画引擎 // #ifdef H5 || APP-IOS || APP-HARMONY this.runWebAnimation(); // #endif // #ifdef APP-ANDROID this.runAndroidAnimation(); // #endif // #ifdef MP this.runMPAnimation(); // #endif } /** * Android平台动画运行方法 */ private runAndroidAnimation(): void { // #ifdef APP-ANDROID const self = this; self.startTimestamp = 0; // 初始化Choreographer if (self.choreographer == null) { self.choreographer = Choreographer.getInstance(); } else { // 清除之前的回调 if (self.frameCallback != null) { self.choreographer.removeFrameCallback(self.frameCallback); } } /** * Android原生帧回调类 */ class frameCallback extends Choreographer.FrameCallback { // @ts-ignore override doFrame(frameTimeNanos: Long) { // 检查动画是否应该停止 if (!self.isRunning || self.isStopping) { return; } // 初始化开始时间 if (self.startTimestamp <= 0) { self.startTimestamp = Date.now(); } // 计算当前进度 const elapsed = Date.now() - self.startTimestamp; const progress = Math.min( elapsed / self.animationDuration + self.currentProgress, 1.0 ); // 执行动画更新 self.updateAnimationFrame(progress); // 检查暂停状态 if (self.isPaused) { self.isRunning = false; self.currentProgress = progress; return; } // 检查动画完成或停止 if (progress >= 1.0 || self.isStopping) { self.handleAnimationComplete(); return; } // 继续下一帧 if (progress < 1.0 && self.isRunning && !self.isStopping) { self.onFrame(progress); if (self.choreographer != null) { self.choreographer.postFrameCallback(this); } } } } // 启动动画 self.onStart(); self.frameCallback = new frameCallback(); self.choreographer!.postFrameCallback(self.frameCallback); // #endif } /** * 小程序平台动画运行方法 */ private runMPAnimation(): void { // #ifdef MP const self = this; self.startTimestamp = 0; // 清除之前的定时器 if (self.displayLinkTimer != 0) { clearTimeout(self.displayLinkTimer); } function animationLoop(): void { // 初始化开始时间 if (self.startTimestamp <= 0) { self.startTimestamp = Date.now(); } // 计算当前进度 const elapsed = Date.now() - self.startTimestamp; const progress = Math.min(elapsed / self.animationDuration + self.currentProgress, 1.0); // 执行动画更新 self.updateAnimationFrame(progress); // 检查暂停状态 if (self.isPaused) { self.isRunning = false; self.currentProgress = progress; return; } // 检查动画完成或停止 if (progress >= 1.0 || self.isStopping) { self.handleAnimationComplete(); return; } // 继续下一帧 if (progress < 1.0 && self.isRunning) { self.onFrame(progress); self.displayLinkTimer = setTimeout(animationLoop, 16) as any; // 约60fps } } // 开始动画 self.onStart(); animationLoop(); // #endif } /** * 开始播放动画 */ play(): AnimationEngine { if (this.isRunning) return this; // 初始化动画状态 this.isRunning = true; this.isStopping = false; this.isPaused = false; this.currentLoop = 0; this.currentAttributeIndex = 0; // 根据平台选择合适的动画引擎 // #ifdef H5 || APP-IOS || APP-HARMONY this.runWebAnimation(); // #endif // #ifdef APP-ANDROID this.runAndroidAnimation(); // #endif // #ifdef MP this.runMPAnimation(); // #endif return this; } /** * 异步播放动画,支持await * @returns Promise,动画完成时resolve */ playAsync(): Promise { return new Promise((resolve) => { const originalComplete = this.onComplete; this.onComplete = () => { originalComplete(); resolve(); }; this.play(); }); } /** * 停止动画 * 会立即停止动画并跳转到结束状态 */ stop(): AnimationEngine { this.isStopping = true; this.currentProgress = 0; this.currentAttributeIndex = this.animationAttributes.length; // 清理平台相关的动画控制器 // #ifdef WEB || APP-IOS || APP-HARMONY if (this.animationFrameId != null) { cancelAnimationFrame(this.animationFrameId); this.animationFrameId = null; } // #endif // #ifdef APP-ANDROID if (this.choreographer != null && this.frameCallback != null) { this.choreographer.removeFrameCallback(this.frameCallback); } // #endif // #ifdef MP if (this.displayLinkTimer != 0) { clearTimeout(this.displayLinkTimer); this.displayLinkTimer = 0; } // #endif this.isRunning = false; return this; } /** * 暂停动画 * 保留当前状态,可以通过play()恢复 */ pause(): AnimationEngine { this.isPaused = true; return this; } /** * 恢复暂停的动画 */ resume(): AnimationEngine { if (this.isPaused) { this.isPaused = false; this.play(); } return this; } /** * 清空应用到元素上的动画样式 * 只清空实际被动画引擎设置过的CSS属性 */ private clearElementStyles(): void { if (this.targetElement == null) return; const element = this.targetElement; // 清空所有动画属性列表中记录的属性 for (const attr of this.animationAttributes) { const propertyName = attr.propertyName; // Transform 相关属性需要清空transform if (this.isTransformProperty(propertyName)) { element.style!.setProperty("transform", ""); } else { // 其他属性直接清空 element.style!.setProperty(propertyName, ""); } } } /** * 重置动画到初始状态,清空所有内容 */ reset(): AnimationEngine { // 停止当前动画 this.stop(); // 清空应用到元素上的所有样式 this.clearElementStyles(); // 重置所有动画状态 this.currentProgress = 0; this.currentLoop = 0; this.currentAttributeIndex = 0; this.isAlternateReversed = false; this.isReversed = false; this.isPaused = false; this.isStopping = true; this.startTimestamp = 0; // 清空动画属性列表 this.animationAttributes = []; // 重置缓动函数 this.currentEasingFunction = null; // 重置回调函数 this.onComplete = () => {}; this.onStart = () => {}; this.onFrame = () => {}; // 清理平台相关的动画控制器 // #ifdef WEB || APP-IOS || APP-HARMONY if (this.animationFrameId != null) { cancelAnimationFrame(this.animationFrameId); this.animationFrameId = null; } // #endif // #ifdef APP-ANDROID if (this.choreographer != null && this.frameCallback != null) { this.choreographer.removeFrameCallback(this.frameCallback); this.frameCallback = null; } this.choreographer = null; // #endif // #ifdef MP if (this.displayLinkTimer != 0) { clearTimeout(this.displayLinkTimer); this.displayLinkTimer = 0; } // #endif return this; } /** * 获取当前动画进度 */ getProgress(): number { return this.currentProgress; } /** * 获取动画是否正在运行 */ isAnimating(): boolean { return this.isRunning; } /** * 获取当前循环次数 */ getCurrentLoop(): number { return this.currentLoop; } /** * 清除所有动画属性 */ clearAttributes(): AnimationEngine { this.animationAttributes = []; return this; } /** * 获取动画属性数量 */ getAttributeCount(): number { return this.animationAttributes.length; } /** * 淡入动画 * @param duration 持续时间 */ fadeIn(duration: number = 300): AnimationEngine { return this.setDuration(duration).opacity("0", "1"); } /** * 淡出动画 * @param duration 持续时间 */ fadeOut(duration: number = 300): AnimationEngine { return this.setDuration(duration).opacity("1", "0"); } /** * 滑入动画(从左) * @param duration 持续时间 */ slideInLeft(duration: number = 300): AnimationEngine { return this.setDuration(duration).translateX("-100%", "0%").opacity("0", "1"); } /** * 滑入动画(从右) * @param duration 持续时间 */ slideInRight(duration: number = 300): AnimationEngine { return this.setDuration(duration).translateX("100%", "0%").opacity("0", "1"); } /** * 滑入动画(从上) * @param duration 持续时间 */ slideInUp(duration: number = 300): AnimationEngine { return this.setDuration(duration) .addAttribute("translateY", "-100%", "0%") .opacity("0", "1"); } /** * 滑入动画(从下) * @param duration 持续时间 */ slideInDown(duration: number = 300): AnimationEngine { return this.setDuration(duration) .addAttribute("translateY", "100%", "0%") .opacity("0", "1"); } /** * 缩放动画(放大) * @param duration 持续时间 */ zoomIn(duration: number = 300): AnimationEngine { return this.setDuration(duration).scale("0", "1").opacity("0", "1"); } /** * 缩放动画(缩小) * @param duration 持续时间 */ zoomOut(duration: number = 300): AnimationEngine { return this.setDuration(duration).scale("1", "0").opacity("1", "0"); } /** * 旋转动画 * @param duration 持续时间 * @param degrees 旋转角度 */ rotateIn(duration: number = 500, degrees: number = 360): AnimationEngine { return this.setDuration(duration).rotate("0deg", `${degrees}deg`).opacity("0", "1"); } /** * 旋转退出动画 * @param duration 持续时间 * @param degrees 旋转角度 */ rotateOut(duration: number = 500, degrees: number = 360): AnimationEngine { return this.setDuration(duration).rotate("0deg", `${degrees}deg`).opacity("1", "0"); } /** * 弹跳动画 * @param duration 持续时间 */ bounce(duration: number = 600): AnimationEngine { return this.setDuration(duration) .addCustomEasing("bounce", [0.68, -0.55, 0.265, 1.55]) .scale("1", "1.1") .setAlternate(true) .setLoopCount(2); } /** * 摇摆动画 * @param duration 持续时间 */ shake(duration: number = 500): AnimationEngine { return this.setDuration(duration) .addAttribute("translateX", "0px", "10px") .setAlternate(true) .setLoopCount(6); } /** * 链式动画:支持多个动画依次执行 * @param animations 动画配置函数数组 */ sequence(animations: ((engine: AnimationEngine) => AnimationEngine)[]): AnimationEngine { const self = this; if (animations.length == 0) { return this; } // 执行第一个动画 const firstEngine = animations[0](new AnimationEngine(this.targetElement, {})); // 如果只有一个动画,直接返回 if (animations.length == 1) { return firstEngine; } // 递归设置后续动画 function setNextAnimation( currentEngine: AnimationEngine, remainingAnimations: ((engine: AnimationEngine) => AnimationEngine)[] ): void { if (remainingAnimations.length == 0) { return; } const originalComplete = currentEngine.onComplete; currentEngine.onComplete = () => { originalComplete(); // 执行下一个动画 const nextEngine = remainingAnimations[0]( new AnimationEngine(self.targetElement, {}) ); // 如果还有更多动画,继续设置链式 if (remainingAnimations.length > 1) { setNextAnimation(nextEngine, remainingAnimations.slice(1)); } nextEngine.play(); }; } // 设置动画链 setNextAnimation(firstEngine, animations.slice(1)); return firstEngine; } /** * 滑出动画(向左) * @param duration 持续时间 */ slideOutLeft(duration: number = 300): AnimationEngine { return this.setDuration(duration).translateX("0%", "-100%").opacity("1", "0"); } /** * 滑出动画(向右) * @param duration 持续时间 */ slideOutRight(duration: number = 300): AnimationEngine { return this.setDuration(duration).translateX("0%", "100%").opacity("1", "0"); } /** * 滑出动画(向上) * @param duration 持续时间 */ slideOutUp(duration: number = 300): AnimationEngine { return this.setDuration(duration) .addAttribute("translateY", "0%", "-100%") .opacity("1", "0"); } /** * 滑出动画(向下) * @param duration 持续时间 */ slideOutDown(duration: number = 300): AnimationEngine { return this.setDuration(duration) .addAttribute("translateY", "0%", "100%") .opacity("1", "0"); } /** * 翻转动画(水平) * @param duration 持续时间 */ flipX(duration: number = 600): AnimationEngine { return this.setDuration(duration) .addAttribute("rotateX", "0deg", "180deg") .addCustomEasing("ease-in-out", [0.25, 0.1, 0.25, 1.0]); } /** * 翻转动画(垂直) * @param duration 持续时间 */ flipY(duration: number = 600): AnimationEngine { return this.setDuration(duration) .addAttribute("rotateY", "0deg", "180deg") .addCustomEasing("ease-in-out", [0.25, 0.1, 0.25, 1.0]); } /** * 弹性进入动画 * @param duration 持续时间 */ elasticIn(duration: number = 600): AnimationEngine { return this.setDuration(duration) .scale("0", "1") .opacity("0", "1") .addCustomEasing("elastic", [0.175, 0.885, 0.32, 1.275]); } /** * 弹性退出动画 * @param duration 持续时间 */ elasticOut(duration: number = 600): AnimationEngine { return this.setDuration(duration) .scale("1", "0") .opacity("1", "0") .addCustomEasing("elastic", [0.68, -0.55, 0.265, 1.55]); } /** * 回弹动画 * @param duration 持续时间 */ rubberBand(duration: number = 1000): AnimationEngine { return this.setDuration(duration) .addAttribute("scaleX", "1", "1.25") .addAttribute("scaleY", "1", "0.75") .setAlternate(true) .setLoopCount(2) .addCustomEasing("ease-in-out", [0.25, 0.1, 0.25, 1.0]); } /** * 摆动动画 * @param duration 持续时间 */ swing(duration: number = 1000): AnimationEngine { return this.setDuration(duration) .addAttribute("rotate", "0deg", "15deg") .setAlternate(true) .setLoopCount(4) .addCustomEasing("ease-in-out", [0.25, 0.1, 0.25, 1.0]); } /** * 抖动动画 * @param duration 持续时间 */ wobble(duration: number = 1000): AnimationEngine { return this.setDuration(duration) .addAttribute("translateX", "0px", "25px") .addAttribute("rotate", "0deg", "5deg") .setAlternate(true) .setLoopCount(4); } /** * 滚动进入动画 * @param duration 持续时间 */ rollIn(duration: number = 600): AnimationEngine { return this.setDuration(duration) .translateX("-100%", "0%") .rotate("-120deg", "0deg") .opacity("0", "1"); } /** * 滚动退出动画 * @param duration 持续时间 */ rollOut(duration: number = 600): AnimationEngine { return this.setDuration(duration) .translateX("0%", "100%") .rotate("0deg", "120deg") .opacity("1", "0"); } /** * 灯光效果动画 * @param duration 持续时间 */ lightSpeed(duration: number = 500): AnimationEngine { return this.setDuration(duration) .translateX("-100%", "0%") .addAttribute("skewX", "-30deg", "0deg") .opacity("0", "1") .addCustomEasing("ease-out", [0.25, 0.46, 0.45, 0.94]); } /** * 浮动动画 * @param duration 持续时间 */ float(duration: number = 3000): AnimationEngine { return this.setDuration(duration) .translateY("0px", "-10px") .setAlternate(true) .setLoopCount(-1) .addCustomEasing("ease-in-out", [0.25, 0.1, 0.25, 1.0]); } /** * 呼吸动画 * @param duration 持续时间 */ breathe(duration: number = 2000): AnimationEngine { return this.setDuration(duration) .scale("1", "1.1") .setAlternate(true) .setLoopCount(-1) .addCustomEasing("ease-in-out", [0.25, 0.1, 0.25, 1.0]); } /** * 发光动画 * @param duration 持续时间 */ glow(duration: number = 1500): AnimationEngine { return this.setDuration(duration) .addAttribute( "boxShadow", "0 0 5px rgba(255,255,255,0.5)", "0 0 20px rgba(255,255,255,1)" ) .setAlternate(true) .setLoopCount(-1) .addCustomEasing("ease-in-out", [0.25, 0.1, 0.25, 1.0]); } /** * 进度条动画 * @param duration 持续时间 * @param progress 进度百分比 (0-100) */ progressBar(duration: number = 1000, progress: number = 100): AnimationEngine { return this.setDuration(duration) .addAttribute("width", "0%", `${progress}%`) .addCustomEasing("ease-out", [0.25, 0.46, 0.45, 0.94]); } /** * 模态框进入动画 * @param duration 持续时间 */ modalIn(duration: number = 300): AnimationEngine { return this.setDuration(duration) .scale("0.7", "1") .opacity("0", "1") .addCustomEasing("ease-out", [0.25, 0.46, 0.45, 0.94]); } /** * 模态框退出动画 * @param duration 持续时间 */ modalOut(duration: number = 300): AnimationEngine { return this.setDuration(duration) .scale("1", "0.7") .opacity("1", "0") .addCustomEasing("ease-in", [0.42, 0.0, 1.0, 1.0]); } /** * 卡片翻转动画 * @param duration 持续时间 */ cardFlip(duration: number = 600): AnimationEngine { return this.setDuration(duration) .addAttribute("rotateY", "0deg", "180deg") .addCustomEasing("ease-in-out", [0.25, 0.1, 0.25, 1.0]); } /** * 波纹扩散动画 * @param duration 持续时间 */ ripple(duration: number = 600): AnimationEngine { return this.setDuration(duration) .scale("0", "4") .opacity("0.7", "0") .addCustomEasing("ease-out", [0.25, 0.46, 0.45, 0.94]); } } /** * 创建动画实例 * @param element 目标元素 * @param options 动画选项 */ export function createAnimation( element: UniElement | null, options: AnimationOptions = {} ): AnimationEngine { return new AnimationEngine(element, options); }