Explorar o código

[Doc 0000] 文档更新关于SVG动画的开发文档

houzekong hai 3 semanas
pai
achega
12d94b8d9f
Modificáronse 1 ficheiros con 334 adicións e 0 borrados
  1. 334 0
      README.md

+ 334 - 0
README.md

@@ -180,3 +180,337 @@ function themifyScript(
   return [varstr, strcopy];
 }
 ```
+
+### SVG模型动画开发
+
+关于svg二维模型动画开发,核心方法之一位置为/mky-vent-base/src/hooks/vent/useSvgAnimation.ts,说明如下:
+
+1、设计团队提供svg图组,通常是一组svg图片,每张图片对应一个动画帧
+
+2、将图组放入指定脚本的工作区内生成可用的vue组件,脚本可见项目的README文档,然后自行新建文件、复制代码、安装依赖并运行
+
+3、将可用的vue组件引入到需要使用动画的页面中,并使用useSvgAnimation钩子进行动画控制,例如`<SVGAnimation :manager="animationManager" />`
+
+4、通过浏览器的元素检查功能,找到svg对应的group,复制group的id,并使用该id进行动画控制
+
+使用示例:
+
+```vue
+<template>
+  <SVGAni :manager="animationManager" />
+</template>
+<script setup>
+  import SVGAni from 'path/to/SVGAnimation.vue';
+  import useSvgAnimation from 'path/to/useSvgAnimation.ts';
+
+  const { animationManager,triggerAnimation } = useSvgAnimation();
+
+  onMounted(() => {
+    // 根据情况触发动画
+    if (condition) {
+      triggerAnimation('id', false);
+      } else {
+      triggerAnimation('id', true);
+      }
+  })
+</script>
+```
+
+上述的生成组件的脚本如下:
+
+```javascript
+/**
+ * 使用方式:node index.js --keys=key1,key2
+ *
+ * 输出位置:当前目录下的 workspace/animated-component.vue 文件
+ */
+const fs = require('fs');
+const path = require('path');
+const { parseString } = require('xml2js');
+const { Builder } = require('xml2js');
+
+/**
+ * 解析命令行参数
+ * 支持 --keys=key1,key2 格式的参数
+ * @returns {Object} 包含解析后参数的对象
+ */
+function parseArgs() {
+  const args = process.argv.slice(2);
+  const params = {};
+
+  args.forEach((arg) => {
+    if (arg.startsWith('--keys=')) {
+      // 分割并处理keys参数
+      params.keys = arg.split('=')[1].split(',');
+    }
+  });
+
+  return params;
+}
+
+/**
+ * 读取并解析SVG文件
+ * @param {string} filePath - SVG文件路径
+ * @returns {Promise<Object>} 解析后的SVG对象
+ */
+async function parseSVG(filePath) {
+  return new Promise((resolve, reject) => {
+    fs.readFile(filePath, 'utf8', (err, data) => {
+      if (err) {
+        reject(new Error(`读取文件失败: ${filePath}, 错误: ${err.message}`));
+        return;
+      }
+
+      parseString(data, (err, result) => {
+        if (err) {
+          reject(new Error(`解析SVG失败: ${filePath}, 错误: ${err.message}`));
+          return;
+        }
+        resolve(result);
+      });
+    });
+  });
+}
+
+/**
+ * 递归查找包含指定id的group元素
+ * @param {Object} node - XML节点对象
+ * @param {string} id - 要查找的id
+ * @returns {Object|null} 找到的group元素或null
+ */
+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;
+          }
+        }
+      }
+    }
+  }
+  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;
+}
+
+/**
+ * 为SVG元素添加动态类绑定
+ * @param {Object} svgData - 解析后的SVG对象
+ * @param {Array<string>} keys - 需要添加绑定的key数组
+ * @returns {Object} 修改后的SVG对象
+ */
+function addDynamicClassBinding(svgData, keys) {
+  keys.forEach((key) => {
+    const group = findGroupWithId(svgData, key);
+    if (group && group.$) {
+      // 添加动态类绑定
+      group.$[':class'] = `{${key}_animate:!manager.${key},${key}_animate_reverse:manager.${key}}`;
+    }
+  });
+  return svgData;
+}
+
+/**
+ * 生成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`;
+    }
+  });
+
+  css += '}\n';
+  return css;
+}
+
+/**
+ * 从SVG对象中提取SVG内容(去除XML声明和根标签)
+ * @param {Object} svgObj - SVG对象
+ * @returns {string} SVG内容字符串
+ */
+function extractSVGContent(svgObj) {
+  const builder = new Builder({
+    headless: true, // 不包含XML声明
+  });
+
+  // 构建SVG内容,但不包含根标签
+  let svgContent = builder.buildObject({
+    svg: svgObj,
+  });
+
+  // 移除可能存在的<?xml>声明和DOCTYPE
+  svgContent = svgContent.replace(/<\?xml[^>]*>\s*/g, '').replace(/<!DOCTYPE[^>]*>\s*/g, '');
+
+  return svgContent;
+}
+
+/**
+ * 生成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组件内容
+ */
+function generateVueComponent(svgContent, transformsByKey, firstTransforms, lastTransforms, keys) {
+  let template = `<template>\n${svgContent}\n</template>\n\n`;
+
+  let script = `<script setup lang="ts">\ndefineProps<{\nmanager:Record<string, boolean>;\n}>();\n</script>\n\n`;
+
+  let style = `<style scoped>\n`;
+
+  // 为每个key生成样式
+  keys.forEach((key) => {
+    const animationName = key.replace(/^___/, '').replace(/_/g, '');
+
+    // 添加keyframes
+    style += generateKeyframes(animationName, transformsByKey[key]);
+
+    // 添加正向动画类
+    style += `.${key}_animate {\n`;
+    style += `transition: transform 3s;\n`;
+    if (lastTransforms[key]) {
+      style += `transform: ${lastTransforms[key]};\n`;
+    }
+    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`;
+    }
+    style += `/*animation: ${animationName} 3s forwards reverse;*/\n`;
+    style += `}\n\n`;
+  });
+
+  style += `</style>`;
+
+  return template + script + style;
+}
+
+/**
+ * 主函数 - 协调整个流程
+ */
+async function main() {
+  try {
+    // 解析命令行参数
+    const { keys } = parseArgs();
+
+    if (!keys || keys.length === 0) {
+      throw new Error('请提供keys参数,例如: --keys=key1,key2');
+    }
+
+    const workspaceDir = path.join(process.cwd(), 'workspace');
+    const outputFile = path.join(workspaceDir, 'animated-component.vue');
+
+    // 检查workspace目录是否存在
+    if (!fs.existsSync(workspaceDir)) {
+      throw new Error('workspace目录不存在');
+    }
+
+    // 读取并过滤SVG文件
+    const files = fs
+      .readdirSync(workspaceDir)
+      .filter((file) => file.endsWith('.svg'))
+      .sort(); // 按字母顺序排序以确保正确的动画顺序
+
+    if (files.length === 0) {
+      throw new Error('workspace目录下没有找到SVG文件');
+    }
+
+    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文件并添加动态类绑定
+    const firstSvgPath = path.join(workspaceDir, files[0]);
+    const firstSvgData = await parseSVG(firstSvgPath);
+
+    // 添加动态类绑定
+    const modifiedSvgData = addDynamicClassBinding(firstSvgData.svg, keys);
+
+    // 提取SVG内容(不包含XML声明和根标签)
+    const svgContent = extractSVGContent(modifiedSvgData);
+
+    // 生成Vue组件
+    const vueComponent = generateVueComponent(svgContent, transformsByKey, firstTransforms, lastTransforms, keys);
+
+    // 写入Vue组件文件
+    fs.writeFileSync(outputFile, vueComponent);
+    console.log(`Vue组件已生成: ${outputFile}`);
+  } catch (error) {
+    console.error('错误:', error.message);
+    process.exit(1);
+  }
+}
+
+// 执行主函数
+main();
+```