svg 元素的水平有序装箱

Horizontal ordered bin packing of svg elements

试图找出最好的装箱方式/将一堆已知宽度的 svg 图像分布在水平行中,如果容器的宽度太紧,它们可以相互堆叠。

集装箱高度最好自调,重心在垂直中心点。创建了一些图像来说明所需的解决方案。

是否有任何 JS 库可以解决这个问题,也许是 d3?这感觉像是一个装箱问题,但可能在顺序和引力方面增加了一些复杂性。对 canvas 解决方案不感兴趣。

如果容器足够宽

太紧,堆叠一些元素

再紧一点,全部叠起来

您尝试实现的是砌体布局。这通常在您不知道元素的高度和数量时使用。真正的砌体布局甚至可以使用可变宽度的元素。

这是一个 JSFIDDLE example(调整大小以查看元素如何打包)。

很多代码都需要一些 js,但仍然不是真正的砌体,因为每列的宽度都相同 (here, or here)。尽管您实际上可以通过使用多列布局以及媒体查询以纯 css(无 js)实现此结果。 但是,由于这仅适用于所有元素具有相同宽度的情况,而且您的情况似乎并非如此,我建议您从下面的列表中选择一个:

我可能忘记了一些,如果是,请在评论区提出,如果确实是真正的砌体(支持可变宽度),按照OP的要求,我会添加到列表。

对于纯基于 D3 的 SVG 解决方案,我的建议是使用我发现的 force simulation with collision detection. The collision detection in the D3 force simulation (d3.forceCollide) is a circular one, that is, it uses the elements' radii as arguments. So, since you have square/rectangular elements, I'm using this rectangular collision 检测。

想法是使用基于您的数据和可用宽度的模拟设置 xy 位置,碰撞基于元素的大小。然后,在调整大小事件中,您 运行 使用新宽度再次模拟。

请记住,与您会发现的大多数 D3 力导向图表相反,我们不想显示整个模拟开发过程,而只想显示 最终位置 。因此,您将设置模拟并立即停止它:

const simulation = d3.forceSimulation(data)
  //etc...
  .stop();

然后,你做:

simulation.tick(n);

或者,在调整大小处理程序中,重新加热它:

simulation.alpha(1).tick(n);

其中 n 是您想要的迭代次数。越多越好,但也越多越慢...

这是一个非常粗略的例子,移动右侧的蓝色手柄来挤压矩形:

const svg = d3.select("svg");
let width = parseInt(svg.style("width"));
const data = d3.range(15).map(d => ({
  id: d,
  size: 5 + (~~(Math.random() * 30))
}));
const collisionForce = rectCollide()
  .size(function(d) {
    return [d.size * 1.2, d.size * 1.2]
  })
const simulation = d3.forceSimulation(data)
  .force("x", d3.forceX(d => (width / data.length) * d.id).strength(0.8))
  .force("y", d3.forceY(d => 100 - d.size / 2).strength(0.1))
  .force("collision", collisionForce.strength(1))
  .stop();

simulation.tick(100);

const rect = svg.selectAll("rect")
  .data(data, d => "id" + d.id);

rect.enter()
  .append("rect")
  .style("fill", d => d3.schemePaired[d.id % 12])
  .attr("x", d => d.x)
  .attr("y", d => d.y)
  .attr("width", d => d.size)
  .attr("height", d => d.size);

const drag = d3.drag()
  .on("drag", function() {
    const width = Math.max(d3.mouse(this.parentNode)[0], 70);
    simulation.nodes(data)
      .force("x", d3.forceX(d => (width / data.length) * d.id).strength(0.8))
      .stop();
    simulation.alpha(1).tick(100);
    const rect = svg.selectAll("rect")
      .data(data, d => "id" + d.id);
    rect.attr("x", d => d.x)
      .attr("y", d => d.y);

    d3.select("#outer").style("width", width + "px");
  });

d3.select("#inner").call(drag);

function rectCollide() {
  var nodes, sizes, masses
  var size = constant([0, 0])
  var strength = 1
  var iterations = 1

  function force() {
    var node, size, mass, xi, yi
    var i = -1
    while (++i < iterations) {
      iterate()
    }

    function iterate() {
      var j = -1
      var tree = d3.quadtree(nodes, xCenter, yCenter).visitAfter(prepare)

      while (++j < nodes.length) {
        node = nodes[j]
        size = sizes[j]
        mass = masses[j]
        xi = xCenter(node)
        yi = yCenter(node)

        tree.visit(apply)
      }
    }

    function apply(quad, x0, y0, x1, y1) {
      var data = quad.data
      var xSize = (size[0] + quad.size[0]) / 2
      var ySize = (size[1] + quad.size[1]) / 2
      if (data) {
        if (data.index <= node.index) {
          return
        }

        var x = xi - xCenter(data)
        var y = yi - yCenter(data)
        var xd = Math.abs(x) - xSize
        var yd = Math.abs(y) - ySize

        if (xd < 0 && yd < 0) {
          var l = Math.sqrt(x * x + y * y)
          var m = masses[data.index] / (mass + masses[data.index])

          if (Math.abs(xd) < Math.abs(yd)) {
            node.vx -= (x *= xd / l * strength) * m
            data.vx += x * (1 - m)
          } else {
            node.vy -= (y *= yd / l * strength) * m
            data.vy += y * (1 - m)
          }
        }
      }

      return x0 > xi + xSize || y0 > yi + ySize ||
        x1 < xi - xSize || y1 < yi - ySize
    }

    function prepare(quad) {
      if (quad.data) {
        quad.size = sizes[quad.data.index]
      } else {
        quad.size = [0, 0]
        var i = -1
        while (++i < 4) {
          if (quad[i] && quad[i].size) {
            quad.size[0] = Math.max(quad.size[0], quad[i].size[0])
            quad.size[1] = Math.max(quad.size[1], quad[i].size[1])
          }
        }
      }
    }
  }

  function xCenter(d) {
    return d.x + d.vx + sizes[d.index][0] / 2
  }

  function yCenter(d) {
    return d.y + d.vy + sizes[d.index][1] / 2
  }

  force.initialize = function(_) {
    sizes = (nodes = _).map(size)
    masses = sizes.map(function(d) {
      return d[0] * d[1]
    })
  }

  force.size = function(_) {
    return (arguments.length ?
      (size = typeof _ === 'function' ? _ : constant(_), force) :
      size)
  }

  force.strength = function(_) {
    return (arguments.length ? (strength = +_, force) : strength)
  }

  force.iterations = function(_) {
    return (arguments.length ? (iterations = +_, force) : iterations)
  }

  return force
};

function constant(_) {
  return function() {
    return _
  }
};
svg {
  width: 100%;
  height: 100%;
}

#outer {
  position: relative;
  width: 95%;
  height: 200px;
}

#inner {
  position: absolute;
  width: 10px;
  top: 0;
  bottom: 0;
  right: -10px;
  background: blue;
  opacity: .5;
  cursor: pointer;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="outer">
  <svg></svg>
  <div id="inner"></div>
</div>

正如我所说,这是一段非常粗糙的代码,仅作为示例。您可以调整优势并改进其他部分,以满足您的需求。