feat: add edge flow animation (dot traveling source→target)
- Add animated toggle per edge in EdgeModal (cyan switch, "Flow Animation") - SVG-native <animate> element for reliable cross-browser dot animation - Persist animated field: backend model, schemas (EdgeBase/EdgeUpdate/EdgeSave), DB migration - Include animated in App.tsx edgesToSave serialization so it survives save/reload - Add animated: bool to EdgeData TypeScript type
This commit is contained in:
@@ -111,6 +111,7 @@ export default function App() {
|
||||
speed: e.data?.speed ?? null,
|
||||
custom_color: e.data?.custom_color ?? null,
|
||||
path_style: e.data?.path_style ?? null,
|
||||
animated: e.data?.animated ?? false,
|
||||
// Normalize stub handle IDs: "top-t" / "bottom-t" are invisible target stubs;
|
||||
// map them back to their canonical source handle ID so reload works correctly.
|
||||
source_handle: e.sourceHandle === 'top-t' ? 'top' : e.sourceHandle === 'bottom-t' ? 'bottom' : (e.sourceHandle ?? null),
|
||||
|
||||
@@ -46,9 +46,32 @@ export function HomelableEdge({ id, sourceX, sourceY, targetX, targetY, sourcePo
|
||||
...(selected ? { stroke: theme.colors.edgeSelectedColor, filter: `drop-shadow(0 0 4px ${theme.colors.edgeSelectedColor}88)` } : {}),
|
||||
}
|
||||
|
||||
// Animated dot: slightly brighter + thicker than the base edge, travels source→target
|
||||
const dotColor = customColor ?? (edgeType === 'vlan' ? getVlanColor(data?.vlan_id as number | undefined) : edgeColors[edgeType as keyof typeof edgeColors] as string)
|
||||
const dotWidth = (style.strokeWidth as number ?? 2) + 1.5
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge id={id} path={edgePath} style={style} />
|
||||
{data?.animated && (
|
||||
<path
|
||||
d={edgePath}
|
||||
fill="none"
|
||||
stroke={dotColor}
|
||||
strokeWidth={dotWidth}
|
||||
strokeDasharray="8 10000"
|
||||
strokeLinecap="round"
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
from="0"
|
||||
to="-10000"
|
||||
dur="2.5s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</path>
|
||||
)}
|
||||
{data?.label && (
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
|
||||
@@ -25,6 +25,7 @@ export function EdgeModal({ open, onClose, onSubmit, onDelete, initial, title =
|
||||
const [vlanId, setVlanId] = useState(initial?.vlan_id?.toString() ?? '')
|
||||
const [customColor, setCustomColor] = useState<string | undefined>(initial?.custom_color)
|
||||
const [pathStyle, setPathStyle] = useState<EdgePathStyle>(initial?.path_style ?? 'bezier')
|
||||
const [animated, setAnimated] = useState(initial?.animated ?? false)
|
||||
|
||||
const effectiveColor = customColor ?? EDGE_DEFAULT_COLORS[type]
|
||||
|
||||
@@ -36,6 +37,7 @@ export function EdgeModal({ open, onClose, onSubmit, onDelete, initial, title =
|
||||
vlan_id: type === 'vlan' && vlanId ? parseInt(vlanId) : undefined,
|
||||
custom_color: customColor,
|
||||
path_style: pathStyle,
|
||||
animated: animated || undefined,
|
||||
})
|
||||
onClose()
|
||||
}
|
||||
@@ -113,6 +115,22 @@ export function EdgeModal({ open, onClose, onSubmit, onDelete, initial, title =
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs text-muted-foreground">Flow Animation</Label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAnimated((a) => !a)}
|
||||
className="relative w-9 h-5 rounded-full transition-colors focus:outline-none shrink-0"
|
||||
style={{ background: animated ? '#00d4ff' : '#30363d' }}
|
||||
aria-pressed={animated}
|
||||
>
|
||||
<span
|
||||
className="absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform"
|
||||
style={{ transform: animated ? 'translateX(16px)' : 'translateX(0)' }}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs text-muted-foreground">Color</Label>
|
||||
|
||||
@@ -115,3 +115,13 @@
|
||||
.font-mono {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
/* Edge flow animation — dot traveling from source to target */
|
||||
@keyframes flow-dot {
|
||||
from { stroke-dashoffset: 0; }
|
||||
to { stroke-dashoffset: -10000; }
|
||||
}
|
||||
.edge-flow-dot {
|
||||
animation: flow-dot 2.5s linear infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ export interface EdgeData extends Record<string, unknown> {
|
||||
speed?: string
|
||||
custom_color?: string
|
||||
path_style?: EdgePathStyle
|
||||
animated?: boolean
|
||||
}
|
||||
|
||||
export const NODE_TYPE_LABELS: Record<NodeType, string> = {
|
||||
|
||||
Reference in New Issue
Block a user