ani.uts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  1. import { CreateAnimationOptions } from './interface'
  2. // #ifdef APP-IOS
  3. import { Ref } from 'vue'
  4. // #endif
  5. // import { addUnit } from '@/uni_modules/lime-shared/addUnit'
  6. const defaultOption : CreateAnimationOptions = {
  7. duration: 400,
  8. timingFunction: 'linear',
  9. delay: 0,
  10. transformOrigin: '50% 50%'
  11. }
  12. function toCamelCase(str : string) : string {
  13. return str
  14. .replace(/(?:^\w|[A-Z]|\b\w)/g, (word : string, index : number, _ : string) : string => {
  15. return index == 0 ? word.toLowerCase() : word.toUpperCase();
  16. })
  17. .replace(/[-_\s]+/g, '');
  18. }
  19. function camelToKebabCase(str : string) : string {
  20. return str
  21. .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
  22. .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
  23. .toLowerCase();
  24. }
  25. function addUnit(value : any) : any {
  26. const regex = new RegExp("^(-)?\\d+(\\.\\d+)?$")
  27. const numberType = ['number', 'Int', 'UInt', 'Long', 'Float', 'Double']
  28. if (typeof value == 'string' && regex.test(value as string)) {
  29. return `${value}px`
  30. } else if (numberType.includes(typeof value)) {
  31. return `${value}px`
  32. }
  33. return value
  34. }
  35. // #ifdef APP-IOS
  36. // #endif
  37. const transitionProperty = [
  38. 'width', 'height', 'left', 'top', 'bottom', 'right', 'opacity', 'background-color', 'transform',
  39. 'border-color', 'border-left-color', 'border-top-color', 'border-right-color', 'border-bottom-color',
  40. 'margin', 'margin-left', 'margin-top', 'margin-right', 'margin-bottom',
  41. 'padding', 'padding-left', 'padding-top', 'padding-right', 'padding-bottom',
  42. // #ifdef WEB
  43. 'skew', 'skewX', 'skewY', 'matrix', 'matrix3d' //'perspective',
  44. // #endif
  45. ]
  46. const transitionSettings = ['transition-delay', 'transition-duration', 'transition-timing-function', 'transform-origin']
  47. /**
  48. * Step 类表示一个动画步骤
  49. */
  50. class Step {
  51. styles : Map<string, any>
  52. transform : Map<string, any>
  53. options : CreateAnimationOptions = {} as CreateAnimationOptions
  54. root : Step | null = null
  55. prev : Step | null = null
  56. next : Step | null = null
  57. stepIndex : number = 0
  58. currentStepIndex : number = 0
  59. stepCount : number = 1
  60. loop : number = 1
  61. loopCount : number = 1
  62. isPlaying : boolean = false
  63. timer : number = 0
  64. onFinish : () => void = () => { }
  65. onUpdate : () => void = () => { }
  66. /**
  67. * 构造函数
  68. * @param options 动画选项
  69. * @param stepIndex 步骤索引
  70. */
  71. constructor(options : CreateAnimationOptions, stepIndex : number = 0) {
  72. this.styles = new Map<string, any>()
  73. this.transform = new Map<string, any>()
  74. this.stepIndex = stepIndex;
  75. this.updateOption(options)
  76. }
  77. /**
  78. * 更新动画选项
  79. * @param options 动画选项
  80. */
  81. updateOption(options : CreateAnimationOptions) {
  82. for (let key in options) {
  83. const item = options[key]
  84. this.options[key] = item ?? defaultOption[key]
  85. }
  86. }
  87. /**
  88. * 添加一个新的动画步骤
  89. * @param options 动画选项
  90. * @returns 新的动画步骤
  91. */
  92. addStep(options : CreateAnimationOptions) : Step {
  93. if (this.next == null) {
  94. const root = this.root ?? this;
  95. this.next = new Step(options, this.stepIndex + 1);
  96. this.next!.prev = this;
  97. this.next!.root = root;
  98. return this.next!
  99. } else {
  100. return this.next!.addStep(options);
  101. }
  102. }
  103. /**
  104. * 停止动画
  105. */
  106. stop() {
  107. const root = this.root ?? this
  108. root.isPlaying = false
  109. clearTimeout(root.timer)
  110. }
  111. /**
  112. * 等待指定时间
  113. * @param time 等待时间
  114. * @returns Promise
  115. */
  116. private sleep(time : number = 0) : Promise<boolean> {
  117. return new Promise((resolve) => {
  118. setTimeout(function () {
  119. resolve(true)
  120. }, time);
  121. })
  122. }
  123. /**
  124. * 播放动画
  125. * @param element 动画元素
  126. */
  127. async play(element : UniElement, stepIndex : number = -1) {
  128. if (stepIndex > -1) {
  129. this.getStepByIndex(stepIndex)?.play(element)
  130. return
  131. }
  132. const duration = this.options.duration ?? 1
  133. const delay = this.options.delay ?? 0;
  134. const root = this.root ?? this
  135. root.isPlaying = true
  136. await this.sleep(this.stepIndex == 0 ? 100 : 0)
  137. // element.offsetHeight;
  138. for (let key in this.options) {
  139. const name = key === 'transformOrigin' ? key : toCamelCase(`transition-${key}`)
  140. let value = this.options[key]
  141. if (['delay', 'duration'].includes(key) && ['number', 'Int'].includes(typeof value)) {
  142. value = `${value}ms`
  143. }
  144. element.style.setProperty(camelToKebabCase(name), value)
  145. }
  146. // element.style.setProperty('transition-property', 'all')
  147. // await this.sleep()
  148. // element.offsetHeight
  149. let transformStr = ((element.style.getPropertyValue('transform') ?? '') as string).replace('none','')
  150. this.transform.forEach((value, key) => {
  151. const regex = new RegExp(`${key}\\([^)]*\\)`);
  152. if (regex.test(transformStr)) {
  153. // 如果存在,则修改 key 的 value
  154. transformStr = transformStr.replace(regex, `${key}(${value})`);
  155. } else {
  156. transformStr += ` ${key}(${value})`
  157. }
  158. })
  159. this.styles.forEach((value, key) => {
  160. element.style.setProperty(key, value)
  161. })
  162. element.style.setProperty('transform', transformStr)
  163. if (this.next != null && this.next!.styles.size != 0) {
  164. root.currentStepIndex = this.next!.stepIndex
  165. // await this.sleep(duration + delay)
  166. root.timer = setTimeout(() => {
  167. this.next?.play(element)
  168. }, duration + delay)
  169. } else if (root.loop == -1 || root.loopCount < root.loop) {
  170. // await this.sleep(duration + delay)
  171. root.currentStepIndex = 0
  172. root.timer = setTimeout(() => {
  173. root.onUpdate()
  174. root.loopCount++
  175. root.play(element)
  176. }, duration + delay)
  177. } else {
  178. let callbackWrapper : UniCallbackWrapper | null = null
  179. const transitionend = (_ : UniEvent) => {
  180. root.isPlaying = false
  181. root.loopCount = 1
  182. root.currentStepIndex = 0
  183. root.onFinish();
  184. // #ifdef WEB
  185. element.removeEventListener('transitionend', transitionend)
  186. // #endif
  187. // #ifdef APP
  188. if (callbackWrapper == null) return
  189. element.removeEventListener('transitionend', callbackWrapper!)
  190. // #endif
  191. }
  192. callbackWrapper = element.addEventListener('transitionend', transitionend)
  193. }
  194. }
  195. /**
  196. * 获取第一个步骤
  197. * @returns 第一个步骤
  198. */
  199. getFirstStep() : Step | null {
  200. if (this.prev == null) {
  201. return this;
  202. }
  203. return this.prev!.getFirstStep();
  204. }
  205. /**
  206. * 获取最后一个步骤
  207. * @returns 最后一个步骤
  208. */
  209. getLastStep() : Step | null {
  210. if (this.next == null) {
  211. return this;
  212. }
  213. return this.next!.getLastStep();
  214. }
  215. /**
  216. * 根据索引获取步骤
  217. * @param index 步骤索引
  218. * @returns 对应索引的步骤
  219. */
  220. getStepByIndex(index : number) : Step | null {
  221. if (this.stepIndex === index) {
  222. return this;
  223. }
  224. if (this.next != null && this.stepIndex < index) {
  225. return this.next!.getStepByIndex(index);
  226. }
  227. if (this.prev != null && this.stepIndex > index) {
  228. return this.prev!.getStepByIndex(index);
  229. }
  230. return null;
  231. }
  232. }
  233. /**
  234. * Animation 类
  235. */
  236. class Animation {
  237. private options : CreateAnimationOptions = {} as CreateAnimationOptions
  238. private steps : Step | null
  239. private _steps : Step | null = null
  240. private currentStep : Step | null
  241. private element : UniElement | null = null
  242. private cacheStyles : Map<string, any> = new Map<string, any>()
  243. onFinish : () => void = () => { }
  244. constructor(options : CreateAnimationOptions) {
  245. this.updateOption(options)
  246. this.steps = new Step(this.options)
  247. this.steps!.onFinish = () => {
  248. this.onFinish()
  249. }
  250. this.currentStep = this.steps
  251. // #ifdef WEB
  252. try {
  253. const directions = ['top', 'left', 'right', 'bottom'];
  254. directions.forEach((key) => {
  255. const name = `--lime-ani-${key}`;
  256. window.CSS.registerProperty({
  257. name,
  258. syntax: '<length-percentage>',
  259. inherits: false,
  260. initialValue: '0'
  261. })
  262. })
  263. } catch (error) {
  264. if (error instanceof DOMException) {
  265. // console.error(`Failed to register property`);
  266. } else {
  267. throw error;
  268. }
  269. }
  270. // #endif
  271. }
  272. /**
  273. * 更新选项
  274. * @param options 创建动画选项
  275. */
  276. private updateOption(options : CreateAnimationOptions) {
  277. for (let key in options) {
  278. const item = options[key]
  279. this.options[key] = item ?? defaultOption[key]
  280. }
  281. }
  282. /**
  283. * 更新 transform 属性
  284. * @param property transform 属性名
  285. * @param value transform 属性值
  286. */
  287. private updateTransform(property : string, value : string) {
  288. // 设置更新后的 transform 属性值
  289. this.currentStep?.styles!.set('transform', '');
  290. this.currentStep?.transform!.set(property, value)
  291. }
  292. /**
  293. * 设置透明度
  294. * @param value 透明度值
  295. * @returns Animation 实例
  296. */
  297. opacity(value : number) : Animation {
  298. this.currentStep?.styles!.set('opacity', value)
  299. return this
  300. }
  301. /**
  302. * 设置背景颜色
  303. * @param color 颜色值
  304. * @returns Animation 实例
  305. */
  306. backgroundColor(color : string) : Animation {
  307. this.currentStep?.styles!.set('backgroundColor', color)
  308. return this
  309. }
  310. width(length : any) : Animation {
  311. return this.add('width', length)
  312. }
  313. height(length : any) : Animation {
  314. return this.add('height', length)
  315. }
  316. top(length : any) : Animation {
  317. return this.add('top', length)
  318. }
  319. left(length : any) : Animation {
  320. return this.add('left', length)
  321. }
  322. right(length : any) : Animation {
  323. return this.add('right', length)
  324. }
  325. bottom(length : any) : Animation {
  326. return this.add('bottom', length)
  327. }
  328. rotateX(deg : number) : Animation {
  329. this.updateTransform('rotateX', `${deg}deg`);
  330. return this
  331. }
  332. rotateY(deg : number) : Animation {
  333. this.updateTransform('rotateY', `${deg}deg`);
  334. return this
  335. }
  336. rotateZ(deg : number) : Animation {
  337. this.updateTransform('rotateZ', `${deg}deg`);
  338. return this
  339. }
  340. rotate(deg : number) : Animation {
  341. this.updateTransform('rotate', `${deg}deg`);
  342. return this
  343. }
  344. scaleX(number : number) : Animation {
  345. this.updateTransform('scaleX', `${number.toString()}`)
  346. return this
  347. }
  348. scaleY(number : number) : Animation {
  349. this.updateTransform('scaleY', `${number.toString()}`)
  350. return this
  351. }
  352. scaleZ(number : number) : Animation {
  353. this.updateTransform('scaleZ', `${number.toString()}`)
  354. return this
  355. }
  356. scale(scaleX : number) : Animation
  357. scale(scaleX : number, scaleY : number = 0) : Animation {
  358. const _scaleY = scaleY == 0 ? scaleX : scaleY;
  359. this.updateTransform('scale', `${scaleX},${_scaleY}`);
  360. return this
  361. }
  362. // #ifdef WEB
  363. skewX(number : number) : Animation {
  364. this.updateTransform('skewX', `${number.toString()}deg`)
  365. return this
  366. }
  367. skewY(number : number) : Animation {
  368. this.updateTransform('skewY', `${number.toString()}deg`)
  369. return this
  370. }
  371. skew(skewX : number) : Animation
  372. skew(skewX : number, skewY : number = 0) : Animation {
  373. const _skewY = skewY == 0 ? skewX : skewY;
  374. this.updateTransform('skew', `${skewX}deg,${_skewY}deg`);
  375. return this
  376. }
  377. //matrix3d
  378. matrix(a : number, b : number, c : number, d : number, tx : number, ty : number) : Animation {
  379. this.updateTransform('skew', `${a},${b},${c},${d},${tx},${ty}`);
  380. return this
  381. }
  382. // #endif
  383. translateX(length : any) : Animation {
  384. const value = addUnit(length)
  385. this.updateTransform('translateX', `${value}`)
  386. return this
  387. }
  388. translateY(length : any) : Animation {
  389. const value = addUnit(length)
  390. this.updateTransform('translateY', `${value}`)
  391. return this
  392. }
  393. translate(x : any, y : any) : Animation {
  394. const _x = addUnit(x)
  395. const _y = addUnit(y)
  396. this.updateTransform('translate', `${_x},${_y}`);
  397. return this
  398. }
  399. margin(value : any) : Animation {
  400. if (typeof value == 'string') {
  401. return this.add('margin', value)
  402. } else if (Array.isArray(value)) {
  403. return this.add('margin', (value as any[]).join(' '))
  404. }
  405. return this
  406. }
  407. marginLeft(value : any) : Animation {
  408. return this.add('marginLeft', value)
  409. }
  410. marginRight(value : any) : Animation {
  411. return this.add('marginRight', value)
  412. }
  413. marginTop(value : any) : Animation {
  414. return this.add('marginTop', value)
  415. }
  416. marginBottom(value : any) : Animation {
  417. return this.add('marginBottom', value)
  418. }
  419. padding(value : any) : Animation {
  420. if (typeof value == 'string') {
  421. return this.add('padding', value)
  422. } else if (Array.isArray(value)) {
  423. return this.add('padding', (value as any[]).join(' '))
  424. }
  425. return this
  426. }
  427. paddingLeft(value : any) : Animation {
  428. return this.add('paddingLeft', value)
  429. }
  430. paddingRight(value : any) : Animation {
  431. return this.add('paddingRight', value)
  432. }
  433. paddingTop(value : any) : Animation {
  434. return this.add('paddingTop', value)
  435. }
  436. paddingBottom(value : any) : Animation {
  437. return this.add('paddingBottom', value)
  438. }
  439. borderColor(color : string) : Animation {
  440. return this.add('borderColor', color)
  441. }
  442. borderTopColor(color : string) : Animation {
  443. return this.add('borderTopColor', color)
  444. }
  445. borderBottomColor(color : string) : Animation {
  446. return this.add('borderBottomColor', color)
  447. }
  448. borderLeftColor(color : string) : Animation {
  449. return this.add('borderLeftColor', color)
  450. }
  451. borderRightColor(color : string) : Animation {
  452. return this.add('borderRightColor', color)
  453. }
  454. /**
  455. * 添加动画属性
  456. * @param type 动画属性类型
  457. * @param value 动画属性值
  458. * @returns Animation 实例
  459. */
  460. add(type : string, value : any) : Animation {
  461. const _v = addUnit(value)
  462. if (['opacity', 'transform', 'background-color', 'backgroundColor'].includes(type)) {
  463. this.currentStep?.styles!.set(type, value)
  464. } else {
  465. this.currentStep?.styles!.set(type, _v)
  466. }
  467. return this
  468. }
  469. step(options : UTSJSONObject | null = null) : Animation {
  470. if (options != null) {
  471. const _options = {} as CreateAnimationOptions
  472. for (let key in this.options) {
  473. const value = options[key]
  474. if (value != null) {
  475. _options[key] = value
  476. }
  477. }
  478. this.currentStep?.updateOption(_options)
  479. }
  480. this.currentStep = this.currentStep?.addStep(this.options)
  481. return this
  482. }
  483. /**
  484. * 设置初始样式
  485. * @param element 元素
  486. */
  487. private setCacheStyles(element : UniElement) {
  488. // @ts-ignore
  489. // #ifndef APP-IOS
  490. // element.style.cssText.split(';').forEach(style => {
  491. // if (style.includes(':')) {
  492. // const [property, value] = style.split(':');
  493. // if(!transitionProperty.includes(property)){
  494. // if(property == 'backgroundColor'){
  495. // this.cacheStyles.set(property, value)
  496. // }
  497. // }
  498. // }
  499. // })
  500. // #endif
  501. transitionProperty.forEach(property => {
  502. const value = this.getCSSValue(element, property) //element.style.getPropertyValue(property)
  503. if (value != null) {
  504. this.cacheStyles.set(property, value)
  505. }
  506. })
  507. transitionSettings.forEach(property => {
  508. const value = this.getCSSValue(element, property) //element.style.getPropertyValue(property)
  509. if (value != null) {
  510. this.cacheStyles.set(property, value)
  511. }
  512. })
  513. // this.cacheStyles.set('transform', '')
  514. }
  515. getCSSValue(el : UniElement, prop : string):any|null {
  516. const uppercasePropName = prop.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
  517. // #ifdef WEB
  518. // @ts-ignore
  519. const style = getComputedStyle(el) ?? el.style
  520. const value = style.getPropertyValue(uppercasePropName) || style[uppercasePropName]
  521. // #endif
  522. // #ifndef WEB
  523. const value = el.style.getPropertyValue(uppercasePropName)
  524. // #endif
  525. return value
  526. }
  527. /**
  528. * 重置样式
  529. * @param element 元素
  530. */
  531. private resetStyles(element : UniElement) {
  532. // @ts-ignore
  533. // #ifndef APP-IOS
  534. // element.style.cssText.split(';').forEach(style => {
  535. // if (style.includes(':')) {
  536. // const property = style.split(':')[0].trim();
  537. // if (property == 'transform-origin') {
  538. // element.style.setProperty(property, '50% 50%')
  539. // } else if (property == 'transition-duration') {
  540. // element.style.setProperty(property, '1ms')
  541. // } else {
  542. // element.style.setProperty(property, this.cacheStyles.get(property) ?? '')
  543. // }
  544. // }
  545. // })
  546. // #endif
  547. const directions = ['top', 'left', 'right', 'bottom'];
  548. transitionSettings.forEach(property => {
  549. const value = this.cacheStyles.get(property)
  550. if (property == 'transition-duration') {
  551. element.style.setProperty(property, value ?? '0ms')
  552. } else if (value != null) {
  553. element.style.setProperty(property, value)
  554. }
  555. })
  556. transitionProperty.forEach((property) => {
  557. const value = this.cacheStyles.get(property)
  558. if (value != null || directions.includes(property) || property == 'transform') {
  559. element.style.setProperty(property, value ?? '')
  560. }
  561. })
  562. element.style.setProperty('transition-property', transitionProperty.join(','))
  563. // #ifdef WEB
  564. directions.forEach(key => {
  565. element.style.setProperty(`${key}`, `var(--lime-ani-${key})`)
  566. })
  567. // #endif
  568. element.getBoundingClientRect()
  569. }
  570. export(element : UniElement, loopCount : number) : UTSJSONObject
  571. export(element : Ref<UniElement | null>, loopCount : number) : UTSJSONObject
  572. export(element : any | null = null, loopCount : number = 1) : UTSJSONObject {
  573. this._steps = this.steps
  574. this.steps = new Step(this.options)
  575. this.currentStep = this.steps
  576. this.steps!.onFinish = () => {
  577. this.onFinish()
  578. }
  579. // @ts-ignore
  580. this.play(element, loopCount)
  581. return {}
  582. }
  583. /**
  584. * 播放动画
  585. * @param element 元素,可选
  586. */
  587. play() : void
  588. play(element : UniElement, loopCount : number) : void
  589. play(element : Ref<UniElement | null>, loopCount : number) : void
  590. play(element : any | null = null, loopCount : number | null = null) : void {
  591. if (this._steps != null && this._steps!.isPlaying) return
  592. const _loopCount = loopCount ?? this._steps?.loop ?? 1
  593. if (element == null) {
  594. this._play(null, _loopCount)
  595. } else if (element instanceof UniElement) {
  596. this._play(element, _loopCount)
  597. } else {
  598. // #ifdef APP-ANDROID
  599. if (!(element instanceof Ref<UniElement | null>)) return
  600. // #endif
  601. // @ts-ignore
  602. watch(element, (el : UniElement) => {
  603. this._play(el, _loopCount)
  604. })
  605. }
  606. }
  607. private _play(element : UniElement | null, loopCount : number = 1) {
  608. if (this.element == null && element != null) {
  609. this.element = element
  610. this.setCacheStyles(this.element!)
  611. }
  612. if (this.element == null || this._steps == null || this._steps!.isPlaying) return
  613. if (this._steps!.currentStepIndex == 0) {
  614. this.resetStyles(this.element!)
  615. }
  616. this._steps!.loop = loopCount
  617. this._steps!.onUpdate = () => {
  618. this.resetStyles(this.element!)
  619. }
  620. this._steps!.play(this.element!, this._steps!.currentStepIndex)
  621. }
  622. /**
  623. * 停止动画
  624. */
  625. stop() {
  626. this._steps?.stop()
  627. }
  628. /**
  629. * 销毁动画
  630. */
  631. destroy() {
  632. this.stop();
  633. this.element = null;
  634. this.steps = null;
  635. this._steps = null;
  636. this.currentStep = null;
  637. this.cacheStyles.clear();
  638. }
  639. }
  640. export function createAnimation(options : CreateAnimationOptions = {} as CreateAnimationOptions) : Animation {
  641. // #ifdef APP||WEB
  642. return new Animation(options)
  643. // #endif
  644. // #ifndef APP||WEB
  645. // @ts-ignore
  646. return uni.createAnimation({ ...(options ?? {}) })
  647. // #endif
  648. }