feat: Catmull-Rom smooth edge curves with gold gradient, ghost brush, and flow animation

This commit is contained in:
2026-06-12 12:42:34 +08:00
parent 215a8db829
commit 97ebe1c8ca

View File

@@ -184,33 +184,36 @@ onMounted(() => {
function edgePath(e: FlowEdge): string { function edgePath(e: FlowEdge): string {
if (e.points.length < 2) return '' if (e.points.length < 2) return ''
const r = 6 const pts = e.points.map(p => ({ h: p.x, v: p.y }))
let d = `M ${e.points[0].x} ${e.points[0].y}` return catmullRomPath(pts)
}
for (let i = 1; i < e.points.length - 1; i++) { function catmullRomPath(pts: { h: number; v: number }[]): string {
const prev = e.points[i - 1] if (pts.length < 2) return ''
const curr = e.points[i] let d = `M ${pts[0].h} ${pts[0].v}`
const next = e.points[i + 1]
const dx1 = curr.x - prev.x if (pts.length === 2) {
const dy1 = curr.y - prev.y d += ` L ${pts[1].h} ${pts[1].v}`
const len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1) || 1 return d
const dx2 = next.x - curr.x
const dy2 = next.y - curr.y
const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) || 1
const rx = Math.min(r, len1 / 2, len2 / 2)
const ax = curr.x - (dx1 / len1) * rx
const ay = curr.y - (dy1 / len1) * rx
const bx = curr.x + (dx2 / len2) * rx
const by = curr.y + (dy2 / len2) * rx
d += ` L ${ax} ${ay} Q ${curr.x} ${curr.y} ${bx} ${by}`
} }
const last = e.points[e.points.length - 1] for (let i = 0; i < pts.length - 1; i++) {
d += ` L ${last.x} ${last.y}` const p0 = pts[Math.max(0, i - 1)]
const p1 = pts[i]
const p2 = pts[i + 1]
const p3 = pts[Math.min(pts.length - 1, i + 2)]
const cp1x = p1.h + (p2.h - p0.h) / 6
const cp1y = p1.v + (p2.v - p0.v) / 6
const cp2x = p2.h - (p3.h - p1.h) / 6
const cp2y = p2.v - (p3.v - p1.v) / 6
if (i === 0) {
d += ` C ${cp1x} ${cp1y} ${cp2x} ${cp2y} ${p2.h} ${p2.v}`
} else {
d += ` S ${cp2x} ${cp2y} ${p2.h} ${p2.v}`
}
}
return d return d
} }
@@ -228,17 +231,59 @@ const svgH = computed(() => containerH.value)
:width="svgW" :width="svgW"
:height="svgH" :height="svgH"
> >
<defs>
<linearGradient id="edge-gold" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#8b6914"/>
<stop offset="50%" stop-color="#e0c060"/>
<stop offset="100%" stop-color="#8b6914"/>
</linearGradient>
<filter id="edge-glow">
<feGaussianBlur stdDeviation="1.2" result="b"/>
<feMerge>
<feMergeNode in="b"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<g <g
v-for="edge in edges" v-for="edge in edges"
:key="edge.from + '-' + edge.to" :key="edge.from + '-' + edge.to"
class="flow-edge-group"
> >
<path <path
v-if="edge.visited"
:d="edgePath(edge)" :d="edgePath(edge)"
fill="none" fill="none"
:stroke="edge.visited ? '#c9a84c' : '#333'" stroke="url(#edge-gold)"
:stroke-width="edge.visited ? 1.5 : 1" stroke-width="2.2"
:stroke-dasharray="edge.visited ? '' : '4 3'" filter="url(#edge-glow)"
opacity="0.35"
/>
<path
v-if="edge.visited"
:d="edgePath(edge)"
fill="none"
stroke="url(#edge-gold)"
stroke-width="1.6"
class="edge-main"
/>
<path
v-if="edge.visited"
:d="edgePath(edge)"
fill="none"
stroke="#e0c060"
stroke-width="0.8"
stroke-dasharray="3 20"
class="edge-flow"
opacity="0.7"
/>
<path
v-if="!edge.visited"
:d="edgePath(edge)"
fill="none"
stroke="#333"
stroke-width="1"
stroke-dasharray="5 4"
/> />
<polygon <polygon
v-if="edge.points.length >= 2" v-if="edge.points.length >= 2"
@@ -247,14 +292,15 @@ const svgH = computed(() => containerH.value)
const last = pts[pts.length - 1] const last = pts[pts.length - 1]
const prev = pts[pts.length - 2] const prev = pts[pts.length - 2]
const angle = Math.atan2(last.y - prev.y, last.x - prev.x) const angle = Math.atan2(last.y - prev.y, last.x - prev.x)
const s = 5 const s = 6
return [ return [
`${last.x + Math.cos(angle) * s} ${last.y + Math.sin(angle) * s}`, `${last.x + Math.cos(angle) * s} ${last.y + Math.sin(angle) * s}`,
`${last.x + Math.cos(angle + 2.5) * s} ${last.y + Math.sin(angle + 2.5) * s}`, `${last.x + Math.cos(angle + 2.5) * s} ${last.y + Math.sin(angle + 2.5) * s}`,
`${last.x + Math.cos(angle - 2.5) * s} ${last.y + Math.sin(angle - 2.5) * s}`, `${last.x + Math.cos(angle - 2.5) * s} ${last.y + Math.sin(angle - 2.5) * s}`,
].join(' ') ].join(' ')
})()" })()"
:fill="edge.visited ? '#c9a84c' : '#333'" :fill="edge.visited ? '#e0c060' : '#333'"
:filter="edge.visited ? 'url(#edge-glow)' : ''"
/> />
</g> </g>
</svg> </svg>
@@ -404,7 +450,16 @@ const svgH = computed(() => containerH.value)
color: #444; color: #444;
} }
.flow-edge-group { @keyframes flowDash {
opacity: 0.6; to { stroke-dashoffset: -23; }
}
.edge-main {
stroke-linecap: round;
}
.edge-flow {
stroke-linecap: round;
animation: flowDash 3s linear infinite;
} }
</style> </style>