Reputation: 5797
I have a rectangular svg that can be dragged around a 2d plane, rotated around it's own origin and resized.
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
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