我正在开发一个项目,需要使用 D3.js 创建一个盒子,并且我想实现以下功能:
主框应使用该元素创建。 在盒子的每个角上,我想添加一个小圆圈,用作缩放和旋转锚点。 当用户单击并拖动任何这些角圆时,该框应根据鼠标移动放大或缩小。 如果用户在拖动圆角时按住“Shift”键,则应该触发旋转而不是缩放。
当拖动框上的任何其他位置(不是角圆上)时,应该拖动框
我已经开始使用 D3.js 创建基本的盒子,但我在集成拖动、缩放和旋转功能方面遇到了困难。有人可以提供指导或代码示例来实现这种交互行为吗?
这是我当前的代码片段:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rotating Rectangle with D3</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
</head>
<body>
<script>
// Set up SVG container
const svgWidth = 400;
const svgHeight = 400;
const svg = d3
.select("body")
.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight);
// Set up rectangle
let rectWidth = 100;
let rectHeight = 50;
const rectangle = svg
.append("rect")
.attr("x", (svgWidth - rectWidth) / 2)
.attr("y", (svgHeight - rectHeight) / 2)
.attr("width", rectWidth)
.attr("height", rectHeight)
.attr("fill", "blue")
.attr(
"transform",
"rotate(0, " + svgWidth / 2 + ", " + svgHeight / 2 + ")"
);
// Set up rotation anchor
const rotationAnchor = svg
.append("circle")
.attr("cx", svgWidth / 2)
.attr("cy", (svgHeight - rectHeight) / 2)
.attr("r", 8)
.attr("fill", "red")
.call(d3.drag().on("drag", rotateHandler));
// Set up resize anchor for width
const widthResizeAnchor = svg
.append("circle")
.attr("cx", (svgWidth + rectWidth) / 2)
.attr("cy", (svgHeight - rectHeight) / 2)
.attr("r", 8)
.attr("fill", "green")
.call(d3.drag().on("drag", widthResizeHandler));
// Set up resize anchor for height
const heightResizeAnchor = svg
.append("circle")
.attr("cx", (svgWidth + rectWidth) / 2)
.attr("cy", (svgHeight + rectHeight) / 2)
.attr("r", 8)
.attr("fill", "purple")
.call(d3.drag().on("drag", heightResizeHandler));
// Drag behavior for rotation anchor
function rotateHandler() {
const mouseX = d3.event.x;
const mouseY = d3.event.y;
// Calculate angle of rotation based on mouse position
const angle =
Math.atan2(mouseY - svgHeight / 2, mouseX - svgWidth / 2) *
(180 / Math.PI);
// Update rectangle's rotation around its center
rectangle.attr(
"transform",
"translate(" +
svgWidth / 2 +
"," +
svgHeight / 2 +
") rotate(" +
angle +
") translate(" +
-(svgWidth / 2) +
"," +
-(svgHeight / 2) +
")"
);
}
// Drag behavior for width resize anchor
function widthResizeHandler() {
const mouseX = d3.event.x;
// Calculate the new width
rectWidth = mouseX - rectangle.attr("x");
// Update rectangle's width
rectangle.attr("width", rectWidth);
}
// Drag behavior for height resize anchor
function heightResizeHandler() {
const mouseY = d3.event.y;
// Calculate the new height
rectHeight = mouseY - rectangle.attr("y");
// Update rectangle's height
rectangle.attr("height", rectHeight);
}
</script>
</body>
</html>
使用 d3.js 的交互式框
我做了一个小程序来帮助你开始。再次需要进行一些修复..但我认为您可以找到解决方案来完成您的解决方案
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D3.js Resizable and Draggable Box</title>
<script src="https://d3js.org/d3.v6.min.js"></script>
<style>
svg {
border: 1px solid #ccc;
}
.box {
fill: lightblue;
stroke: #333;
}
.handle {
fill: white;
stroke: #333;
cursor: pointer;
}
</style>
</head>
<body>
<script>
const width = 400;
const height = 300;
const handleRadius = 4;
// Create SVG container
const svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
const drag = d3.drag()
.on("start", dragStart)
.on("drag", dragging)
.on("end", dragEnd);
// Create resizable and draggable box with handles
const box = svg.append("rect")
.attr("x", 50)
.attr("y", 50)
.attr("width", 200)
.attr("height", 150)
.attr("class", "box")
.style("cursor", "pointer")
.call(drag);
function dragStart(event) {
box.raise().classed("active", true);
}
function dragging(event) {
const newX = Math.max(0, Math.min(width - +box.attr("width"), event.x));
const newY = Math.max(0, Math.min(height - +box.attr("height"), event.y));
box.attr("x", newX);
box.attr("y", newY);
// Update handles' positions
handles.data([
{ x: newX, y: newY ,cursor: "nw-resize"},
{ x: newX + +box.attr("width"), y: newY, cursor: "ne-resize" },
{ x: newX, y: newY + +box.attr("height"), cursor: "sw-resize" },
{ x: newX + +box.attr("width"), y: newY + +box.attr("height") , cursor: "se-resize"}
])
.attr("cx", d => d.x)
.attr("cy", d => d.y);
}
function dragEnd(event) {
box.classed("active", false);
}
const handles = svg.selectAll("circle")
.data([
{ x: 50, y: 50, cursor: "nw-resize" },
{ x: 250, y: 50, cursor: "ne-resize" },
{ x: 50, y: 200, cursor: "sw-resize" },
{ x: 250, y: 200, cursor: "se-resize" }
])
.enter().append("circle")
.attr("class", "handle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", handleRadius)
.style("cursor", d => d.cursor)
.call(d3.drag()
.on("start", handleDragStart)
.on("drag", handleDrag)
);
let rotating = false;
d3.select("body")
.on("keydown", (event) => {
if (event.key === "Shift") {
rotating = true;
}
})
.on("keyup", (event) => {
if (event.key === "Shift") {
rotating = false;
}
});
function handleDragStart(event, d) {
d.startX = event.x;
d.startY = event.y;
d.initialBox = {
x: +box.attr("x"),
y: +box.attr("y"),
width: +box.attr("width"),
height: +box.attr("height"),
rotation: +box.attr("transform")?.split("(")[1].split(")")[0] || 0
};
}
function handleDrag(event, d) {
const dx = event.x - d.startX;
const dy = event.y - d.startY;
if (rotating) {
const angle = Math.atan2(event.y - height / 2, event.x - width / 2);
box.attr("transform", `translate(${width / 2},${height / 2}) rotate(${angle * (180 / Math.PI) + 90}) translate(${-
width / 2},${-height / 2})`);
handles.attr("cx", (d, i) => {
const radians = (angle + 90) * (Math.PI / 180);
const distance = Math.sqrt((d.x - box.attr("x")) ** 2 + (d.y - box.attr("y")) ** 2);
return +box.attr("x") + distance * Math.cos(radians);
})
.attr("cy", (d, i) => {
const radians = (angle + 90) * (Math.PI / 180);
const distance = Math.sqrt((d.x - box.attr("x")) ** 2 + (d.y - box.attr("y")) ** 2);
return +box.attr("y") + distance * Math.sin(radians);
});
} else {
if (d.cursor.includes("nw")) {
box.attr("x", Math.min(d.initialBox.x + d.initialBox.width, Math.max(0, d.initialBox.x + dx)));
box.attr("y", Math.min(d.initialBox.y + d.initialBox.height, Math.max(0, d.initialBox.y + dy)));
box.attr("width", Math.max(0, d.initialBox.width - dx));
box.attr("height", Math.max(0, d.initialBox.height - dy));
} else if (d.cursor.includes("se")) {
box.attr("width", Math.max(0, d.initialBox.width + dx));
box.attr("height", Math.max(0, d.initialBox.height + dy));
} else if (d.cursor.includes("sw")) {
box.attr("x", Math.min(d.initialBox.x + d.initialBox.width, Math.max(0, d.initialBox.x + dx)));
box.attr("width", Math.max(0, d.initialBox.width - dx));
box.attr("height", Math.max(0, d.initialBox.height + dy));
} else if (d.cursor.includes("ne")) {
box.attr("y", Math.min(d.initialBox.y + d.initialBox.height, Math.max(0, d.initialBox.y + dy)));
box.attr("width", Math.max(0, d.initialBox.width + dx));
box.attr("height", Math.max(0, d.initialBox.height - dy));
}
}
// Update the position of all circles during resizing
handles.attr("cx", (d, i) => {
let t = 0;
if( i == 0){
t = +box.attr("x");
} else if(i==1 ){
t = +box.attr("x") + +box.attr("width");
} else if (i == 2){
t = +box.attr("x")
} else if (i == 3){
t = +box.attr("x") + +box.attr("width");
}
//const t = (i % 2 === 0) ? +box.attr("x") : +box.attr("x") + +box.attr("width");
//console.log("cx", i, t);
return t;
})
.attr("cy", (d, i) => {
let t = 0;
if( i == 0)
{
t = +box.attr("y");
} else if( i == 1){
t = +box.attr("y");
} else if( i ==2){
t = +box.attr("y") + +box.attr("height");
}else if( i == 3){
t = +box.attr("y") + +box.attr("height");
}
//console.log("cy", i, t);
return t;
//(i < 2) ? +box.attr("y") : +box.attr("y") + +box.attr("height")
});
}
</script>
</body>
</html>