这个d3.drag示例中“preserving local offset”是什么意思?

5
当拖动this d3 example中的任何圆圈时,是什么阻止了圆圈的中心与鼠标捕捉?
换句话说:当您通过单击圆圈外缘附近的某个位置启动圆圈拖动时,代码中的什么会保留在拖动开始时暗示的偏移量(相对于圆圈中心)?
我看到这些.attr()调用:
.attr("cx", d.x = d3.event.x)
.attr("cy", d.y = d3.event.y)

但我期望d3.event.x(和.y)是鼠标的坐标-没有考虑偏移-因此,我认为圆的中心将会(从用户体验的角度来看不正确地)恰好在鼠标下方。

1个回答

10

我相信这个问题与d3拖动主题方法有关:

如果指定了主题,则将主题访问器设置为指定的对象或函数,并返回拖动行为。如果未指定主题,则返回当前主题访问器,默认为:

function subject(d) { return d == null ? {x: d3.event.x, y: d3.event.y} : d; }

拖动手势的主题表示被拖动的东西。当接收到初始输入事件时(例如mousedown或touchstart),立即在拖动手势开始前计算主题。然后,在此手势的后续拖动事件中将主题公开为event.subject。(链接

我们可以看到,如果我们既没有提供一个主题函数,也没有提供具有x和y属性的数据,那么拖动事件将导致圆形居中/捕捉到拖动起始点:

var svg = d3.select("body")
  .append("svg")
  .attr("width",500)
  .attr("height",300);
  
var datum = {x:250,y:150}
  
var g = svg.append("g")
    
  g.append("rect")
  .attr("width",500)
  .attr("height",300)
  .attr("fill","#ddd");
  
  g.append("circle")
  .datum(datum)
  .attr("cx",function(d) { return d.x; })
  .attr("cy",function(d) { return d.y; })
  .attr("r",10);
    
g.call(d3.drag().on("drag", dragged))
  
function dragged(d) {
  d3.select(this)
    .select("circle")
    .attr("cx", d3.event.x)
    .attr("cy", d3.event.y);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

以相同的示例为例,将数据分配给父g元素,使拖动可以访问主题的x和y属性(在上面的示例中不存在)。 在这里,拖动与初始数据相对(保持不变),并且节点将使用在数据中指定的初始x和y属性作为每次拖动的起点重新居中(拖动超过一次以查看):

var svg = d3.select("body")
  .append("svg")
  .attr("width",500)
  .attr("height",300);
  
var datum = {x:250,y:150}
  
var g = svg.append("g")
  .datum(datum);
  
  g.append("rect")
  .attr("width",500)
  .attr("height",300)
  .attr("fill","#ddd");
  
  g.append("circle")
  .attr("cx",function(d) { return d.x; })
  .attr("cy",function(d) { return d.y; })
  .attr("r",10);
    
g.call(d3.drag().on("drag", dragged))
  
function dragged(d) {
  d3.select(this)
    .select("circle")
    .attr("cx", d3.event.x)
    .attr("cy", d3.event.y);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

那么我们可以更新主体的数据,这样每个拖动事件都相对于圆的当前位置而不是初始位置:

var svg = d3.select("body")
  .append("svg")
  .attr("width",500)
  .attr("height",300);
  
var datum = {x:250,y:150}
  
var g = svg.append("g")
  .datum(datum);
  
  g.append("rect")
  .attr("width",500)
  .attr("height",300)
  .attr("fill","#ddd");
  
  g.append("circle")
  .attr("cx",function(d) { return d.x; })
  .attr("cy",function(d) { return d.y; })
  .attr("r",10);
    
g.call(d3.drag().on("drag", dragged))
  
function dragged(d) {
  d3.select(this)
    .select("circle")
    .attr("cx", d.x = d3.event.x)
    .attr("cy", d.y = d3.event.y);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

深入研究拖拽代码后,我们可以看到,在开始拖拽时,如果主体方法未提供任何功能,则计算拖拽 x、y 的起点与主体 x、y 之间的差异。
  dx = s.x - p[0] || 0;
  dy = s.y - p[1] || 0;

p 是起始鼠标位置,s 是主题。

这就解释了为什么当没有提供 x 或 y 属性时,圆形会吸附到拖动开始的地方。在计算输出时,d3 将 x 和 y 的值设置为:

p[0] + dx,
p[1] + dy

这里的 p 是当前鼠标位置。

因此,d3.event.x/.y 不应该是鼠标的绝对位置,而是给定拖动中相对位置变化后圆圈的绝对位置。通过 subject,将鼠标位置的相对变化转化为被拖动项目的绝对位置。

以下是一个使用自定义 subject 的示例,其中拖动将相对于 [100,100],并且圆圈将在每个拖动事件开始时固定在那里:

var svg = d3.select("body")
  .append("svg")
  .attr("width",500)
  .attr("height",300);
  
var datum = {x:250,y:150}
  
var g = svg.append("g")
  .datum(datum);
  
  g.append("rect")
  .attr("width",500)
  .attr("height",300)
  .attr("fill","#ddd");
  
  g.append("circle")
  .attr("cx",function(d) { return d.x; })
  .attr("cy",function(d) { return d.y; })
  .attr("r",10);
    
g.call(d3.drag()
   .on("drag", dragged)
   .subject({x:100,y:100})
   )
  
function dragged(d) {
  d3.select(this)
    .select("circle")
    .attr("cx", d3.event.x)
    .attr("cy", d3.event.y);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>


Andrew,非常感谢您的深入探索和回复以及可运行的代码(!)。现在很有意义。 - meetamit
谢谢,这是我最喜欢的问题类型,有趣、新颖,可以迫使我们更仔细地审视通常被认为是理所当然的事情。 - Andrew Reid

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接