Showing
1 changed file
with
113 additions
and
48 deletions
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-03-28 09:23:04 | 2 | * @Date: 2025-03-28 09:23:04 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-03-28 23:33:13 | 4 | + * @LastEditTime: 2025-03-29 00:31:56 |
| 5 | * @FilePath: /mlaj/src/views/animation.vue | 5 | * @FilePath: /mlaj/src/views/animation.vue |
| 6 | * @Description: 贝塞尔曲线动画路径组件 | 6 | * @Description: 贝塞尔曲线动画路径组件 |
| 7 | * | 7 | * |
| ... | @@ -115,8 +115,11 @@ import img_svg3 from "@/assets/3.svg"; | ... | @@ -115,8 +115,11 @@ import img_svg3 from "@/assets/3.svg"; |
| 115 | import img_svg4 from "@/assets/4.svg"; | 115 | import img_svg4 from "@/assets/4.svg"; |
| 116 | import img_svg5 from "@/assets/5.svg"; | 116 | import img_svg5 from "@/assets/5.svg"; |
| 117 | 117 | ||
| 118 | -// 定义节点坐标数组,每个节点包含x和y坐标 | 118 | +/** |
| 119 | -// 这些点将用于生成贝塞尔曲线的路径 | 119 | + * 节点坐标数组 |
| 120 | + * 每个元素是一个包含x和y坐标的对象 | ||
| 121 | + * 这些点将用于生成贝塞尔曲线的路径 | ||
| 122 | + */ | ||
| 120 | const points = ref([ | 123 | const points = ref([ |
| 121 | { x: 700, y: 50 }, | 124 | { x: 700, y: 50 }, |
| 122 | { x: 300, y: 300 }, | 125 | { x: 300, y: 300 }, |
| ... | @@ -125,16 +128,32 @@ const points = ref([ | ... | @@ -125,16 +128,32 @@ const points = ref([ |
| 125 | { x: 700, y: 1000 }, | 128 | { x: 700, y: 1000 }, |
| 126 | ]); | 129 | ]); |
| 127 | 130 | ||
| 128 | -// 当前激活的节点索引,初始值为-1表示没有节点被激活 | 131 | +/** |
| 132 | + * activeNodeIndex 是一个响应式数据,用于存储当前激活的节点索引。 | ||
| 133 | + * 初始值为 -1,表示没有节点被激活。 | ||
| 134 | + * 当用户点击节点时,该索引会被更新为当前节点的索引。 | ||
| 135 | + * 当用户点击下一步按钮时,该索引会被更新为下一个节点的索引。 | ||
| 136 | + */ | ||
| 129 | const activeNodeIndex = ref(-1); | 137 | const activeNodeIndex = ref(-1); |
| 130 | 138 | ||
| 131 | -// 存储已激活的路径段数组,每个元素是一个SVG路径字符串 | 139 | +/** |
| 132 | -// 用于显示高亮的贝塞尔曲线路径 | 140 | + * activePathSegments 是一个响应式数据,用于存储已激活的路径段数组。 |
| 141 | + * 初始值为空数组,表示没有路径段被激活。 | ||
| 142 | + * 当用户点击下一步按钮时,该数组会被更新为包含当前节点和下一个节点之间的路径段。 | ||
| 143 | + * 存储已激活的路径段数组,每个元素是一个SVG路径字符串 | ||
| 144 | + * 用于显示高亮的贝塞尔曲线路径 | ||
| 145 | + */ | ||
| 133 | const activePathSegments = ref([]); | 146 | const activePathSegments = ref([]); |
| 134 | 147 | ||
| 135 | -// 计算完整的贝塞尔曲线路径 | 148 | +/** |
| 136 | -// 使用三次贝塞尔曲线(Cubic Bezier)创建平滑的曲线效果 | 149 | + * 计算完整的贝塞尔曲线路径 |
| 137 | -// 控制点的位置通过当前点和下一个点的中点来计算 | 150 | + * 该函数使用 points 数组中的点来生成完整的贝塞尔曲线路径 |
| 151 | + * 每个点之间的路径段通过 calculatePathSegment 函数计算得到 | ||
| 152 | + * 控制点的位置通过当前点和下一个点的中点来计算 | ||
| 153 | + * 返回一个包含所有路径段的字符串 | ||
| 154 | + * 用于在SVG中显示完整的路径 | ||
| 155 | + * @returns 返回SVG路径字符串 | ||
| 156 | + */ | ||
| 138 | const pathData = computed(() => { | 157 | const pathData = computed(() => { |
| 139 | const path = []; | 158 | const path = []; |
| 140 | points.value.forEach((point, index) => { | 159 | points.value.forEach((point, index) => { |
| ... | @@ -149,19 +168,23 @@ const pathData = computed(() => { | ... | @@ -149,19 +168,23 @@ const pathData = computed(() => { |
| 149 | return path.join(" "); | 168 | return path.join(" "); |
| 150 | }); | 169 | }); |
| 151 | 170 | ||
| 152 | -// 计算两点之间的贝塞尔曲线路径段 | 171 | +/** |
| 153 | -// 该函数使用三次贝塞尔曲线算法生成平滑的曲线路径: | 172 | + * 该函数使用三次贝塞尔曲线算法生成平滑的曲线路径, 计算两点之间的贝塞尔曲线路径段: |
| 154 | -// 1. 计算路径的方向向量和单位向量 | 173 | + * 1. 计算路径的方向向量和单位向量 |
| 155 | -// 2. 添加垂直偏移以创建弧形效果 | 174 | + * 2. 添加垂直偏移以创建弧形效果 |
| 156 | -// 3. 根据路径方向动态调整控制点 | 175 | + * 3. 根据路径方向动态调整控制点 |
| 157 | -// 4. 生成标准的SVG路径命令字符串 | 176 | + * 4. 生成标准的SVG路径命令字符串 |
| 158 | -// @param startPoint - 起始点坐标 {x, y} | 177 | + * @param startPoint - 起始点坐标 {x, y} |
| 159 | -// @param endPoint - 结束点坐标 {x, y} | 178 | + * @param endPoint - 结束点坐标 {x, y} |
| 160 | -// @returns 返回SVG路径字符串 | 179 | + * @returns 返回SVG路径字符串 |
| 180 | + */ | ||
| 161 | const calculatePathSegment = (startPoint, endPoint) => { | 181 | const calculatePathSegment = (startPoint, endPoint) => { |
| 162 | // 计算路径的方向向量 | 182 | // 计算路径的方向向量 |
| 183 | + // 计算起点和终点之间的水平距离差 | ||
| 163 | const dx = endPoint.x - startPoint.x; | 184 | const dx = endPoint.x - startPoint.x; |
| 185 | + // 计算起点和终点之间的垂直距离差 | ||
| 164 | const dy = endPoint.y - startPoint.y; | 186 | const dy = endPoint.y - startPoint.y; |
| 187 | + // 使用勾股定理计算两点之间的直线距离 | ||
| 165 | const distance = Math.sqrt(dx * dx + dy * dy); | 188 | const distance = Math.sqrt(dx * dx + dy * dy); |
| 166 | 189 | ||
| 167 | // 设置节点间的偏移距离(可以根据需要调整) | 190 | // 设置节点间的偏移距离(可以根据需要调整) |
| ... | @@ -173,26 +196,43 @@ const calculatePathSegment = (startPoint, endPoint) => { | ... | @@ -173,26 +196,43 @@ const calculatePathSegment = (startPoint, endPoint) => { |
| 173 | const unitY = dy / distance; | 196 | const unitY = dy / distance; |
| 174 | 197 | ||
| 175 | // 计算偏移后的起点和终点,根据路径方向调整垂直偏移 | 198 | // 计算偏移后的起点和终点,根据路径方向调整垂直偏移 |
| 199 | + // 根据路径方向调整起点坐标 | ||
| 200 | + // x坐标: 在原始起点基础上沿单位向量方向偏移 | ||
| 201 | + // y坐标: 根据路径是向上还是向下来决定垂直偏移的方向 | ||
| 176 | const adjustedStart = { | 202 | const adjustedStart = { |
| 177 | x: startPoint.x + unitX * offset, | 203 | x: startPoint.x + unitX * offset, |
| 178 | y: startPoint.y + (dy > 0 ? -verticalOffset : verticalOffset), | 204 | y: startPoint.y + (dy > 0 ? -verticalOffset : verticalOffset), |
| 179 | }; | 205 | }; |
| 206 | + | ||
| 207 | + // 根据路径方向调整终点坐标 | ||
| 208 | + // x坐标: 在原始终点基础上沿单位向量反方向偏移 | ||
| 209 | + // y坐标: 与起点相反的垂直偏移方向,确保曲线的平滑过渡 | ||
| 180 | const adjustedEnd = { | 210 | const adjustedEnd = { |
| 181 | x: endPoint.x - unitX * offset, | 211 | x: endPoint.x - unitX * offset, |
| 182 | y: endPoint.y + (dy > 0 ? verticalOffset : -verticalOffset), | 212 | y: endPoint.y + (dy > 0 ? verticalOffset : -verticalOffset), |
| 183 | }; | 213 | }; |
| 184 | 214 | ||
| 185 | // 计算控制点 | 215 | // 计算控制点 |
| 216 | + // 计算第一个控制点的x坐标 - 与起点x坐标相同 | ||
| 186 | const cpx1 = adjustedStart.x; | 217 | const cpx1 = adjustedStart.x; |
| 218 | + // 计算第一个控制点的y坐标 - 在起点和终点y坐标的中点 | ||
| 187 | const cpy1 = adjustedStart.y + (adjustedEnd.y - adjustedStart.y) * 0.5; | 219 | const cpy1 = adjustedStart.y + (adjustedEnd.y - adjustedStart.y) * 0.5; |
| 220 | + // 计算第二个控制点的x坐标 - 与终点x坐标相同 | ||
| 188 | const cpx2 = adjustedEnd.x; | 221 | const cpx2 = adjustedEnd.x; |
| 222 | + // 计算第二个控制点的y坐标 - 与第一个控制点y坐标相同,确保平滑过渡 | ||
| 189 | const cpy2 = adjustedStart.y + (adjustedEnd.y - adjustedStart.y) * 0.5; | 223 | const cpy2 = adjustedStart.y + (adjustedEnd.y - adjustedStart.y) * 0.5; |
| 190 | 224 | ||
| 191 | return `M ${adjustedStart.x} ${adjustedStart.y} C ${cpx1} ${cpy1}, ${cpx2} ${cpy2}, ${adjustedEnd.x} ${adjustedEnd.y}`; | 225 | return `M ${adjustedStart.x} ${adjustedStart.y} C ${cpx1} ${cpy1}, ${cpx2} ${cpy2}, ${adjustedEnd.x} ${adjustedEnd.y}`; |
| 192 | }; | 226 | }; |
| 193 | 227 | ||
| 194 | -// 未激活路径的颜色 | 228 | +/** |
| 195 | -// 使用浅灰色(#ccc)表示未激活状态 | 229 | + * 计算未激活路径的颜色 |
| 230 | + * 该函数根据当前激活的节点索引来计算路径的颜色 | ||
| 231 | + * 当没有节点被激活时,路径颜色为浅灰色(#ccc) | ||
| 232 | + * 当有节点被激活时,路径颜色为绿色(#4CAF50) | ||
| 233 | + * 返回路径颜色的CSS字符串 | ||
| 234 | + * @returns 返回路径颜色的CSS字符串 | ||
| 235 | + */ | ||
| 196 | const pathColor = computed(() => { | 236 | const pathColor = computed(() => { |
| 197 | return "#ccc"; | 237 | return "#ccc"; |
| 198 | }); | 238 | }); |
| ... | @@ -215,25 +255,35 @@ const pathColor = computed(() => { | ... | @@ -215,25 +255,35 @@ const pathColor = computed(() => { |
| 215 | // } | 255 | // } |
| 216 | // }; | 256 | // }; |
| 217 | 257 | ||
| 218 | -// 处理下一步按钮点击事件 | 258 | +/** |
| 219 | -// 该函数实现了动画的核心交互逻辑: | 259 | + * 处理下一步按钮点击事件 |
| 220 | -// 1. 按顺序激活下一个节点 | 260 | + * 该函数实现了动画的核心交互逻辑: |
| 221 | -// 2. 创建新的高亮路径段并添加到动画序列 | 261 | + * 1. 按顺序激活下一个节点 |
| 222 | -// 3. 更新节点状态和路径显示 | 262 | + * 2. 创建新的高亮路径段并添加到动画序列 |
| 223 | -// 4. 当到达最后一个节点时自动禁用按钮 | 263 | + * 3. 更新节点状态和路径显示 |
| 224 | -// 5. 确保动画流程的连贯性和用户体验 | 264 | + * 4. 当到达最后一个节点时自动禁用按钮 |
| 265 | + * 5. 确保动画流程的连贯性和用户体验 | ||
| 266 | + */ | ||
| 225 | const nextStep = () => { | 267 | const nextStep = () => { |
| 268 | + // 检查是否还有下一个节点可以激活 | ||
| 226 | if (activeNodeIndex.value < points.value.length - 1) { | 269 | if (activeNodeIndex.value < points.value.length - 1) { |
| 270 | + // 激活下一个节点 | ||
| 227 | activeNodeIndex.value++; | 271 | activeNodeIndex.value++; |
| 272 | + | ||
| 273 | + // 获取当前激活节点的坐标 | ||
| 228 | const startPoint = points.value[activeNodeIndex.value]; | 274 | const startPoint = points.value[activeNodeIndex.value]; |
| 275 | + // 获取下一个节点的坐标 | ||
| 229 | const endPoint = points.value[activeNodeIndex.value + 1]; | 276 | const endPoint = points.value[activeNodeIndex.value + 1]; |
| 277 | + | ||
| 230 | // 只有当存在下一个节点时才添加新的路径段 | 278 | // 只有当存在下一个节点时才添加新的路径段 |
| 231 | if (endPoint) { | 279 | if (endPoint) { |
| 280 | + // 计算当前节点到下一个节点的贝塞尔曲线路径 | ||
| 232 | const newSegment = calculatePathSegment(startPoint, endPoint); | 281 | const newSegment = calculatePathSegment(startPoint, endPoint); |
| 233 | - // 保留之前的路径段,添加新的路径段 | 282 | + // 将新的路径段添加到激活路径数组中,保留之前的路径段 |
| 234 | activePathSegments.value.push(newSegment); | 283 | activePathSegments.value.push(newSegment); |
| 235 | } | 284 | } |
| 236 | - // 如果是最后一个节点,直接将其设置为激活状态 | 285 | + |
| 286 | + // 如果当前是最后一个节点,确保其状态为激活 | ||
| 237 | if (activeNodeIndex.value === points.value.length - 1) { | 287 | if (activeNodeIndex.value === points.value.length - 1) { |
| 238 | activeNodeIndex.value = points.value.length - 1; | 288 | activeNodeIndex.value = points.value.length - 1; |
| 239 | } | 289 | } |
| ... | @@ -288,10 +338,14 @@ const svgObj = [ | ... | @@ -288,10 +338,14 @@ const svgObj = [ |
| 288 | }, | 338 | }, |
| 289 | }, | 339 | }, |
| 290 | ]; | 340 | ]; |
| 291 | -// 计算SVG容器尺寸 | 341 | + |
| 292 | -// 该函数用于获取动画容器的实际尺寸,以便于后续计算SVG视图框 | 342 | +/** |
| 293 | -// 返回一个包含容器宽度和高度的对象 | 343 | + * 计算SVG容器尺寸 |
| 294 | -// 如果容器不存在,则返回默认值{width: 0, height: 0} | 344 | + * 该函数用于获取动画容器的实际尺寸,以便于后续计算SVG视图框 |
| 345 | + * 返回一个包含容器宽度和高度的对象 | ||
| 346 | + * 如果容器不存在,则返回默认值{width: 0, height: 0} | ||
| 347 | + * @returns 返回SVG容器尺寸的对象 | ||
| 348 | + */ | ||
| 295 | const containerSize = computed(() => { | 349 | const containerSize = computed(() => { |
| 296 | const container = document.querySelector('.animation-container'); | 350 | const container = document.querySelector('.animation-container'); |
| 297 | if (!container) return { width: 0, height: 0 }; | 351 | if (!container) return { width: 0, height: 0 }; |
| ... | @@ -318,37 +372,48 @@ onUnmounted(() => { | ... | @@ -318,37 +372,48 @@ onUnmounted(() => { |
| 318 | window.removeEventListener('resize', updateContainerSize); | 372 | window.removeEventListener('resize', updateContainerSize); |
| 319 | }); | 373 | }); |
| 320 | 374 | ||
| 321 | -// 计算并更新SVG视图框(viewBox) | 375 | +/** |
| 322 | -// 该函数动态计算SVG视图框的尺寸和位置,以确保: | 376 | + * 计算并更新SVG视图框(viewBox) |
| 323 | -// 1. 所有节点都在可视区域内 | 377 | + * 该函数根据当前节点的位置和容器尺寸来计算SVG视图框的尺寸和位置 |
| 324 | -// 2. 保持适当的宽高比 | 378 | + * 1. 所有节点都在可视区域内 |
| 325 | -// 3. 根据容器尺寸自适应调整 | 379 | + * 2. 保持适当的宽高比 |
| 326 | -// 4. 在不同屏幕尺寸下保持良好的显示效果 | 380 | + * 3. 根据容器尺寸自适应调整 |
| 381 | + * 4. 在不同屏幕尺寸下保持良好的显示效果 | ||
| 382 | + * 返回一个包含视图框尺寸和位置的字符串 | ||
| 383 | + * 格式为"minX minY width height" | ||
| 384 | + * 用于设置SVG的viewBox属性 | ||
| 385 | + * @returns 返回SVG视图框的字符串 | ||
| 386 | + */ | ||
| 327 | const svgViewBox = computed(() => { | 387 | const svgViewBox = computed(() => { |
| 388 | + // 计算所有节点中的最大和最小坐标值 | ||
| 328 | const maxX = Math.max(...points.value.map(p => p.x)); | 389 | const maxX = Math.max(...points.value.map(p => p.x)); |
| 329 | const maxY = Math.max(...points.value.map(p => p.y)); | 390 | const maxY = Math.max(...points.value.map(p => p.y)); |
| 330 | const minX = Math.min(...points.value.map(p => p.x)); | 391 | const minX = Math.min(...points.value.map(p => p.x)); |
| 331 | const minY = Math.min(...points.value.map(p => p.y)); | 392 | const minY = Math.min(...points.value.map(p => p.y)); |
| 393 | + // 设置内边距,确保节点不会贴近边缘 | ||
| 332 | const padding = 100; | 394 | const padding = 100; |
| 333 | 395 | ||
| 396 | + // 计算内容区域的实际尺寸 | ||
| 397 | + const contentWidth = maxX - minX + padding * 2; // 内容宽度 = 最大x - 最小x + 两侧内边距 | ||
| 398 | + const contentHeight = maxY - minY + padding * 2; // 内容高度 = 最大y - 最小y + 上下内边距 | ||
| 334 | // 计算内容的宽高比 | 399 | // 计算内容的宽高比 |
| 335 | - const contentWidth = maxX - minX + padding * 2; | ||
| 336 | - const contentHeight = maxY - minY + padding * 2; | ||
| 337 | const contentRatio = contentWidth / contentHeight; | 400 | const contentRatio = contentWidth / contentHeight; |
| 338 | 401 | ||
| 339 | // 获取容器的宽高比 | 402 | // 获取容器的宽高比 |
| 340 | const containerRatio = containerSize.value.width / containerSize.value.height; | 403 | const containerRatio = containerSize.value.width / containerSize.value.height; |
| 341 | 404 | ||
| 342 | - // 根据宽高比调整viewBox | 405 | + // 根据容器和内容的宽高比例关系调整viewBox |
| 343 | if (containerRatio > contentRatio) { | 406 | if (containerRatio > contentRatio) { |
| 344 | - // 容器更宽,以高度为基准 | 407 | + // 当容器更宽时,以内容高度为基准进行缩放 |
| 345 | - const adjustedWidth = contentHeight * containerRatio; | 408 | + const adjustedWidth = contentHeight * containerRatio; // 调整后的宽度 |
| 346 | - const extraPadding = (adjustedWidth - contentWidth) / 2; | 409 | + const extraPadding = (adjustedWidth - contentWidth) / 2; // 计算额外需要的水平内边距 |
| 410 | + // 返回viewBox参数:x起点、y起点、宽度、高度 | ||
| 347 | return `${minX - padding - extraPadding} ${minY - padding} ${adjustedWidth} ${contentHeight}`; | 411 | return `${minX - padding - extraPadding} ${minY - padding} ${adjustedWidth} ${contentHeight}`; |
| 348 | } else { | 412 | } else { |
| 349 | - // 容器更高,以宽度为基准 | 413 | + // 当容器更高时,以内容宽度为基准进行缩放 |
| 350 | - const adjustedHeight = containerRatio ? contentWidth / containerRatio : contentHeight; | 414 | + const adjustedHeight = containerRatio ? contentWidth / containerRatio : contentHeight; // 调整后的高度 |
| 351 | - const extraPadding = isFinite(adjustedHeight) ? (adjustedHeight - contentHeight) / 2 : 0; | 415 | + const extraPadding = isFinite(adjustedHeight) ? (adjustedHeight - contentHeight) / 2 : 0; // 计算额外需要的垂直内边距 |
| 416 | + // 返回viewBox参数:x起点、y起点、宽度、高度 | ||
| 352 | return `${minX - padding} ${minY - padding - extraPadding} ${contentWidth} ${adjustedHeight}`; | 417 | return `${minX - padding} ${minY - padding - extraPadding} ${contentWidth} ${adjustedHeight}`; |
| 353 | } | 418 | } |
| 354 | }); | 419 | }); | ... | ... |
-
Please register or login to post a comment