不才的破站 蹭饭图制作工具 ,有连线的实现:
不过具体实现是基于 ZRender 的,像你图中这种线,我称为“曲柄线”,其算法实现如下:
import { Circle, Polyline } from "zrender";
// 卡片包围盒的四条边的中点位置
export type EndPoints = [
[number, number], // 上
[number, number], // 右
[number, number], // 下
[number, number] // 左
];
export type LineRender = (args: {
// 地图中心点的坐标
mapCenter: [number, number];
tagPoints: EndPoints;
style: Partial<{
stroke: string;
lineDash: number[];
lineWidth: number;
}>;
pointStyle: Partial<{
stroke: string;
lineDash: number[];
lineWidth: number;
}>
}) => ZRender.Displayable[];
/**
* @function getNearestPoint - 获取四个点中距地图最近的一个点
* @param param0
* @param tagPoints
* @returns [number, [number, number]] - 最近点的序号 + 坐标
*/
export const getNearestPoint = (
[x, y]: [number, number],
tagPoints: EndPoints
) => {
const [nearestX, nearestY] = [...tagPoints[0]];
let shrtestIndex = 0;
let shortestDistance = (x - nearestX) ** 2 + (y - nearestY) ** 2;
for (let index = 1; index < 4; index++) {
const [a, b] = tagPoints[index];
const currentDistance = (x - a) ** 2 + (y - b) ** 2;
if (currentDistance < shortestDistance) {
shortestDistance = currentDistance;
shrtestIndex = index;
}
}
return [shrtestIndex, tagPoints[shrtestIndex]] as [number, [number, number]];
};
/**
* 曲柄线,即拐角为 90° 的 "Z" 形线
*/
const crank: LineRender = ({ tagPoints, mapCenter, style, pointStyle }) => {
// 先计算最近的点
const [mapX, mapY] = mapCenter;
const [index, [tagX, tagY]] = getNearestPoint(mapCenter, tagPoints);
// 固定在 1/2 处拐弯,其实不该叫做 quater,而是 half
const quaterOffsetX = (tagX - mapX) / 2;
const quaterOffsetY = (tagY - mapY) / 2;
// 确定曲柄是竖直方向还是水平方向
const isVertical = +Boolean(index % 2); // 上下, y 不变
const isHorizon = +!Boolean(index % 2); // 左右, x 不变
return [
// 曲柄线对象
new ZRender.Polyline({
shape: {
points: [
[...mapCenter],
[mapX + quaterOffsetX * isVertical, mapY + quaterOffsetY * isHorizon],
[tagX - quaterOffsetX * isVertical, tagY - quaterOffsetY * isHorizon],
[tagX, tagY],
],
smooth: 0,
},
silent: true,
style,
zlevel: 2,
}),
// 曲柄线端点,地图一侧
new ZRender.Circle({
style: pointStyle,
shape: {
cx: mapX,
cy: mapY,
r: style?.lineWidth || 1,
},
zlevel: 2,
}),
// 曲柄线端点,卡片一侧
new ZRender.Circle({
style: pointStyle,
shape: {
cx: tagX,
cy: tagY,
r: style?.lineWidth || 1,
},
zlevel: 2,
}),
];
};
这里面的拐弯处坐标固定为起止点连线的中点 ,由用户自己调整以避免线条交叉。
我的卡片数量、大小也是不定的,但是排版和布线的包袱丢给了用户。如果想实现自动排版和布线的话,你需要写更复杂的算法以确定多个卡片的位置和各个拐点的坐标,计算好之后线条的形状算法依旧可以照搬我的实现。
至于在 ECharts 中如何画线,可以参考 echarts.graphic.extendShape。
不过我觉得 ECharts 默认的 LabelLine 其实并不比曲柄线差,考虑一下说服设计师直接用 LabelLine 得了。