Skip to content

Commit b6f957e

Browse files
committed
fix(layout): stabilize smart layout
1 parent ab9f541 commit b6f957e

2 files changed

Lines changed: 1249 additions & 872 deletions

File tree

src/layout/animation.ts

Lines changed: 163 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -6,149 +6,168 @@
66
*/
77
import type { GraphLike } from "../types";
88

9-
/**
10-
* 真正有效的平滑缩放函数
11-
* @param {Object} graph - G6 图形实例
12-
* @param {number} duration - 动画持续时间(毫秒)
13-
* @param {string} easing - 缓动函数类型
14-
*/
15-
export const smoothFitView = (
16-
graph: GraphLike,
17-
duration = 800,
18-
easing = 'easeOutCubic',
19-
) => {
20-
if (!graph || graph.destroyed) return;
21-
22-
try {
23-
const nodes = graph.getNodes();
24-
if (!nodes || nodes.length === 0) {
25-
graph.fitView(20);
26-
return;
27-
}
28-
29-
let minX = Infinity, maxX = -Infinity;
30-
let minY = Infinity, maxY = -Infinity;
31-
32-
nodes.forEach(node => {
33-
const bbox = node.getBBox();
34-
minX = Math.min(minX, bbox.minX);
35-
maxX = Math.max(maxX, bbox.maxX);
36-
minY = Math.min(minY, bbox.minY);
37-
maxY = Math.max(maxY, bbox.maxY);
38-
});
39-
40-
const contentWidth = maxX - minX;
41-
const contentHeight = maxY - minY;
42-
const contentCenterX = (minX + maxX) / 2;
43-
const contentCenterY = (minY + maxY) / 2;
44-
45-
if (contentWidth === 0 || contentHeight === 0) {
46-
graph.fitView(20);
47-
return;
48-
}
49-
50-
const graphWidth = graph.get('width');
51-
const graphHeight = graph.get('height');
52-
const padding = 40;
53-
54-
const scaleX = (graphWidth - padding * 2) / contentWidth;
55-
const scaleY = (graphHeight - padding * 2) / contentHeight;
56-
const targetZoom = Math.min(scaleX, scaleY);
57-
58-
const targetCenterX = graphWidth / 2 - contentCenterX * targetZoom;
59-
const targetCenterY = graphHeight / 2 - contentCenterY * targetZoom;
60-
61-
const currentZoom = graph.getZoom();
62-
const currentMatrix = graph.get('group').getMatrix();
63-
const currentCenterX = currentMatrix ? currentMatrix[6] : 0;
64-
const currentCenterY = currentMatrix ? currentMatrix[7] : 0;
65-
66-
const startTime = performance.now();
67-
68-
const animate = (currentTime: number) => {
69-
if (!graph || graph.destroyed) return;
70-
71-
const elapsed = currentTime - startTime;
72-
let progress = Math.min(elapsed / duration, 1);
73-
74-
if (easing === 'easeOutQuart') {
75-
progress = 1 - Math.pow(1 - progress, 4);
76-
} else {
77-
progress = 1 - Math.pow(1 - progress, 3);
78-
}
79-
80-
const frameZoom = currentZoom + (targetZoom - currentZoom) * progress;
81-
const frameCenterX = currentCenterX + (targetCenterX - currentCenterX) * progress;
82-
const frameCenterY = currentCenterY + (targetCenterY - currentCenterY) * progress;
83-
84-
const groupMatrix = [frameZoom, 0, 0, 0, frameZoom, 0, frameCenterX, frameCenterY, 1];
85-
graph.get('group').setMatrix(groupMatrix);
86-
graph.paint();
87-
88-
if (progress < 1) {
89-
requestAnimationFrame(animate);
90-
}
91-
};
92-
93-
requestAnimationFrame(animate);
94-
95-
} catch (error) {
96-
console.warn('Smooth fit view failed, falling back to instant fit:', error);
97-
graph.fitView(20);
98-
}
99-
};
9+
const nodeAnimationTokens = new WeakMap<GraphLike, number>();
10+
const fitViewTokens = new WeakMap<GraphLike, number>();
11+
12+
const nextToken = (tokens: WeakMap<GraphLike, number>, graph: GraphLike) => {
13+
const token = (tokens.get(graph) ?? 0) + 1;
14+
tokens.set(graph, token);
15+
return token;
16+
};
17+
18+
const isCurrentToken = (tokens: WeakMap<GraphLike, number>, graph: GraphLike, token: number) =>
19+
tokens.get(graph) === token;
10020

101-
/**
102-
* 基于目标坐标的节点平滑移动
103-
* @param {Object} graph - G6 图形实例
104-
* @param {Map} targets - 目标位置映射 (nodeId -> {x, y})
105-
* @param {number} duration - 动画持续时间(毫秒)
106-
* @param {Function} onFinish - 动画完成回调
107-
*/
108-
export const animateNodesToTargets = (
109-
graph: GraphLike,
110-
targets: Map<string, { x?: number; y?: number }>,
111-
duration = 800,
112-
onFinish?: () => void,
113-
) => {
114-
if (!graph || graph.destroyed || !targets?.size) {
115-
if (onFinish) onFinish();
116-
return;
117-
}
118-
119-
const startPositions = new Map<string, { x?: number; y?: number }>();
120-
graph.getNodes().forEach(node => {
121-
const model = node.getModel();
122-
startPositions.set(model.id, { x: model.x, y: model.y });
123-
});
124-
125-
const startTime = performance.now();
126-
graph.setAutoPaint(false);
127-
128-
const step = (currentTime: number) => {
129-
if (!graph || graph.destroyed) return;
130-
const elapsed = currentTime - startTime;
131-
const rawProgress = Math.min(elapsed / duration, 1);
132-
const progress = 1 - Math.pow(1 - rawProgress, 3);
133-
134-
targets.forEach((target, id) => {
135-
const node = graph.findById(id);
136-
if (!node) return;
137-
const start = startPositions.get(id) || target;
138-
const x = (start.x || 0) + ((target.x || 0) - (start.x || 0)) * progress;
139-
const y = (start.y || 0) + ((target.y || 0) - (start.y || 0)) * progress;
140-
graph.updateItem(node, { x, y });
141-
});
142-
143-
graph.paint();
144-
145-
if (rawProgress < 1) {
146-
requestAnimationFrame(step);
147-
} else {
148-
graph.setAutoPaint(true);
149-
if (onFinish) onFinish();
150-
}
151-
};
152-
153-
requestAnimationFrame(step);
21+
/**
22+
* 真正有效的平滑缩放函数
23+
* @param {Object} graph - G6 图形实例
24+
* @param {number} duration - 动画持续时间(毫秒)
25+
* @param {string} easing - 缓动函数类型
26+
*/
27+
export const smoothFitView = (graph: GraphLike, duration = 800, easing = "easeOutCubic") => {
28+
if (!graph || graph.destroyed) return;
29+
30+
const token = nextToken(fitViewTokens, graph);
31+
32+
try {
33+
const nodes = graph.getNodes();
34+
if (!nodes || nodes.length === 0) {
35+
graph.fitView(20);
36+
return;
37+
}
38+
39+
let minX = Infinity,
40+
maxX = -Infinity;
41+
let minY = Infinity,
42+
maxY = -Infinity;
43+
44+
nodes.forEach((node) => {
45+
const bbox = node.getBBox();
46+
minX = Math.min(minX, bbox.minX);
47+
maxX = Math.max(maxX, bbox.maxX);
48+
minY = Math.min(minY, bbox.minY);
49+
maxY = Math.max(maxY, bbox.maxY);
50+
});
51+
52+
const contentWidth = maxX - minX;
53+
const contentHeight = maxY - minY;
54+
const contentCenterX = (minX + maxX) / 2;
55+
const contentCenterY = (minY + maxY) / 2;
56+
57+
if (contentWidth === 0 || contentHeight === 0) {
58+
graph.fitView(20);
59+
return;
60+
}
61+
62+
const graphWidth = graph.get("width");
63+
const graphHeight = graph.get("height");
64+
const padding = 40;
65+
66+
const scaleX = (graphWidth - padding * 2) / contentWidth;
67+
const scaleY = (graphHeight - padding * 2) / contentHeight;
68+
const targetZoom = Math.min(scaleX, scaleY);
69+
70+
const targetCenterX = graphWidth / 2 - contentCenterX * targetZoom;
71+
const targetCenterY = graphHeight / 2 - contentCenterY * targetZoom;
72+
73+
const currentZoom = graph.getZoom();
74+
const currentMatrix = graph.get("group").getMatrix();
75+
const currentCenterX = currentMatrix ? currentMatrix[6] : 0;
76+
const currentCenterY = currentMatrix ? currentMatrix[7] : 0;
77+
78+
const startTime = performance.now();
79+
80+
const animate = (currentTime: number) => {
81+
if (!graph || graph.destroyed || !isCurrentToken(fitViewTokens, graph, token)) return;
82+
83+
const elapsed = currentTime - startTime;
84+
let progress = Math.min(elapsed / duration, 1);
85+
86+
if (easing === "easeOutQuart") {
87+
progress = 1 - Math.pow(1 - progress, 4);
88+
} else {
89+
progress = 1 - Math.pow(1 - progress, 3);
90+
}
91+
92+
const frameZoom = currentZoom + (targetZoom - currentZoom) * progress;
93+
const frameCenterX = currentCenterX + (targetCenterX - currentCenterX) * progress;
94+
const frameCenterY = currentCenterY + (targetCenterY - currentCenterY) * progress;
95+
96+
const groupMatrix = [frameZoom, 0, 0, 0, frameZoom, 0, frameCenterX, frameCenterY, 1];
97+
graph.get("group").setMatrix(groupMatrix);
98+
graph.paint();
99+
100+
if (progress < 1) {
101+
requestAnimationFrame(animate);
102+
}
154103
};
104+
105+
requestAnimationFrame(animate);
106+
} catch (error) {
107+
console.warn("Smooth fit view failed, falling back to instant fit:", error);
108+
graph.fitView(20);
109+
}
110+
};
111+
112+
/**
113+
* 基于目标坐标的节点平滑移动
114+
* @param {Object} graph - G6 图形实例
115+
* @param {Map} targets - 目标位置映射 (nodeId -> {x, y})
116+
* @param {number} duration - 动画持续时间(毫秒)
117+
* @param {Function} onFinish - 动画完成回调
118+
*/
119+
export const animateNodesToTargets = (
120+
graph: GraphLike,
121+
targets: Map<string, { x?: number; y?: number }>,
122+
duration = 800,
123+
onFinish?: () => void,
124+
) => {
125+
if (!graph || graph.destroyed || !targets?.size) {
126+
if (onFinish) onFinish();
127+
return;
128+
}
129+
130+
const token = nextToken(nodeAnimationTokens, graph);
131+
nextToken(fitViewTokens, graph);
132+
133+
const startPositions = new Map<string, { x?: number; y?: number }>();
134+
graph.getNodes().forEach((node) => {
135+
const model = node.getModel();
136+
startPositions.set(model.id, { x: model.x, y: model.y });
137+
});
138+
139+
const startTime = performance.now();
140+
graph.setAutoPaint(false);
141+
142+
const step = (currentTime: number) => {
143+
if (!graph || graph.destroyed || !isCurrentToken(nodeAnimationTokens, graph, token)) return;
144+
145+
const elapsed = currentTime - startTime;
146+
const rawProgress = Math.min(elapsed / duration, 1);
147+
const progress = 1 - Math.pow(1 - rawProgress, 3);
148+
149+
targets.forEach((target, id) => {
150+
const node = graph.findById(id);
151+
if (!node) return;
152+
const start = startPositions.get(id) || target;
153+
const startX = typeof start.x === "number" ? start.x : 0;
154+
const startY = typeof start.y === "number" ? start.y : 0;
155+
const targetX = typeof target.x === "number" ? target.x : startX;
156+
const targetY = typeof target.y === "number" ? target.y : startY;
157+
const x = startX + (targetX - startX) * progress;
158+
const y = startY + (targetY - startY) * progress;
159+
graph.updateItem(node, { x, y });
160+
});
161+
162+
graph.paint();
163+
164+
if (rawProgress < 1) {
165+
requestAnimationFrame(step);
166+
} else {
167+
graph.setAutoPaint(true);
168+
if (isCurrentToken(nodeAnimationTokens, graph, token) && onFinish) onFinish();
169+
}
170+
};
171+
172+
requestAnimationFrame(step);
173+
};

0 commit comments

Comments
 (0)