James
James

Reputation: 5797

Resize an SVG rotated around it's origin

I have a rectangular svg that can be dragged around a 2d plane, rotated around it's own origin and resized.

enter image description here

class SVG extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      x: 100,
      y: 100,
      width: 50,
      height: 50,
      angle: 0,
      focusedElement: null
    }
  }
  
  handleMouseDown = (e) => {
    const focusedElement = e.target.getAttribute('data-element-type')
    this.setState({focusedElement})
  }
  
  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})
  }
  
  moveRectangle = (e) => {
    const {width, height} = this.state
    
    this.setState({
      x: e.clientX - width / 2,
      y: e.clientY - height / 2
    })
  }
  
  resizeRectangle = (e) => {
    const {x, y} = this.state
    this.setState({
      width: e.clientX - x,
      height: e.clientY - y
    })
  }
  
  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>

clicking and dragging the body allows moving around the plane, the blue circle on the right rotates, and the bottom right square resizes

resizing, moving around the plane and rotating from 0 degrees all work as desired

issues arise when I attempt to resize after a rotation has occurred, the svg's width and height change as if it has not been rotated

my question is, how do you approach scaling the width, height, x, and y of the shape in order to achieve a UX more like photoshop or how http://editor.method.ac/ handles resizing rotated elements?

Here is the full example in a JSBin https://jsbin.com/mapumif/edit?js,output

note JSBin appears to be buggy so if it doesn't render right away please mash the "Run with JS" button 10x or so

I'm using a react component to keep state but any solution is more than welcome

As always, any and all insights are appreciated, thanks for looking

Upvotes: 3

Views: 1801

Answers (1)

Chris
Chris

Reputation: 56

This solution works by using a matrix transformation to account for the shape's angle when resizing the height and width, while cos and sin functions account for the coordinate change caused by resizing the width and height.

resizeRectangle = (e) => {
const {x, y, angle, width, height} = this.state

const point = this.svg.createSVGPoint()
point.x = e.clientX
point.y = e.clientY


const rotatedPoint = point.matrixTransform(
  this.rect.getScreenCTM().inverse()     
)

const widthDifference = rotatedPoint.x - width
const heightDifference = rotatedPoint.y - height

const widthOriginMovementRight = widthDifference * Math.cos(angle * Math.PI / 180) 
const widthOriginMovementDown = widthDifference * Math.sin(angle * Math.PI / 180)
const heightOriginMovementRight = heightDifference * Math.cos((angle+90) * Math.PI / 180)
const heightOriginMovementDown = heightDifference * Math.sin((angle+90) * Math.PI / 180)
const sumMovementX = widthOriginMovementRight + heightOriginMovementRight - widthDifference
const sumMovementY = widthOriginMovementDown + heightOriginMovementDown - heightDifference


this.setState({
  width: rotatedPoint.x,
  height: rotatedPoint.y,
  x: x + (sumMovementX /2) , 
  y: y + (sumMovementY /2)
})

the same technique used to find the rotated point has to be introduced to the rendering logic as well

 render() {
const {width, height, x, y, angle, focusedElement, start} = this.state


if (this.svg) {
  const point = this.svg.createSVGPoint()
  point.x = x 
  point.y = y
  var rotatedPoint = point.matrixTransform(
    this.rect.getScreenCTM().inverse()
  )
}

and in the return statement

 return (
  <div>
  <svg
    ref={node => this.svg = node}
    viewPort="0 0 300 300"
    style={{width: 300, height: 300, backgroundColor: '#999'}}
    onMouseUp={this.handleMouseUp}
    onMouseMove={this.handleMouseMove}
    onMouseDown={this.handleMouseDown}
  >

    <g
      transform={
       ((!focusedElement && !!rotatedPoint) || focusedElement === 'resize') ?
        `
         translate(${x}, ${y})
         rotate(${angle})
         translate(${-rotatedPoint.x}, ${-rotatedPoint.y})

       `
        :
       `
         translate(${x}, ${y})
         rotate(${angle}, ${(width / 2)}, ${(height / 2)})
       `
      }
      ref={node => this.rect = node}
    >    

Upvotes: 4

Related Questions