Public
Edited
Aug 28, 2024
Fork of Path tween
Insert cell
Insert cell
{
const d0 = "M0,0c100,0 0,100 100,100c100,0 0,-100 100,-100"; // 过渡开始时的 source path 原始路径
const d1 = "M0,0c100,0 0,100 100,100c100,0 0,-100 100,-100c100,0 0,100 100,100"; // 过渡结束时的 target path 目标路径

/**
*
* 创建 svg 容器
*
*/
const width = 928; // svg 元素的宽度
const height = 500; // svg 元素的高度
// 返回的是一个包含 svg 元素的选择集
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");

/**
*
* 绘制路径
*
*/
// 使用 <path> 元素将线段路径绘制到页面上
svg.append("path")
// 通过设置 CSS 的 transform 属性将 <path> 元素移动到 svg 容器的中间
.attr("transform", "translate(180,150)scale(2,2)")
// 只需要路径的描边作为折线,不需要填充,所以属性 fill 设置为 none
.attr("fill", "none")
// 设置描边颜色,采用 "currentColor" 默认颜色(继承自父元素,这里是黑色)
.attr("stroke", "currentColor")
.attr("stroke-width", 1.5) // 设置描边宽度
// 通过设置 `<path>` 元素的属性 `d` 绘制出路径的原始形状
.attr("d", d0)
// 设置过渡动效(通过更改 `<path>` 的属性 d 实现)
// 通过 selection.transition() 创建过渡管理器
// 过渡管理器和选择集类似,有相似的方法,例如为选中的 DOM 元素设置样式属性
// 具体参考官方文档 https://d3js.org/d3-transition
// 或这一篇笔记 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-transition
.transition()
.duration(2000) // 设置过渡的时间
// 使用过渡管理器方法 transition.on(typeNames[, listener]) 监听过渡所分发的(自定义)事件,并执行相应的回调操作 listener
// 这里监听 `"start"` 事件,它在过渡开始被分发,然后回调函数 repeat() 会被执行
.on("start", function repeat() {
// 在回调函数中 this 指向(在过渡管理器所绑定的选择集合中)当前所遍历的元素
// 在这里的过渡管理器所绑定的选择集中只有一个 `<path>` 元素
// 通过方法 d3.active(node[, name]) 获取指定元素的指定名称的执行中的过渡管理器
// 使用过渡管理器方法 `transition.attrTween(attrName[, factory])` 设置元素的属性 `attrName`,可以自定义插值器 `factory` 用于进行插值计算,即计算过渡期间属性 `attrName` 在各个时间点的值
// 这里更改的是 `<path>` 元素的属性 `d`,自定义了插值函数 pathTween() 该函数的具体代码实现可以查看 👇 下一个 cell
// 💡 另一类似的方法是 `transition.attr(attrName, value)` 它也是用于设置元素的属性 `attrName`,但直接设置了目标值 `value`(过渡结束时的最终值),而不需要设置过渡期间各个时间点的值(因为 D3 会根据属性值的数据类型,自动调用相应插值器)
// 关于方法 `transition.attr()` 和 `transition.attrTween()` 的详细介绍可以参考这一篇笔记 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-transition#过渡参数配置
d3.active(this)
// 这里的过渡动画的目的将路径形状从 d0 变换为 d1
.attrTween("d", pathTween(d1, 4))
// 然后通过 `transition.transition()` 基于原有的过渡管理器所绑定的选择集合,创建一个新的过渡管理器
// 新的过渡管理器会**继承了原有过渡的名称、时间、缓动函数等配置**
// 而且新的过渡会**在前一个过渡结束后开始执行**
// 一般通过该方法为同一个选择集合设置一系列**依次执行的过渡动效**
.transition()
// 同样使用方法 `transition.attrTween()` 设置 `<path>` 元素的属性 `d`
// 这里的过渡动画的目的将路径形状从 d1 恢复为 d0
.attrTween("d", pathTween(d0, 4))
// 再使用创建一个过渡管理器(它会接着上一个过渡动画结束时触发)
.transition()
// 又再一次调用 repeat() 函数
.on("start", repeat);
// 函数 repeat() 的作用是先将路径的形成再一次从 d0 切换为 d1,然后再恢复为 d0,最后又递归调用自身,形成循环动画,所以过渡动画的最终效果是路径在 d0 和 d1 形状之间不断切换
});

return svg.node();
}
Insert cell
// 该函数称为插值器工厂函数 interpolator factory,它生成一个插值器
// 💡 D3 在 d3-interpolate 模块提供了一些内置插值器,具体可以查看官方文档 https://d3js.org/d3-interpolate
// 或这一篇笔记 https://datavis-note.benbinbin.com/article/d3/core-concept/d3-concept-transition#插值器
// 该函数接收两个参数,第一个参数 `d1` 是过渡的目标值/最终值,第二个参数 `precision` 是采样的精度
// 通过采样将路径从贝塞尔曲线转换为分段折线(便于插值计算)
function pathTween(d1, precision) {
// 返回一个自定义的插值器
return function() {
// 函数内的 this 指向(在过渡管理器所绑定的选择集合中)当前所遍历的元素,在这个示例中选择集中只有一个 `<path>` 元素
const path0 = this;
// 通过 JS 原生方法 node.cloneNode() 拷贝该 DOM 元素
const path1 = path0.cloneNode();
// 将该 `<path>` 元素的属性 `d` 设置为 `d1`(过渡的目标值/最终值),所以该元素的形状与过渡完成时的路径形状一样
path1.setAttribute("d", d1);
// 使用方法 SVGGeometryElement.getTotalLength() 获取 `<path>` 元素的长度(以浮点数表示)
const n0 = path0.getTotalLength(); // 过渡起始时路径的总长度
const n1 = path1.getTotalLength(); // 过渡结束时路径的总长度
// Uniform sampling of distance based on specified precision.
// 基于给定的精度 precision 对(过渡前)path0 和(过渡后)path1 两个路径进行均匀采样
// 💡 可以得到一系列配对的采样点(它们分别是路径上某一点的起始状态和最终状态)
// 💡 然后为**每对采样点(已知起始状态和最终值)构建一个插值器**,用于实现路径切换动画
// 用一个数组 distances 来存储采样点(相对于路径的)位置,每一个元素都表示一个采样点
// 即每个元素/采用点都是一个 0 到 1 的数字,它是采样点到该路径开头的距离与**该路径总长度**的比值(占比)
// 💡 使用相对值来表示采样点的位置,以便将采样点进行配对
const distances = [0]; // 第一个采样点是路径的起点
// 对采样的精度/步长进行标准化,使用它进行迭代采样就可以得到采样点的相对(总路径)位置
// 其中 precise 的单位是 px 像素,是采样精度的绝对值
// 通过精度与路径的总长度作比 precise / Math.max(n0, n1) 将精度从绝对值转换为相对值
// 其中路径总长度是基于变换前后最长的路径,以保证在较长的路径上的采样密度(数量)也是足够
const dt = precision / Math.max(n0, n1);
// 通过 while 循环进行采用,每次距离增加一个标准化的步长 dt
let i = 0; while ((i += dt) < 1) distances.push(i);
distances.push(1); // 最后一个采样点是路径的终点

// Compute point-interpolators at each distance.
// 遍历数组 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;
};
}
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more