function pathTween(d1, precision) {
return function() {
const path0 = this;
const path1 = path0.cloneNode();
path1.setAttribute("d", d1);
const n0 = path0.getTotalLength();
const n1 = path1.getTotalLength();
const distances = [0];
const dt = precision / Math.max(n0, n1);
let i = 0; while ((i += dt) < 1) distances.push(i);
distances.push(1);
// 遍历数组 distances 为不同的采样点构建一系列的插值器
const points = distances.map((t) => {
// t 为当前所遍历的采样点的位置的相对值(与它所在的路径总长度的占比)
// 通过 t * n0 或 t * n1 可以求出该采样点距离 path0 或 path1 路径的起点的具体距离
// 再使用 SVG 元素的原生方法 path.getPointAtLength(distance) 可以获取距离路径起点特定距离 distance 的位置的具体信息
// 具体可以参考 https://developer.mozilla.org/en-US/docs/Web/API/SVGGeometryElement/getPointAtLength
// 该方法返回一个 DOMPoint 对象,它表示坐标系中的 2D 或 3D 点,其中属性 x 和 y 分别描述该点的水平坐标和垂直坐标
// 具体可以参考 https://developer.mozilla.org/en-US/docs/Web/API/DOMPoint
// 在 path0(过渡开始时的路径)上的采样点作为插值的起始状态
const p0 = path0.getPointAtLength(t * n0);
// 在 path1(过渡结束时的路径)上的采样点作为插值的最终状态
const p1 = path1.getPointAtLength(t * n1);
// 所以 [p0.0, p0.y] 是插值的起点的坐标值,[p1.x, p1.y] 是插值的终点的坐标值
// 这里使用 D3 所提供的内置通用插值器构造函数 d3.interpolate(a, b) 来构建一个插值器
// 它会根据 b 的值类型自动调用相应的数据类型插值器
// 具体可以参考这一篇笔记 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-transition#通用类型插值器
// 这里为每个采样位置构建出一个插值器,然后在过渡期间就可以计算出特定时间点该点运动到什么地方(即它的 x,y 坐标值)
return d3.interpolate([p0.x, p0.y], [p1.x, p1.y]);
});
// 插值器最后需要返回一个函数,它接受标准时间 t 作为参数(其值的范围是 [0, 1])
// 返回的这个函数会在过渡期间被不断调用,用于生成不同时间点的 `<path>` 元素的属性 `d` 的值
// 当过渡未结束时(标准化时间 t < 1 时),通过调用一系列的插值器 points 计算各个采样点的运动到何处,并使用指令 `L` 将这些点连起来构成一个折线
// 而过渡结束时(标准化时间 t = 1 时),将路径替换为真正的形状 d1(而不再使用采样点模拟生成的近似形状)
return (t) => t < 1 ? "M" + points.map((p) => p(t)).join("L") : d1;
};
}