Преглед изворни кода

[Doc 0000] SVG动画脚本更新

houzekong пре 1 месец
родитељ
комит
8b06fbd6af
1 измењених фајлова са 169 додато и 139 уклоњено
  1. 169 139
      README.md

+ 169 - 139
README.md

@@ -219,11 +219,6 @@ function themifyScript(
 上述的生成组件的脚本如下:
 
 ```javascript
-/**
- * 使用方式:node index.js --keys=key1,key2
- *
- * 输出位置:当前目录下的 workspace/animated-component.vue 文件
- */
 const fs = require('fs');
 const path = require('path');
 const { parseString } = require('xml2js');
@@ -273,88 +268,74 @@ async function parseSVG(filePath) {
 }
 
 /**
- * 递归查找包含指定id的group元素
+ * 递归查找所有包含指定id引用的group元素
  * @param {Object} node - XML节点对象
  * @param {string} id - 要查找的id
- * @returns {Object|null} 找到的group元素或null
+ * @returns {Array} 找到的group元素数组
  */
-function findGroupWithId(node, id) {
-  // 检查当前节点是否有g元素
-  if (node.g && Array.isArray(node.g)) {
-    for (const group of node.g) {
-      // 检查group的id属性是否匹配
-      if (group.$ && group.$.id === id) {
-        return group;
-      }
-
-      // 递归检查子group
-      if (group.g) {
-        const result = findGroupWithId(group, id);
-        if (result) return group; // 返回找到的父group
-      }
-
-      // 检查use元素是否引用了目标id
-      if (group.use && Array.isArray(group.use)) {
-        for (const use of group.use) {
-          if (use.$ && use.$['xlink:href'] === `#${id}`) {
-            return group;
+function findGroupsWithIdRef(node, id) {
+  const groups = [];
+
+  function traverse(currentNode) {
+    if (!currentNode) return;
+
+    // 检查当前节点是否为group且有use元素引用目标id
+    if (currentNode.g && Array.isArray(currentNode.g)) {
+      for (const group of currentNode.g) {
+        // 检查group是否有use元素引用目标id
+        if (group.use && Array.isArray(group.use)) {
+          for (const use of group.use) {
+            if (use.$ && use.$['xlink:href'] === `#${id}`) {
+              groups.push(currentNode);
+              break;
+            }
           }
         }
+
+        // 递归检查子元素
+        traverse(group);
       }
     }
   }
-  return null;
-}
 
-/**
- * 从group元素中提取transform值
- * @param {Object} group - group元素对象
- * @returns {string|null} transform值或null
- */
-function extractTransform(group) {
-  if (!group || !group.$ || !group.$.transform) return null;
-  return group.$.transform;
+  traverse(node);
+  return groups;
 }
 
 /**
- * 为SVG元素添加动态类绑定
+ * 为SVG元素添加唯一标识和初始transform
  * @param {Object} svgData - 解析后的SVG对象
- * @param {Array<string>} keys - 需要添加绑定的key数组
- * @returns {Object} 修改后的SVG对象
+ * @param {Array<string>} keys - 需要处理的key数组
+ * @returns {Object} 修改后的SVG对象和元素映射
  */
-function addDynamicClassBinding(svgData, keys) {
+function addElementIdentifiers(svgData, keys) {
+  const elementInfoMap = new Map();
+
   keys.forEach((key) => {
-    const group = findGroupWithId(svgData, key);
-    if (group && group.$) {
-      // 添加动态类绑定
-      group.$[':class'] = `{${key}_animate:!manager.${key},${key}_animate_reverse:manager.${key}}`;
-    }
-  });
-  return svgData;
-}
+    const groups = findGroupsWithIdRef(svgData, key);
 
-/**
- * 生成CSS keyframes动画
- * @param {string} animationName - 动画名称
- * @param {Array<string>} transforms - 变换矩阵数组
- * @returns {string} 生成的CSS代码
- */
-function generateKeyframes(animationName, transforms) {
-  const steps = transforms.length;
-  let css = `@keyframes ${animationName} {\n`;
-
-  transforms.forEach((transform, index) => {
-    if (transform) {
-      // 计算当前关键帧的百分比
-      const percentage = (index / (steps - 1)) * 100;
-      css += `  ${percentage.toFixed(0)}% {\n`;
-      css += `    transform: ${transform};\n`;
-      css += `  }\n`;
-    }
+    groups.forEach((group, counter) => {
+      const elementId = `anim_${key}_${counter++}`;
+
+      // 确保group有属性对象
+      if (!group.$) group.$ = {};
+
+      // 添加唯一标识
+      group.$['data-anim-id'] = elementId;
+
+      // 保存初始transform
+      const transform = group.$.transform || '';
+
+      // 存储元素信息
+      elementInfoMap.set(elementId, {
+        key,
+        initialTransform: transform,
+        transforms: [], // 将在后续步骤填充
+      });
+    });
   });
 
-  css += '}\n';
-  return css;
+  return { modifiedSvg: svgData, elementInfoMap };
 }
 
 /**
@@ -379,50 +360,127 @@ function extractSVGContent(svgObj) {
 }
 
 /**
- * 生成Vue组件文件内容
- * @param {string} svgContent - SVG内容字符串
- * @param {Object} transformsByKey - 每个key的transform数组
- * @param {Object} firstTransforms - 第一个SVG文件中每个key的transform
- * @param {Object} lastTransforms - 最后一个SVG文件中每个key的transform
- * @param {Array<string>} keys - key数组
- * @returns {string} 生成的Vue组件内容
+ * 收集所有SVG文件中每个元素的transform变化
+ * @param {string} workspaceDir - 工作目录路径
+ * @param {Array} files - SVG文件列表
+ * @param {Map} elementInfoMap - 元素信息Map
+ * @returns {Promise<Map>} 更新后的元素信息Map
  */
-function generateVueComponent(svgContent, transformsByKey, firstTransforms, lastTransforms, keys) {
-  let template = `<template>\n${svgContent}\n</template>\n\n`;
+async function collectTransforms(workspaceDir, files, elementInfoMap) {
+  // 为每个元素初始化transform序列
+  for (const [elementId, info] of elementInfoMap) {
+    info.transforms = [];
+  }
 
-  let script = `<script setup lang="ts">\ndefineProps<{\nmanager:Record<string, boolean>;\n}>();\n</script>\n\n`;
+  // 按顺序处理所有SVG文件
+  for (const file of files) {
+    const filePath = path.join(workspaceDir, file);
+    const svgData = await parseSVG(filePath);
 
-  let style = `<style scoped>\n`;
+    // 为每个元素查找对应的transform
+    for (const [elementId, info] of elementInfoMap) {
+      const key = info.key;
+      const groups = findGroupsWithIdRef(svgData.svg, key);
 
-  // 为每个key生成样式
-  keys.forEach((key) => {
-    const animationName = key.replace(/^___/, '').replace(/_/g, '');
+      // 查找具有相同相对位置的group(假设顺序一致)
+      const elementIndex = parseInt(elementId.split('_').pop());
+      if (groups[elementIndex] && groups[elementIndex].$ && groups[elementIndex].$.transform) {
+        info.transforms.push(groups[elementIndex].$.transform);
+      } else {
+        // 如果找不到transform,使用前一个值或初始值
+        const lastTransform = info.transforms.length > 0 ? info.transforms[info.transforms.length - 1] : info.initialTransform;
+        info.transforms.push(lastTransform);
+      }
+    }
+  }
 
-    // 添加keyframes
-    style += generateKeyframes(animationName, transformsByKey[key]);
+  return elementInfoMap;
+}
 
-    // 添加正向动画类
-    style += `.${key}_animate {\n`;
-    style += `transition: transform 3s;\n`;
-    if (lastTransforms[key]) {
-      style += `transform: ${lastTransforms[key]};\n`;
+/**
+ * 生成Vue组件文件内容
+ * @param {string} svgContent - SVG内容字符串
+ * @param {Map} elementInfoMap - 元素信息Map
+ * @param {Array<string>} keys - key数组
+ * @returns {string} 生成的Vue组件内容
+ */
+function generateVueComponent(svgContent, elementInfoMap, keys) {
+  return `
+<template>\n${svgContent}\n</template>\n\n
+<script setup lang="ts">
+import { watch, onMounted, defineExpose, defineProps } from "vue";
+
+const props = defineProps<{
+  manager: Record<string, boolean>;
+}>();
+
+// 存储所有动画元素(不在模板中使用,不需要ref)
+const animElements = new Map<string, HTMLElement>();
+
+// 元素信息(常量数据,使用Map)
+const elementInfo = new Map([
+${Array.from(elementInfoMap.entries())
+  .map(([key, value]) => `  ["${key}", ${JSON.stringify(value)}]`)
+  .join(',\n')}
+]);
+
+// 初始化元素引用
+onMounted(() => {
+  elementInfo.forEach((info, elementId) => {
+    const el = document.querySelector(\`[data-anim-id="\${elementId}"]\`);
+    if (el) {
+      animElements.set(elementId, el as HTMLElement);
+      // 设置初始transform
+      const initialTransform = info.transforms[0] || '';
+      el.setAttribute('transform', initialTransform);
     }
-    style += `/*animation: ${animationName} 3s forwards;*/\n`;
-    style += `}\n\n`;
-
-    // 添加反向动画类
-    style += `.${key}_animate_reverse {\n`;
-    style += `transition: transform 3s;\n`;
-    if (firstTransforms[key]) {
-      style += `transform: ${firstTransforms[key]};\n`;
+  });
+});
+
+// 监听manager变化并执行动画
+watch(() => props.manager, (newManager) => {
+  Object.keys(newManager).forEach(key => {
+    const isActive = newManager[key];
+    
+    // 找到所有属于这个key的元素
+    animateByKey(key, isActive);
+  });
+}, { deep: true });
+
+// 直接控制动画的方法
+const animateElement = (elementId: string, toEnd: boolean, duration: number = 3000) => {
+  const el = animElements.get(elementId);
+  const info = elementInfo.get(elementId);
+  
+  if (el && info && info.transforms.length > 1) {
+    el.style.transition = \`transform \${duration}ms\`;
+    el.setAttribute('transform', toEnd ? info.transforms[info.transforms.length - 1] : info.transforms[0]);
+  }
+};
+
+// 批量控制同一key的所有元素
+const animateByKey = (key: string, toEnd: boolean, duration: number = 3000) => {
+  animElements.forEach((el, elementId) => {
+    const info = elementInfo.get(elementId);
+    if (info && info.key === key) {
+      animateElement(elementId, toEnd, duration);
     }
-    style += `/*animation: ${animationName} 3s forwards reverse;*/\n`;
-    style += `}\n\n`;
   });
+};
 
-  style += `</style>`;
 
-  return template + script + style;
+// 导出方法以便外部调用
+defineExpose({
+  animateElement,
+  animateByKey
+});
+</script>
+<style scoped>
+/* 可以添加一些基础样式 */
+[data-anim-id] {
+  transition: transform 3s;
+}
+</style>`;
 }
 
 /**
@@ -457,54 +515,26 @@ async function main() {
 
     console.log(`找到 ${files.length} 个SVG文件`);
 
-    // 为每个key创建transform数组
-    const transformsByKey = {};
-    const firstTransforms = {};
-    const lastTransforms = {};
-
-    keys.forEach((key) => {
-      transformsByKey[key] = [];
-    });
-
-    // 按顺序处理所有SVG文件
-    for (const file of files) {
-      const filePath = path.join(workspaceDir, file);
-      const svgData = await parseSVG(filePath);
-
-      // 为每个key查找对应的group并提取transform
-      for (const key of keys) {
-        const group = findGroupWithId(svgData.svg, key);
-        const transform = extractTransform(group);
-        transformsByKey[key].push(transform);
-
-        // 如果是第一个文件,保存transform
-        if (file === files[0]) {
-          firstTransforms[key] = transform;
-        }
-
-        // 如果是最后一个文件,保存transform
-        if (file === files[files.length - 1]) {
-          lastTransforms[key] = transform;
-        }
-      }
-    }
-
-    // 读取第一个SVG文件并添加动态类绑定
+    // 读取第一个SVG文件
     const firstSvgPath = path.join(workspaceDir, files[0]);
     const firstSvgData = await parseSVG(firstSvgPath);
 
-    // 添加动态类绑定
-    const modifiedSvgData = addDynamicClassBinding(firstSvgData.svg, keys);
+    // 为SVG元素添加唯一标识
+    const { modifiedSvg, elementInfoMap } = addElementIdentifiers(firstSvgData.svg, keys);
+
+    // 收集所有SVG文件中每个元素的transform变化
+    const updatedElementInfoMap = await collectTransforms(workspaceDir, files, elementInfoMap);
 
     // 提取SVG内容(不包含XML声明和根标签)
-    const svgContent = extractSVGContent(modifiedSvgData);
+    const svgContent = extractSVGContent(modifiedSvg);
 
     // 生成Vue组件
-    const vueComponent = generateVueComponent(svgContent, transformsByKey, firstTransforms, lastTransforms, keys);
+    const vueComponent = generateVueComponent(svgContent, updatedElementInfoMap, keys);
 
     // 写入Vue组件文件
     fs.writeFileSync(outputFile, vueComponent);
     console.log(`Vue组件已生成: ${outputFile}`);
+    console.log(`共找到 ${updatedElementInfoMap.size} 个动画元素`);
   } catch (error) {
     console.error('错误:', error.message);
     process.exit(1);