Files
WAI_Project_UNIX/cool/animation/index.ts
2025-09-11 18:37:44 +08:00

1770 lines
50 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.
// #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<string, number[]>([
["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<void> {
return new Promise<void>((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);
}