|
@@ -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();
|
|
|
+```
|