(D3.js)如何将多个可拖动节点与贝塞尔曲线连接起来,并使它们响应节点的拖动事件?

时间:2018-12-10 15:28:31

标签: javascript d3.js

我正在构建一个流生成器,并且我将使用火盆曲线在它们之间连接多个可拖动节点。为了将一个节点连接到另一个节点,只需将其悬停在该节点上,单击符号并将一条线拖到另一个节点即可。将绘制贝塞尔曲线。

我使用通用路径画线,但是在完成拖动事件之后,我想克隆它,并将其存储在 g 组中。问题在于,拖动圆时,新路径不会更新坐标。如何动态地做到这一点?我还要附加一个JSFiddle。为了示例起见,我试图简化代码,它最初是在AngularJs环境中运行的。

    "use strict";
      const sidebar = document.getElementById('right_sidebar');

      const width = document.getElementById('IvrBuilderController').offsetWidth,
        height = document.getElementById('IvrBuilderController').offsetHeight;

      var xLoc = width / 2 - 25,
        yLoc = 100,
        radius = 40;

      var zoom = d3.zoom()
        .scaleExtent([0.7, 5])
        .on("zoom", zoomed);
      /** MAIN SVG **/
      var svg = d3.select("#IvrBuilderController").append("svg")
        .attr("width", width)
        .attr("height", height)
        .call(zoom)
        ;
      var state = {
        shiftNodeDrag: false,
        dragLineOverNode: false,
        startDragLineNode: null,
        endDragLineNode: null
      };

      var g = svg.append("g");
      var paths_container = g.append("g").attr('id', 'paths_container');

      var dragLine = g.append('svg:path')
        .attr('class', 'link dragline hidden')
        .attr('d', 'M0,0L0,0');

      var nodes_container = g.append("g").attr('id', 'nodes_container');

      function zoomed() {
        const currentTransform = d3.event.transform;
        g.attr("transform", currentTransform);
      }

      // starting node
      var xLoc = width / 2 - 25,
        yLoc = 100;
      var nodes = [{ x: xLoc, y: yLoc, id: 0, title: "start" }, { x: 350, y: 250, id: 1, title: "newNode" }, { x: 100, y: 250, id: 2, title: "NewNode" }];
      var nodeConnections = [];

      function updateNodes() {
        const container = nodes_container.selectAll(".node")
          .data(nodes)
          .enter();
        const node = container
          .append("g").attr("class", "node")
          .attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; })
          .call(d3.drag()
            .on("start", dragstarted)
            .on("drag", dragged)
            .on("end", dragended));
        node
          .append("circle")
          .attr("r", radius)
          .attr("class", "circle_node")
          .on("click", selectNode)
          .on("mouseover", mouseOverNode)
          .on("mouseout", mouseOutNode)

        node
          .append("image")
          .attr("xlink:href", "https://i.imgur.com/W02KovJ.png")
          .attr("x", -8)
          .attr("y", -48)
          .attr("width", 16)
          .attr("height", 16)
          .attr("class", "add_node_icon")
          .on("mousedown", (d) => {
            // reposition dragged directed edge      
            dragLine.classed('hidden', false)
              .attr('d', 'M' + d.x + ',' + d.y + 'L' + d.x + ',' + d.y);
            return;
          })
          .call(d3.drag()
            .on("start", () => {
              state.shiftNodeDrag = true;
            })
            .on("drag", function (d, i) {
              dragLine.attr('d', 'M' + d.x + ',' + d.y + 'L' + d3.mouse(g.node())[0] + ',' + d3.mouse(g.node())[1]);
              state.startDragLineNode = d;
              state.startDragLineNode.el = d3.select(this);
            })
            .on("end", function (d) {
              if (state.dragLineOverNode) {
                console.log('connected1');
                dragLine
                  .attr("d", function (d) {
                    const startX = state.startDragLineNode.x;
                    const startY = state.startDragLineNode.y - 41;

                    const endX = state.endDragLineNode.x;
                    const endY = state.endDragLineNode.y + 41;

                    const dx = Math.abs(startX - endX) * 0.675;

                    const p2y = startY - dx / 3;
                    const p3y = endY + dx / 3;

                    const data = `M${startX} ${startY} C ${startX} ${p2y} ${endX} ${p3y + 50} ${endX} ${endY}`;
                    let dragLineClone = paths_container
                      .append('path')
                      .attr("class", "link dragline")
                      .attr("d", data);
                    nodeConnections.push(dragLineClone);
                    return data;
                  });
                
              } else {
                dragLine.classed('hidden', true);
              }
              state.shiftNodeDrag = false;
              updateNodes();
            })
          );

        node
          .append("image")
          .attr("xlink:href", "https://i.imgur.com/W02KovJ.png")
          .attr("x", -8)
          .attr("y", 32)
          .attr("width", 16)
          .attr("height", 16)
          .attr("class", "add_node_icon")
          .on("mousedown", (d) => {
            // reposition dragged directed edge      
            dragLine.classed('hidden', false)
              .attr('d', 'M' + d.x + ',' + d.y + 'L' + d.x + ',' + d.y);
            return;
          })
          .call(d3.drag()
            .on("start", () => {
              state.shiftNodeDrag = true;
            })
            .on("drag", (d, i) => {
              dragLine.attr('d', 'M' + d.x + ',' + d.y + 'L' + d3.mouse(g.node())[0] + ',' + d3.mouse(g.node())[1]);
              state.startDragLineNode = d;
            })
            .on("end", (d) => {
              if (state.dragLineOverNode) {
                console.log('connected2');
                dragLine
                .attr("d", function (d) {
                  const startX = state.startDragLineNode.x;
                  const startY = state.startDragLineNode.y - 41;

                  const endX = state.endDragLineNode.x;
                  const endY = state.endDragLineNode.y + 41;

                  const dx = Math.abs(startX - endX) * 0.675;

                  const p2y = startY - dx / 3;
                  const p3y = endY + dx / 3;

                  const data = `M${startX} ${startY} C ${startX} ${p2y} ${endX} ${p3y + 50} ${endX} ${endY}`;
                    return data;
                  });
              
              } else {
                dragLine.classed('hidden', true);
              }
              state.shiftNodeDrag = false;
              })
            );
            nodeConnections.forEach(element => {
              paths_container
                .append('path')
                .attr("class", "link dragline")
                .attr("d", element);
            });
      }
      updateNodes(); // first init
      function addNode(title, id, x, y) {
        nodes.push({ title: title, id: id, x: x, y: y });
        updateNodes();
      }
      function mouseOverNode(d, i) {
        if (state.shiftNodeDrag && (state.startDragLineNode !== d)) {
          state.dragLineOverNode = true;
          state.endDragLineNode = d;
          state.endDragLineNode.el = d3.select(this);
        } else {
          state.dragLineOverNode = false;
        }
      }
      function mouseOutNode(d, i) {
        state.dragLineOverNode = false;
      }
      function selectNode(d) {
        if (!d3.select(this).classed("selected")) {
          d3.select(this).raise().classed("selected", true);
          sidebar.classList.add("open");
        } else {
          d3.select(this).raise().classed("selected", false);
          sidebar.classList.remove("open");
        }
        if (d3.event) { if (d3.event.defaultPrevented) return; }
      }
      function deselectNode() {
        d3.select("body").selectAll(".selected").classed("selected", false);
      }
      function dragstarted(d) {
        d3.event.sourceEvent.stopPropagation();
        d3.select(this).raise().classed("dragged", true);
      }

      function dragged(d) {
        if (!state.shiftNodeDrag) {
          d.x = d3.event.x;
          d.y = d3.event.y;
          d3.select(this).attr("transform", "translate(" + d3.event.x + "," + d3.event.y + ")");
        }
        dragLine
          .attr("d", function (d) {
            const startX = state.startDragLineNode.x;
            const startY = state.startDragLineNode.y - 41;

            const endX = state.endDragLineNode.x;
            const endY = state.endDragLineNode.y + 41;

            const dx = Math.abs(startX - endX) * 0.675;

            const p2y = startY - dx / 3;
            const p3y = endY + dx / 3;

            const data = `M${startX} ${startY} C ${startX} ${p2y} ${endX} ${p3y + 50} ${endX} ${endY}`;
            return data;
          });
      }

      function dragended(d) {
        d3.select(this).classed("dragged", false);
      }
      var closeSidebar = function () {
        document.getElementById('right_sidebar').classList.remove('open');
        deselectNode();
      }

    function allowDrop(ev) {
        ev.preventDefault();
      }

      function drag(ev) {
        ev.dataTransfer.setData("text", ev.target.id);
        document.getElementById('drop-zone').style.opacity = 1;
        document.getElementById('drop-zone').style.display = 'block';

        var child = ev.target;
        var parent = child.parentNode;
        var index = Array.prototype.indexOf.call(parent.children, child);
        ev.dataTransfer.setData("target_id", index);
      }

      function drop(ev) {
        ev.preventDefault();
        const data = ev.dataTransfer.getData("text");
        const target_id = ev.dataTransfer.getData("target_id");
        ev.target.appendChild(document.getElementById(data));
        document.getElementById('drop-zone').style.opacity = 0;
        document.getElementById('drop-zone').style.display = 'none';

        const target = ev.target.firstElementChild;
        const toolbox = document.getElementById('toolbox');

        toolbox.insertBefore(target, toolbox.children[target_id]); // clone the dragged icon at the same position

        let nodeLastIndex;
        nodes.forEach(el => {
          nodeLastIndex = el.id;
        });
        addNode(target.attributes.name.nodeValue, nodeLastIndex + 1, ev.layerX, ev.layerY);
      }
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0;
  overflow: hidden;
  background-color: #ffffff!important;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

p {
  text-align: center;
  overflow: overlay;
  position: relative;
}

#IvrBuilderController {
  position: relative;
  height: 100vh;
  background-color: #ffffff;
}

.page-header.navbar {
  background-color: #16191c !important;
}


.node text {
  pointer-events: none;
}

g.node circle {
  stroke: #333;
  stroke-width: 2px;
}

.circle_node:hover {
  stroke: #333;
  cursor: pointer;
}

.circle_node {
  fill: transparent;
  stroke: #333;
}

.circle_node.selected {
  stroke: red;
}

.add_node_icon {
  stroke: transparent;
  cursor: pointer;
  display: none;
}

g.node .delete {
  transition: stroke .2s;
}

g.node .delete:hover {
  stroke: #cf2929;
}

g.node .delete+text {
  transition: fill .2s;
}

g.node .delete:hover+text {
  fill: #cf2929;
}

g.node .delete:hover {
  cursor: pointer;
}

g.node:hover .add_node_icon {
  display: block;
}

g.selected circle {
  fill: rgb(250, 232, 255);
}

g.selected:hover circle {
  fill: rgb(250, 232, 255);
}

path.link {
  fill: none;
  stroke: #333;
  stroke-width: 2px;
  cursor: default;
}

path.link:hover {
  stroke: rgb(94, 196, 204);
}

g.connect-node circle {
  fill: #BEFFFF;
}

path.link.hidden {
  stroke-width: 0;
}

path.link.selected {
  stroke: rgb(229, 172, 247);
}

.draggable {
  position: absolute;
  z-index: 9;
  background-color: #f1f1f1;
  border: 1px solid #d3d3d3;
  text-align: center;
  width: 150px;
  cursor: move;
}

#right_sidebar {
  width: 300px;
  overflow: hidden;
  background: #EEEEEE;
  -webkit-transition: right 0.3s;
  -moz-transition: right 0.3s;
  -ms-transition: right 0.3s;
  -o-transition: right 0.3s;
  transition: right 0.3s;
  position: fixed;
  right: -320px;
  height: 100vh;
  border-left: 1px solid black;
  display: flex;
  flex-direction: column;
  padding: 10px;
}

#right_sidebar.open {
  right: 0;
}

#right_sidebar i {
  cursor: pointer;
  font-size: 20px;
}

#right_sidebar a {
  cursor: pointer;
  font-size: 20px;
  text-transform: uppercase;
  color: black;
  margin-top: 10px;
}

#drop-zone {
  border: 2px solid red;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  background-color: rgba(255, 255, 255, 0.9);
  z-index: 10;
  opacity: 0;
  display: none;
  transition: opacity .2s;
}

#drop-zone:after {
  content: 'drop zone';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: grey;
  text-transform: uppercase;
}

#drop-zone p {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  font-size: 20px;
  color: grey;
}

#IvrBuilderController {
  background-image: linear-gradient(0deg, transparent 24%, #f2f2f2 25%, #f2f2f2 26%, transparent 27%, transparent 74%, #f2f2f2 75%, #f2f2f2 76%, transparent 77%, transparent), linear-gradient(90deg, transparent 24%, #f2f2f2 25%, #f2f2f2 26%, transparent 27%, transparent 74%, #f2f2f2 75%, #f2f2f2 76%, transparent 77%, transparent);
  background-size: 50px 50px;
}
svg {
  height: 100%;
}

.error {
  /* display: none; */
  color: red;
  margin: 5px 0;
  text-align: left;
}

.muteButton {
  display: none;
}

.zoom_container {
  z-index: 2;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.2/d3.min.js"></script>
<div id="IvrBuilderController">
        <div id="drop-zone" ondrop="drop(event)" ondragover="allowDrop(event)"></div>

        <div id="toolbox">
         
        </div>
        <div id="right_sidebar">
          <i class="fa fa-times" aria-hidden="true" ng-click="closeSidebar()"></i>
          <ul>
            <li><a href="#" id="deleteNode">Delete</a></li>
            <li><a href="#" id="editNodeName">Edit name</a></li>
          </ul>
        </div>
      </div>

0 个答案:

没有答案