d3.js でドラッグ可能で物理シミュレーションするノードのサンプルとして、女の子がバルーンを持っている絵を描画するプログラムを作ってみました。女の子をドラッグすると風船もついてきます。
ソースコードです。画像URL以外はコピペで動くと思います。
<svg width="400" height="400"></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"
integrity="sha512-M7nHCiNUOwFt6Us3r8alutZLm9qMt4s9951uo8jqO4UwJ1hziseL6O3ndFyigx6+LREfZqnhHxYjKRJ8ZQ69DQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
const width = document.querySelector("svg").clientWidth;
const height = document.querySelector("svg").clientHeight;
const nodeCount = 21;
const nodesData = [];
const imageWidth = 100;
const imageHeight = 100;
class Node {
constructor(index, x, y, radius, width, height, href, imageOffsetX, imageOffsetY,
forceRateY = 1.0
) {
this.index = index;
this.x = x;
this.y = y;
this.radius = radius;
this.width = width;
this.height = height;
this.href = href;
this.imageOffsetX = imageOffsetX;
this.imageOffsetY = imageOffsetY;
this.forceRateY = forceRateY;
}
}
nodesData.push(new Node(0,
width * 0.5, height - 100,
1,
100, 100,
"blog/images/family_fusen_boy_girl.png",
70, 40,
-0.4
));
const radiusRandRate = 0.1;
const radiusMax = 60;
const heightOffset = 100;
const widthOffset = width * 0.9;
for (let i = 1; i < nodeCount; i++) {
const radiusRandWidth = radiusMax * radiusRandRate;
const radius = (radiusRandWidth * Math.random() + radiusMax - radiusRandWidth) * 0.5;
const node = new Node(i,
(width - widthOffset) * Math.random() + widthOffset * 0.5,
(height - heightOffset) * Math.random(),
radius,
0,
0,
null,
0,
0,
);
nodesData.push(node);
}
class Link {
constructor(source, target, distance) {
this.source = source;
this.target = target;
this.distance = distance;
}
}
const linksData = [];
let i = 0;
for (let j = i + 1; j < nodeCount; j++) {
linksData.push(new Link(
i,
j,
Math.random() * 120 + 50 + nodesData[i].radius + nodesData[j].radius)
);
}
const link = d3.select("svg")
.selectAll("line")
.data(linksData)
.enter()
.append("line")
.attr("stroke-width", 1)
.attr("stroke", "gray");
const colorScale = d3.scaleOrdinal(d3.schemeSet3);
const node = d3.select("svg")
.selectAll("circle")
.data(nodesData)
.enter()
.append("circle")
.attr("r", function (d) { return d.radius })
.attr("fill", function (d, i) {
return colorScale(i);
})
.attr("stroke", "black")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
const images = d3.select("svg")
.selectAll("image")
.data(nodesData)
.enter()
.append("image")
.attr("href", d => d.href)
.attr("height", d => d.height)
.attr("width", d => d.width)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
const simulation = d3.forceSimulation()
.force("link",
d3.forceLink()
.distance(function (d) { return d.distance; })
.strength(0.03)
.iterations(16))
.force("collide",
d3.forceCollide()
.radius(function (d) { return d.radius - d.radius * 0.3; })
.strength(1.1)
.iterations(16))
.force("y",
d3.forceY()
.strength(d => d.forceRateY * 0.12)
.y(0))
;
simulation
.nodes(nodesData)
.on("tick", ticked);
simulation.force("link")
.links(linksData)
.id(function (d) { return d.index; });
function ticked() {
link
.attr("x1", function (d) { return d.source.x; })
.attr("y1", function (d) { return d.source.y; })
.attr("x2", function (d) { return d.target.x; })
.attr("y2", function (d) { return d.target.y; });
node
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; });
images
.attr("x", d => d.x - d.imageOffsetX)
.attr("y", d => d.y - d.imageOffsetY);
}
function dragstarted(e, d) {
if (!e.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(e, d) {
d.fx = e.x;
d.fy = e.y;
}
function dragended(e, d) {
if (!e.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
</script>