useSvgAnimation.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import { ref } from 'vue';
  2. import _ from 'lodash';
  3. /**
  4. * svg二维模型动画使用的钩子,需要配合指定的组件使用,即svg模型组件(README里有更详细的说明)
  5. *
  6. * 备注:一个元素的动画仅有两种状态,正常播放、倒放;例如:`triggerAnimation(id1, false)`代表触发id1对应的动画,false代表触发正常播放的动画
  7. */
  8. export function useSvgAnimation(elementInfo: Map<string, { key: string; transforms?: string[]; opacity?: string[] }>) {
  9. /** 所有动画元素 */
  10. const animationElements = new Map<string, HTMLElement>();
  11. /** 管理节点是否处于初始状态 */
  12. const animationManager = ref<{ [id: string]: boolean }>({});
  13. /**
  14. * 触发动画函数,该函数用来根据id查找SVG图片中的对应group,然后触发绑定在此group上的动画
  15. *
  16. * 动画有且仅有两个状态,一种是初始状态,一种是结束状态,当动画触发后,会根据reverse传参自动切换状态
  17. *
  18. * @param id 标识符号(可以在页面中使用元素选择器选择具体元素后查询其id),可以传数组
  19. * @param reverse 是否需要反向执行动画,如果id传了数组该参数可以传数组以一一匹配,默认为false
  20. * @param config.duration 动画持续时长,越长动画执行的越慢
  21. * @param config.progress 指定动画执行的进度,默认为1,即动画执行到100%,该数字范围为0-1
  22. */
  23. function triggerAnimation(
  24. id: string | string[],
  25. reverse: boolean | boolean[] = false,
  26. config: {
  27. duration?: number;
  28. progress?: number;
  29. } = {}
  30. ) {
  31. const idArray = typeof id === 'string' ? [id] : id;
  32. const reverseArray = typeof reverse === 'boolean' ? idArray.map(() => reverse) : reverse;
  33. idArray.forEach((id, index) => {
  34. if (animationManager.value[id] === undefined) {
  35. animationManager.value[id] = true;
  36. }
  37. const unchanged = animationManager.value[id];
  38. const reverse = reverseArray[index] || false;
  39. // 不指定反向播放且group处于初始状态时播放正常动画
  40. if (!reverse && unchanged) {
  41. animationManager.value[id] = false;
  42. animateByKey(id, false, config);
  43. return;
  44. }
  45. if (reverse && !unchanged) {
  46. animationManager.value[id] = true;
  47. animateByKey(id, true, config);
  48. return;
  49. }
  50. });
  51. }
  52. // 直接控制动画的方法
  53. const animateElement = (
  54. elementId: string,
  55. reverse: boolean = false,
  56. config: {
  57. duration?: number;
  58. progress?: number;
  59. } = {}
  60. ) => {
  61. const { duration = 3000, progress = 1 } = config;
  62. const el = animationElements.get(elementId);
  63. const info = elementInfo.get(elementId);
  64. const percentage = _.clamp(progress, 0, 1);
  65. if (!el || !info) return;
  66. // 应用动画
  67. if (info.transforms && info.transforms.length > 1) {
  68. const endTransform = info.transforms[Math.floor((info.transforms.length - 1) * percentage)];
  69. const startTransform = info.transforms[Math.floor((info.transforms.length - 1) * (1 - percentage))];
  70. el.style.transition = `transform ${duration}ms`;
  71. el.setAttribute('transform', reverse ? startTransform : endTransform);
  72. }
  73. if (info.opacity && info.opacity.length > 1) {
  74. const endOpacity = info.opacity[Math.floor((info.opacity.length - 1) * percentage)];
  75. const startOpacity = info.opacity[Math.floor((info.opacity.length - 1) * (1 - percentage))];
  76. el.style.transition = `opacity ${duration}ms`;
  77. el.setAttribute('opacity', reverse ? startOpacity : endOpacity);
  78. }
  79. };
  80. // 批量控制同一key的所有元素
  81. const animateByKey = (
  82. key: string,
  83. reverse: boolean = false,
  84. config: {
  85. duration?: number;
  86. progress?: number;
  87. } = {}
  88. ) => {
  89. animationElements.forEach((__, elementId) => {
  90. const info = elementInfo.get(elementId);
  91. if (info && info.key === key) {
  92. animateElement(elementId, reverse, config);
  93. }
  94. });
  95. };
  96. return {
  97. animationElements,
  98. triggerAnimation,
  99. animateElement,
  100. animateByKey,
  101. };
  102. }