【发布时间】:2023-04-02 02:00:01
【问题描述】:
我正在开发一个 svg 形状工作区,您可以在其中拖动、旋转和调整不同形状的大小。我附上了一个最小的复制品。
我想在拖动调整大小时保持形状的纵横比。到目前为止,我的实现对边正确地做到了这一点,但对角落却没有。我已经尝试了一些错误的开始修复,所以我想我不妨问问你们:
如何在通过拖动角调整大小手柄调整形状大小的同时保持形状的纵横比?
我这样做是为了在您调整大小时将相反的调整大小手柄固定到位,因为我发现这是最自然且最不令人惊讶的。所以拖动W时E调整大小手柄固定到位,拖动SE时NW固定。即使形状本身的纵横比在调整大小时被锁定,也必须如此。
如果您在 figma.com 上调整大小时按住 shift,它会按预期工作:
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
</head>
<body>
<script>
let x = 300;
let y = 100;
let width = 180;
let height = 120;
let rotationAngle = 0;
const ROTATION_HANDLE_RADIUS = 10;
const ROTATION_HANDLE_MARGIN = 12;
const svg = d3.select('body').append('svg')
.attr('width', 600)
.attr('height', 400)
.style('background-color', 'lightgray');
const shapeGroup = svg.append('g')
.call(
d3.drag()
.on('drag', () => onDrag())
);
const rectangle = shapeGroup.append('rect')
.attr('fill', 'rebeccapurple');
const rotationGroup = shapeGroup
.append('g')
.attr('transform', 'translate(0,-2)')
.call(
d3.drag()
.on('drag', () => onRotation())
);
const deviceRotationLine = rotationGroup
.append('line')
.style('outline','1px solid darkblue');
const deviceRotationCircle = rotationGroup
.append('circle')
.style('fill','darkblue')
.style('cursor','grab');
const deviceRotationAngleLabel = shapeGroup
.append('text')
.attr('text-anchor', 'middle')
.style('fill','darkblue')
.attr('alignment-baseline', 'central');
const resizeGroup = shapeGroup.append('g');
const resizeHandles = [
['NW', 'N', 'NE'],
['W', undefined, 'E'],
['SW', 'S', 'SE']
].map((resizeHandleRow) => {
return resizeHandleRow.map((handle) => {
if (!handle) {
return undefined;
}
const resizeCursors = {
'NW': 'nwse-resize',
'N': 'ns-resize',
'NE': 'nesw-resize',
'W': 'ew-resize',
'E': 'ew-resize',
'SW': 'nesw-resize',
'S': 'ns-resize',
'SE': 'nwse-resize'
}
const resizeHandle = resizeGroup
.append('rect')
.attr('width', 8 * 2)
.attr('height', 8 * 2)
.attr('x', -8)
.attr('y', -8)
.attr('cursor', resizeCursors[handle])
.attr('fill', 'fuchsia')
.call(
d3.drag()
.on('drag', () => onResize(handle))
);
return resizeHandle;
});
});
function onRotation() {
function angleBetweenTwoPointsRadians(point1, point2) {
if (point1[0] === point2[0] && point1[1] === point2[1]) {
return Math.PI / 2;
}
return Math.atan2(point2[1] - point1[1], point2[0] - point1[0]);
}
function radiansToDegrees(radians) {
return radians / (Math.PI / 180);
}
function normalizeAngle(angle) {
return Math.round((angle + 360) % 360);
}
const rotateHandleVerticalPos = (height / 2) + ROTATION_HANDLE_MARGIN;
let deltaAngleRadians = angleBetweenTwoPointsRadians([0, 0], [d3.event.x, d3.event.y]);
deltaAngleRadians = deltaAngleRadians - angleBetweenTwoPointsRadians([0, 0], [0, -rotateHandleVerticalPos]);
const deltaAngleDegrees = radiansToDegrees(deltaAngleRadians);
rotationAngle = normalizeAngle(rotationAngle + deltaAngleDegrees);
renderShape();
}
function onResize(handle) {
const event = d3.event;
const heightOverWidth = height / width;
const widthOverHeight = width / height;
const oldX = x;
const oldY = y;
const oldWidth = width;
const oldHeight = height;
switch (handle) {
case 'N':
height += event.y * -1;
y += event.y / 2;
width += event.y * widthOverHeight * -1;
break;
case 'NE':
width += event.dx;
height += event.y * -1;
x += event.dx / 2;
y += event.y / 2;
break;
case 'E':
width += event.dx;
x += event.dx / 2;
height += event.dx * heightOverWidth;
break;
case 'SE':
width += event.dx;
height += event.dy;
x += event.dx / 2;
y += event.dy / 2;
break;
case 'S':
height += event.dy;
width += event.dy * widthOverHeight;
y += event.dy / 2;
break;
case 'SW':
width += event.x * -1;
height += event.dy;
x += event.x / 2;
y += event.dy / 2;
break;
case 'W':
width += event.x * -1;
x += event.x / 2;
height += event.x * heightOverWidth * -1;
break;
case 'NW':
width += event.x * -1;
height += event.y * -1;
x += event.x / 2;
y += event.y / 2;
break;
}
// Enforce min width & height
if (width <= 50 || height <= 50) {
x = oldX;
y = oldY;
width = oldWidth;
height = oldHeight;
}
renderShape();
}
function onDrag() {
x += d3.event.dx;
y += d3.event.dy;
renderShape();
}
function renderShape() {
shapeGroup
.attr('transform', `translate(${x}, ${y}) rotate(${rotationAngle})`);
rectangle
.attr('width', width)
.attr('height', height)
.attr('transform', `translate(${-(width/2)}, ${-(height/2)})`);
resizeGroup.attr('transform', `translate(${-(width/2)}, ${-(height/2)})`)
// Render resize handles
for (const [i, row] of resizeHandles.entries()) {
const offsetY = height * (i / 2);
for (const [j, handle] of row.entries()) {
if (handle) {
const offsetX = width * (j / 2);
handle.attr('transform', `translate(${offsetX}, ${offsetY})`);
}
}
}
// Render rotation handle
const rotateHandleVerticalPos = height / 2 + ROTATION_HANDLE_MARGIN;
deviceRotationLine
.attr('y1', -rotateHandleVerticalPos)
.attr('y2', -(height / 2));
deviceRotationCircle
.attr('cy', -(rotateHandleVerticalPos + ROTATION_HANDLE_RADIUS))
.attr('r', ROTATION_HANDLE_RADIUS);
deviceRotationAngleLabel
.attr(
'transform',
'translate(' +
(ROTATION_HANDLE_RADIUS * 3) + ','
+ -(rotateHandleVerticalPos + ROTATION_HANDLE_MARGIN) +
') rotate(' +
-rotationAngle +
')'
)
.text(rotationAngle + String.fromCharCode(176));
}
renderShape();
</script>
</body>
</html>
【问题讨论】:
-
您是否尝试过使用 viewBox 属性设置宽度和高度?
-
viewBox 仅适用于 svg dom 元素本身,对吧?也许我的问题并不清楚:我想在我的 svg 元素工作区中保持形状的纵横比。
-
我以前见过它实现过。例如。 diagrams.net 和 Libreoffice Draw。在两者中,您在调整大小以锁定纵横比的同时按住 shift 按钮。但我看到 diagrams.net 有点作弊,只在拖动一个角的同时缩放一个轴。不过,它是一个非常简单的解决方法。我很想看到一个类似于 Libreoffice Draw 的解决方案。
-
我在 figma.com 上添加了一个按预期工作的 gif
标签: javascript svg d3.js