简易路径调试工具
需求:UI太懒,不做动画,需要自己做类似流程图线条动画效果
成品:
调试工具:
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>简易路径调试工具</title><style>body {margin: 0;padding: 20px;font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;background: #f8f9fa;}.container {max-width: 1200px;margin: 0 auto;background: white;border-radius: 12px;padding: 20px;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);}.main-area {display: flex;gap: 20px;margin-top: 20px;}.image-section {flex: 1;}.image-container {position: relative;width: 100%;min-height: 400px;border: 2px dashed #d1d5db;border-radius: 8px;display: flex;align-items: center;justify-content: center;background: #f9fafb;cursor: crosshair;}.image-container.has-image {border: none;cursor: crosshair;}.uploaded-image {max-width: 100%;max-height: 500px;object-fit: contain;border-radius: 8px;}.controls-panel {width: 300px;background: #f9fafb;padding: 20px;border-radius: 8px;border: 1px solid #e5e7eb;height: fit-content;}.control-group {margin-bottom: 20px;}.control-group label {display: block;margin-bottom: 8px;font-weight: 600;color: #374151;}.control-group input[type="range"] {width: 100%;margin-bottom: 5px;}.control-group input[type="color"] {width: 100%;height: 40px;border: none;border-radius: 4px;}.value-display {font-size: 12px;color: #6b7280;margin-left: 10px;}button {background: #8b5cf6;color: white;border: none;padding: 10px 15px;border-radius: 6px;cursor: pointer;margin: 5px 0;width: 100%;font-size: 14px;}button:hover {background: #7c3aed;}.upload-hint {text-align: center;color: #6b7280;}.point-item {background: white;padding: 10px;border-radius: 6px;margin-bottom: 8px;border: 1px solid #e5e7eb;display: flex;align-items: center;justify-content: space-between;}.point-coords {font-family: monospace;font-size: 12px;color: #6b7280;}.delete-point {background: #ef4444;padding: 4px 8px;font-size: 12px;margin: 0;width: auto;}.instructions {background: #dbeafe;padding: 15px;border-radius: 8px;margin-bottom: 20px;border-left: 4px solid #3b82f6;}.instructions h3 {margin: 0 0 10px 0;color: #1e40af;}.code-output {background: #f3f4f6;padding: 15px;border-radius: 8px;font-family: monospace;font-size: 12px;white-space: pre-wrap;max-height: 200px;overflow-y: auto;border: 1px solid #e5e7eb;}.preview-canvas {position: absolute;top: 0;left: 0;width: 100%;height: 100%;pointer-events: none;z-index: 10;}.click-overlay {position: absolute;top: 0;left: 0;width: 100%;height: 100%;z-index: 20;cursor: crosshair;}</style>
</head>
<body><div class="container"><h1>🎯 简易路径调试工具</h1><div class="instructions"><h3>使用说明:</h3><p><strong>步骤1:</strong> 点击"选择图片"上传您的界面截图</p><p><strong>步骤2:</strong> 在图片上按顺序点击,标记流动路径的关键点</p><p><strong>步骤3:</strong> 调整动画参数,实时预览效果</p><p><strong>步骤4:</strong> 点击"生成代码"获取React组件代码</p></div><div class="main-area"><div class="image-section"><div class="image-container" id="imageContainer"><div class="upload-hint"><p>📁 点击上传您的界面图片</p><button onclick="document.getElementById('fileInput').click()" style="width: auto; margin: 10px;">选择图片</button><p style="font-size: 12px; margin-top: 10px;">支持 JPG、PNG、GIF 格式</p></div><input type="file" id="fileInput" accept="image/*" style="display: none;" onchange="handleImageUpload(this)"></div></div><div class="controls-panel"><div class="control-group"><label>动画颜色</label><input type="color" id="animationColor" value="#8B5CF6" onchange="updatePreview()"></div><div class="control-group"><label>动画速度 <span class="value-display" id="speedDisplay">3.0s</span></label><input type="range" id="animationSpeed" min="1" max="8" value="3" step="0.5" oninput="updateSpeedDisplay(); updatePreview();"></div><div class="control-group"><label>线条粗细 <span class="value-display" id="thicknessDisplay">3px</span></label><input type="range" id="lineThickness" min="1" max="8" value="3" step="0.5" oninput="updateThicknessDisplay(); updatePreview();"></div><hr style="margin: 20px 0; border: none; border-top: 1px solid #e5e7eb;"><div class="control-group"><label>路径点列表 <span style="font-size: 12px; color: #6b7280;">(点击图片添加)</span></label><div id="pointsList"><div style="text-align: center; color: #9ca3af; padding: 20px;">暂无路径点<br><small>在图片上点击添加</small></div></div></div><button onclick="clearAllPoints()">🗑️ 清空所有点</button><button onclick="undoLastPoint()">↶ 撤销上一点</button><button onclick="addSamplePath()" style="background: #10b981;">📝 添加示例路径</button><hr style="margin: 20px 0; border: none; border-top: 1px solid #e5e7eb;"><button onclick="generateFinalCode()" style="background: #f59e0b;">🚀 生成React代码</button></div></div><div style="margin-top: 30px;"><h3>当前路径预览:</h3><div class="code-output" id="pathPreview">暂无路径数据</div></div></div><script>let pathPoints = [];let imageElement = null;let canvas = null;let ctx = null;// 初始化function init() {updateSpeedDisplay();updateThicknessDisplay();}// 处理图片上传function handleImageUpload(input) {if (input.files && input.files[0]) {const file = input.files[0];const reader = new FileReader();reader.onload = function(e) {const container = document.getElementById('imageContainer');container.innerHTML = `<img src="${e.target.result}" class="uploaded-image" id="uploadedImage"><canvas class="preview-canvas" id="previewCanvas"></canvas><div class="click-overlay" id="clickOverlay"></div>`;container.classList.add('has-image');imageElement = document.getElementById('uploadedImage');canvas = document.getElementById('previewCanvas');ctx = canvas.getContext('2d');// 等待图片加载完成后设置canvas尺寸imageElement.onload = function() {// 延迟一点时间确保图片完全渲染setTimeout(() => {setupCanvas();setupClickHandler();}, 100);};// 如果图片已经加载完成(缓存情况)if (imageElement.complete) {setTimeout(() => {setupCanvas();setupClickHandler();}, 100);}};reader.readAsDataURL(file);}}// 设置canvasfunction setupCanvas() {// 获取图片的实际显示尺寸const rect = imageElement.getBoundingClientRect();const containerRect = document.getElementById('imageContainer').getBoundingClientRect();// 设置canvas的实际像素尺寸canvas.width = rect.width;canvas.height = rect.height;// 设置canvas的CSS尺寸以匹配图片canvas.style.width = rect.width + 'px';canvas.style.height = rect.height + 'px';// 确保canvas覆盖在图片上的正确位置canvas.style.left = (rect.left - containerRect.left) + 'px';canvas.style.top = (rect.top - containerRect.top) + 'px';updatePreview();}// 设置点击处理function setupClickHandler() {const overlay = document.getElementById('clickOverlay');overlay.addEventListener('click', function(e) {const rect = imageElement.getBoundingClientRect();// 计算相对于图片的精确坐标const x = e.clientX - rect.left;const y = e.clientY - rect.top;// 转换为百分比坐标(相对于图片的实际显示尺寸)const xPercent = (x / rect.width) * 100;const yPercent = (y / rect.height) * 100;// 确保坐标在有效范围内const validX = Math.max(0, Math.min(100, xPercent));const validY = Math.max(0, Math.min(100, yPercent));// 添加点pathPoints.push({x: validX,y: validY});updatePointsList();updatePreview();updatePathPreview();});}// 更新点列表显示function updatePointsList() {const pointsList = document.getElementById('pointsList');if (pathPoints.length === 0) {pointsList.innerHTML = `<div style="text-align: center; color: #9ca3af; padding: 20px;">暂无路径点<br><small>在图片上点击添加</small></div>`;return;}pointsList.innerHTML = '';pathPoints.forEach((point, index) => {const pointDiv = document.createElement('div');pointDiv.className = 'point-item';pointDiv.innerHTML = `<div><strong>点 ${index + 1}</strong><div class="point-coords">(${point.x.toFixed(1)}%, ${point.y.toFixed(1)}%)</div></div><button class="delete-point" onclick="removePoint(${index})">删除</button>`;pointsList.appendChild(pointDiv);});}// 更新预览function updatePreview() {if (!ctx) return;// 清除画布ctx.clearRect(0, 0, canvas.width, canvas.height);if (pathPoints.length === 0) return;const color = document.getElementById('animationColor').value;const thickness = document.getElementById('lineThickness').value;// 绘制路径(如果有2个或以上的点)if (pathPoints.length >= 2) {ctx.strokeStyle = color;ctx.lineWidth = thickness;ctx.lineCap = 'round';ctx.lineJoin = 'round';ctx.beginPath();const firstPoint = pathPoints[0];ctx.moveTo((firstPoint.x / 100) * canvas.width, (firstPoint.y / 100) * canvas.height);for (let i = 1; i < pathPoints.length; i++) {const point = pathPoints[i];const x = (point.x / 100) * canvas.width;const y = (point.y / 100) * canvas.height;if (i === pathPoints.length - 1 && pathPoints.length > 2) {// 最后一段使用曲线const prevPoint = pathPoints[i - 1];const prevX = (prevPoint.x / 100) * canvas.width;const prevY = (prevPoint.y / 100) * canvas.height;const controlX = (prevX + x) / 2;const controlY = (prevY + y) / 2;ctx.quadraticCurveTo(controlX, controlY, x, y);} else {ctx.lineTo(x, y);}}ctx.stroke();}// 绘制控制点(总是显示所有点)pathPoints.forEach((point, index) => {const x = (point.x / 100) * canvas.width;const y = (point.y / 100) * canvas.height;// 根据点的位置设置颜色ctx.fillStyle = index === 0 ? '#10b981' : index === pathPoints.length - 1 ? '#ef4444' : '#3b82f6';ctx.beginPath();ctx.arc(x, y, 8, 0, 2 * Math.PI);ctx.fill();// 白色边框ctx.strokeStyle = 'white';ctx.lineWidth = 2;ctx.stroke();// 标号ctx.fillStyle = 'white';ctx.font = 'bold 12px sans-serif';ctx.textAlign = 'center';ctx.textBaseline = 'middle';ctx.fillText((index + 1).toString(), x, y);});}// 更新路径预览function updatePathPreview() {const preview = document.getElementById('pathPreview');if (pathPoints.length < 2) {preview.textContent = '暂无路径数据(至少需要2个点)';return;}let pathData = `M ${pathPoints[0].x.toFixed(1)} ${pathPoints[0].y.toFixed(1)}`;for (let i = 1; i < pathPoints.length; i++) {const point = pathPoints[i];if (i === pathPoints.length - 1 && pathPoints.length > 2) {const prevPoint = pathPoints[i - 1];const controlX = ((prevPoint.x + point.x) / 2).toFixed(1);const controlY = ((prevPoint.y + point.y) / 2).toFixed(1);pathData += ` Q ${controlX} ${controlY} ${point.x.toFixed(1)} ${point.y.toFixed(1)}`;} else {pathData += ` L ${point.x.toFixed(1)} ${point.y.toFixed(1)}`;}}preview.textContent = `SVG路径: ${pathData}`;}// 控制函数function updateSpeedDisplay() {const speed = document.getElementById('animationSpeed').value;document.getElementById('speedDisplay').textContent = speed + 's';}function updateThicknessDisplay() {const thickness = document.getElementById('lineThickness').value;document.getElementById('thicknessDisplay').textContent = thickness + 'px';}function removePoint(index) {pathPoints.splice(index, 1);updatePointsList();updatePreview();updatePathPreview();}function clearAllPoints() {pathPoints = [];updatePointsList();updatePreview();updatePathPreview();}function undoLastPoint() {if (pathPoints.length > 0) {pathPoints.pop();updatePointsList();updatePreview();updatePathPreview();}}function addSamplePath() {pathPoints = [{x: 25, y: 15},{x: 25, y: 45},{x: 60, y: 70},{x: 85, y: 85}];updatePointsList();updatePreview();updatePathPreview();}function generateFinalCode() {if (pathPoints.length < 2) {alert('请至少添加2个路径点!');return;}const color = document.getElementById('animationColor').value;const speed = document.getElementById('animationSpeed').value;const thickness = document.getElementById('lineThickness').value;let pathData = `M ${pathPoints[0].x.toFixed(1)} ${pathPoints[0].y.toFixed(1)}`;for (let i = 1; i < pathPoints.length; i++) {const point = pathPoints[i];if (i === pathPoints.length - 1 && pathPoints.length > 2) {const prevPoint = pathPoints[i - 1];const controlX = ((prevPoint.x + point.x) / 2).toFixed(1);const controlY = ((prevPoint.y + point.y) / 2).toFixed(1);pathData += ` Q ${controlX} ${controlY} ${point.x.toFixed(1)} ${point.y.toFixed(1)}`;} else {pathData += ` L ${point.x.toFixed(1)} ${point.y.toFixed(1)}`;}}const reactCode = `// React组件中的SVG路径动画代码
const pathData = "${pathData}";<svg width="100%" height="100%" viewBox="0 0 100 100" className="absolute inset-0"><defs><linearGradient id="flowGradient" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stopColor="rgba(255,255,255,0)" /><stop offset="30%" stopColor="${color}" stopOpacity="0.6" /><stop offset="70%" stopColor="${color}" stopOpacity="1" /><stop offset="100%" stopColor="rgba(255,255,255,0)" /></linearGradient></defs>{/* 流动路径 */}<pathd={pathData}stroke="url(#flowGradient)"strokeWidth="${thickness}"fill="none"strokeDasharray="25 100"strokeLinecap="round"><animateattributeName="stroke-dashoffset"from="125"to="0"dur="${speed}s"repeatCount="indefinite"/></path>{/* 流动点 */}<circle r="4" fill="${color}" opacity="0.9"><animateMotiondur="${speed}s"repeatCount="indefinite"path={pathData}/><animateattributeName="opacity"values="0;1;1;0.5;0"dur="${speed}s"repeatCount="indefinite"/></circle>
</svg>`;// 显示代码模态框showCodeModal(reactCode);}function showCodeModal(code) {const modal = document.createElement('div');modal.style.cssText = `position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 20px;`;modal.innerHTML = `<div style="background: white; padding: 30px; border-radius: 12px; max-width: 90%; max-height: 90%; overflow: auto;"><div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;"><h3 style="margin: 0;">🎉 生成的React代码</h3><button onclick="this.parentElement.parentElement.parentElement.remove()" style="background: #6b7280; width: auto; margin: 0; padding: 5px 10px;">✕</button></div><pre style="background: #f3f4f6; padding: 20px; border-radius: 8px; overflow: auto; white-space: pre-wrap; font-size: 13px; line-height: 1.4;">${code}</pre><div style="text-align: center; margin-top: 20px;"><button onclick="copyToClipboard(\`${code.replace(/`/g, '\\`')}\`)" style="margin-right: 10px; width: auto;">📋 复制代码</button><button onclick="downloadCode(\`${code.replace(/`/g, '\\`')}\`)" style="background: #10b981; width: auto;">💾 下载文件</button></div></div>`;document.body.appendChild(modal);}function copyToClipboard(text) {navigator.clipboard.writeText(text).then(() => {alert('✅ 代码已复制到剪贴板!');}).catch(() => {// 备用方案const textarea = document.createElement('textarea');textarea.value = text;document.body.appendChild(textarea);textarea.select();document.execCommand('copy');document.body.removeChild(textarea);alert('✅ 代码已复制到剪贴板!');});}function downloadCode(code) {const blob = new Blob([code], { type: 'text/plain' });const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = 'flow-animation-code.tsx';document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);}// 窗口大小改变时重新设置canvaswindow.addEventListener('resize', function() {if (imageElement && canvas) {setTimeout(setupCanvas, 100);}});// 添加一个辅助函数来强制刷新canvas位置function refreshCanvas() {if (imageElement && canvas) {setTimeout(() => {setupCanvas();}, 50);}}// 初始化init();</script>
</body>
</html>
生成的path路径直接应用到react 组件:
import React from 'react';interface SimpleFlowAnimationProps {className?: string;duration?: number;color?: string;isActive?: boolean;
}const SimpleFlowAnimation: React.FC<SimpleFlowAnimationProps> = ({className = '',duration = 2.5,color = '#8B5CF6',isActive = true
}) => {if (!isActive) return null;return (<div className={`absolute inset-0 pointer-events-none ${className}`}><svgwidth="100%"height="100%"viewBox="0 0 100 100"preserveAspectRatio="none"className="w-full h-full"style={{ position: 'absolute', top: 0, left: 0 }}><defs>{/* 流动渐变 */}<linearGradient id="simpleFlowGradient" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stopColor="rgba(255,255,255,0)" /><stop offset="30%" stopColor={color} stopOpacity="0.6" /><stop offset="70%" stopColor={color} stopOpacity="1" /><stop offset="100%" stopColor="rgba(255,255,255,0)" /></linearGradient></defs>{/* 精确的流动路径 - 基于您的最新调试结果 */}<pathd="M 49.7 13.2 L 80.4 13.8 L 80.4 46.4 Q 76.0 46.6 71.6 46.8"stroke="url(#simpleFlowGradient)"strokeWidth="0.8"fill="none"strokeDasharray="5 25"strokeLinecap="round"><animateattributeName="stroke-dashoffset"from="30"to="0"dur={`${duration}s`}repeatCount="indefinite"/></path>{/* 流动的小点 */}<circle r="1" fill={color} opacity="0.9"><animateMotiondur={`${duration}s`}repeatCount="indefinite"path="M 49.7 13.2 L 80.4 13.8 L 80.4 46.4 Q 76.0 46.6 71.6 46.8"/><animateattributeName="opacity"values="0;1;1;0.5;0"dur={`${duration}s`}repeatCount="indefinite"/></circle>{/* 第二个延迟的流动点 */}<circle r="0.8" fill={color} opacity="0.7"><animateMotiondur={`${duration}s`}repeatCount="indefinite"path="M 49.7 13.2 L 80.4 13.8 L 80.4 46.4 Q 76.0 46.6 71.6 46.8"begin="1.2s"/><animateattributeName="opacity"values="0;0.7;0.7;0.3;0"dur={`${duration}s`}repeatCount="indefinite"begin="1.2s"/></circle>{/* 第三个流动点增强效果 */}<circle r="0.6" fill={color} opacity="0.5"><animateMotiondur={`${duration}s`}repeatCount="indefinite"path="M 49.7 13.2 L 80.4 13.8 L 80.4 46.4 Q 76.0 46.6 71.6 46.8"begin="2.4s"/><animateattributeName="opacity"values="0;0.5;0.5;0.2;0"dur={`${duration}s`}repeatCount="indefinite"begin="2.4s"/></circle></svg></div>);
};export default SimpleFlowAnimation;
再优化了一下,支持圆弧,预览:
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>高级路径调试工具 - 支持圆弧和断续路径</title><style>body {margin: 0;padding: 20px;font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;background: #f8f9fa;}.container {max-width: 1400px;margin: 0 auto;background: white;border-radius: 12px;padding: 20px;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);}.main-area {display: flex;gap: 20px;margin-top: 20px;}.image-section {flex: 1;}.image-container {position: relative;width: 100%;min-height: 400px;border: 2px dashed #d1d5db;border-radius: 8px;display: flex;align-items: center;justify-content: center;background: #f9fafb;cursor: crosshair;}.image-container.has-image {border: none;cursor: crosshair;}.uploaded-image {max-width: 100%;max-height: 600px;object-fit: contain;border-radius: 8px;}.controls-panel {width: 350px;background: #f9fafb;padding: 20px;border-radius: 8px;border: 1px solid #e5e7eb;height: fit-content;}.control-group {margin-bottom: 20px;}.control-group label {display: block;margin-bottom: 8px;font-weight: 600;color: #374151;}.control-group input[type="range"] {width: 100%;margin-bottom: 5px;}.control-group input[type="color"] {width: 100%;height: 40px;border: none;border-radius: 4px;}.control-group select {width: 100%;padding: 8px;border: 1px solid #d1d5db;border-radius: 4px;background: white;}.value-display {font-size: 12px;color: #6b7280;margin-left: 10px;}button {background: #8b5cf6;color: white;border: none;padding: 10px 15px;border-radius: 6px;cursor: pointer;margin: 5px 0;width: 100%;font-size: 14px;}button:hover {background: #7c3aed;}.upload-hint {text-align: center;color: #6b7280;}.segment-item {background: white;padding: 12px;border-radius: 6px;margin-bottom: 10px;border: 1px solid #e5e7eb;}.segment-header {display: flex;justify-content: space-between;align-items: center;margin-bottom: 8px;}.segment-type {font-weight: 600;color: #374151;}.segment-controls {display: grid;grid-template-columns: 1fr 1fr;gap: 8px;margin-bottom: 8px;}.segment-controls input {padding: 4px 8px;border: 1px solid #d1d5db;border-radius: 4px;font-size: 12px;}.delete-segment {background: #ef4444;padding: 4px 8px;font-size: 12px;margin: 0;width: auto;}.instructions {background: #dbeafe;padding: 15px;border-radius: 8px;margin-bottom: 20px;border-left: 4px solid #3b82f6;}.instructions h3 {margin: 0 0 10px 0;color: #1e40af;}.code-output {background: #f3f4f6;padding: 15px;border-radius: 8px;font-family: monospace;font-size: 12px;white-space: pre-wrap;max-height: 200px;overflow-y: auto;border: 1px solid #e5e7eb;}.preview-canvas {position: absolute;top: 0;left: 0;width: 100%;height: 100%;pointer-events: none;z-index: 10;}.click-overlay {position: absolute;top: 0;left: 0;width: 100%;height: 100%;z-index: 20;cursor: crosshair;}.mode-selector {display: flex;gap: 10px;margin-bottom: 15px;}.mode-btn {flex: 1;padding: 8px 12px;border: 2px solid #e5e7eb;background: white;border-radius: 6px;cursor: pointer;text-align: center;font-size: 12px;transition: all 0.2s;}.mode-btn.active {border-color: #8b5cf6;background: #8b5cf6;color: white;}.arc-controls {display: none;margin-top: 10px;padding: 10px;background: #f3f4f6;border-radius: 4px;}.arc-controls.show {display: block;}.checkbox-group {display: flex;align-items: center;gap: 8px;margin-bottom: 10px;}</style>
</head>
<body><div class="container"><h1>🎯 高级路径调试工具</h1><p style="color: #6b7280; margin-bottom: 20px;">支持直线、圆弧、断续路径的专业动画调试工具</p><div class="instructions"><h3>功能特点:</h3><p><strong>📏 多种路径类型:</strong> 直线(L)、二次贝塞尔曲线(Q)、圆弧(A)</p><p><strong>✂️ 断续显示:</strong> 支持路径段的显示/隐藏控制</p><p><strong>🎨 实时预览:</strong> 立即看到路径和动画效果</p><p><strong>📝 代码生成:</strong> 一键生成完整的React组件代码</p></div><div class="main-area"><div class="image-section"><div class="image-container" id="imageContainer"><div class="upload-hint"><p>📁 点击上传您的界面图片</p><button onclick="document.getElementById('fileInput').click()" style="width: auto; margin: 10px;">选择图片</button><p style="font-size: 12px; margin-top: 10px;">支持 JPG、PNG、GIF 格式</p></div><input type="file" id="fileInput" accept="image/*" style="display: none;" onchange="handleImageUpload(this)"></div></div><div class="controls-panel"><!-- 绘制模式选择 --><div class="control-group"><label>绘制模式</label><div class="mode-selector"><div class="mode-btn active" onclick="setDrawMode('line')" id="lineMode">📏 直线</div><div class="mode-btn" onclick="setDrawMode('smooth')" id="smoothMode">🌊 平滑</div><div class="mode-btn" onclick="setDrawMode('corner')" id="cornerMode">📐 转角</div></div><div style="font-size: 11px; color: #6b7280; margin-top: 5px;">💡 <strong>平滑</strong>:自动生成平滑曲线<br>💡 <strong>转角</strong>:创建圆角转弯效果</div></div><!-- 转角控制参数 --><div class="arc-controls" id="cornerControls"><label style="font-size: 12px; color: #6b7280;">转角半径:</label><div style="display: flex; gap: 8px; margin-top: 5px; align-items: center;"><input type="range" id="cornerRadius" min="5" max="50" value="20" style="flex: 1;"><span id="cornerRadiusValue" style="font-size: 12px; color: #6b7280;">20px</span></div><div style="font-size: 11px; color: #9ca3af; margin-top: 5px;">用于创建圆角转弯效果</div></div><div class="control-group"><label>动画颜色</label><input type="color" id="animationColor" value="#8B5CF6" onchange="updatePreview()"></div><div class="control-group"><label>动画速度 <span class="value-display" id="speedDisplay">3.0s</span></label><input type="range" id="animationSpeed" min="1" max="8" value="3" step="0.5" oninput="updateSpeedDisplay(); updatePreview();"></div><div class="control-group"><label>线条粗细 <span class="value-display" id="thicknessDisplay">3px</span></label><input type="range" id="lineThickness" min="1" max="8" value="3" step="0.5" oninput="updateThicknessDisplay(); updatePreview();"></div><hr style="margin: 20px 0; border: none; border-top: 1px solid #e5e7eb;"><div class="control-group"><label>路径段列表 <span style="font-size: 12px; color: #6b7280;">(点击图片添加)</span></label><div id="segmentsList"><div style="text-align: center; color: #9ca3af; padding: 20px;">暂无路径段<br><small>在图片上点击添加路径点</small></div></div></div><button onclick="clearAllSegments()">🗑️ 清空所有段</button><button onclick="undoLastSegment()">↶ 撤销上一段</button><button onclick="addSamplePath()" style="background: #10b981;">📝 添加示例路径</button><hr style="margin: 20px 0; border: none; border-top: 1px solid #e5e7eb;"><div class="control-group"><label>实时预览</label><button id="previewToggle" onclick="toggleAnimationPreview()" style="background: #10b981;">▶️ 动画预览</button><div style="font-size: 11px; color: #6b7280; margin-top: 5px;">💡 预览动画点的实际运动轨迹(包含跳跃效果)</div></div><button onclick="generateAdvancedCode()" style="background: #f59e0b;">🚀 生成高级代码</button></div></div><div style="margin-top: 30px;"><h3>当前路径预览:</h3><div class="code-output" id="pathPreview">暂无路径数据</div></div></div><script>let pathSegments = []; // 存储路径段let currentDrawMode = 'line'; // 当前绘制模式let tempPoint = null; // 临时点(用于圆弧绘制)let imageElement = null;let canvas = null;let ctx = null;// 路径段类型定义const SEGMENT_TYPES = {MOVE: 'M', // 移动到LINE: 'L', // 直线到CURVE: 'Q', // 二次贝塞尔曲线ARC: 'A' // 椭圆弧};// 初始化function init() {updateSpeedDisplay();updateThicknessDisplay();updateSegmentsList();}// 设置绘制模式function setDrawMode(mode) {currentDrawMode = mode;tempPoint = null; // 重置临时点// 更新按钮状态document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active'));document.getElementById(mode + 'Mode').classList.add('active');// 显示/隐藏转角控制const cornerControls = document.getElementById('cornerControls');if (mode === 'corner') {cornerControls.classList.add('show');} else {cornerControls.classList.remove('show');}// 更新界面提示updateModeHint(mode);}// 更新模式提示function updateModeHint(mode) {const container = document.getElementById('imageContainer');if (!container.classList.contains('has-image')) return;let hint = '';switch (mode) {case 'line':hint = '点击添加直线路径点';break;case 'smooth':hint = '点击添加点,自动生成平滑曲线';break;case 'corner':hint = '点击添加点,创建圆角转弯';break;}// 可以在这里添加提示显示逻辑}// 更新转角半径显示function updateCornerRadius() {const radius = document.getElementById('cornerRadius').value;document.getElementById('cornerRadiusValue').textContent = radius + 'px';}// 添加转角半径更新监听document.addEventListener('DOMContentLoaded', function() {const cornerRadiusSlider = document.getElementById('cornerRadius');if (cornerRadiusSlider) {cornerRadiusSlider.addEventListener('input', updateCornerRadius);}});// 处理图片上传function handleImageUpload(input) {if (input.files && input.files[0]) {const file = input.files[0];const reader = new FileReader();reader.onload = function(e) {const container = document.getElementById('imageContainer');container.innerHTML = `<img src="${e.target.result}" class="uploaded-image" id="uploadedImage"><canvas class="preview-canvas" id="previewCanvas"></canvas><div class="click-overlay" id="clickOverlay"></div>`;container.classList.add('has-image');imageElement = document.getElementById('uploadedImage');canvas = document.getElementById('previewCanvas');ctx = canvas.getContext('2d');imageElement.onload = function() {setTimeout(() => {setupCanvas();setupClickHandler();}, 100);};if (imageElement.complete) {setTimeout(() => {setupCanvas();setupClickHandler();}, 100);}};reader.readAsDataURL(file);}}// 设置canvasfunction setupCanvas() {const rect = imageElement.getBoundingClientRect();const containerRect = document.getElementById('imageContainer').getBoundingClientRect();canvas.width = rect.width;canvas.height = rect.height;canvas.style.width = rect.width + 'px';canvas.style.height = rect.height + 'px';canvas.style.left = (rect.left - containerRect.left) + 'px';canvas.style.top = (rect.top - containerRect.top) + 'px';updatePreview();}// 设置点击处理function setupClickHandler() {const overlay = document.getElementById('clickOverlay');overlay.addEventListener('click', function(e) {const rect = imageElement.getBoundingClientRect();const x = e.clientX - rect.left;const y = e.clientY - rect.top;const xPercent = (x / rect.width) * 100;const yPercent = (y / rect.height) * 100;const validX = Math.max(0, Math.min(100, xPercent));const validY = Math.max(0, Math.min(100, yPercent));addPathPoint(validX, validY);});}// 添加路径点function addPathPoint(x, y) {const point = { x, y };if (pathSegments.length === 0) {// 第一个点,添加移动命令pathSegments.push({type: SEGMENT_TYPES.MOVE,points: [point],visible: true,animationVisible: true});} else {// 根据当前模式添加路径段switch (currentDrawMode) {case 'line':pathSegments.push({type: SEGMENT_TYPES.LINE,points: [point],visible: true,animationVisible: true});break;case 'smooth':// 平滑模式:根据前面的点自动生成控制点if (pathSegments.length >= 2) {const prevSegment = pathSegments[pathSegments.length - 1];const prevPrevSegment = pathSegments[pathSegments.length - 2];let prevPoint, prevPrevPoint;if (prevSegment.type === 'M') {prevPoint = prevSegment.points[0];} else {prevPoint = prevSegment.points[prevSegment.points.length - 1];}if (prevPrevSegment.type === 'M') {prevPrevPoint = prevPrevSegment.points[0];} else {prevPrevPoint = prevPrevSegment.points[prevPrevSegment.points.length - 1];}// 计算控制点,创建平滑过渡const controlX = prevPoint.x + (prevPoint.x - prevPrevPoint.x) * 0.3;const controlY = prevPoint.y + (prevPoint.y - prevPrevPoint.y) * 0.3;pathSegments.push({type: SEGMENT_TYPES.CURVE,points: [{x: controlX, y: controlY}, point],visible: true,animationVisible: true});} else {// 前两个点用直线pathSegments.push({type: SEGMENT_TYPES.LINE,points: [point],visible: true,animationVisible: true});}break;case 'corner':// 转角模式:创建圆角效果if (pathSegments.length >= 2) {const prevSegment = pathSegments[pathSegments.length - 1];let prevPoint;if (prevSegment.type === 'M') {prevPoint = prevSegment.points[0];} else {prevPoint = prevSegment.points[prevSegment.points.length - 1];}const radius = parseFloat(document.getElementById('cornerRadius').value) || 20;// 计算转角的中间点const midX = (prevPoint.x + point.x) / 2;const midY = (prevPoint.y + point.y) / 2;// 添加一个平滑的转角pathSegments.push({type: SEGMENT_TYPES.CURVE,points: [{x: midX, y: midY}, point],visible: true,animationVisible: true,cornerRadius: radius});} else {// 前两个点用直线pathSegments.push({type: SEGMENT_TYPES.LINE,points: [point],visible: true,animationVisible: true});}break;}}updateSegmentsList();updatePreview();updatePathPreview();}// 更新路径段列表function updateSegmentsList() {const segmentsList = document.getElementById('segmentsList');if (pathSegments.length === 0) {segmentsList.innerHTML = `<div style="text-align: center; color: #9ca3af; padding: 20px;">暂无路径段<br><small>在图片上点击添加路径点</small></div>`;return;}segmentsList.innerHTML = '';// 倒序显示,最新的在最上面for (let i = pathSegments.length - 1; i >= 0; i--) {const segment = pathSegments[i];const segmentDiv = document.createElement('div');segmentDiv.className = 'segment-item';// 最新的段高亮显示if (i === pathSegments.length - 1 && pathSegments.length > 1) {segmentDiv.style.border = '2px solid #8b5cf6';segmentDiv.style.backgroundColor = '#faf5ff';}let segmentInfo = '';let segmentDescription = '';let segmentIcon = '';switch (segment.type) {case 'M':segmentInfo = `起点 (${segment.points[0].x.toFixed(1)}, ${segment.points[0].y.toFixed(1)})`;segmentDescription = '移动到起始位置';segmentIcon = '🚩';break;case 'L':segmentInfo = `直线到 (${segment.points[0].x.toFixed(1)}, ${segment.points[0].y.toFixed(1)})`;segmentDescription = '隐藏时:跳跃到此点(无连线)';segmentIcon = '📏';break;case 'Q':if (segment.cornerRadius) {segmentInfo = `圆角到 (${segment.points[1].x.toFixed(1)}, ${segment.points[1].y.toFixed(1)}) 半径${segment.cornerRadius}px`;segmentDescription = '圆角转弯效果';segmentIcon = '📐';} else {segmentInfo = `平滑到 控制点(${segment.points[0].x.toFixed(1)}, ${segment.points[0].y.toFixed(1)}) 终点(${segment.points[1].x.toFixed(1)}, ${segment.points[1].y.toFixed(1)})`;segmentDescription = '自动生成的平滑曲线';segmentIcon = '🌊';}break;}segmentDiv.innerHTML = `<div class="segment-header"><div class="segment-type">${segmentIcon} ${segment.type} - 段 ${i + 1} ${i === pathSegments.length - 1 ? '<span style="color: #8b5cf6; font-size: 10px;">最新</span>' : ''}</div><button class="delete-segment" onclick="removeSegment(${i})">删除</button></div><div style="font-size: 12px; color: #6b7280; margin-bottom: 4px;">${segmentInfo}</div><div style="font-size: 11px; color: #9ca3af; margin-bottom: 8px; font-style: italic;">${segmentDescription}</div><div class="checkbox-group"><input type="checkbox" id="visible_${i}" ${segment.visible ? 'checked' : ''} onchange="toggleSegmentVisibility(${i})"><label for="visible_${i}">显示连接线</label></div><div class="checkbox-group"><input type="checkbox" id="animVisible_${i}" ${segment.animationVisible !== false ? 'checked' : ''} onchange="toggleAnimationVisibility(${i})"><label for="animVisible_${i}">动画经过此段</label></div>`;segmentsList.appendChild(segmentDiv);}}// 切换路径段可见性function toggleSegmentVisibility(index) {pathSegments[index].visible = document.getElementById(`visible_${index}`).checked;updatePreview();updatePathPreview();}// 切换动画可见性function toggleAnimationVisibility(index) {pathSegments[index].animationVisible = document.getElementById(`animVisible_${index}`).checked;updatePreview();updatePathPreview();}// 预览相关变量let animationFrameId = null;let animationStartTime = null;let previewAnimationEnabled = false;// 更新预览function updatePreview() {if (!ctx) return;drawStaticPreview();// 如果启用了动画预览,开始动画循环if (previewAnimationEnabled && pathSegments.length > 1) {startAnimationPreview();} else {stopAnimationPreview();}}// 绘制静态预览function drawStaticPreview() {ctx.clearRect(0, 0, canvas.width, canvas.height);if (pathSegments.length === 0) return;const color = document.getElementById('animationColor').value;const thickness = document.getElementById('lineThickness').value;// 绘制可见的路径段ctx.strokeStyle = color;ctx.lineWidth = thickness;ctx.lineCap = 'round';ctx.lineJoin = 'round';let currentPos = null;let allPoints = []; // 收集所有点用于动画路径pathSegments.forEach((segment, index) => {const points = segment.points.map(p => ({x: (p.x / 100) * canvas.width,y: (p.y / 100) * canvas.height}));// 收集动画路径点if (segment.type === 'M') {allPoints.push(points[0]);currentPos = points[0];} else {allPoints.push(points[points.length - 1]);}// 只绘制可见的路径段if (!segment.visible) {// 更新当前位置但不绘制if (segment.type !== 'M') {currentPos = points[points.length - 1];}return;}switch (segment.type) {case 'M':currentPos = points[0];// 绘制起点ctx.fillStyle = '#10b981';ctx.beginPath();ctx.arc(points[0].x, points[0].y, 8, 0, 2 * Math.PI);ctx.fill();// 添加标签ctx.fillStyle = 'white';ctx.font = 'bold 10px sans-serif';ctx.textAlign = 'center';ctx.textBaseline = 'middle';ctx.fillText('S', points[0].x, points[0].y);break;case 'L':if (currentPos) {ctx.beginPath();ctx.moveTo(currentPos.x, currentPos.y);ctx.lineTo(points[0].x, points[0].y);ctx.stroke();}currentPos = points[0];break;case 'Q':if (currentPos) {ctx.beginPath();ctx.moveTo(currentPos.x, currentPos.y);ctx.quadraticCurveTo(points[0].x, points[0].y, points[1].x, points[1].y);ctx.stroke();// 绘制控制点ctx.fillStyle = '#f59e0b';ctx.beginPath();ctx.arc(points[0].x, points[0].y, 6, 0, 2 * Math.PI);ctx.fill();ctx.fillStyle = 'white';ctx.font = 'bold 8px sans-serif';ctx.textAlign = 'center';ctx.textBaseline = 'middle';ctx.fillText('C', points[0].x, points[0].y);}currentPos = points[1];break;case 'A':// 圆弧绘制(简化版本)if (currentPos) {ctx.beginPath();ctx.moveTo(currentPos.x, currentPos.y);// 使用椭圆弧近似const centerX = (currentPos.x + points[0].x) / 2;const centerY = (currentPos.y + points[0].y) / 2;const radius = Math.min(segment.params.rx, segment.params.ry) * 2;ctx.arc(centerX, centerY, radius, 0, Math.PI);ctx.stroke();}currentPos = points[0];break;}// 绘制端点if (segment.type !== 'M') {ctx.fillStyle = index === pathSegments.length - 1 ? '#ef4444' : '#3b82f6';ctx.beginPath();ctx.arc(currentPos.x, currentPos.y, 6, 0, 2 * Math.PI);ctx.fill();// 标号ctx.fillStyle = 'white';ctx.font = 'bold 10px sans-serif';ctx.textAlign = 'center';ctx.textBaseline = 'middle';ctx.fillText(index.toString(), currentPos.x, currentPos.y);}});// 绘制隐藏段的点(用虚线表示跳跃)let lastVisiblePos = null;pathSegments.forEach((segment, index) => {const points = segment.points.map(p => ({x: (p.x / 100) * canvas.width,y: (p.y / 100) * canvas.height}));if (segment.visible) {lastVisiblePos = segment.type === 'M' ? points[0] : points[points.length - 1];} else if (segment.type !== 'M' && lastVisiblePos) {// 绘制跳跃虚线const targetPos = points[points.length - 1];ctx.strokeStyle = '#9ca3af';ctx.lineWidth = 1;ctx.setLineDash([5, 5]);ctx.beginPath();ctx.moveTo(lastVisiblePos.x, lastVisiblePos.y);ctx.lineTo(targetPos.x, targetPos.y);ctx.stroke();ctx.setLineDash([]); // 重置虚线// 绘制跳跃目标点ctx.fillStyle = '#9ca3af';ctx.beginPath();ctx.arc(targetPos.x, targetPos.y, 5, 0, 2 * Math.PI);ctx.fill();ctx.fillStyle = 'white';ctx.font = 'bold 8px sans-serif';ctx.textAlign = 'center';ctx.textBaseline = 'middle';ctx.fillText('J', targetPos.x, targetPos.y); // J for JumplastVisiblePos = targetPos;}});// 绘制临时点(曲线模式)if (tempPoint) {const tempX = (tempPoint.x / 100) * canvas.width;const tempY = (tempPoint.y / 100) * canvas.height;ctx.fillStyle = '#f59e0b';ctx.beginPath();ctx.arc(tempX, tempY, 8, 0, 2 * Math.PI);ctx.fill();ctx.fillStyle = 'white';ctx.font = 'bold 10px sans-serif';ctx.textAlign = 'center';ctx.textBaseline = 'middle';ctx.fillText('?', tempX, tempY);}}// 开始动画预览function startAnimationPreview() {if (animationFrameId) return; // 已经在运行animationStartTime = Date.now();animatePreview();}// 停止动画预览function stopAnimationPreview() {if (animationFrameId) {cancelAnimationFrame(animationFrameId);animationFrameId = null;}}// 动画循环function animatePreview() {drawStaticPreview();if (!previewAnimationEnabled) return;const currentTime = Date.now();const elapsed = (currentTime - animationStartTime) / 1000; // 秒const speed = parseFloat(document.getElementById('animationSpeed').value) || 3;const progress = (elapsed % speed) / speed; // 0-1 的进度// 计算动画路径和可见性const animationPoints = [];const segmentVisibility = []; // 记录每段的动画可见性pathSegments.forEach((segment, index) => {if (segment.type === 'M') {animationPoints.push(segment.points[0]);segmentVisibility.push(segment.animationVisible !== false);} else {animationPoints.push(segment.points[segment.points.length - 1]);segmentVisibility.push(segment.animationVisible !== false);}});if (animationPoints.length >= 2) {// 计算当前动画点位置const totalSegments = animationPoints.length - 1;const currentSegmentFloat = progress * totalSegments;const currentSegment = Math.floor(currentSegmentFloat);const segmentProgress = currentSegmentFloat - currentSegment;if (currentSegment < totalSegments) {// 检查当前段的动画是否可见const currentSegmentVisible = segmentVisibility[currentSegment + 1]; // +1 因为第0个是起点if (currentSegmentVisible) {const startPoint = animationPoints[currentSegment];const endPoint = animationPoints[currentSegment + 1];const x = startPoint.x + (endPoint.x - startPoint.x) * segmentProgress;const y = startPoint.y + (endPoint.y - startPoint.y) * segmentProgress;const canvasX = (x / 100) * canvas.width;const canvasY = (y / 100) * canvas.height;// 绘制动画点const color = document.getElementById('animationColor').value;ctx.fillStyle = color;ctx.beginPath();ctx.arc(canvasX, canvasY, 8, 0, 2 * Math.PI);ctx.fill();// 添加发光效果ctx.shadowColor = color;ctx.shadowBlur = 15;ctx.beginPath();ctx.arc(canvasX, canvasY, 4, 0, 2 * Math.PI);ctx.fill();ctx.shadowBlur = 0;// 添加标识ctx.fillStyle = 'white';ctx.font = 'bold 10px sans-serif';ctx.textAlign = 'center';ctx.textBaseline = 'middle';ctx.fillText('●', canvasX, canvasY);}// 如果当前段不可见,动画点就完全隐藏,不绘制任何内容}}animationFrameId = requestAnimationFrame(animatePreview);}// 切换动画预览function toggleAnimationPreview() {previewAnimationEnabled = !previewAnimationEnabled;const btn = document.getElementById('previewToggle');if (previewAnimationEnabled) {btn.textContent = '⏸️ 停止预览';btn.style.background = '#ef4444';startAnimationPreview();} else {btn.textContent = '▶️ 动画预览';btn.style.background = '#10b981';stopAnimationPreview();drawStaticPreview(); // 重绘静态预览}}// 更新路径预览function updatePathPreview() {const preview = document.getElementById('pathPreview');if (pathSegments.length === 0) {preview.textContent = '暂无路径数据';return;}let visiblePath = ''; // 显示的路径(用于绘制)let animationPath = ''; // 动画路径(包含跳跃点)pathSegments.forEach(segment => {// 动画路径:包含所有点位置(用于动画跟随)switch (segment.type) {case 'M':animationPath += `M ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} `;if (segment.visible) {visiblePath += `M ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} `;}break;case 'L':// 动画路径:总是包含目标点animationPath += `L ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} `;// 可见路径:只有当显示连接线时才添加if (segment.visible) {visiblePath += `L ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} `;} else {// 隐藏连接线时,添加移动命令(跳跃)visiblePath += `M ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} `;}break;case 'Q':animationPath += `Q ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} ${segment.points[1].x.toFixed(1)} ${segment.points[1].y.toFixed(1)} `;if (segment.visible) {visiblePath += `Q ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} ${segment.points[1].x.toFixed(1)} ${segment.points[1].y.toFixed(1)} `;} else {visiblePath += `M ${segment.points[1].x.toFixed(1)} ${segment.points[1].y.toFixed(1)} `;}break;case 'A':animationPath += `A ${segment.params.rx} ${segment.params.ry} ${segment.params.rotation} ${segment.params.large} ${segment.params.sweep} ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} `;if (segment.visible) {visiblePath += `A ${segment.params.rx} ${segment.params.ry} ${segment.params.rotation} ${segment.params.large} ${segment.params.sweep} ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} `;} else {visiblePath += `M ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} `;}break;}});preview.innerHTML = `<div><strong>显示路径:</strong> ${visiblePath.trim()}</div><div style="margin-top: 8px;"><strong>动画路径:</strong> ${animationPath.trim()}</div><div style="margin-top: 8px; font-size: 11px; color: #6b7280;">💡 <strong>显示连接线</strong>:控制线条可见性<br>💡 <strong>动画经过此段</strong>:控制动画点是否在此段显示</div>`;}// 控制函数function updateSpeedDisplay() {const speed = document.getElementById('animationSpeed').value;document.getElementById('speedDisplay').textContent = speed + 's';}function updateThicknessDisplay() {const thickness = document.getElementById('lineThickness').value;document.getElementById('thicknessDisplay').textContent = thickness + 'px';}function removeSegment(index) {pathSegments.splice(index, 1);updateSegmentsList();updatePreview();updatePathPreview();}function clearAllSegments() {pathSegments = [];tempPoint = null;updateSegmentsList();updatePreview();updatePathPreview();}function undoLastSegment() {if (tempPoint) {tempPoint = null;} else if (pathSegments.length > 0) {pathSegments.pop();}updateSegmentsList();updatePreview();updatePathPreview();}function addSamplePath() {pathSegments = [{ type: 'M', points: [{x: 20, y: 20}], visible: true, animationVisible: true },{ type: 'L', points: [{x: 50, y: 20}], visible: true, animationVisible: true },{ type: 'Q', points: [{x: 65, y: 35}, {x: 80, y: 50}], visible: true, animationVisible: true }, // 平滑转弯{ type: 'L', points: [{x: 80, y: 80}], visible: false, animationVisible: false }, // 隐藏段{ type: 'Q', points: [{x: 60, y: 85}, {x: 40, y: 80}], visible: true, animationVisible: true } // 圆角结束];updateSegmentsList();updatePreview();updatePathPreview();}function generateAdvancedCode() {if (pathSegments.length === 0) {alert('请至少添加一个路径段!');return;}const color = document.getElementById('animationColor').value;const speed = document.getElementById('animationSpeed').value;const thickness = document.getElementById('lineThickness').value;// 生成显示路径和动画路径let visiblePath = ''; // 用于显示线条let animationPath = ''; // 用于动画跟随(包含跳跃)pathSegments.forEach(segment => {// 动画路径:包含所有点位置switch (segment.type) {case 'M':animationPath += `M ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} `;if (segment.visible) {visiblePath += `M ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} `;}break;case 'L':animationPath += `L ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} `;if (segment.visible) {visiblePath += `L ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} `;} else {visiblePath += `M ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} `;}break;case 'Q':animationPath += `Q ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} ${segment.points[1].x.toFixed(1)} ${segment.points[1].y.toFixed(1)} `;if (segment.visible) {visiblePath += `Q ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} ${segment.points[1].x.toFixed(1)} ${segment.points[1].y.toFixed(1)} `;} else {visiblePath += `M ${segment.points[1].x.toFixed(1)} ${segment.points[1].y.toFixed(1)} `;}break;case 'A':animationPath += `A ${segment.params.rx} ${segment.params.ry} ${segment.params.rotation} ${segment.params.large} ${segment.params.sweep} ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} `;if (segment.visible) {visiblePath += `A ${segment.params.rx} ${segment.params.ry} ${segment.params.rotation} ${segment.params.large} ${segment.params.sweep} ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} `;} else {visiblePath += `M ${segment.points[0].x.toFixed(1)} ${segment.points[0].y.toFixed(1)} `;}break;}});const reactCode = `// 高级路径动画组件 - 支持跳跃和断续显示
const visiblePath = "${visiblePath.trim()}"; // 显示的线条路径
const animationPath = "${animationPath.trim()}"; // 动画跟随路径<svg width="100%" height="100%" viewBox="0 0 100 100" className="absolute inset-0"><defs><linearGradient id="advancedFlowGradient" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stopColor="rgba(255,255,255,0)" /><stop offset="30%" stopColor="${color}" stopOpacity="0.6" /><stop offset="70%" stopColor="${color}" stopOpacity="1" /><stop offset="100%" stopColor="rgba(255,255,255,0)" /></linearGradient></defs>{/* 显示路径 - 只显示可见的连接线 */}<pathd={visiblePath}stroke="url(#advancedFlowGradient)"strokeWidth="${thickness}"fill="none"strokeDasharray="25 100"strokeLinecap="round"><animateattributeName="stroke-dashoffset"from="125"to="0"dur="${speed}s"repeatCount="indefinite"/></path>{/* 流动点1 - 跟随完整动画路径(包含跳跃) */}<circle r="4" fill="${color}" opacity="0.9"><animateMotiondur="${speed}s"repeatCount="indefinite"path={animationPath}/><animateattributeName="opacity"values="0;1;1;0.5;0"dur="${speed}s"repeatCount="indefinite"/></circle>{/* 流动点2 - 延迟效果 */}<circle r="3" fill="${color}" opacity="0.7"><animateMotiondur="${speed}s"repeatCount="indefinite"path={animationPath}begin="1.5s"/><animateattributeName="opacity"values="0;0.7;0.7;0.3;0"dur="${speed}s"repeatCount="indefinite"begin="1.5s"/></circle>{/* 流动点3 - 更长延迟 */}<circle r="2" fill="${color}" opacity="0.5"><animateMotiondur="${speed}s"repeatCount="indefinite"path={animationPath}begin="3s"/><animateattributeName="opacity"values="0;0.5;0.5;0.2;0"dur="${speed}s"repeatCount="indefinite"begin="3s"/></circle>
</svg>// 💡 说明:
// - visiblePath: 控制显示哪些连接线
// - animationPath: 控制动画点的完整运动轨迹(包含跳跃到隐藏点)
// - 隐藏连接线时,动画点会瞬间跳跃到目标位置// 路径段配置
const pathSegments = ${JSON.stringify(pathSegments, null, 2)};`;showCodeModal(reactCode);}function showCodeModal(code) {const modal = document.createElement('div');modal.style.cssText = `position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 20px;`;modal.innerHTML = `<div style="background: white; padding: 30px; border-radius: 12px; max-width: 95%; max-height: 95%; overflow: auto;"><div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;"><h3 style="margin: 0;">🎉 高级路径动画代码</h3><button onclick="this.parentElement.parentElement.parentElement.remove()" style="background: #6b7280; width: auto; margin: 0; padding: 5px 10px;">✕</button></div><pre style="background: #f3f4f6; padding: 20px; border-radius: 8px; overflow: auto; white-space: pre-wrap; font-size: 12px; line-height: 1.4; max-height: 70vh;">${code}</pre><div style="text-align: center; margin-top: 20px;"><button onclick="copyToClipboard(\`${code.replace(/`/g, '\\`')}\`)" style="margin-right: 10px; width: auto;">📋 复制代码</button><button onclick="downloadCode(\`${code.replace(/`/g, '\\`')}\`)" style="background: #10b981; width: auto;">💾 下载文件</button></div></div>`;document.body.appendChild(modal);}function copyToClipboard(text) {navigator.clipboard.writeText(text).then(() => {alert('✅ 代码已复制到剪贴板!');}).catch(() => {const textarea = document.createElement('textarea');textarea.value = text;document.body.appendChild(textarea);textarea.select();document.execCommand('copy');document.body.removeChild(textarea);alert('✅ 代码已复制到剪贴板!');});}function downloadCode(code) {const blob = new Blob([code], { type: 'text/plain' });const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = 'advanced-flow-animation.tsx';document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);}window.addEventListener('resize', function() {if (imageElement && canvas) {stopAnimationPreview();setTimeout(() => {setupCanvas();if (previewAnimationEnabled) {startAnimationPreview();}}, 100);}});// 页面卸载时清理动画window.addEventListener('beforeunload', function() {stopAnimationPreview();});init();</script>
</body>
</html>