@@ -697,6 +697,159 @@ function setupResponsive() {
697697 handleResize ();
698698}
699699
700+ function getIdealDashoffsetDelta (pathElement , options = {}) {
701+ const {
702+ direction = - 1 ,
703+ mode = " oneLapNearest" ,
704+ dasharray = null
705+ } = options;
706+
707+ if (! pathElement || typeof pathElement .getTotalLength !== " function" ) {
708+ if (debug .value ) {
709+ console .warn (
710+ " VueUiDag @getIdealDashoffsetDelta: invalid path element" ,
711+ pathElement
712+ );
713+ }
714+ return 0 ;
715+ }
716+
717+ const pathLength = pathElement .getTotalLength ();
718+
719+ const dasharrayValue =
720+ dasharray ??
721+ pathElement .getAttribute (" stroke-dasharray" ) ??
722+ (typeof getComputedStyle === " function"
723+ ? getComputedStyle (pathElement).strokeDasharray
724+ : " " );
725+
726+ const patternLength = sumDasharray (dasharrayValue);
727+
728+ if (! Number .isFinite (patternLength) || patternLength <= 0 ) {
729+ return direction * pathLength;
730+ }
731+
732+ const nearestMultiple = Math .max (1 , Math .round (pathLength / patternLength));
733+ const ceilMultiple = Math .max (1 , Math .ceil (pathLength / patternLength));
734+ const floorMultiple = Math .max (1 , Math .floor (pathLength / patternLength));
735+
736+ let delta;
737+ if (mode === " pattern" ) delta = patternLength;
738+ else if (mode === " oneLapCeil" ) delta = ceilMultiple * patternLength;
739+ else if (mode === " oneLapFloor" ) delta = floorMultiple * patternLength;
740+ else delta = nearestMultiple * patternLength;
741+
742+ return direction * delta;
743+
744+ function sumDasharray (value ) {
745+ if (! value || value === " none" ) return NaN ;
746+
747+ const numbers = String (value)
748+ .replace (/ ,/ g , " " )
749+ .trim ()
750+ .split (/ \s + / )
751+ .map ((token ) => Number .parseFloat (token))
752+ .filter ((number ) => Number .isFinite (number));
753+
754+ if (! numbers .length ) return NaN ;
755+
756+ const sum = numbers .reduce ((accumulator , current ) => accumulator + current, 0 );
757+ return numbers .length % 2 === 1 ? sum * 2 : sum;
758+ }
759+ }
760+
761+ const edgePathElementByIdentifier = ref (new Map ());
762+ const edgeAnimationByIdentifier = ref (new Map ());
763+
764+ function registerEdgePathElement (edgeIdentifier ) {
765+ return function register (element ) {
766+ if (element) {
767+ edgePathElementByIdentifier .value .set (edgeIdentifier, element);
768+ } else {
769+ edgePathElementByIdentifier .value .delete (edgeIdentifier);
770+ }
771+ };
772+ }
773+
774+ function stopAllEdgeAnimations () {
775+ edgeAnimationByIdentifier .value .forEach ((animationHandle ) => {
776+ try {
777+ animationHandle .cancel ();
778+ } catch {
779+ // whatever
780+ }
781+ });
782+ edgeAnimationByIdentifier .value .clear ();
783+ }
784+
785+ function startEdgeAnimations () {
786+ stopAllEdgeAnimations ();
787+
788+ const edges = layoutData .value ? .edges ?? [];
789+ if (! edges .length ) return ;
790+
791+ const animationDefaults = FINAL_CONFIG .value .style .chart .edges .animations ;
792+
793+ const referenceDistance =
794+ Number (animationDefaults .referenceDistance ) > 0
795+ ? Number (animationDefaults .referenceDistance )
796+ : 24 ;
797+
798+ edges .forEach ((edge ) => {
799+ const isAnimated = !! edge? .original ? .animated ;
800+ if (! isAnimated) return ;
801+
802+ const pathElement = edgePathElementByIdentifier .value .get (edge .id );
803+ if (! pathElement) return ;
804+
805+ const dasharrayValue = edge? .original ? .dasharray ?? animationDefaults .dasharray ;
806+
807+ pathElement .style .strokeDasharray = String (dasharrayValue);
808+ pathElement .style .strokeDashoffset = " 0" ;
809+
810+ const hasEdgeDirection = ! [undefined , null ].includes (edge? .original ? .animationDirection );
811+ const hasConfigDirection = ! [undefined , null ].includes (animationDefaults .animationDirection );
812+
813+ const direction = hasEdgeDirection
814+ ? Number (edge .original .animationDirection )
815+ : hasConfigDirection
816+ ? Number (animationDefaults .animationDirection )
817+ : - 1 ;
818+
819+ const dashoffsetDelta = getIdealDashoffsetDelta (pathElement, {
820+ direction,
821+ mode: " oneLapNearest" ,
822+ dasharray: String (dasharrayValue)
823+ });
824+
825+ // Duration is derived from speed (referenceDistance / durationReferenceMs)
826+ const durationReferenceMsRaw = edge? .original ? .animationDurationMs ?? animationDefaults .animationDurationMs ?? 1000 ;
827+
828+ const durationReferenceMs = Number (durationReferenceMsRaw);
829+ const hasDurationReference = Number .isFinite (durationReferenceMs) && durationReferenceMs > 0 ;
830+ const speedUnitsPerMs = hasDurationReference ? referenceDistance / durationReferenceMs : referenceDistance / 1000 ;
831+ const durationMilliseconds = Math .max (1 , Math .round (Math .abs (dashoffsetDelta) / Math .max (1e-9 , speedUnitsPerMs)));
832+
833+ const animationHandle = pathElement .animate (
834+ [{ strokeDashoffset: 0 }, { strokeDashoffset: dashoffsetDelta }],
835+ { duration: durationMilliseconds, iterations: Infinity , easing: " linear" }
836+ );
837+
838+ edgeAnimationByIdentifier .value .set (edge .id , animationHandle);
839+ });
840+ }
841+
842+ watch (() => layoutData .value && layoutData .value .edges , async () => {
843+ await nextTick ();
844+ startEdgeAnimations ();
845+ },
846+ { deep: true , immediate: true }
847+ );
848+
849+ onBeforeUnmount (() => {
850+ stopAllEdgeAnimations ();
851+ });
852+
700853async function getImage ({ scale = 2 } = {}) {
701854 if (! dagChart .value ) return
702855 const { width , height } = dagChart .value .getBoundingClientRect ()
@@ -712,6 +865,17 @@ async function getImage({ scale = 2} = {}) {
712865 }
713866}
714867
868+ const backgroundPatternGridSpacing = computed (() => {
869+ const nodeHeight = Number (FINAL_CONFIG .value .style .chart .layout .nodeHeight );
870+ return Number .isFinite (nodeHeight) && nodeHeight > 0
871+ ? nodeHeight / FINAL_CONFIG .value .style .chart .backgroundPattern .spacingRatio
872+ : 12 ;
873+ });
874+
875+ const backgroundPatternDotRadius = computed (() => {
876+ return backgroundPatternGridSpacing .value * (FINAL_CONFIG .value .style .chart .backgroundPattern .dotRadiusRatio / 100 );
877+ });
878+
715879function getData () {
716880 return layoutData .value ;
717881}
@@ -731,7 +895,6 @@ defineExpose({
731895})
732896< / script>
733897
734-
735898< template>
736899 < div
737900 : class = " `vue-data-ui-component vue-ui-dag ${isFullscreen ? 'vue-data-ui-wrapper-fullscreen' : ''} ${FINAL_CONFIG.responsive ? 'vue-ui-dag-responsive' : ''}`"
@@ -881,6 +1044,41 @@ defineExpose({
8811044 >
8821045 < PackageVersion / >
8831046
1047+ < defs v- if = " FINAL_CONFIG.style.chart.backgroundPattern.show" >
1048+ < pattern
1049+ : id= " `dag_bg_pattern_${uid}`"
1050+ patternUnits= " userSpaceOnUse"
1051+ : width= " backgroundPatternGridSpacing"
1052+ : height= " backgroundPatternGridSpacing"
1053+ >
1054+ < slot name= " background-pattern" v- bind= " {
1055+ x: backgroundPatternGridSpacing / 2,
1056+ y: backgroundPatternGridSpacing / 2,
1057+ color: FINAL_CONFIG.style.chart.backgroundPattern.dotColor
1058+ }" >
1059+ < circle
1060+ : cx= " backgroundPatternGridSpacing / 2"
1061+ : cy= " backgroundPatternGridSpacing / 2"
1062+ : r= " backgroundPatternDotRadius"
1063+ : fill= " FINAL_CONFIG.style.chart.backgroundPattern.dotColor"
1064+ / >
1065+ < / slot>
1066+ < / pattern>
1067+ < / defs>
1068+
1069+ < rect
1070+ v- if = " FINAL_CONFIG.style.chart.backgroundPattern.show"
1071+ : x= " panZoomViewBox?.x ?? 0"
1072+ : y= " panZoomViewBox?.y ?? 0"
1073+ : width= " panZoomViewBox?.width ?? 0"
1074+ : height= " panZoomViewBox?.height ?? 0"
1075+ : fill= " `url(#dag_bg_pattern_${uid})`"
1076+ : style= " {
1077+ pointerEvents: 'none',
1078+ opacity: FINAL_CONFIG.style.chart.backgroundPattern.opacity
1079+ }"
1080+ / >
1081+
8841082 <!-- Arrow marker . Hidden when arrowShape is " undirected" . -->
8851083 < defs v- if = " layoutData.arrowShape !== 'undirected'" >
8861084 < template v- for = " color in edgeColors" : key= " color" >
@@ -919,12 +1117,13 @@ defineExpose({
9191117 < template v- for = " edge in layoutData.edges" : key= " edge.id" >
9201118 < path
9211119 data- cy- edge
922- : d= " edge.pathData"
923- fill= " none"
1120+ : ref= " registerEdgePathElement(edge.id)"
1121+ : d= " edge.pathData"
1122+ fill= " none"
9241123 : stroke= " edge.original.color ?? FINAL_CONFIG.style.chart.edges.stroke"
925- : stroke- width= " FINAL_CONFIG.style.chart.edges.strokeWidth * ((edge.from === highlightedNodeId || edge.id === tooltipEdge?.id) ? 2 : 1)"
926- stroke- linecap= " round"
927- stroke- linejoin= " round"
1124+ : stroke- width= " FINAL_CONFIG.style.chart.edges.strokeWidth * ((edge.from === highlightedNodeId || edge.id === tooltipEdge?.id) ? 2 : 1)"
1125+ stroke- linecap= " round"
1126+ stroke- linejoin= " round"
9281127 style= " pointer-events: none; transition: stroke-width 0.2s ease-in-out"
9291128 / >
9301129 < circle
0 commit comments