【问题标题】:How do I maintain the aspect ratio while resizing a d3 svg shape by dragging the corner?如何在通过拖动角调整 d3 svg 形状的大小时保持纵横比?
【发布时间】: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


【解决方案1】:

只需夹住dx/dy 对以适应原始纵横比。以SE句柄为例:

case "SE": {
  const shiftKey = event.sourceEvent.shiftKey;
  let { dx, dy } = event;

  if (shiftKey) {
    if (dx / dy > widthOverHeight) {
      // dx exceed original ratio
      dx = widthOverHeight * dy;
    } else if (dy / dx > heightOverWidth) {
      // dy exceed original ratio
      dy = dx * heightOverWidth;
    }
  }

  width += dx;
  height += dy;
  x += dx / 2;
  y += dy / 2;

  break;
}

【讨论】:

  • 不幸的是,它不适合我。我尝试将其粘贴到我的示例中,并在单击并拖动 SE 手柄时按住 shift 键,但它并没有为我锁定纵横比。
【解决方案2】:

延伸自@hackape 的回答。按下 shift 键时无需检查任何条件。而是直接根据句柄值改变必要的宽度、高度、x、y值。 SE 的句柄在示例中。

<!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;
      const shiftKey = event.sourceEvent.shiftKey;
      let { dx, dy } = event;
      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':
          if (shiftKey) {
              dx = widthOverHeight * dy;
              dy = dx * heightOverWidth;
          }

          width += dx;
          height += dy;
          x += dx / 2;
          y += 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>

【讨论】:

  • 很抱歉,但这不会像问题描述中的示例那样在 x 和 y 方向上拖动。
猜你喜欢
  • 2017-11-16
  • 1970-01-01
  • 2019-08-15
  • 2013-05-06
  • 1970-01-01
  • 2011-11-30
  • 2013-07-29
  • 2020-06-14
  • 2013-06-26
相关资源
最近更新 更多