index.ts 50 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769
  1. // #ifdef APP-ANDROID
  2. import Choreographer from "android.view.Choreographer"; // Android 帧同步器,提供垂直同步信号
  3. import FrameCallback from "android.view.Choreographer.FrameCallback"; // 帧回调接口
  4. import Long from "kotlin.Long"; // Kotlin Long 类型
  5. // #endif
  6. /**
  7. * 缓动函数类型定义
  8. */
  9. export type EasingFunction = (progress: number) => number;
  10. /**
  11. * 动画属性配置
  12. */
  13. export type AnimationAttribute = {
  14. /** 起始值 */
  15. fromValue: string;
  16. /** 结束值 */
  17. toValue: string;
  18. /** 单位 (px, %, deg等) */
  19. unit: string;
  20. /** 当前值 */
  21. currentValue: string;
  22. /** 当前进度 (0-1) */
  23. progress: number;
  24. /** 属性名称 */
  25. propertyName: string;
  26. };
  27. /**
  28. * 动画配置选项
  29. */
  30. export type AnimationOptions = {
  31. /** 动画持续时间(毫秒) */
  32. duration?: number;
  33. /** 循环次数 (-1为无限循环) */
  34. loop?: number;
  35. /** 是否往返播放 */
  36. alternate?: boolean;
  37. /** 是否按属性顺序依次执行动画 */
  38. sequential?: boolean;
  39. /** 缓动函数名称 */
  40. timingFunction?: string;
  41. /** 自定义贝塞尔曲线参数 */
  42. bezier?: number[];
  43. /** 动画完成回调 */
  44. complete?: () => void;
  45. /** 动画开始回调 */
  46. start?: () => void;
  47. /** 每帧回调 */
  48. frame?: (progress: number) => void;
  49. };
  50. // 贝塞尔曲线计算常量
  51. const BEZIER_SPLINE_SIZE = 11; // 样本点数量,用于预计算优化
  52. const BEZIER_SAMPLE_STEP = 1.0 / (BEZIER_SPLINE_SIZE - 1.0); // 样本步长
  53. /**
  54. * 贝塞尔曲线系数A
  55. * 三次贝塞尔曲线的三次项系数
  56. */
  57. function getBezierCoefficientA(x1: number, x2: number): number {
  58. 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³ 系数
  59. }
  60. /**
  61. * 贝塞尔曲线系数B
  62. * 三次贝塞尔曲线的二次项系数
  63. */
  64. function getBezierCoefficientB(x1: number, x2: number): number {
  65. return 3.0 * x2 - 6.0 * x1; // 二次项系数
  66. }
  67. /**
  68. * 贝塞尔曲线系数C
  69. * 三次贝塞尔曲线的一次项系数
  70. */
  71. function getBezierCoefficientC(x1: number): number {
  72. return 3.0 * x1; // 一次项系数
  73. }
  74. /**
  75. * 计算贝塞尔曲线值
  76. * 使用霍纳法则提高计算效率
  77. * @param t 时间参数 (0-1)
  78. * @param x1 控制点1的x坐标
  79. * @param x2 控制点2的x坐标
  80. */
  81. function calculateBezierValue(t: number, x1: number, x2: number): number {
  82. const a = getBezierCoefficientA(x1, x2); // 获取三次项系数
  83. const b = getBezierCoefficientB(x1, x2); // 获取二次项系数
  84. const c = getBezierCoefficientC(x1); // 获取一次项系数
  85. return ((a * t + b) * t + c) * t; // 霍纳法则:((at + b)t + c)t,减少乘法运算
  86. }
  87. /**
  88. * 计算贝塞尔曲线斜率
  89. * 对贝塞尔曲线求导得到斜率函数
  90. * @param t 时间参数 (0-1)
  91. * @param x1 控制点1的x坐标
  92. * @param x2 控制点2的x坐标
  93. */
  94. function getBezierSlope(t: number, x1: number, x2: number): number {
  95. const a = getBezierCoefficientA(x1, x2); // 三次项系数
  96. const b = getBezierCoefficientB(x1, x2); // 二次项系数
  97. const c = getBezierCoefficientC(x1); // 一次项系数
  98. return 3.0 * a * t * t + 2.0 * b * t + c; // 导数:3at² + 2bt + c
  99. }
  100. /**
  101. * 二分法求解贝塞尔曲线参数
  102. * 用于根据x值反推t参数,适用于斜率较小的情况
  103. * @param targetX 目标x值
  104. * @param startT 起始t值
  105. * @param endT 结束t值
  106. * @param x1 控制点1的x坐标
  107. * @param x2 控制点2的x坐标
  108. */
  109. function binarySearchBezierT(
  110. targetX: number,
  111. startT: number,
  112. endT: number,
  113. x1: number,
  114. x2: number
  115. ): number {
  116. let currentX: number; // 当前计算的x值
  117. let currentT: number; // 当前的t参数
  118. let iterations = 0; // 迭代次数计数器
  119. const maxIterations = 10; // 最大迭代次数,避免无限循环
  120. const precision = 0.0000001; // 精度要求
  121. do {
  122. currentT = startT + (endT - startT) / 2.0; // 取中点
  123. currentX = calculateBezierValue(currentT, x1, x2) - targetX; // 计算误差
  124. if (currentX > 0.0) {
  125. // 如果当前x值大于目标值
  126. endT = currentT; // 缩小右边界
  127. } else {
  128. // 如果当前x值小于目标值
  129. startT = currentT; // 缩小左边界
  130. }
  131. iterations++; // 增加迭代计数
  132. } while (Math.abs(currentX) > precision && iterations < maxIterations); // 直到精度满足或达到最大迭代次数
  133. return currentT; // 返回找到的t参数
  134. }
  135. /**
  136. * 牛顿-拉夫逊法求解贝塞尔曲线参数
  137. * 适用于斜率较大的情况,收敛速度快
  138. * @param targetX 目标x值
  139. * @param initialGuess 初始猜测值
  140. * @param x1 控制点1的x坐标
  141. * @param x2 控制点2的x坐标
  142. */
  143. function newtonRaphsonBezierT(
  144. targetX: number,
  145. initialGuess: number,
  146. x1: number,
  147. x2: number
  148. ): number {
  149. let t = initialGuess; // 当前t值,从初始猜测开始
  150. const maxIterations = 4; // 最大迭代次数,牛顿法收敛快
  151. for (let i = 0; i < maxIterations; i++) {
  152. const slope = getBezierSlope(t, x1, x2); // 计算当前点的斜率
  153. if (slope == 0.0) {
  154. // 如果斜率为0,避免除零错误
  155. return t;
  156. }
  157. const currentX = calculateBezierValue(t, x1, x2) - targetX; // 计算当前误差
  158. t = t - currentX / slope; // 牛顿法迭代公式:t_new = t - f(t)/f'(t)
  159. }
  160. return t; // 返回收敛后的t值
  161. }
  162. /**
  163. * 创建贝塞尔缓动函数
  164. * 根据四个控制点坐标生成缓动函数,类似CSS的cubic-bezier
  165. * @param x1 控制点1的x坐标 (0-1)
  166. * @param y1 控制点1的y坐标 (0-1)
  167. * @param x2 控制点2的x坐标 (0-1)
  168. * @param y2 控制点2的y坐标 (0-1)
  169. */
  170. function createBezierEasing(x1: number, y1: number, x2: number, y2: number): EasingFunction | null {
  171. // 验证控制点坐标范围,x坐标必须在0-1之间
  172. if (!(0 <= x1 && x1 <= 1 && 0 <= x2 && x2 <= 1)) {
  173. return null; // 参数无效时返回null
  174. }
  175. const sampleValues: number[] = []; // 预计算的样本值数组
  176. // 预计算样本值以提高性能,仅对非线性曲线进行预计算
  177. if (x1 != y1 || x2 != y2) {
  178. // 如果不是线性函数
  179. for (let i = 0; i < BEZIER_SPLINE_SIZE; i++) {
  180. // 计算等间距的样本点,用于快速查找
  181. sampleValues.push(calculateBezierValue(i * BEZIER_SAMPLE_STEP, x1, x2));
  182. }
  183. }
  184. /**
  185. * 根据x值获取对应的t参数
  186. * 使用预计算样本进行快速查找和插值
  187. * @param x 输入的x值 (0-1)
  188. */
  189. function getTParameterForX(x: number): number {
  190. let intervalStart = 0.0; // 区间起始位置
  191. let currentSample = 1; // 当前样本索引
  192. const lastSample = BEZIER_SPLINE_SIZE - 1; // 最后一个样本索引
  193. // 找到x值所在的区间,线性搜索预计算的样本
  194. for (; currentSample != lastSample && sampleValues[currentSample] <= x; currentSample++) {
  195. intervalStart += BEZIER_SAMPLE_STEP; // 移动区间起始位置
  196. }
  197. currentSample--; // 回退到正确的区间
  198. // 线性插值获得初始猜测值,提高后续求解精度
  199. const dist =
  200. (x - sampleValues[currentSample]) /
  201. (sampleValues[currentSample + 1] - sampleValues[currentSample]); // 计算在区间内的相对位置
  202. const initialGuess = intervalStart + dist * BEZIER_SAMPLE_STEP; // 计算初始猜测的t值
  203. const initialSlope = getBezierSlope(initialGuess, x1, x2); // 计算初始点的斜率
  204. // 根据斜率选择合适的求解方法
  205. if (initialSlope >= 0.001) {
  206. // 斜率足够大时使用牛顿法
  207. return newtonRaphsonBezierT(x, initialGuess, x1, x2);
  208. } else if (initialSlope == 0.0) {
  209. // 斜率为0时直接返回
  210. return initialGuess;
  211. }
  212. // 斜率太小时使用二分法,更稳定
  213. return binarySearchBezierT(x, intervalStart, intervalStart + BEZIER_SAMPLE_STEP, x1, x2);
  214. }
  215. // 返回缓动函数,这是最终的缓动函数接口
  216. return function (progress: number): number {
  217. // 线性情况直接返回,优化性能
  218. if (x1 == y1 && x2 == y2) {
  219. return progress;
  220. }
  221. // 边界情况处理,避免计算误差
  222. if (progress == 0.0 || progress == 1.0) {
  223. return progress;
  224. }
  225. // 计算贝塞尔曲线值:先根据progress(x)找到对应的t,再计算y值
  226. return calculateBezierValue(getTParameterForX(progress), y1, y2);
  227. };
  228. }
  229. /**
  230. * 颜色工具函数:标准化颜色值格式
  231. * 处理不同格式的颜色输入,确保返回有效的颜色值
  232. */
  233. function getDefaultColor(colorValue: string): string {
  234. // 简化的颜色处理,实际项目中可能需要更完整的颜色转换
  235. if (colorValue.startsWith("#")) {
  236. // 十六进制颜色格式
  237. return colorValue;
  238. }
  239. if (colorValue.startsWith("rgb")) {
  240. // RGB或RGBA颜色格式
  241. return colorValue;
  242. }
  243. // 默认返回黑色,作为兜底处理
  244. return "#000000";
  245. }
  246. /**
  247. * 十六进制颜色转RGB对象
  248. * 将#RRGGBB格式的颜色转换为{r,g,b,a}对象,用于颜色动画插值
  249. * @param hex 十六进制颜色值,如"#FF0000"
  250. * @returns 包含r,g,b,a属性的颜色对象
  251. */
  252. function hexToRgb(hex: string): UTSJSONObject {
  253. // 使用正则表达式解析十六进制颜色,支持带#和不带#的格式
  254. const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  255. if (result != null) {
  256. // 解析成功
  257. return {
  258. r: parseInt(result[1] ?? "0", 16), // 红色分量,16进制转10进制
  259. g: parseInt(result[2] ?? "0", 16), // 绿色分量
  260. b: parseInt(result[3] ?? "0", 16), // 蓝色分量
  261. a: 1.0 // 透明度,默认不透明
  262. } as UTSJSONObject;
  263. }
  264. // 解析失败时返回黑色
  265. return {
  266. r: 0,
  267. g: 0,
  268. b: 0,
  269. a: 1.0
  270. } as UTSJSONObject;
  271. }
  272. /**
  273. * 高性能动画引擎类
  274. * 支持多平台的流畅动画效果,提供丰富的缓动函数和动画控制
  275. */
  276. export class AnimationEngine {
  277. /** 预定义缓动函数映射,存储常用的贝塞尔曲线参数 */
  278. private readonly easingPresets = new Map<string, number[]>([
  279. ["linear", [0.0, 0.0, 1.0, 1.0]], // 线性缓动
  280. ["ease", [0.25, 0.1, 0.25, 1.0]], // 默认缓动
  281. ["easeIn", [0.42, 0.0, 1.0, 1.0]], // 加速进入
  282. ["easeOut", [0.0, 0.0, 0.58, 1.0]], // 减速退出
  283. ["easeInOut", [0.42, 0.0, 0.58, 1.0]], // 先加速后减速
  284. ["easeInQuad", [0.55, 0.085, 0.68, 0.53]], // 二次方加速
  285. ["easeOutQuad", [0.25, 0.46, 0.45, 0.94]], // 二次方减速
  286. ["easeInOutQuad", [0.455, 0.03, 0.515, 0.955]], // 二次方先加速后减速
  287. ["easeInCubic", [0.55, 0.055, 0.675, 0.19]], // 三次方加速
  288. ["easeOutCubic", [0.215, 0.61, 0.355, 1.0]], // 三次方减速
  289. ["easeInOutCubic", [0.645, 0.045, 0.355, 1.0]], // 三次方先加速后减速
  290. ["easeInQuart", [0.895, 0.03, 0.685, 0.22]], // 四次方加速
  291. ["easeOutQuart", [0.165, 0.84, 0.44, 1.0]], // 四次方减速
  292. ["easeInOutQuart", [0.77, 0.0, 0.175, 1.0]], // 四次方先加速后减速
  293. ["easeInQuint", [0.755, 0.05, 0.855, 0.06]], // 五次方加速
  294. ["easeOutQuint", [0.23, 1.0, 0.32, 1.0]], // 五次方减速
  295. ["easeInOutQuint", [0.86, 0.0, 0.07, 1.0]], // 五次方先加速后减速
  296. ["easeInSine", [0.47, 0.0, 0.745, 0.715]], // 正弦加速
  297. ["easeOutSine", [0.39, 0.575, 0.565, 1.0]], // 正弦减速
  298. ["easeInOutSine", [0.445, 0.05, 0.55, 0.95]], // 正弦先加速后减速
  299. ["easeInExpo", [0.95, 0.05, 0.795, 0.035]], // 指数加速
  300. ["easeOutExpo", [0.19, 1.0, 0.22, 1.0]], // 指数减速
  301. ["easeInOutExpo", [1.0, 0.0, 0.0, 1.0]], // 指数先加速后减速
  302. ["easeInCirc", [0.6, 0.04, 0.98, 0.335]], // 圆形加速
  303. ["easeOutCirc", [0.075, 0.82, 0.165, 1.0]], // 圆形减速
  304. ["easeInOutBack", [0.68, -0.55, 0.265, 1.55]] // 回弹效果
  305. ]);
  306. /** 目标DOM元素,动画作用的对象 */
  307. private targetElement: UniElement | null = null;
  308. /** 动画持续时间(毫秒),默认500ms */
  309. private animationDuration: number = 500;
  310. /** 动画是否正在运行,用于控制动画循环 */
  311. private isRunning: boolean = false;
  312. /** 动画是否暂停,暂停时保留当前进度 */
  313. private isPaused: boolean = false;
  314. /** 当前动画进度 (0-1),用于恢复暂停的动画 */
  315. private currentProgress: number = 0;
  316. /** 是否反向播放,影响动画方向 */
  317. private isReversed: boolean = false;
  318. /** 是否往返播放模式,控制动画是否来回播放 */
  319. private isAlternate: boolean = false;
  320. /** 往返播放时是否处于反向状态 */
  321. private isAlternateReversed: boolean = false;
  322. /** 循环播放次数 (-1为无限循环) */
  323. private loopCount: number = 1;
  324. /** 当前已完成的循环次数 */
  325. private currentLoop: number = 0;
  326. /** 动画是否正在停止,用于提前终止动画 */
  327. private isStopping: boolean = true;
  328. /** 当前执行的属性索引(顺序执行模式),用于控制属性依次动画 */
  329. private currentAttributeIndex: number = 0;
  330. /** 回调函数,提供动画生命周期钩子 */
  331. private onComplete: () => void = () => {}; // 动画完成回调
  332. private onStart: () => void = () => {}; // 动画开始回调
  333. private onFrame: (progress: number) => void = () => {}; // 每帧回调
  334. /** 动画属性列表,存储所有要动画的CSS属性 */
  335. private animationAttributes: AnimationAttribute[] = [];
  336. /** 动画开始时间戳,用于计算动画进度 */
  337. private startTimestamp: number = 0;
  338. /** 当前使用的缓动函数,将线性进度转换为缓动进度 */
  339. private currentEasingFunction: EasingFunction | null = null;
  340. /** 是否按属性顺序依次执行动画,而非并行执行 */
  341. private isSequentialMode: boolean = false;
  342. // 平台相关的动画控制器
  343. // Android平台使用Choreographer提供高性能动画
  344. // #ifdef APP-ANDROID
  345. private choreographer: Choreographer | null = null; // Android系统帧同步器
  346. private frameCallback: FrameCallback | null = null; // 帧回调处理器
  347. // #endif
  348. // iOS/小程序平台使用定时器
  349. // #ifdef APP-IOS
  350. private displayLinkTimer: number = 0; // iOS定时器ID
  351. // #endif
  352. // Web平台使用requestAnimationFrame
  353. private animationFrameId: number | null = null; // 动画帧ID
  354. /**
  355. * 创建动画引擎实例
  356. * 初始化动画引擎,设置目标元素和动画配置
  357. * @param element 目标DOM元素,null时仅做计算不应用样式
  358. * @param options 动画配置选项,包含持续时间、缓动函数等
  359. */
  360. constructor(element: UniElement | null, options: AnimationOptions) {
  361. this.targetElement = element; // 保存目标元素引用
  362. // 设置动画参数,使用选项值或默认值
  363. this.animationDuration =
  364. options.duration != null ? options.duration : this.animationDuration; // 设置动画持续时间
  365. this.loopCount = options.loop != null ? options.loop : this.loopCount; // 设置循环次数
  366. this.isAlternate = options.alternate != null ? options.alternate : this.isAlternate; // 设置往返播放
  367. this.isSequentialMode =
  368. options.sequential != null ? options.sequential : this.isSequentialMode; // 设置顺序执行模式
  369. // 设置缓动函数,优先使用预定义函数
  370. if (options.timingFunction != null) {
  371. const easingParams = this.easingPresets.get(options.timingFunction); // 查找预定义缓动参数
  372. if (easingParams != null) {
  373. // 根据贝塞尔参数创建缓动函数
  374. this.currentEasingFunction = createBezierEasing(
  375. easingParams[0], // x1坐标
  376. easingParams[1], // y1坐标
  377. easingParams[2], // x2坐标
  378. easingParams[3] // y2坐标
  379. );
  380. }
  381. }
  382. // 自定义贝塞尔曲线,会覆盖预定义函数
  383. if (options.bezier != null && options.bezier.length == 4) {
  384. this.currentEasingFunction = createBezierEasing(
  385. options.bezier[0], // 自定义x1坐标
  386. options.bezier[1], // 自定义y1坐标
  387. options.bezier[2], // 自定义x2坐标
  388. options.bezier[3] // 自定义y2坐标
  389. );
  390. }
  391. // 设置回调函数,提供动画生命周期钩子
  392. if (options.complete != null) {
  393. this.onComplete = options.complete; // 动画完成回调
  394. }
  395. if (options.start != null) {
  396. this.onStart = options.start; // 动画开始回调
  397. }
  398. if (options.frame != null) {
  399. this.onFrame = options.frame; // 每帧更新回调
  400. }
  401. }
  402. /**
  403. * 从样式值中提取单位
  404. * 解析CSS值中的单位部分,用于动画计算
  405. * @param value 样式值,如 "100px", "50%"
  406. * @param propertyName CSS属性名称,用于判断是否需要默认单位
  407. * @returns 单位字符串
  408. */
  409. private extractUnit(value?: string, propertyName?: string): string {
  410. if (value == null) return "px"; // 默认单位为px
  411. const unit = value.replace(/[\d|\-|\+|\.]/g, ""); // 移除数字、负号、正号、小数点,保留单位
  412. // opacity、z-index等属性无需单位
  413. if (propertyName == "opacity" || propertyName == "z-index") {
  414. return ""; // 返回空字符串表示无单位
  415. }
  416. return unit == "" ? "px" : unit; // 如果没有单位则默认为px
  417. }
  418. /**
  419. * 添加自定义缓动函数
  420. * 向引擎注册新的缓动函数,可在后续动画中使用
  421. * @param name 缓动函数名称
  422. * @param bezierParams 贝塞尔曲线参数 [x1, y1, x2, y2]
  423. */
  424. addCustomEasing(name: string, bezierParams: number[]): AnimationEngine {
  425. if (bezierParams.length == 4) {
  426. // 验证参数数量
  427. this.easingPresets.set(name, bezierParams); // 添加到预设映射中
  428. }
  429. return this; // 返回自身支持链式调用
  430. }
  431. /**
  432. * 设置动画反向播放
  433. * 控制动画从结束值向起始值播放
  434. * @param reverse 是否反向播放,null表示切换当前状态
  435. */
  436. setReverse(reverse: boolean | null = null): AnimationEngine {
  437. if (reverse != null) {
  438. this.isReversed = reverse; // 设置指定状态
  439. } else {
  440. this.isReversed = !this.isReversed; // 切换当前状态
  441. }
  442. return this; // 支持链式调用
  443. }
  444. /**
  445. * 设置循环播放次数
  446. * 控制动画重复执行的次数
  447. * @param count 循环次数,-1表示无限循环
  448. */
  449. setLoopCount(count: number): AnimationEngine {
  450. this.loopCount = count; // 设置循环次数
  451. return this; // 支持链式调用
  452. }
  453. /**
  454. * 设置动画持续时间
  455. * 控制动画从开始到结束的总时长
  456. * @param duration 持续时间(毫秒)
  457. */
  458. setDuration(duration: number): AnimationEngine {
  459. this.animationDuration = duration; // 设置动画持续时间
  460. return this; // 支持链式调用
  461. }
  462. /**
  463. * 设置往返播放模式
  464. * 控制动画是否在每次循环时反向播放
  465. * @param alternate 是否往返播放
  466. */
  467. setAlternate(alternate: boolean): AnimationEngine {
  468. this.isAlternate = alternate; // 设置往返播放标志
  469. return this; // 支持链式调用
  470. }
  471. /**
  472. * 设置顺序执行模式
  473. * 控制多个属性是同时动画还是依次动画
  474. * @param sequential 是否按属性顺序依次执行
  475. */
  476. setSequential(sequential: boolean): AnimationEngine {
  477. this.isSequentialMode = sequential; // 设置执行模式
  478. return this; // 支持链式调用
  479. }
  480. /**
  481. * 添加动画属性
  482. * 向动画引擎添加一个CSS属性的动画配置
  483. * @param propertyName CSS属性名称
  484. * @param fromValue 起始值(支持数字+单位,如"100px"、"50%")
  485. * @param toValue 结束值(单位必须与起始值一致)
  486. * @param unique 是否唯一,true时同名属性会被替换
  487. */
  488. addAttribute(
  489. propertyName: string,
  490. fromValue: string,
  491. toValue: string,
  492. unique: boolean = true
  493. ): AnimationEngine {
  494. const isColor = this.isColorProperty(propertyName); // 检测是否为颜色属性
  495. const unit = isColor ? "" : this.extractUnit(fromValue, propertyName); // 提取单位
  496. // 根据属性类型处理值
  497. const processedFromValue = isColor
  498. ? getDefaultColor(fromValue) // 颜色属性标准化
  499. : parseFloat(fromValue).toString(); // 数值属性提取数字
  500. const processedToValue = isColor
  501. ? getDefaultColor(toValue) // 颜色属性标准化
  502. : parseFloat(toValue).toString(); // 数值属性提取数字
  503. // 查找是否已存在同名属性,用于决定是替换还是新增
  504. let existingIndex = this.animationAttributes.findIndex(
  505. (attr: AnimationAttribute): boolean => attr.propertyName == propertyName
  506. );
  507. if (!unique) {
  508. existingIndex = -1; // 强制添加新属性,不替换
  509. }
  510. // 创建新的动画属性对象
  511. const newAttribute: AnimationAttribute = {
  512. fromValue: processedFromValue, // 处理后的起始值
  513. toValue: processedToValue, // 处理后的结束值
  514. unit: unit, // 单位
  515. progress: 0, // 初始进度为0
  516. currentValue: processedFromValue, // 当前值初始化为起始值
  517. propertyName: propertyName // 属性名称
  518. };
  519. if (existingIndex == -1) {
  520. this.animationAttributes.push(newAttribute); // 添加新属性
  521. } else {
  522. this.animationAttributes[existingIndex] = newAttribute; // 替换现有属性
  523. }
  524. return this; // 支持链式调用
  525. }
  526. /**
  527. * 快捷方法:添加变换属性
  528. */
  529. transform(property: string, fromValue: string, toValue: string): AnimationEngine {
  530. return this.addAttribute(property, fromValue, toValue);
  531. }
  532. /**
  533. * 快捷方法:添加位移动画
  534. */
  535. translate(fromX: string, fromY: string, toX: string, toY: string): AnimationEngine {
  536. this.addAttribute("translateX", fromX, toX);
  537. this.addAttribute("translateY", fromY, toY);
  538. return this;
  539. }
  540. /**
  541. * 添加X轴位移动画
  542. * @param fromX 起始X位置,可以使用"current"表示当前位置
  543. * @param toX 结束X位置
  544. * @returns
  545. */
  546. translateX(fromX: string, toX: string): AnimationEngine {
  547. return this.addAttribute("translateX", fromX, toX);
  548. }
  549. /**
  550. * 添加Y轴位移动画
  551. * @param fromY 起始Y位置,可以使用"current"表示当前位置
  552. * @param toY 结束Y位置
  553. * @returns
  554. */
  555. translateY(fromY: string, toY: string): AnimationEngine {
  556. return this.addAttribute("translateY", fromY, toY);
  557. }
  558. /**
  559. * 快捷方法:添加缩放动画
  560. */
  561. scale(fromScale: string, toScale: string): AnimationEngine {
  562. return this.addAttribute("scale", fromScale, toScale);
  563. }
  564. /**
  565. * 快捷方法:添加旋转动画
  566. */
  567. rotate(fromDegree: string, toDegree: string): AnimationEngine {
  568. return this.addAttribute("rotate", fromDegree, toDegree);
  569. }
  570. /**
  571. * 快捷方法:添加透明度动画
  572. */
  573. opacity(fromOpacity: string, toOpacity: string): AnimationEngine {
  574. return this.addAttribute("opacity", fromOpacity, toOpacity);
  575. }
  576. /**
  577. * 线性插值计算
  578. * 根据进度在两个数值之间进行插值,用于计算动画中间值
  579. * @param startValue 起始值
  580. * @param endValue 结束值
  581. * @param progress 进度 (0-1)
  582. */
  583. private interpolateValue(startValue: number, endValue: number, progress: number): number {
  584. return startValue + (endValue - startValue) * progress; // 线性插值公式:start + (end - start) * progress
  585. }
  586. /**
  587. * 判断是否为颜色相关属性
  588. * 检测CSS属性名是否与颜色相关,用于特殊的颜色动画处理
  589. * @param propertyName 属性名称
  590. */
  591. private isColorProperty(propertyName: string): boolean {
  592. return (
  593. propertyName.indexOf("background") > -1 || // 背景颜色相关
  594. propertyName.indexOf("color") > -1 || // 文字颜色相关
  595. propertyName.indexOf("border-color") > -1 || // 边框颜色相关
  596. propertyName.indexOf("shadow") > -1 // 阴影颜色相关
  597. );
  598. }
  599. /**
  600. * 判断是否为Transform相关属性
  601. * 检测属性名是否为transform相关的CSS属性
  602. * @param propertyName CSS属性名称
  603. * @returns 是否为transform属性
  604. */
  605. private isTransformProperty(propertyName: string): boolean {
  606. return (
  607. propertyName == "scaleX" || // X轴缩放
  608. propertyName == "scaleY" || // Y轴缩放
  609. propertyName == "scale" || // 等比缩放
  610. propertyName == "rotateX" || // X轴旋转
  611. propertyName == "rotateY" || // Y轴旋转
  612. propertyName == "rotate" || // Z轴旋转
  613. propertyName == "translateX" || // X轴位移
  614. propertyName == "translateY" || // Y轴位移
  615. propertyName == "translate" // 双轴位移
  616. );
  617. }
  618. /**
  619. * 设置元素样式属性
  620. * 根据属性类型应用相应的样式值,支持transform、颜色、普通数值属性
  621. * @param propertyName 属性名称
  622. * @param currentValue 当前值
  623. * @param unit 单位
  624. * @param progress 动画进度
  625. * @param attribute 动画属性对象
  626. */
  627. private setElementProperty(
  628. propertyName: string,
  629. currentValue: number,
  630. unit: string,
  631. progress: number,
  632. attribute: AnimationAttribute
  633. ): void {
  634. if (this.targetElement == null) return; // 没有目标元素时直接返回
  635. const element = this.targetElement; // 获取目标元素引用
  636. const valueStr = currentValue.toFixed(2); // 数值保留两位小数
  637. // #ifdef MP
  638. if (element.style == null) {
  639. return;
  640. }
  641. // #endif
  642. // Transform 相关属性处理,使用CSS transform属性
  643. switch (propertyName) {
  644. case "scaleX": // X轴缩放
  645. element.style!.setProperty("transform", `scaleX(${currentValue})`);
  646. break;
  647. case "scaleY": // Y轴缩放
  648. element.style!.setProperty("transform", `scaleY(${currentValue})`);
  649. break;
  650. case "scale": // 等比缩放
  651. element.style!.setProperty("transform", `scale(${currentValue})`);
  652. break;
  653. case "rotateX": // X轴旋转
  654. element.style!.setProperty("transform", `rotateX(${valueStr + unit})`);
  655. break;
  656. case "rotateY": // Y轴旋转
  657. element.style!.setProperty("transform", `rotateY(${valueStr + unit})`);
  658. break;
  659. case "rotate": // Z轴旋转
  660. element.style!.setProperty("transform", `rotate(${valueStr + unit})`);
  661. break;
  662. case "translateX": // X轴位移
  663. element.style!.setProperty("transform", `translateX(${valueStr + unit})`);
  664. break;
  665. case "translateY": // Y轴位移
  666. element.style!.setProperty("transform", `translateY(${valueStr + unit})`);
  667. break;
  668. case "translate": // 双轴位移
  669. element.style!.setProperty(
  670. "transform",
  671. `translate(${valueStr + unit},${valueStr + unit})`
  672. );
  673. break;
  674. default:
  675. // 颜色属性处理,需要进行RGBA插值
  676. if (this.isColorProperty(propertyName)) {
  677. const startColor = hexToRgb(attribute.fromValue); // 解析起始颜色
  678. const endColor = hexToRgb(attribute.toValue); // 解析结束颜色
  679. // 提取起始颜色的RGBA分量,兼容不同的JSON对象访问方式
  680. const startR =
  681. startColor.getNumber != null
  682. ? startColor.getNumber("r")
  683. : (startColor["r"] as number);
  684. const startG =
  685. startColor.getNumber != null
  686. ? startColor.getNumber("g")
  687. : (startColor["g"] as number);
  688. const startB =
  689. startColor.getNumber != null
  690. ? startColor.getNumber("b")
  691. : (startColor["b"] as number);
  692. const startA =
  693. startColor.getNumber != null
  694. ? startColor.getNumber("a")
  695. : (startColor["a"] as number);
  696. // 提取结束颜色的RGBA分量
  697. const endR =
  698. endColor.getNumber != null
  699. ? endColor.getNumber("r")
  700. : (endColor["r"] as number);
  701. const endG =
  702. endColor.getNumber != null
  703. ? endColor.getNumber("g")
  704. : (endColor["g"] as number);
  705. const endB =
  706. endColor.getNumber != null
  707. ? endColor.getNumber("b")
  708. : (endColor["b"] as number);
  709. const endA =
  710. endColor.getNumber != null
  711. ? endColor.getNumber("a")
  712. : (endColor["a"] as number);
  713. // 对每个颜色分量进行插值计算
  714. const r = this.interpolateValue(
  715. startR != null ? startR : 0,
  716. endR != null ? endR : 0,
  717. progress
  718. );
  719. const g = this.interpolateValue(
  720. startG != null ? startG : 0,
  721. endG != null ? endG : 0,
  722. progress
  723. );
  724. const b = this.interpolateValue(
  725. startB != null ? startB : 0,
  726. endB != null ? endB : 0,
  727. progress
  728. );
  729. const a = this.interpolateValue(
  730. startA != null ? startA : 1,
  731. endA != null ? endA : 1,
  732. progress
  733. );
  734. // 设置RGBA颜色值
  735. element.style!.setProperty(
  736. propertyName,
  737. `rgba(${r.toFixed(0)},${g.toFixed(0)},${b.toFixed(0)},${a.toFixed(1)})`
  738. );
  739. } else {
  740. // 普通数值属性处理,直接设置数值和单位
  741. element.style!.setProperty(propertyName, valueStr + unit);
  742. }
  743. break;
  744. }
  745. }
  746. /**
  747. * Web平台动画运行方法 (H5/iOS/Harmony)
  748. * 使用requestAnimationFrame实现流畅的动画循环
  749. */
  750. private runWebAnimation(): void {
  751. // #ifdef H5 || APP-IOS || APP-HARMONY
  752. const self = this; // 保存this引用,避免在内部函数中this指向改变
  753. self.startTimestamp = 0; // 重置开始时间戳
  754. // 取消之前的动画帧,避免重复执行
  755. if (self.animationFrameId != null) {
  756. cancelAnimationFrame(self.animationFrameId);
  757. }
  758. function animationLoop(): void {
  759. // 初始化开始时间,首次执行时记录时间戳
  760. if (self.startTimestamp <= 0) {
  761. self.startTimestamp = Date.now();
  762. }
  763. // 计算当前进度:(已用时间 / 总时间) + 暂停前的进度
  764. const elapsed = Date.now() - self.startTimestamp; // 已经过的时间
  765. const progress = Math.min(elapsed / self.animationDuration + self.currentProgress, 1.0); // 限制进度不超过1
  766. // 执行动画更新,应用当前进度到所有属性
  767. self.updateAnimationFrame(progress);
  768. // 检查暂停状态
  769. if (self.isPaused) {
  770. self.isRunning = false; // 停止运行标志
  771. self.currentProgress = progress; // 保存当前进度,用于恢复
  772. console.log("动画已暂停");
  773. return; // 退出动画循环
  774. }
  775. // 检查动画完成或停止
  776. if (progress >= 1.0 || self.isStopping) {
  777. self.handleAnimationComplete(); // 处理动画完成逻辑
  778. return; // 退出动画循环
  779. }
  780. // 继续下一帧,动画未完成且仍在运行
  781. if (progress < 1.0 && self.isRunning) {
  782. self.onFrame(progress); // 触发每帧回调
  783. self.animationFrameId = requestAnimationFrame(animationLoop); // 请求下一帧
  784. }
  785. }
  786. // 开始动画,触发开始回调并启动动画循环
  787. self.onStart();
  788. animationLoop();
  789. // #endif
  790. }
  791. /**
  792. * 更新动画帧
  793. * 根据执行模式更新所有或当前属性的动画值
  794. * @param progress 当前进度 (0-1)
  795. */
  796. private updateAnimationFrame(progress: number): void {
  797. if (this.targetElement == null) return; // 没有目标元素时直接返回
  798. if (!this.isSequentialMode) {
  799. // 并行执行所有属性动画,所有属性同时进行动画
  800. for (let i = 0; i < this.animationAttributes.length; i++) {
  801. this.updateSingleAttribute(this.animationAttributes[i], progress);
  802. }
  803. } else {
  804. // 顺序执行属性动画,一个接一个地执行属性动画
  805. if (this.currentAttributeIndex < this.animationAttributes.length) {
  806. this.updateSingleAttribute(
  807. this.animationAttributes[this.currentAttributeIndex],
  808. progress
  809. );
  810. }
  811. }
  812. }
  813. /**
  814. * 更新单个属性的动画
  815. * 计算属性的当前值并应用到元素上
  816. * @param attribute 动画属性
  817. * @param progress 进度
  818. */
  819. private updateSingleAttribute(attribute: AnimationAttribute, progress: number): void {
  820. attribute.progress = progress; // 更新属性的进度记录
  821. if (!this.isColorProperty(attribute.propertyName)) {
  822. // 数值属性处理
  823. const fromValue = parseFloat(attribute.fromValue); // 起始数值
  824. const toValue = parseFloat(attribute.toValue); // 结束数值
  825. // 应用缓动函数,将线性进度转换为缓动进度
  826. let easedProgress = progress;
  827. if (this.currentEasingFunction != null) {
  828. easedProgress = this.currentEasingFunction(progress);
  829. }
  830. // 计算当前值,使用缓动进度进行插值
  831. let currentValue = this.interpolateValue(fromValue, toValue, easedProgress);
  832. // 处理反向和往返播放,交换起始和结束值
  833. if (this.isReversed || this.isAlternateReversed) {
  834. currentValue = this.interpolateValue(toValue, fromValue, easedProgress);
  835. }
  836. // 应用计算出的值到元素属性
  837. this.setElementProperty(
  838. attribute.propertyName,
  839. currentValue,
  840. attribute.unit,
  841. progress,
  842. attribute
  843. );
  844. } else {
  845. // 颜色属性处理,progress参数会在setElementProperty中用于颜色插值
  846. this.setElementProperty(attribute.propertyName, 0, attribute.unit, progress, attribute);
  847. }
  848. }
  849. /**
  850. * 处理动画完成
  851. */
  852. private handleAnimationComplete(): void {
  853. // 顺序模式下检查是否还有未执行的属性
  854. if (
  855. this.isSequentialMode &&
  856. this.currentAttributeIndex < this.animationAttributes.length - 1
  857. ) {
  858. this.currentAttributeIndex++;
  859. this.currentProgress = 0;
  860. this.restartAnimation();
  861. return;
  862. }
  863. // 重置状态
  864. // #ifdef H5 || APP-IOS || APP-HARMONY
  865. if (this.animationFrameId != null) {
  866. cancelAnimationFrame(this.animationFrameId);
  867. }
  868. // #endif
  869. this.currentAttributeIndex = 0;
  870. this.currentProgress = 0;
  871. // 处理往返播放
  872. if (this.isAlternate) {
  873. this.isAlternateReversed = !this.isAlternateReversed;
  874. }
  875. // 处理循环播放
  876. if (this.loopCount == -1) {
  877. // 无限循环
  878. this.restartAnimation();
  879. return;
  880. } else {
  881. this.currentLoop++;
  882. if (this.currentLoop < this.loopCount) {
  883. this.restartAnimation();
  884. return;
  885. }
  886. }
  887. // 动画完成
  888. this.isRunning = false;
  889. this.onComplete();
  890. }
  891. /**
  892. * 根据平台重新启动动画
  893. */
  894. private restartAnimation(): void {
  895. // 重置开始时间戳,确保循环动画正确计时
  896. this.startTimestamp = 0;
  897. // 根据平台选择合适的动画引擎
  898. // #ifdef H5 || APP-IOS || APP-HARMONY
  899. this.runWebAnimation();
  900. // #endif
  901. // #ifdef APP-ANDROID
  902. this.runAndroidAnimation();
  903. // #endif
  904. // #ifdef MP
  905. this.runMPAnimation();
  906. // #endif
  907. }
  908. /**
  909. * Android平台动画运行方法
  910. */
  911. private runAndroidAnimation(): void {
  912. // #ifdef APP-ANDROID
  913. const self = this;
  914. self.startTimestamp = 0;
  915. // 初始化Choreographer
  916. if (self.choreographer == null) {
  917. self.choreographer = Choreographer.getInstance();
  918. } else {
  919. // 清除之前的回调
  920. if (self.frameCallback != null) {
  921. self.choreographer.removeFrameCallback(self.frameCallback);
  922. }
  923. }
  924. /**
  925. * Android原生帧回调类
  926. */
  927. class frameCallback extends Choreographer.FrameCallback {
  928. // @ts-ignore
  929. override doFrame(frameTimeNanos: Long) {
  930. // 检查动画是否应该停止
  931. if (!self.isRunning || self.isStopping) {
  932. return;
  933. }
  934. // 初始化开始时间
  935. if (self.startTimestamp <= 0) {
  936. self.startTimestamp = Date.now();
  937. }
  938. // 计算当前进度
  939. const elapsed = Date.now() - self.startTimestamp;
  940. const progress = Math.min(
  941. elapsed / self.animationDuration + self.currentProgress,
  942. 1.0
  943. );
  944. // 执行动画更新
  945. self.updateAnimationFrame(progress);
  946. // 检查暂停状态
  947. if (self.isPaused) {
  948. self.isRunning = false;
  949. self.currentProgress = progress;
  950. return;
  951. }
  952. // 检查动画完成或停止
  953. if (progress >= 1.0 || self.isStopping) {
  954. self.handleAnimationComplete();
  955. return;
  956. }
  957. // 继续下一帧
  958. if (progress < 1.0 && self.isRunning && !self.isStopping) {
  959. self.onFrame(progress);
  960. if (self.choreographer != null) {
  961. self.choreographer.postFrameCallback(this);
  962. }
  963. }
  964. }
  965. }
  966. // 启动动画
  967. self.onStart();
  968. self.frameCallback = new frameCallback();
  969. self.choreographer!.postFrameCallback(self.frameCallback);
  970. // #endif
  971. }
  972. /**
  973. * 小程序平台动画运行方法
  974. */
  975. private runMPAnimation(): void {
  976. // #ifdef MP
  977. const self = this;
  978. self.startTimestamp = 0;
  979. // 清除之前的定时器
  980. if (self.displayLinkTimer != 0) {
  981. clearTimeout(self.displayLinkTimer);
  982. }
  983. function animationLoop(): void {
  984. // 初始化开始时间
  985. if (self.startTimestamp <= 0) {
  986. self.startTimestamp = Date.now();
  987. }
  988. // 计算当前进度
  989. const elapsed = Date.now() - self.startTimestamp;
  990. const progress = Math.min(elapsed / self.animationDuration + self.currentProgress, 1.0);
  991. // 执行动画更新
  992. self.updateAnimationFrame(progress);
  993. // 检查暂停状态
  994. if (self.isPaused) {
  995. self.isRunning = false;
  996. self.currentProgress = progress;
  997. return;
  998. }
  999. // 检查动画完成或停止
  1000. if (progress >= 1.0 || self.isStopping) {
  1001. self.handleAnimationComplete();
  1002. return;
  1003. }
  1004. // 继续下一帧
  1005. if (progress < 1.0 && self.isRunning) {
  1006. self.onFrame(progress);
  1007. self.displayLinkTimer = setTimeout(animationLoop, 16) as any; // 约60fps
  1008. }
  1009. }
  1010. // 开始动画
  1011. self.onStart();
  1012. animationLoop();
  1013. // #endif
  1014. }
  1015. /**
  1016. * 开始播放动画
  1017. */
  1018. play(): AnimationEngine {
  1019. if (this.isRunning) return this;
  1020. // 初始化动画状态
  1021. this.isRunning = true;
  1022. this.isStopping = false;
  1023. this.isPaused = false;
  1024. this.currentLoop = 0;
  1025. this.currentAttributeIndex = 0;
  1026. // 根据平台选择合适的动画引擎
  1027. // #ifdef H5 || APP-IOS || APP-HARMONY
  1028. this.runWebAnimation();
  1029. // #endif
  1030. // #ifdef APP-ANDROID
  1031. this.runAndroidAnimation();
  1032. // #endif
  1033. // #ifdef MP
  1034. this.runMPAnimation();
  1035. // #endif
  1036. return this;
  1037. }
  1038. /**
  1039. * 异步播放动画,支持await
  1040. * @returns Promise,动画完成时resolve
  1041. */
  1042. playAsync(): Promise<void> {
  1043. return new Promise<void>((resolve) => {
  1044. const originalComplete = this.onComplete;
  1045. this.onComplete = () => {
  1046. originalComplete();
  1047. resolve();
  1048. };
  1049. this.play();
  1050. });
  1051. }
  1052. /**
  1053. * 停止动画
  1054. * 会立即停止动画并跳转到结束状态
  1055. */
  1056. stop(): AnimationEngine {
  1057. this.isStopping = true;
  1058. this.currentProgress = 0;
  1059. this.currentAttributeIndex = this.animationAttributes.length;
  1060. // 清理平台相关的动画控制器
  1061. // #ifdef WEB || APP-IOS || APP-HARMONY
  1062. if (this.animationFrameId != null) {
  1063. cancelAnimationFrame(this.animationFrameId);
  1064. this.animationFrameId = null;
  1065. }
  1066. // #endif
  1067. // #ifdef APP-ANDROID
  1068. if (this.choreographer != null && this.frameCallback != null) {
  1069. this.choreographer.removeFrameCallback(this.frameCallback);
  1070. }
  1071. // #endif
  1072. // #ifdef MP
  1073. if (this.displayLinkTimer != 0) {
  1074. clearTimeout(this.displayLinkTimer);
  1075. this.displayLinkTimer = 0;
  1076. }
  1077. // #endif
  1078. this.isRunning = false;
  1079. return this;
  1080. }
  1081. /**
  1082. * 暂停动画
  1083. * 保留当前状态,可以通过play()恢复
  1084. */
  1085. pause(): AnimationEngine {
  1086. this.isPaused = true;
  1087. return this;
  1088. }
  1089. /**
  1090. * 恢复暂停的动画
  1091. */
  1092. resume(): AnimationEngine {
  1093. if (this.isPaused) {
  1094. this.isPaused = false;
  1095. this.play();
  1096. }
  1097. return this;
  1098. }
  1099. /**
  1100. * 清空应用到元素上的动画样式
  1101. * 只清空实际被动画引擎设置过的CSS属性
  1102. */
  1103. private clearElementStyles(): void {
  1104. if (this.targetElement == null) return;
  1105. const element = this.targetElement;
  1106. // 清空所有动画属性列表中记录的属性
  1107. for (const attr of this.animationAttributes) {
  1108. const propertyName = attr.propertyName;
  1109. // Transform 相关属性需要清空transform
  1110. if (this.isTransformProperty(propertyName)) {
  1111. element.style!.setProperty("transform", "");
  1112. } else {
  1113. // 其他属性直接清空
  1114. element.style!.setProperty(propertyName, "");
  1115. }
  1116. }
  1117. }
  1118. /**
  1119. * 重置动画到初始状态,清空所有内容
  1120. */
  1121. reset(): AnimationEngine {
  1122. // 停止当前动画
  1123. this.stop();
  1124. // 清空应用到元素上的所有样式
  1125. this.clearElementStyles();
  1126. // 重置所有动画状态
  1127. this.currentProgress = 0;
  1128. this.currentLoop = 0;
  1129. this.currentAttributeIndex = 0;
  1130. this.isAlternateReversed = false;
  1131. this.isReversed = false;
  1132. this.isPaused = false;
  1133. this.isStopping = true;
  1134. this.startTimestamp = 0;
  1135. // 清空动画属性列表
  1136. this.animationAttributes = [];
  1137. // 重置缓动函数
  1138. this.currentEasingFunction = null;
  1139. // 重置回调函数
  1140. this.onComplete = () => {};
  1141. this.onStart = () => {};
  1142. this.onFrame = () => {};
  1143. // 清理平台相关的动画控制器
  1144. // #ifdef WEB || APP-IOS || APP-HARMONY
  1145. if (this.animationFrameId != null) {
  1146. cancelAnimationFrame(this.animationFrameId);
  1147. this.animationFrameId = null;
  1148. }
  1149. // #endif
  1150. // #ifdef APP-ANDROID
  1151. if (this.choreographer != null && this.frameCallback != null) {
  1152. this.choreographer.removeFrameCallback(this.frameCallback);
  1153. this.frameCallback = null;
  1154. }
  1155. this.choreographer = null;
  1156. // #endif
  1157. // #ifdef MP
  1158. if (this.displayLinkTimer != 0) {
  1159. clearTimeout(this.displayLinkTimer);
  1160. this.displayLinkTimer = 0;
  1161. }
  1162. // #endif
  1163. return this;
  1164. }
  1165. /**
  1166. * 获取当前动画进度
  1167. */
  1168. getProgress(): number {
  1169. return this.currentProgress;
  1170. }
  1171. /**
  1172. * 获取动画是否正在运行
  1173. */
  1174. isAnimating(): boolean {
  1175. return this.isRunning;
  1176. }
  1177. /**
  1178. * 获取当前循环次数
  1179. */
  1180. getCurrentLoop(): number {
  1181. return this.currentLoop;
  1182. }
  1183. /**
  1184. * 清除所有动画属性
  1185. */
  1186. clearAttributes(): AnimationEngine {
  1187. this.animationAttributes = [];
  1188. return this;
  1189. }
  1190. /**
  1191. * 获取动画属性数量
  1192. */
  1193. getAttributeCount(): number {
  1194. return this.animationAttributes.length;
  1195. }
  1196. /**
  1197. * 淡入动画
  1198. * @param duration 持续时间
  1199. */
  1200. fadeIn(duration: number = 300): AnimationEngine {
  1201. return this.setDuration(duration).opacity("0", "1");
  1202. }
  1203. /**
  1204. * 淡出动画
  1205. * @param duration 持续时间
  1206. */
  1207. fadeOut(duration: number = 300): AnimationEngine {
  1208. return this.setDuration(duration).opacity("1", "0");
  1209. }
  1210. /**
  1211. * 滑入动画(从左)
  1212. * @param duration 持续时间
  1213. */
  1214. slideInLeft(duration: number = 300): AnimationEngine {
  1215. return this.setDuration(duration).translateX("-100%", "0%").opacity("0", "1");
  1216. }
  1217. /**
  1218. * 滑入动画(从右)
  1219. * @param duration 持续时间
  1220. */
  1221. slideInRight(duration: number = 300): AnimationEngine {
  1222. return this.setDuration(duration).translateX("100%", "0%").opacity("0", "1");
  1223. }
  1224. /**
  1225. * 滑入动画(从上)
  1226. * @param duration 持续时间
  1227. */
  1228. slideInUp(duration: number = 300): AnimationEngine {
  1229. return this.setDuration(duration)
  1230. .addAttribute("translateY", "-100%", "0%")
  1231. .opacity("0", "1");
  1232. }
  1233. /**
  1234. * 滑入动画(从下)
  1235. * @param duration 持续时间
  1236. */
  1237. slideInDown(duration: number = 300): AnimationEngine {
  1238. return this.setDuration(duration)
  1239. .addAttribute("translateY", "100%", "0%")
  1240. .opacity("0", "1");
  1241. }
  1242. /**
  1243. * 缩放动画(放大)
  1244. * @param duration 持续时间
  1245. */
  1246. zoomIn(duration: number = 300): AnimationEngine {
  1247. return this.setDuration(duration).scale("0", "1").opacity("0", "1");
  1248. }
  1249. /**
  1250. * 缩放动画(缩小)
  1251. * @param duration 持续时间
  1252. */
  1253. zoomOut(duration: number = 300): AnimationEngine {
  1254. return this.setDuration(duration).scale("1", "0").opacity("1", "0");
  1255. }
  1256. /**
  1257. * 旋转动画
  1258. * @param duration 持续时间
  1259. * @param degrees 旋转角度
  1260. */
  1261. rotateIn(duration: number = 500, degrees: number = 360): AnimationEngine {
  1262. return this.setDuration(duration).rotate("0deg", `${degrees}deg`).opacity("0", "1");
  1263. }
  1264. /**
  1265. * 旋转退出动画
  1266. * @param duration 持续时间
  1267. * @param degrees 旋转角度
  1268. */
  1269. rotateOut(duration: number = 500, degrees: number = 360): AnimationEngine {
  1270. return this.setDuration(duration).rotate("0deg", `${degrees}deg`).opacity("1", "0");
  1271. }
  1272. /**
  1273. * 弹跳动画
  1274. * @param duration 持续时间
  1275. */
  1276. bounce(duration: number = 600): AnimationEngine {
  1277. return this.setDuration(duration)
  1278. .addCustomEasing("bounce", [0.68, -0.55, 0.265, 1.55])
  1279. .scale("1", "1.1")
  1280. .setAlternate(true)
  1281. .setLoopCount(2);
  1282. }
  1283. /**
  1284. * 摇摆动画
  1285. * @param duration 持续时间
  1286. */
  1287. shake(duration: number = 500): AnimationEngine {
  1288. return this.setDuration(duration)
  1289. .addAttribute("translateX", "0px", "10px")
  1290. .setAlternate(true)
  1291. .setLoopCount(6);
  1292. }
  1293. /**
  1294. * 链式动画:支持多个动画依次执行
  1295. * @param animations 动画配置函数数组
  1296. */
  1297. sequence(animations: ((engine: AnimationEngine) => AnimationEngine)[]): AnimationEngine {
  1298. const self = this;
  1299. if (animations.length == 0) {
  1300. return this;
  1301. }
  1302. // 执行第一个动画
  1303. const firstEngine = animations[0](new AnimationEngine(this.targetElement, {}));
  1304. // 如果只有一个动画,直接返回
  1305. if (animations.length == 1) {
  1306. return firstEngine;
  1307. }
  1308. // 递归设置后续动画
  1309. function setNextAnimation(
  1310. currentEngine: AnimationEngine,
  1311. remainingAnimations: ((engine: AnimationEngine) => AnimationEngine)[]
  1312. ): void {
  1313. if (remainingAnimations.length == 0) {
  1314. return;
  1315. }
  1316. const originalComplete = currentEngine.onComplete;
  1317. currentEngine.onComplete = () => {
  1318. originalComplete();
  1319. // 执行下一个动画
  1320. const nextEngine = remainingAnimations[0](
  1321. new AnimationEngine(self.targetElement, {})
  1322. );
  1323. // 如果还有更多动画,继续设置链式
  1324. if (remainingAnimations.length > 1) {
  1325. setNextAnimation(nextEngine, remainingAnimations.slice(1));
  1326. }
  1327. nextEngine.play();
  1328. };
  1329. }
  1330. // 设置动画链
  1331. setNextAnimation(firstEngine, animations.slice(1));
  1332. return firstEngine;
  1333. }
  1334. /**
  1335. * 滑出动画(向左)
  1336. * @param duration 持续时间
  1337. */
  1338. slideOutLeft(duration: number = 300): AnimationEngine {
  1339. return this.setDuration(duration).translateX("0%", "-100%").opacity("1", "0");
  1340. }
  1341. /**
  1342. * 滑出动画(向右)
  1343. * @param duration 持续时间
  1344. */
  1345. slideOutRight(duration: number = 300): AnimationEngine {
  1346. return this.setDuration(duration).translateX("0%", "100%").opacity("1", "0");
  1347. }
  1348. /**
  1349. * 滑出动画(向上)
  1350. * @param duration 持续时间
  1351. */
  1352. slideOutUp(duration: number = 300): AnimationEngine {
  1353. return this.setDuration(duration)
  1354. .addAttribute("translateY", "0%", "-100%")
  1355. .opacity("1", "0");
  1356. }
  1357. /**
  1358. * 滑出动画(向下)
  1359. * @param duration 持续时间
  1360. */
  1361. slideOutDown(duration: number = 300): AnimationEngine {
  1362. return this.setDuration(duration)
  1363. .addAttribute("translateY", "0%", "100%")
  1364. .opacity("1", "0");
  1365. }
  1366. /**
  1367. * 翻转动画(水平)
  1368. * @param duration 持续时间
  1369. */
  1370. flipX(duration: number = 600): AnimationEngine {
  1371. return this.setDuration(duration)
  1372. .addAttribute("rotateX", "0deg", "180deg")
  1373. .addCustomEasing("ease-in-out", [0.25, 0.1, 0.25, 1.0]);
  1374. }
  1375. /**
  1376. * 翻转动画(垂直)
  1377. * @param duration 持续时间
  1378. */
  1379. flipY(duration: number = 600): AnimationEngine {
  1380. return this.setDuration(duration)
  1381. .addAttribute("rotateY", "0deg", "180deg")
  1382. .addCustomEasing("ease-in-out", [0.25, 0.1, 0.25, 1.0]);
  1383. }
  1384. /**
  1385. * 弹性进入动画
  1386. * @param duration 持续时间
  1387. */
  1388. elasticIn(duration: number = 600): AnimationEngine {
  1389. return this.setDuration(duration)
  1390. .scale("0", "1")
  1391. .opacity("0", "1")
  1392. .addCustomEasing("elastic", [0.175, 0.885, 0.32, 1.275]);
  1393. }
  1394. /**
  1395. * 弹性退出动画
  1396. * @param duration 持续时间
  1397. */
  1398. elasticOut(duration: number = 600): AnimationEngine {
  1399. return this.setDuration(duration)
  1400. .scale("1", "0")
  1401. .opacity("1", "0")
  1402. .addCustomEasing("elastic", [0.68, -0.55, 0.265, 1.55]);
  1403. }
  1404. /**
  1405. * 回弹动画
  1406. * @param duration 持续时间
  1407. */
  1408. rubberBand(duration: number = 1000): AnimationEngine {
  1409. return this.setDuration(duration)
  1410. .addAttribute("scaleX", "1", "1.25")
  1411. .addAttribute("scaleY", "1", "0.75")
  1412. .setAlternate(true)
  1413. .setLoopCount(2)
  1414. .addCustomEasing("ease-in-out", [0.25, 0.1, 0.25, 1.0]);
  1415. }
  1416. /**
  1417. * 摆动动画
  1418. * @param duration 持续时间
  1419. */
  1420. swing(duration: number = 1000): AnimationEngine {
  1421. return this.setDuration(duration)
  1422. .addAttribute("rotate", "0deg", "15deg")
  1423. .setAlternate(true)
  1424. .setLoopCount(4)
  1425. .addCustomEasing("ease-in-out", [0.25, 0.1, 0.25, 1.0]);
  1426. }
  1427. /**
  1428. * 抖动动画
  1429. * @param duration 持续时间
  1430. */
  1431. wobble(duration: number = 1000): AnimationEngine {
  1432. return this.setDuration(duration)
  1433. .addAttribute("translateX", "0px", "25px")
  1434. .addAttribute("rotate", "0deg", "5deg")
  1435. .setAlternate(true)
  1436. .setLoopCount(4);
  1437. }
  1438. /**
  1439. * 滚动进入动画
  1440. * @param duration 持续时间
  1441. */
  1442. rollIn(duration: number = 600): AnimationEngine {
  1443. return this.setDuration(duration)
  1444. .translateX("-100%", "0%")
  1445. .rotate("-120deg", "0deg")
  1446. .opacity("0", "1");
  1447. }
  1448. /**
  1449. * 滚动退出动画
  1450. * @param duration 持续时间
  1451. */
  1452. rollOut(duration: number = 600): AnimationEngine {
  1453. return this.setDuration(duration)
  1454. .translateX("0%", "100%")
  1455. .rotate("0deg", "120deg")
  1456. .opacity("1", "0");
  1457. }
  1458. /**
  1459. * 灯光效果动画
  1460. * @param duration 持续时间
  1461. */
  1462. lightSpeed(duration: number = 500): AnimationEngine {
  1463. return this.setDuration(duration)
  1464. .translateX("-100%", "0%")
  1465. .addAttribute("skewX", "-30deg", "0deg")
  1466. .opacity("0", "1")
  1467. .addCustomEasing("ease-out", [0.25, 0.46, 0.45, 0.94]);
  1468. }
  1469. /**
  1470. * 浮动动画
  1471. * @param duration 持续时间
  1472. */
  1473. float(duration: number = 3000): AnimationEngine {
  1474. return this.setDuration(duration)
  1475. .translateY("0px", "-10px")
  1476. .setAlternate(true)
  1477. .setLoopCount(-1)
  1478. .addCustomEasing("ease-in-out", [0.25, 0.1, 0.25, 1.0]);
  1479. }
  1480. /**
  1481. * 呼吸动画
  1482. * @param duration 持续时间
  1483. */
  1484. breathe(duration: number = 2000): AnimationEngine {
  1485. return this.setDuration(duration)
  1486. .scale("1", "1.1")
  1487. .setAlternate(true)
  1488. .setLoopCount(-1)
  1489. .addCustomEasing("ease-in-out", [0.25, 0.1, 0.25, 1.0]);
  1490. }
  1491. /**
  1492. * 发光动画
  1493. * @param duration 持续时间
  1494. */
  1495. glow(duration: number = 1500): AnimationEngine {
  1496. return this.setDuration(duration)
  1497. .addAttribute(
  1498. "boxShadow",
  1499. "0 0 5px rgba(255,255,255,0.5)",
  1500. "0 0 20px rgba(255,255,255,1)"
  1501. )
  1502. .setAlternate(true)
  1503. .setLoopCount(-1)
  1504. .addCustomEasing("ease-in-out", [0.25, 0.1, 0.25, 1.0]);
  1505. }
  1506. /**
  1507. * 进度条动画
  1508. * @param duration 持续时间
  1509. * @param progress 进度百分比 (0-100)
  1510. */
  1511. progressBar(duration: number = 1000, progress: number = 100): AnimationEngine {
  1512. return this.setDuration(duration)
  1513. .addAttribute("width", "0%", `${progress}%`)
  1514. .addCustomEasing("ease-out", [0.25, 0.46, 0.45, 0.94]);
  1515. }
  1516. /**
  1517. * 模态框进入动画
  1518. * @param duration 持续时间
  1519. */
  1520. modalIn(duration: number = 300): AnimationEngine {
  1521. return this.setDuration(duration)
  1522. .scale("0.7", "1")
  1523. .opacity("0", "1")
  1524. .addCustomEasing("ease-out", [0.25, 0.46, 0.45, 0.94]);
  1525. }
  1526. /**
  1527. * 模态框退出动画
  1528. * @param duration 持续时间
  1529. */
  1530. modalOut(duration: number = 300): AnimationEngine {
  1531. return this.setDuration(duration)
  1532. .scale("1", "0.7")
  1533. .opacity("1", "0")
  1534. .addCustomEasing("ease-in", [0.42, 0.0, 1.0, 1.0]);
  1535. }
  1536. /**
  1537. * 卡片翻转动画
  1538. * @param duration 持续时间
  1539. */
  1540. cardFlip(duration: number = 600): AnimationEngine {
  1541. return this.setDuration(duration)
  1542. .addAttribute("rotateY", "0deg", "180deg")
  1543. .addCustomEasing("ease-in-out", [0.25, 0.1, 0.25, 1.0]);
  1544. }
  1545. /**
  1546. * 波纹扩散动画
  1547. * @param duration 持续时间
  1548. */
  1549. ripple(duration: number = 600): AnimationEngine {
  1550. return this.setDuration(duration)
  1551. .scale("0", "4")
  1552. .opacity("0.7", "0")
  1553. .addCustomEasing("ease-out", [0.25, 0.46, 0.45, 0.94]);
  1554. }
  1555. }
  1556. /**
  1557. * 创建动画实例
  1558. * @param element 目标元素
  1559. * @param options 动画选项
  1560. */
  1561. export function createAnimation(
  1562. element: UniElement | null,
  1563. options: AnimationOptions = {}
  1564. ): AnimationEngine {
  1565. return new AnimationEngine(element, options);
  1566. }