import * as React from 'react'; import { Vector2 } from 'babylonjs/Maths/math.vector'; import { KeyframeSvgPoint, IKeyframeSvgPoint } from './keyframeSvgPoint'; interface ISvgDraggableAreaProps { keyframeSvgPoints: IKeyframeSvgPoint[]; updatePosition: (updatedKeyframe: IKeyframeSvgPoint, id: string) => void; scale: number; viewBoxScale: number; selectKeyframe: (id: string, multiselect: boolean) => void; selectedControlPoint: (type: string, id: string) => void; deselectKeyframes: () => void; removeSelectedKeyframes: (points: IKeyframeSvgPoint[]) => void; panningY: (panningY: number) => void; panningX: (panningX: number) => void; setCurrentFrame: (direction: number) => void; positionCanvas?: number; repositionCanvas?: boolean; canvasPositionEnded: () => void; resetActionableKeyframe: () => void; } export class SvgDraggableArea extends React.Component< ISvgDraggableAreaProps, { panX: number; panY: number } > { private _active: boolean; private _isCurrentPointControl: string; private _currentPointId: string; private _draggableArea: React.RefObject; private _panStart: Vector2; private _panStop: Vector2; private _playheadDrag: number; private _playheadSelected: boolean; constructor(props: ISvgDraggableAreaProps) { super(props); this._currentPointId = ''; this._isCurrentPointControl = ''; this._draggableArea = React.createRef(); this._panStart = new Vector2(0, 0); this._panStop = new Vector2(0, 0); this._playheadDrag = 0; this._playheadSelected = false; this.state = { panX: 0, panY: 0 }; } componentDidMount() { this._draggableArea.current?.addEventListener( 'keydown', this.keyDown.bind(this) ); this._draggableArea.current?.addEventListener( 'keyup', this.keyUp.bind(this) ); setTimeout(() => { this._draggableArea.current?.clientWidth !== undefined ? this._draggableArea.current?.clientWidth : 0; }, 500); } componentWillReceiveProps(newProps: ISvgDraggableAreaProps) { if ( newProps.positionCanvas !== this.props.positionCanvas && newProps.positionCanvas !== undefined && newProps.repositionCanvas ) { this.setState({ panX: newProps.positionCanvas }, () => { this.props.canvasPositionEnded(); }); } } dragStart(e: React.TouchEvent): void; dragStart(e: React.MouseEvent): void; dragStart(e: any): void { e.preventDefault(); if (e.target.classList.contains('draggable')) { this._active = true; this._currentPointId = e.target.getAttribute('data-id'); if (e.target.classList.contains('control-point')) { this._isCurrentPointControl = e.target.getAttribute('type'); } } if (e.target.classList.contains('svg-playhead')) { this._active = true; this._playheadSelected = true; this._playheadDrag = e.clientX - e.currentTarget.getBoundingClientRect().left; } if (e.target.classList.contains('pannable')) { this._active = true; this._panStart.set(e.clientX, e.clientY); } } drag(e: React.TouchEvent): void; drag(e: React.MouseEvent): void; drag(e: any): void { if (this._active) { e.preventDefault(); var coord = this.getMousePosition(e); if (coord !== undefined) { if (e.target.classList.contains('pannable')) { if (this._panStart.x !== 0 && this._panStart.y !== 0) { this._panStop.set(e.clientX, e.clientY); this.panDirection(); } } if ( e.currentTarget.classList.contains('linear') && this._playheadDrag !== 0 && this._playheadSelected ) { const moving = e.clientX - e.currentTarget.getBoundingClientRect().left; const distance = moving - this._playheadDrag; const draggableAreaWidth = e.currentTarget.clientWidth; const framesInCavas = 20; const unit = draggableAreaWidth / framesInCavas; if (Math.abs(distance) >= unit / 1.25) { this.props.setCurrentFrame(Math.sign(distance)); this._playheadDrag = this._playheadDrag + distance; } } else { var newPoints = [...this.props.keyframeSvgPoints]; let point = newPoints.find((kf) => kf.id === this._currentPointId); if (point) { // Check for NaN values here. if (this._isCurrentPointControl === 'left') { point.leftControlPoint = coord; point.isLeftActive = true; } else if (this._isCurrentPointControl === 'right') { point.rightControlPoint = coord; point.isRightActive = true; } else { point.keyframePoint = coord; point.isRightActive = false; point.isLeftActive = false; } this.props.updatePosition(point, this._currentPointId); } } } } } dragEnd(e: React.TouchEvent): void; dragEnd(e: React.MouseEvent): void; dragEnd(e: any): void { e.preventDefault(); this._active = false; this._currentPointId = ''; this._isCurrentPointControl = ''; this._panStart.set(0, 0); this._panStop.set(0, 0); this._playheadDrag = 0; this._playheadSelected = false; } getMousePosition(e: React.TouchEvent): Vector2 | undefined; getMousePosition( e: React.MouseEvent ): Vector2 | undefined; getMousePosition(e: any): Vector2 | undefined { if (e.touches) { e = e.touches[0]; } if (this._draggableArea.current) { var svg = this._draggableArea.current as SVGSVGElement; var CTM = svg.getScreenCTM(); if (CTM) { return new Vector2( (e.clientX - CTM.e) / CTM.a, (e.clientY - CTM.f) / CTM.d ); } else { return undefined; } } else { return undefined; } } panDirection() { let movementX = this._panStart.x - this._panStop.x; let movementY = this._panStart.y - this._panStop.y; let newX = this.state.panX + movementX / 20; let newY = this.state.panY + movementY / 20; this.setState({ panX: Math.round(newX), panY: Math.round(newY), }); this.props.panningY(Math.round(newY)); this.props.panningX(Math.round(newX)); } panTo(direction: string, value: number) { switch (direction) { case 'left': (this._draggableArea.current ?.parentElement as HTMLDivElement).scrollLeft -= value * 1; break; case 'right': (this._draggableArea.current ?.parentElement as HTMLDivElement).scrollLeft += value * 1; break; case 'top': break; case 'down': break; } } keyDown(e: KeyboardEvent) { e.preventDefault(); if (e.keyCode === 17) { this._draggableArea.current?.style.setProperty('cursor', 'grab'); } } keyUp(e: KeyboardEvent) { e.preventDefault(); if (e.keyCode === 17) { this._draggableArea.current?.style.setProperty('cursor', 'initial'); } if (e.keyCode === 8) { const pointsToDelete = this.props.keyframeSvgPoints.filter( (kf) => kf.selected ); this.props.removeSelectedKeyframes(pointsToDelete); } } focus(e: React.MouseEvent) { e.preventDefault(); this._draggableArea.current?.focus(); if ((e.target as SVGSVGElement).className.baseVal == 'linear pannable') { if (this.isNotControlPointActive()) { this.props.deselectKeyframes(); } this.props.resetActionableKeyframe(); } } isNotControlPointActive() { const activeControlPoints = this.props.keyframeSvgPoints.filter( (x) => x.isLeftActive || x.isRightActive ); if (activeControlPoints.length !== 0) { return false; } else { return true; } } render() { return ( <> this.drag(e)} onTouchMove={(e) => this.drag(e)} onTouchStart={(e) => this.dragStart(e)} onTouchEnd={(e) => this.dragEnd(e)} onMouseDown={(e) => this.dragStart(e)} onMouseUp={(e) => this.dragEnd(e)} onMouseLeave={(e) => this.dragEnd(e)} onClick={(e) => this.focus(e)} viewBox={`${this.state.panX} ${this.state.panY} ${Math.round( this.props.scale * 200 )} ${Math.round(this.props.scale * 100)}`} > {this.props.children} {this.props.keyframeSvgPoints.map((keyframe, i) => ( this.props.selectedControlPoint(type, id) } selectKeyframe={(id: string, multiselect: boolean) => this.props.selectKeyframe(id, multiselect) } /> ))} ); } }