旋转后的SVG元素尺寸调整:实现类Photoshop交互体验问询
解决旋转后SVG矩形调整尺寸的坐标转换问题
你的问题核心在于:旋转矩形后,你仍然在使用全局坐标系来计算width和height,但此时矩形的局部坐标系已经和全局坐标系产生了旋转偏移,导致尺寸调整的方向不符合直觉。要实现类似Photoshop的交互,我们需要将鼠标的全局坐标转换到矩形自身的局部坐标系(即旋转前的坐标系)来计算尺寸,同时更新矩形的位置属性。
关键思路
- 坐标转换:将鼠标在全局坐标系中的位置,转换到以矩形旋转中心为原点、旋转了对应角度的局部坐标系中。
- 固定旋转后的锚点:调整尺寸时,保持旋转后的矩形左上角位置不变(和你原始逻辑一致),让右下角跟随鼠标,通过坐标反推新的
width、height和x、y。 - 维护旋转状态:旋转角度始终保持不变,仅更新尺寸和位置。
修改后的完整代码
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>
核心代码解释
handleMouseDown中的锚点记录:当点击调整尺寸的黑方块时,计算并保存旋转后的矩形左上角全局坐标,确保调整尺寸时该点位置固定。resizeRectangle中的坐标转换:- 先计算鼠标(右下角)与固定左上角的全局向量。
- 将这个向量反向旋转(抵消矩形的旋转角度),得到矩形局部坐标系中的宽度和高度。
- 通过新的尺寸和固定锚点,反推矩形的新位置
x、y,确保旋转后的矩形形态符合直觉。
- 绝对值处理:使用
Math.abs()确保width和height始终为正,避免拖动方向导致的负数尺寸。
这样修改后,无论矩形旋转多少角度,调整尺寸时都会沿着矩形自身的边进行,完全符合Photoshop或method.ac编辑器的交互体验。
内容的提问来源于stack exchange,提问作者James
相关产品推荐
相关产品推荐

