svg 元素的水平有序装箱
Horizontal ordered bin packing of svg elements
试图找出最好的装箱方式/将一堆已知宽度的 svg 图像分布在水平行中,如果容器的宽度太紧,它们可以相互堆叠。
集装箱高度最好自调,重心在垂直中心点。创建了一些图像来说明所需的解决方案。
是否有任何 JS 库可以解决这个问题,也许是 d3?这感觉像是一个装箱问题,但可能在顺序和引力方面增加了一些复杂性。对 canvas 解决方案不感兴趣。
如果容器足够宽
太紧,堆叠一些元素
再紧一点,全部叠起来
您尝试实现的是砌体布局。这通常在您不知道元素的高度和数量时使用。真正的砌体布局甚至可以使用可变宽度的元素。
这是一个 JSFIDDLE example(调整大小以查看元素如何打包)。
很多代码都需要一些 js,但仍然不是真正的砌体,因为每列的宽度都相同 (here, or here)。尽管您实际上可以通过使用多列布局以及媒体查询以纯 css(无 js)实现此结果。
但是,由于这仅适用于所有元素具有相同宽度的情况,而且您的情况似乎并非如此,我建议您从下面的列表中选择一个:
https://isotope.metafizzy.co/layout-modes/masonry.html
支持很多功能:
- 适合宽度
- Horizontal/vertical 排序
- 装订线
- 完全响应
- 有垂直(默认)砌体模式和水平模式
- https://masonry.desandro.com/v3/
- https://vestride.github.io/Shuffle/
https://codepen.io/jh3y/pen/mPgyqw this one is pure css, but is
trickier to use
这是第一个动作:
我可能忘记了一些,如果是,请在评论区提出,如果确实是真正的砌体(支持可变宽度),按照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 检测。
想法是使用基于您的数据和可用宽度的模拟设置 x
和 y
位置,碰撞基于元素的大小。然后,在调整大小事件中,您 运行 使用新宽度再次模拟。
请记住,与您会发现的大多数 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>
正如我所说,这是一段非常粗糙的代码,仅作为示例。您可以调整优势并改进其他部分,以满足您的需求。
试图找出最好的装箱方式/将一堆已知宽度的 svg 图像分布在水平行中,如果容器的宽度太紧,它们可以相互堆叠。
集装箱高度最好自调,重心在垂直中心点。创建了一些图像来说明所需的解决方案。
是否有任何 JS 库可以解决这个问题,也许是 d3?这感觉像是一个装箱问题,但可能在顺序和引力方面增加了一些复杂性。对 canvas 解决方案不感兴趣。
如果容器足够宽
太紧,堆叠一些元素
再紧一点,全部叠起来
您尝试实现的是砌体布局。这通常在您不知道元素的高度和数量时使用。真正的砌体布局甚至可以使用可变宽度的元素。
这是一个 JSFIDDLE example(调整大小以查看元素如何打包)。
很多代码都需要一些 js,但仍然不是真正的砌体,因为每列的宽度都相同 (here, or here)。尽管您实际上可以通过使用多列布局以及媒体查询以纯 css(无 js)实现此结果。 但是,由于这仅适用于所有元素具有相同宽度的情况,而且您的情况似乎并非如此,我建议您从下面的列表中选择一个:
https://isotope.metafizzy.co/layout-modes/masonry.html
支持很多功能:
- 适合宽度
- Horizontal/vertical 排序
- 装订线
- 完全响应
- 有垂直(默认)砌体模式和水平模式
- https://masonry.desandro.com/v3/
- https://vestride.github.io/Shuffle/
https://codepen.io/jh3y/pen/mPgyqw
this one is pure css, but is trickier to use
这是第一个动作:
我可能忘记了一些,如果是,请在评论区提出,如果确实是真正的砌体(支持可变宽度),按照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 检测。
想法是使用基于您的数据和可用宽度的模拟设置 x
和 y
位置,碰撞基于元素的大小。然后,在调整大小事件中,您 运行 使用新宽度再次模拟。
请记住,与您会发现的大多数 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>
正如我所说,这是一段非常粗糙的代码,仅作为示例。您可以调整优势并改进其他部分,以满足您的需求。