使用 svg 变换更新位置时,力模拟会出现抖动
Force simulation is jittery when using svg transforms to update position
我注意到在 d3-force 图表中更新 svg 元素的位置时,使用(在圆圈的情况下)更新元素的位置 cx
和 cy
attributes 比使用 transform
attribute 更流畅。
在示例 JSFiddle 中,有两个并排的独立力模拟。左边的那个使用 transform
属性更新位置:
sim_transform.on('tick', function () {
circles_transform.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
});
右边的那个使用圆的 cx
和 cy
属性更新位置:
sim_position.on('tick', function () {
circles_position
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
})
});
模拟看起来完全相同,直到它们即将变为静态,此时使用变换的模拟开始抖动很多。任何想法是什么造成的?能否修复它以便使用变换使动画保持流畅?
在我看来,您观察到的问题(只能在 FireFox 中重现,如 )与 FF 使用浮点数的方式有关 translate
transform
属性。
如果我们将两个模拟设置为使用整数,执行 ~~(d.x)
和 ~~(d.y)
,我们可以看到这一点。看看,都会抖动:
var svg = d3.select('svg');
var graph_transform = gen_data();
var graph_position = gen_data();
var force_left = d3.forceCenter(
parseInt(svg.style('width')) / 3,
parseInt(svg.style('height')) / 2
)
var force_right = d3.forceCenter(
2 * parseInt(svg.style('width')) / 3,
parseInt(svg.style('height')) / 2
)
var sim_transform = d3.forceSimulation()
.force('left', force_left)
.force('collide', d3.forceCollide(65))
.force('link', d3.forceLink().id(id));
var sim_position = d3.forceSimulation()
.force('right', force_right)
.force('collide', d3.forceCollide(65))
.force('link', d3.forceLink().id(id));
var g_transform = svg.append('g');
var g_position = svg.append('g');
var circles_transform = g_transform.selectAll('circle')
.data(graph_transform.nodes)
.enter()
.append('circle')
.attr('r', 40);
var circles_position = g_position.selectAll('circle')
.data(graph_position.nodes)
.enter()
.append('circle')
.attr('r', 40);
sim_transform
.nodes(graph_transform.nodes)
.force('link')
.links(graph_transform.links);
sim_position
.nodes(graph_position.nodes)
.force('link')
.links(graph_position.links);
sim_transform.on('tick', function() {
circles_transform.attr('transform', function(d) {
return 'translate(' + (~~(d.x)) + ',' + (~~(d.y)) + ')';
});
});
sim_position.on('tick', function() {
circles_position
.attr('cx', function(d) {
return ~~d.x;
})
.attr('cy', function(d) {
return ~~d.y;
})
});
function id(d) {
return d.id;
}
function gen_data() {
var nodes = [{
id: 'a'
},
{
id: 'b'
},
{
id: 'c'
},
{
id: 'd'
}
]
var links = [{
source: 'a',
target: 'b'
},
{
source: 'b',
target: 'c'
},
{
source: 'c',
target: 'd'
},
{
source: 'd',
target: 'a'
}
];
return {
nodes: nodes,
links: links
}
}
svg {
width: 100%;
height: 500px;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
因此,在您的原始代码中,使用 cx
和 cy
时圆圈似乎正确移动,但使用 translate
时它们从整数跳到整数(或者可能半像素,见上一个演示)。如果这里的假设是正确的,那么你只是在模拟冷却时看到效果的原因是因为那一刻运动较小。
演示
现在,如果我们摆脱模拟,我们可以看到这种奇怪的行为也发生在非常基本的 transform
上。为了检查这一点,我为一个大的黑色圆圈创建了一个过渡,使用线性缓动和很长的时间(以便于看到问题)。圆圈将向右移动 30px。我还放了一条网格线,使 跳跃 更明显。
(警告:下面的演示只能在 FireFox 中重现,您在 Chrome/Safari 中看不到任何差异)
如果我们使用cx
,过渡是平滑的:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98)
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr("cx", "230")
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
但是,如果我们使用translate
,你可以看到圆在每次移动时跳跃1px:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98)
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr("transform", "translate(30,0)")
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
对于 运行 Chrome/Safari 中的各位,这就是最后一个片段在 Firefox 中的样子。就像圆圈在每次变化时都移动了半个像素......绝对不如变化 cx
:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98);
var timer = d3.timer(function(t){
if(t>10000) timer.stop();
circle.attr("cx", 200 + (~~(60/(10000/t))/2));
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
由于这是一个仅在 FF 中可见的实现问题,因此可能值得报告一个错误。
我注意到在 d3-force 图表中更新 svg 元素的位置时,使用(在圆圈的情况下)更新元素的位置 cx
和 cy
attributes 比使用 transform
attribute 更流畅。
在示例 JSFiddle 中,有两个并排的独立力模拟。左边的那个使用 transform
属性更新位置:
sim_transform.on('tick', function () {
circles_transform.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
});
右边的那个使用圆的 cx
和 cy
属性更新位置:
sim_position.on('tick', function () {
circles_position
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
})
});
模拟看起来完全相同,直到它们即将变为静态,此时使用变换的模拟开始抖动很多。任何想法是什么造成的?能否修复它以便使用变换使动画保持流畅?
在我看来,您观察到的问题(只能在 FireFox 中重现,如 translate
transform
属性。
如果我们将两个模拟设置为使用整数,执行 ~~(d.x)
和 ~~(d.y)
,我们可以看到这一点。看看,都会抖动:
var svg = d3.select('svg');
var graph_transform = gen_data();
var graph_position = gen_data();
var force_left = d3.forceCenter(
parseInt(svg.style('width')) / 3,
parseInt(svg.style('height')) / 2
)
var force_right = d3.forceCenter(
2 * parseInt(svg.style('width')) / 3,
parseInt(svg.style('height')) / 2
)
var sim_transform = d3.forceSimulation()
.force('left', force_left)
.force('collide', d3.forceCollide(65))
.force('link', d3.forceLink().id(id));
var sim_position = d3.forceSimulation()
.force('right', force_right)
.force('collide', d3.forceCollide(65))
.force('link', d3.forceLink().id(id));
var g_transform = svg.append('g');
var g_position = svg.append('g');
var circles_transform = g_transform.selectAll('circle')
.data(graph_transform.nodes)
.enter()
.append('circle')
.attr('r', 40);
var circles_position = g_position.selectAll('circle')
.data(graph_position.nodes)
.enter()
.append('circle')
.attr('r', 40);
sim_transform
.nodes(graph_transform.nodes)
.force('link')
.links(graph_transform.links);
sim_position
.nodes(graph_position.nodes)
.force('link')
.links(graph_position.links);
sim_transform.on('tick', function() {
circles_transform.attr('transform', function(d) {
return 'translate(' + (~~(d.x)) + ',' + (~~(d.y)) + ')';
});
});
sim_position.on('tick', function() {
circles_position
.attr('cx', function(d) {
return ~~d.x;
})
.attr('cy', function(d) {
return ~~d.y;
})
});
function id(d) {
return d.id;
}
function gen_data() {
var nodes = [{
id: 'a'
},
{
id: 'b'
},
{
id: 'c'
},
{
id: 'd'
}
]
var links = [{
source: 'a',
target: 'b'
},
{
source: 'b',
target: 'c'
},
{
source: 'c',
target: 'd'
},
{
source: 'd',
target: 'a'
}
];
return {
nodes: nodes,
links: links
}
}
svg {
width: 100%;
height: 500px;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
因此,在您的原始代码中,使用 cx
和 cy
时圆圈似乎正确移动,但使用 translate
时它们从整数跳到整数(或者可能半像素,见上一个演示)。如果这里的假设是正确的,那么你只是在模拟冷却时看到效果的原因是因为那一刻运动较小。
演示
现在,如果我们摆脱模拟,我们可以看到这种奇怪的行为也发生在非常基本的 transform
上。为了检查这一点,我为一个大的黑色圆圈创建了一个过渡,使用线性缓动和很长的时间(以便于看到问题)。圆圈将向右移动 30px。我还放了一条网格线,使 跳跃 更明显。
(警告:下面的演示只能在 FireFox 中重现,您在 Chrome/Safari 中看不到任何差异)
如果我们使用cx
,过渡是平滑的:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98)
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr("cx", "230")
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
但是,如果我们使用translate
,你可以看到圆在每次移动时跳跃1px:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98)
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr("transform", "translate(30,0)")
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
对于 运行 Chrome/Safari 中的各位,这就是最后一个片段在 Firefox 中的样子。就像圆圈在每次变化时都移动了半个像素......绝对不如变化 cx
:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98);
var timer = d3.timer(function(t){
if(t>10000) timer.stop();
circle.attr("cx", 200 + (~~(60/(10000/t))/2));
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
由于这是一个仅在 FF 中可见的实现问题,因此可能值得报告一个错误。