import React from 'react' import PropTypes from 'prop-types' import clone from 'lodash/lang/clone' import cloneDeep from 'lodash/lang/cloneDeep' import assign from 'lodash/object/assign' import shallowEquals from 'shallow-equals' import TranshandDesign from './TranshandDesign' import DefaultCoordinator from './DefaultCoordinator' import Cursor from './Cursor' import fixCursorStyle from './fixCursorStyle' import CursorHint from './cursorHint/CursorHint' import {radDiff, distToSegment, distToPointInAngle, isInside, equPoints} from './utils' export default class Transhand extends React.Component { static propTypes = { transform: PropTypes.shape({ tx: PropTypes.number.isRequired, ty: PropTypes.number.isRequired, sx: PropTypes.number.isRequired, sy: PropTypes.number.isRequired, rz: PropTypes.number.isRequired, ox: PropTypes.number.isRequired, oy: PropTypes.number.isRequired, }).isRequired, transformTypes: PropTypes.array, rect: PropTypes.shape({ x: PropTypes.number.isRequired, y: PropTypes.number.isRequired, w: PropTypes.number.isRequired, h: PropTypes.number.isRequired, }), rotateFingerDist: PropTypes.number, originRadius: PropTypes.number, coordinator: PropTypes.shape({ localToGlobal: PropTypes.func.isRequired, globalToLocal: PropTypes.func.isRequired, }), stroke: PropTypes.object, DesignComponent:PropTypes.func, hitns:PropTypes.object, CursorHintDesignComponent:PropTypes.func, onChange:PropTypes.func, onClick:PropTypes.func, onStartDrag:PropTypes.func, onEndDrag:PropTypes.func, grabEvent:PropTypes.any, cursor:PropTypes.object, } static defaultProps = { transform: { tx: 0, ty: 0, sx: 1, sy: 1, rz: 0, ox: 0.5, oy: 0.5, }, rect: { x: 0, y: 0, w: 0, h: 0, }, transformTypes: ['translate', 'rotate', 'scale', 'origin'], rotateFingerDist: 16, originRadius: 6, coordinator: new DefaultCoordinator(), stroke: { strokeWidth: '1', stroke: 'lime', }, DesignComponent: TranshandDesign, grabEvent: null, cursor: new Cursor(), hints: { scaleXY: [ 'shift - keep shape', 'alt - from the opposite side' ], scaleXorY: [ 'alt - from the opposite side' ], rotate: [ 'shift - 15° steps' ], move: [ 'shift - move in one dimension' ], } } constructor(props) { super(props) this.state = { points: [{}, {}, {}, {}], pOrigin: {}, } } componentWillMount() { this.refreshPoints() window.addEventListener('mousemove', this.handleMouseMove) } componentWillUnmount() { window.removeEventListener('mousemove', this.handleMouseMove) } componentWillReceiveProps(nextProps) { this.refreshPoints(nextProps) } shouldComponentUpdate(nextProps, nextState) { var {points, pOrigin} = this.state return !( shallowEquals(this.props, nextProps) && shallowEquals(this.state, nextState) && shallowEquals(this.props.stroke, nextProps.stroke) && equPoints(pOrigin, nextState.pOrigin) && equPoints(points[0], nextState.points[0]) && equPoints(points[1], nextState.points[1]) && equPoints(points[2], nextState.points[2]) && equPoints(points[3], nextState.points[3])) } componentDidMount() { var {grabEvent} = this.props if (grabEvent) { this.handleNewGrabEvent(grabEvent) } } componentDidUpdate(prevProps) { var {grabEvent} = this.props if (grabEvent && grabEvent !== prevProps.grabEvent) { this.handleNewGrabEvent(grabEvent) } } handleNewGrabEvent(grabEvent) { this._finger = 'move' this.handleMouseDown(grabEvent) } allowed(transformType) { return this.props.transformTypes.indexOf(transformType) !== -1 } refreshPoints(props) { props = props || this.props var {rect, transform} = props, p = this.state.points.map(point => clone(point)), po = clone(this.state.pOrigin) rect = clone(rect) rect.x += transform.tx rect.y += transform.ty po.x = rect.x + (rect.w * transform.ox) po.y = rect.y + (rect.h * transform.oy) var tox = rect.x + transform.ox * rect.w, toy = rect.y + transform.oy * rect.h t(p[0], rect.x, rect.y) t(p[1], rect.x + rect.w, rect.y) t(p[2], rect.x + rect.w, rect.y + rect.h) t(p[3], rect.x, rect.y + rect.h) this.setState({ points: p, pOrigin: po, }) function t(point, x, y) { var dx = (x - tox) * transform.sx var dy = (y - toy) * transform.sy var d = Math.sqrt(dx**2 + dy**2) var rad = Math.atan2(dy, dx) + transform.rz var nx = Math.cos(rad) var ny = Math.sin(rad) var rx = d * nx var ry = d * ny var px = tox + rx var py = toy + ry point.x = px point.y = py } } setHint(name) { var {hints} = this.props this.setState({ activeHint: hints && hints[name] }) } // Event Handlers //////////////////////////////////////////////////////////// handleMouseDown = (e) => { if (!this._finger) { return } e.stopPropagation() e.preventDefault() this._isDraggedSinceDown = false this._isHandle = true // ReactDOM.findDOMNode(this).style.pointerEvents = 'auto' this._mdPos = { m: this.props.coordinator.globalToLocal({x: e.clientX, y: e.clientY}), transform: cloneDeep(this.props.transform), points: cloneDeep(this.state.points), pOrigin: clone(this.state.pOrigin) } window.addEventListener('mouseup', this.handleMouseUp) window.addEventListener('mouseleave', this.handleMouseUp) // window.addEventListener('mousemove', this.handleDrag) if (this.props.onStartDrag) { this.props.onStartDrag() } } handleMouseUp = (e) => { window.removeEventListener('mouseup', this.handleMouseUp) window.removeEventListener('mouseleave', this.handleMouseUp) // window.removeEventListener('mousemove', this.handleDrag) e.stopPropagation() e.preventDefault() if (this._rafOnDragRafId) { this.deferredHandleDrag() } this._isHandle = false //hack! fix to click behind the handler on releasing it // var deRoot = ReactDOM.findDOMNode(this) // setTimeout(() => { // deRoot.style.pointerEvents = 'none' // }) if (this.props.onEndDrag) { this.props.onEndDrag() } if (!this._isDraggedSinceDown && this.props.onClick) { this.props.onClick(e) } } handleMouseMove = (e) => { this._isDraggedSinceDown = true if (this._isHandle) { this._onDragMe = e if (!this._rafOnDragRafId) { this._rafOnDragRafId = requestAnimationFrame(this.deferredHandleDrag) } this.setHint(null) } else /*if (this.state.hoverHitbox)*/ { this._finger = this.getFinger(e) } var cursor = this.getCursor(e) this.setState({cursor}) } deferredHandleDrag = () => { var e = this._onDragMe this._onDragMe = undefined window.cancelAnimationFrame(this._rafOnDragRafId) this._rafOnDragRafId = undefined var {rect, transform, coordinator, onChange} = this.props var {pOrigin} = this.state var md = this._mdPos var m = coordinator.globalToLocal({x: e.clientX, y: e.clientY}) var dx = m.x - md.m.x var dy = m.y - md.m.y var alt = e.altKey var shift = e.shiftKey var change = {} var finger = this._finger if (finger === 'origin') { setOrigin() } if (finger === 'move') { setTransform() } if (finger && finger.charAt(0) === '1') { setScale(-Math.PI/2, 'sy', -1) } if (finger && finger.charAt(1) === '1') { setScale(0, 'sx', 1) } if (finger && finger.charAt(2) === '1') { setScale(Math.PI/2, 'sy', 1) } if (finger && finger.charAt(3) === '1') { setScale(Math.PI, 'sx', -1) } if (finger === 'rotate') { setRotation() } if (shift && 'sx' in change && 'sy' in change) { fixProportion() } if (onChange) { onChange(change) } function setScale(r, sN, way) { var rad = r + md.transform.rz var mdDist = distToPointInAngle(md.pOrigin, md.m, rad) var dragDist = distToPointInAngle(md.pOrigin, m, rad) var scale = (dragDist / mdDist) * md.transform[sN] if (alt) { let es = (scale - md.transform[sN]) / 2 let tN = 't' + sN.charAt(1) let dN = sN.charAt(1) === 'x' ? 'w' : 'h' scale -= es change[tN] = transform[tN] = md.transform[tN] + rect[dN] * (es / 2) * way } change[sN] = transform[sN] = scale } function fixProportion() { var mx = m.x - pOrigin.x var my = m.y - pOrigin.y var mr = Math.abs(radDiff(transform.rz, Math.atan2(my, mx))) var isVertical = mr > Math.PI/4 && mr < Math.PI/4 * 3 var spx = transform.sx / md.transform.sx var spy = transform.sy / md.transform.sy spx *= spx < 0 ? -1 : 1 spy *= spy < 0 ? -1 : 1 var sp = isVertical ? spy : spx change.sx = transform.sx = md.transform.sx * sp change.sy = transform.sy = md.transform.sy * sp } function setRotation() { var mdx = md.m.x - pOrigin.x, mdy = md.m.y - pOrigin.y, mdr = Math.atan2(mdy, mdx), mx = m.x - pOrigin.x, my = m.y - pOrigin.y, mr = Math.atan2(my, mx), r = mr - mdr if (shift) { r = Math.floor(r / (Math.PI / 12)) * (Math.PI / 12) } change.rz = transform.rz = md.transform.rz + r } function setTransform() { if (shift) { if (Math.abs(dx) > Math.abs(dy)) { change.tx = transform.tx = md.transform.tx + dx change.ty = transform.ty = md.transform.ty } else { change.tx = transform.tx = md.transform.tx change.ty = transform.ty = md.transform.ty + dy } } else { change.tx = transform.tx = md.transform.tx + dx change.ty = transform.ty = md.transform.ty + dy } } function setOrigin() { var mx = m.x - md.pOrigin.x, my = m.y - md.pOrigin.y, dist = Math.sqrt(mx ** 2 + my ** 2), r = Math.atan2(my, mx) - transform.rz, x = (Math.cos(r) * dist) / transform.sx, y = (Math.sin(r) * dist) / transform.sy x = parseInt(x * 1000) / 1000 //?hack?? y = parseInt(y * 1000) / 1000 change.ox = transform.ox = md.transform.ox + (x / rect.w) change.oy = transform.oy = md.transform.oy + (y / rect.h) change.tx = transform.tx = md.transform.tx + (mx - x) change.ty = transform.ty = md.transform.ty + (my - y) } } getFinger(e) { var {rect, transform, coordinator, originRadius} = this.props var p = this.state.points var po = this.state.pOrigin var diff = 3 var rDiff = 16 var m = coordinator.globalToLocal({x: e.clientX, y: e.clientY}) var dox = po.x - m.x var doy = po.y - m.y var dOrigin = Math.sqrt(dox**2 + doy**2) var dTop = distToSegment(m, p[0], p[1]) var dLeft = distToSegment(m, p[1], p[2]) var dBottom = distToSegment(m, p[2], p[3]) var dRight = distToSegment(m, p[3], p[0]) var top = dTop < diff var left = dLeft < diff var bottom = dBottom < diff var right = dRight < diff var inside = isInside(m, p) var finger = null if (rect.w * transform.sx < diff * 2 && inside) { left = false right = false } if (rect.h * transform.sy < diff * 2 && inside) { top = false bottom = false } if (dOrigin < originRadius) { if (this.allowed('origin')) { finger = 'origin' this.setHint(null) } } else if (this.allowed('scale') && (top || right || bottom || left)) { //TODO its sould be top-right-bottom-left finger = (top ? '1' : '0') + (left ? '1' : '0') + (bottom ? '1' : '0') + (right ? '1' : '0') if ((finger + finger).indexOf('11') !== -1) { this.setHint('scaleXY') } else { this.setHint('scaleXorY') } } else if (inside) { if (this.allowed('translate')) { finger = 'move' this.setHint('move') } } else if (dTop < rDiff || dRight < rDiff || dBottom < rDiff || dLeft < rDiff || dOrigin < rDiff) { if (this.allowed('rotate')) { finger = 'rotate' this.setHint('rotate') } } if (finger === null) { this.setHint(null) } return finger } getCursor(e) { return this.props.cursor.getCursor( this.props, assign({finger: this._finger}, this.state), e.clientX, e.clientY ) } getHitEvents = () => { //TODO onFirstMove - handle when transhand appears under the mouse return { onMouseEnter: () => this.setState({hoverHitbox: true}), onMouseLeave: () => this.setState({hoverHitbox: false}), onMouseDown: e => this.handleMouseDown(e), } } render() { var {DesignComponent, CursorHintDesignComponent, ...props} = this.props var {cursor, points, pOrigin, activeHint} = this.state var style = { cursor, position: 'fixed', pointerEvents: this._isHandle ? 'auto' : 'none', left: '0px', top: '0px', width: '100%', height: '100%', } return
e.stopPropagation()}>
} componentDidUpdate() { fixCursorStyle(this.refs.root, this.state.cursor)//HACK } }