Files
jindengchen-ai-report/cool-unix/cool/animation/index.ts

1770 lines
50 KiB
TypeScript
Raw Normal View History

2025-11-13 10:36:23 +08:00
// #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 1x坐标
* @param x2 2x坐标
*/
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 1x坐标
* @param x2 2x坐标
*/
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 1x坐标
* @param x2 2x坐标
*/
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 1x坐标
* @param x2 2x坐标
*/
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 1x坐标 (0-1)
* @param y1 1y坐标 (0-1)
* @param x2 2x坐标 (0-1)
* @param y2 2y坐标 (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 Promiseresolve
*/
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);
}