使用D3 V3版本绘制
项目中多次遇到飞线的需求,故此抽个时间做个总结。本篇飞线demo效果:
主要思路:
- 先绘制一条由起点到终点的弧线;
- 绘制弧线由起始点增长至终点效果;
- 设置蒙版,位置跟随弧线起点移动,则飞线始终显示蒙版内大小,营造出飞线效果。
准备知识:
- 贝塞尔曲线的绘制;
- 二阶贝塞尔曲线控制点的计算;
- svg蒙版的使用。
1. 绘制贝塞尔曲线
此处由二阶贝塞尔绘制弧形曲线效果。二阶贝塞尔曲线命令:
Q x1 y1, x y (或 q dx1 dy1, dx dy)
其中:x1 y1 为控制点坐标,x y为终点坐标
绘制命令及效果如下:
<path d="M10 80 Q 95 10 180 80" stroke="black" fill="transparent"/>
2. 二阶贝塞尔曲线控制点的计算
方法一
上面图形中求控制点坐标的方法有很多种,下面提供其中一种思路:
- 求出已知两点斜率
K
= y 2 − y 1 x 2 − x 1 \frac{y_2-y_1} {x_2-x_1} x2−x1y2−y1,则控制点与垂线的斜率为- 1/K
;- 求出AB中点O( x o , y o x_o,y_o xo,yo)的坐标( x 1 + x 2 2 , y 1 + y 2 2 \frac{x_1+x_2} {2},\frac{y_1+y_2} {2} 2x1+x2,2y1+y2),根据两点间距离公式
OC = H
,得 ( x c − x o ) 2 + ( y c − y o ) 2 2 \sqrt[2]{(x_c-x_o)^2+(y_c-y_o)^2} 2(xc−xo)2+(yc−yo)2 = H;- 由1、2列出方程求出点C坐标( x c , y c x_c,y_c xc,yc)。
方法二
使用O点坐标加上△x,△y的方法计算C点坐标。
function computeControlPoint(ps, pe, arc = 0.5) {
const deltaX = pe[0] - ps[0];
const deltaY = pe[1] - ps[1];
const theta = Math.atan(deltaY / deltaX);
const len = Math.sqrt((deltaX * deltaX) + (deltaY * deltaY)) / 2 * arc;
const newTheta = theta - Math.PI / 2;
return [
(ps[0] + pe[0]) / 2 + len * Math.cos(newTheta),
(ps[1] + pe[1]) / 2 + len * Math.sin(newTheta),
];
}
( θ − π 2 ) (θ- \frac{π} {2}) (θ−2π) 则
sinθ —> - cosθ
cosθ —> sinθ
,atan(K)
的取值范围为 [ − π 2 , π 2 ] [-\frac{π} {2}, \frac{π} {2}] [−2π,2π];
注1:由于svg坐标系原因,图中(1)中情况时,k为负值,此时 − π 2 < θ < 0 -\frac{π} {2}<θ<0 −2π<θ<0 ;
注2:因svg坐标原点在左上方,故当△y<0时控制点一直在上侧;
注3:θ平移 π 2 \frac{π} {2} 2π 也是为了保证控制点一直位于连线上方,此时弧形向下凹陷。
1.当 ( − π 2 < x < 0 ) (-\frac{π} {2}<x<0) (−2π<x<0)时,cosθ<0;sinθ<0; 即△x<0,△y<0;
2.当 ( 0 < x < π 2 ) (0<x<\frac{π} {2}) (0<x<2π)时,cosθ>0;sinθ<0;即△x>0,△y<0;
故最终视图:
3. 使用蒙版
使用 svg 蒙板,在蒙板中定义一个透明度从内到外逐渐降低径向渐变的圆,渲染飞线“头粗尾巴细”的效果。
<svg>
<defs>
<mask id="Mask">
<circle id="circle" r="150" fill="url(#grad)" />
</mask>
<radialGradient
id="grad"
cx="0.5"
cy="0.5"
r="0.5" >
<stop offset="0%" stop-color="#fff" stop-opacity='1'/>
<stop offset="100%" stop-color="#fff" stop-opacity='0' />
</radialGradient>
</defs>
</svg>
绘制飞线:
1. 绘制地图
获取地图路径,根据投影函数绘制地图,代码如下:
// 定义地图的投影
const projection = d3.geo.mercator()
.center(mapConfig.center)
.scale(mapConfig.scale);
// 定义地理路径生成器
const path = d3.geo.path()
.projection(projection);
// 生成地图
const mapGroups = svg.append('g').attr('class', 'mapGroups');
mapGroups.selectAll('path')
.data(mapJson.features)
.enter()
.append('path')
.style('fill', mapColor)
.attr('stroke', strokeColor)
.attr('d', path);
2. 绘制弧线
const pointData = [];
for (let i = 0; i < dataset.length; i += 1) {
// 计算飞线点坐标
const startPoint = projection(mapConfig.start);
const endPoint = projection(dataset[i].centroids);
pointData.push({
startPoint,
endPoint,
controlPoint: computeControlPoint(startPoint, endPoint, 0.5),
});
const baseLineGroups = svg.append('g').attr('class', 'baseLineGroups');
baseLineGroups.append('path')
.attr({
stroke: 'none',
fill: 'none',
class: `line-path${i}`,
})
.attr('d', () => transPath(pointData[i]));
}
注:此处绘制弧线为基础弧线,做飞线动画时需以此弧线为基础获取长度,并计算各个点的坐标。
3. 飞线动画
使用 attrTween(),插入中间帧函数,不断变更
// 生成飞线并设置动画
lineGroups.append('path')
.attr({
stroke: '#FFCE00',
'stroke-width': 3,
fill: 'none',
//mask: `url(#mask${i})`,
})
.transition()
.duration(2500)
.ease('linear')
.delay(1200)
.attrTween('d', () => {
const $path = d3.select(`.line-path${i}`).node();
const l = $path.getTotalLength();
const coord = $path.getAttribute('d').replace(/(M|Q)/g, '').match(/((\d|\.)+)/g);
const x1 = +coord[0]; const y1 = +coord[1]; // 起点
const x2 = +coord[2]; const y2 = +coord[3]; // 控制点
return function (t) {
const p = $path.getPointAtLength(t * l); // 新的终点
const x = ((1 - t) * x1) + (t * x2); const y = ((1 - t) * y1) + (t * y2);
// d3.select(`#circle${i}`).attr('cx', p.x).attr('cy', p.y); // 蒙版坐标
return `M${x1},${y1} Q${x},${y} ${p.x},${p.y}`;
};
})
.transition()
.duration(2400)
.style('opacity', 0);
4. 飞线样式–添加蒙版
- 圆心 cy,cx 为飞线终点;
- 设置的半径即为可视区域;
- 蒙板动态跟随飞线变化。
也可根据需求添加飞线终点样式。