You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

旋转后的SVG元素尺寸调整:实现类Photoshop交互体验问询

解决旋转后SVG矩形调整尺寸的坐标转换问题

你的问题核心在于:旋转矩形后,你仍然在使用全局坐标系来计算width和height,但此时矩形的局部坐标系已经和全局坐标系产生了旋转偏移,导致尺寸调整的方向不符合直觉。要实现类似Photoshop的交互,我们需要将鼠标的全局坐标转换到矩形自身的局部坐标系(即旋转前的坐标系)来计算尺寸,同时更新矩形的位置属性。

关键思路

  1. 坐标转换:将鼠标在全局坐标系中的位置,转换到以矩形旋转中心为原点、旋转了对应角度的局部坐标系中。
  2. 固定旋转后的锚点:调整尺寸时,保持旋转后的矩形左上角位置不变(和你原始逻辑一致),让右下角跟随鼠标,通过坐标反推新的width、height和x、y。
  3. 维护旋转状态:旋转角度始终保持不变,仅更新尺寸和位置。

修改后的完整代码

class SVG extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      x: 100,
      y: 100,
      width: 50,
      height: 50,
      angle: 0,
      focusedElement: null,
      fixedLeftTop: null // 记录旋转后固定的左上角全局坐标
    }
  }

  handleMouseDown = (e) => {
    const focusedElement = e.target.getAttribute('data-element-type')
    let fixedLeftTop = null

    // 当开始调整尺寸时,计算旋转后的左上角全局坐标并保存
    if (focusedElement === 'resize') {
      const { x, y, width, height, angle } = this.state
      const rad = angle * Math.PI / 180
      const centerX = x + width / 2
      const centerY = y + height / 2

      // 计算旋转后的左上角全局坐标
      fixedLeftTop = {
        x: centerX + (x - centerX) * Math.cos(rad) - (y - centerY) * Math.sin(rad),
        y: centerY + (x - centerX) * Math.sin(rad) + (y - centerY) * Math.cos(rad)
      }
    }

    this.setState({ focusedElement, fixedLeftTop })
  }

  handleMouseMove = (e) => {
    const { focusedElement } = this.state
    if (!focusedElement) return
    else if (focusedElement === 'rectangle') this.moveRectangle(e)
    else if (focusedElement === 'resize') this.resizeRectangle(e)
    else if (focusedElement === 'rotate') this.rotateRectangle(e)
  }

  handleMouseUp = () => {
    this.setState({ focusedElement: null, fixedLeftTop: null })
  }

  moveRectangle = (e) => {
    const { width, height } = this.state
    this.setState({
      x: e.clientX - width / 2,
      y: e.clientY - height / 2
    })
  }

  resizeRectangle = (e) => {
    const { fixedLeftTop, angle } = this.state
    if (!fixedLeftTop) return

    const { x: fixedX, y: fixedY } = fixedLeftTop
    const newRightBottomX = e.clientX
    const newRightBottomY = e.clientY
    const rad = angle * Math.PI / 180

    // 计算全局坐标系中,右下角到左上角的向量
    const dxGlobal = newRightBottomX - fixedX
    const dyGlobal = newRightBottomY - fixedY

    // 将全局向量旋转 -angle 转换为局部坐标系中的尺寸(width和height)
    const newWidth = dxGlobal * Math.cos(rad) + dyGlobal * Math.sin(rad)
    const newHeight = -dxGlobal * Math.sin(rad) + dyGlobal * Math.cos(rad)

    // 计算新的矩形中心全局坐标
    const centerGlobalX = fixedX + dxGlobal / 2
    const centerGlobalY = fixedY + dyGlobal / 2

    // 计算局部坐标系中心到原点的偏移,旋转后得到全局偏移,从而反推新的x、y
    const localCenterX = Math.abs(newWidth) / 2
    const localCenterY = Math.abs(newHeight) / 2
    const dxGlobalOffset = localCenterX * Math.cos(rad) - localCenterY * Math.sin(rad)
    const dyGlobalOffset = localCenterX * Math.sin(rad) + localCenterY * Math.cos(rad)

    const newX = centerGlobalX - dxGlobalOffset
    const newY = centerGlobalY - dyGlobalOffset

    this.setState({
      x: newX,
      y: newY,
      width: Math.abs(newWidth),
      height: Math.abs(newHeight)
    })
  }

  rotateRectangle = (e) => {
    const { x, y, width, height } = this.state
    const origin = {
      x: x + (width / 2),
      y: y + (height / 2),
    }
    const angle = Math.atan2(
      e.clientY - origin.y,
      e.clientX - origin.x
    ) * 180 / Math.PI
    this.setState({ angle })
  }

  render() {
    const { width, height, x, y, angle } = this.state
    return (
      <svg viewPort="0 0 300 300" style={{ width: 300, height: 300, backgroundColor: '#999' }} onMouseUp={this.handleMouseUp} onMouseMove={this.handleMouseMove} onMouseDown={this.handleMouseDown} >
        <g transform={` translate(${x}, ${y}) rotate(${angle}, ${(width / 2)}, ${(height / 2)}) `} >
          <rect width={width} height={height} fill="salmon" data-element-type="rectangle" />
          <rect width={10} height={10} x={width - 10} y={height - 10} data-element-type="resize" fill="black" />
          <circle r="7" cx={width + 7} cy={height / 2} data-element-type="rotate" fill="blue" />
        </g>
      </svg>
    )
  }
}

ReactDOM.render(<SVG />, document.getElementById('app'))
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>

核心代码解释

  1. handleMouseDown中的锚点记录:当点击调整尺寸的黑方块时,计算并保存旋转后的矩形左上角全局坐标,确保调整尺寸时该点位置固定。
  2. resizeRectangle中的坐标转换:
    • 先计算鼠标(右下角)与固定左上角的全局向量。
    • 将这个向量反向旋转(抵消矩形的旋转角度),得到矩形局部坐标系中的宽度和高度。
    • 通过新的尺寸和固定锚点,反推矩形的新位置x、y,确保旋转后的矩形形态符合直觉。
  3. 绝对值处理:使用Math.abs()确保width和height始终为正,避免拖动方向导致的负数尺寸。

这样修改后,无论矩形旋转多少角度,调整尺寸时都会沿着矩形自身的边进行,完全符合Photoshop或method.ac编辑器的交互体验。

内容的提问来源于stack exchange,提问作者James

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 06:39:17