惯性阻力布局

时间:2015-05-27 10:49:03

标签: d3.js

我使用最新的强制布局d3js来创建交互式图形,如下所示: enter image description here

要求是:

  1. 可以拖动节点(惯性拖动)
  2. 节点在敲击边框时反弹。
  3. 节点互不重叠(我可以根据碰撞检测样本做到这一点)
  4. 有人请帮助我1& 2。

    此问题的背景是this相关问题。

    谢谢。

1 个答案:

答案 0 :(得分:2)

背景

这个答案的背景是我对相关问题的回答here

这个问题是关于为什么节点在发布后跳回来的主要原因是,force.drag行为期间前一节点位置(d.px,d.py)和当前位置( dx,dy)实际上是逆转的。因此,当释放阻力时,初始速度因此反转,导致跳回行为 这实际上是由于在拖动事件上更新先前位置的拖动行为以及将先前值复制到每个位置计算的当前值的内部force.tick方法。 (顺便提一下,我确信这是有充分理由的,我怀疑它与this有关...)

惯性拖动

为了实现惯性拖动,需要更正此速度反转,因此在dragend之后,当前和之前的点需要立即反转

这是一个良好的开端,但还存在其他一些问题:

  1. 当前一个位置被复制到当前位置时,每个刻度都会丢失速度状态。
  2. “粘性节点”行为(在mouseover上)在dragend上重新建立,这往往会重新捕获节点并破坏惯性效应。
  3. 第一个意味着如果在释放阻力和校正速度之间发生嘀嗒声,即紧接在dragend之后,那么速度将为零并且节点将停止死亡。这种情况经常发生,令人讨厌。一种解决方案是保留d3.event.dxd3.event.dy的记录,并使用这些记录在dragend上修改(d.px,d.py)。这也避免了因前一点和当前点的反转而引起的问题。

    可以通过延迟粘性节点行为恢复直到mouseout之后来解决剩下的第二个问题。如果鼠标在 mouseout之后立即重新进入节点,建议mouseout之后的小延迟。

    执行力度

    实现上述两个修正的基本策略是将前一个力布局中的拖动事件和后一个中的力布局中的鼠标事件挂钩。出于防御原因,各种钩子的标准回调存储在节点的datum对象上,并在取消挂钩时从那里检索。

    摩擦参数在代码中设置为1,这意味着它们无限期地保持其速度,看到稳定的惯性效果将其设置为0.9 ......我像弹跳球一样开玩笑。

    $(function() {
      var width = 1200,
        height = 800;
      var circles = [{
          x: width / 2 + 100,
          y: height / 2,
          radius: 100
        }, {
          x: width / 2 - 100,
          y: height / 2,
          radius: 100
        }, ],
        nodeFill = "#006E3C";
    
      var force = d3.layout.force()
        .gravity(0)
        .charge(-100)
        .friction(1)
        .size([width, height])
        .nodes(circles)
        .linkDistance(250)
        .linkStrength(1)
        .on("tick", tick)
        .start();
    
      SliderControl("#frictionSlider", "friction", force.friction, [0, 1], ",.3f");
    
      var svg = d3.select("body")
        .append("svg")
        .attr("width", width)
        .attr("height", height)
        .style("background-color", "white");
      var nodes = svg.selectAll(".node");
      nodes = nodes.data(circles);
      nodes.exit().remove();
      var enterNode = nodes.enter().append("g")
        .attr("class", "node")
        .call(force.drag);
      console.log(enterNode);
      //Add circle to group
      enterNode.append("circle")
        .attr("r", function(d) {
          return d.radius;
        })
        .style("fill", "#006E3C")
        .style("opacity", 0.6);
    
      ;
      (function(d3, force) {
        //Drag behaviour///////////////////////////////////////////////////////////////////
        //  hook drag behavior on force
    
        //VELOCITY
        //  maintain velocity state in case a force tick occurs emidiately before dragend
        //  the tick wipes out the previous position
        var dragVelocity = (function() {
          var dx, dy;
    
          function f(d) {
            if (d3.event) {
              dx = d3.event.dx;
              dy = d3.event.dy;
            }
            return {
              dx: dx,
              dy: dy
            }
          };
          f.correct = function(d) {
            //tick occured and set px/y to x/y, re-establish velocity state
            d.px = d.x - dx;
            d.py = d.y - dy;
          }
          f.reset = function() {
            dx = dy = 0
          }
          return f;
        })()
    
        //DRAGSTART HOOK
        var stdDragStart = force.drag().on("dragstart.force");
    
        force.drag().on("dragstart.force", myDragStart);
    
        function myDragStart(d) {
            var that = this,
              node = d3.select(this);
    
            nonStickyMouse();
            dragVelocity.reset();
            stdDragStart.call(this, d)
    
            function nonStickyMouse() {
    
              if (!d.___hooked) {
                //node is not hooked
                //hook mouseover/////////////////////////
                //remove sticky node on mouseover behavior and save listeners
                d.___mouseover_force = node.on("mouseover.force");
                node.on("mouseover.force", null);
    
                d.___mouseout_force = node.on("mouseout.force");
    
                d.___hooked = true;
    
                //standard mouseout will clear d.fixed
                d.___mouseout_force.call(that, d);
              }
              //dissable mouseout/////////////////////////
              node.on("mouseout.force", null);
            }
          }
          //DRAG HOOK
        var stdDrag = force.drag().on("drag.force");
    
        force.drag().on("drag.force", myDrag);
    
        function myDrag(d) {
          var v, p;
          //maintain back-up velocity state
          v = dragVelocity();
          p = {
            x: d3.event.x,
            y: d3.event.y
          };
          stdDrag.call(this, d)
        }
    
        //DRAGEND HOOK
        var stdDragEnd = force.drag().on("dragend.force");
    
        force.drag().on("dragend.force", myDragEnd);
    
        function myDragEnd(d) {
          var that = this,
            node = d3.select(this);
          //correct the final velocity vector at drag end
          dragVelocity.correct(d)
    
          //hook mouseout/////////////////////////
          //re-establish standard behavior on mouseout
          node.on("mouseout.force", function mouseout(d) {
            myForceMouseOut.call(this, d)
          });
    
          stdDragEnd.call(that, d);
    
          function myForceMouseOut(d) {
            var timerID = window.setTimeout((function() {
              var that = this,
                node = d3.select(this);
              return function unhookMouseover() {
                //if (node.on("mouseover.force") != d.___mouseout_force) {
                if (node.datum().___hooked) {
                  //un-hook mouseover and mouseout////////////
                  node.on("mouseout.force", d.___mouseout_force);
                  node.on("mouseover.force", d.___mouseover_force);
                  node.datum().___hooked = false;
                }
              }
            }).call(this), 500);
            return timerID;
          }
        }
    
      })(d3, force);
    
      function tick(e) {
        //contain the nodes...
        nodes.attr("transform", function(d) {
          var r = 100;
          if (d.x - r <= 0 && d.px > d.x) d.px -= (d.px - d.x) * 2;
          if (d.x + r >= width && d.px < d.x) d.px += (d.x - d.px) * 2;
          if (d.y - r <= 0 && d.py > d.y) d.py -= (d.py - d.y) * 2;
          if (d.y + r >= height && d.py < d.y) d.py += (d.y - d.py) * 2;
          return "translate(" + d.x + "," + d.y + ")";
        });
        //indicate status by color
        nodes.selectAll("circle")
          .style("fill", function(d, i) {
            return ((d.___hooked && !d.fixed) ? "red" : nodeFill)
          })
        force.start();
      }
    
      function SliderControl(selector, title, value, domain, format) {
        var accessor = d3.functor(value),
          rangeMax = 1000,
          _scale = d3.scale.linear().domain(domain).range([0, rangeMax]),
          _$outputDiv = $("<div />", {
            class: "slider-value"
          }),
          _update = function(value) {
            _$outputDiv.css("left", 'calc( ' + (_$slider.position().left + _$slider.outerWidth()) + 'px + 1em )')
            _$outputDiv.text(d3.format(format)(value));
            $(".input").width(_$outputDiv.position().left + _$outputDiv.outerWidth() - _innerLeft)
    
          },
    
          _$slider = $(selector).slider({
            value: _scale(accessor()),
            max: rangeMax,
            slide: function(e, ui) {
              _update(_scale.invert(ui.value));
              accessor(_scale.invert(ui.value)).start();
            }
          }),
          _$wrapper = _$slider.wrap("<div class='input'></div>")
          .before($("<div />").text(title + ":"))
          .after(_$outputDiv).parent(),
          _innerLeft = _$wrapper.children().first().position().left;
    
        _update(_scale.invert($(selector).slider("value")))
    
      };
    
    });
    body {
      /*font-family: 'Open Sans', sans-serif;*/
      font-family: 'Roboto', sans-serif;
    }
    svg {
      outline: 1px solid black;
      background-color: rgba(255, 127, 80, 0.6);
    }
    div {
      display: inline-block;
    }
    #method,
    #clear {
      margin-left: 20px;
      background-color: rgba(255, 127, 80, 0.6);
      border: none;
    }
    #clear {
      float: right;
    }
    #inputs {
      font-size: 16px;
      display: block;
      width: 900px;
    }
    .input {
      display: inline-block;
      background-color: rgba(255, 127, 80, 0.37);
      outline: 1px solid black;
      position: relative;
      margin: 10px 10px 0 0;
      padding: 3px 10px;
    }
    .input div {
      width: 60px;
    }
    .method {
      display: block;
    }
    .ui-slider,
    span.ui-slider-handle.ui-state-default {
      width: 3px;
      background: black;
      border-radius: 0;
    }
    span.ui-slider-handle.ui-state-default {
      top: calc(50% - 1em / 2);
      height: 1em;
      margin: 0;
      border: none;
    }
    div.ui-slider-horizontal {
      width: 200px;
      margin: auto 10px auto 10px;
      /*position: absolute;*/
      /*bottom: 0.1em;*/
      position: absolute;
      bottom: calc(50% - 2.5px);
      /*vertical-align: middle;*/
      height: 5px;
      border: none;
    }
    .slider-value {
      position: absolute;
      text-align: right;
    }
    input,
    select,
    button {
      font-family: inherit;
      font-size: inherit;
    }
    <link href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css" rel="stylesheet" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
    <div id="inputs">
    
      <div id="frictionSlider"></div>
    </div>