import * as React from "react"; import { Vector2 } from "babylonjs/Maths/math.vector"; import { IKeyframeSvgPoint } from "./keyframeSvgPoint"; interface ISvgDraggableAreaProps { keyframeSvgPoints: IKeyframeSvgPoint[]; updatePosition: (updatedKeyframe: IKeyframeSvgPoint, id: string) => void; scale: number; viewBoxScale: number; deselectKeyframes: () => void; removeSelectedKeyframes: (points: IKeyframeSvgPoint[]) => void; panningY: (panningY: number) => void; panningX: (panningX: number) => void; setCurrentFrame: (direction: number) => void; positionCanvas?: Vector2; repositionCanvas?: boolean; canvasPositionEnded: () => void; resetActionableKeyframe: () => void; framesInCanvasView: { from: number; to: number }; framesResized: number; } /** * The SvgDraggableArea is a wrapper for SVG Canvas the interaction * * Here we control the drag and key behavior for the SVG components. */ export class SvgDraggableArea extends React.Component { 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; private _movedX: number; private _movedY: number; private _isControlKeyPress: boolean; readonly _dragBuffer: number; readonly _draggingMultiplier: number; 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._movedX = 0; this._movedY = 0; this._dragBuffer = 3; this._draggingMultiplier = 10; this._isControlKeyPress = 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); } componentDidUpdate(prevProps: ISvgDraggableAreaProps) { if ( this.props.positionCanvas !== prevProps.positionCanvas && this.props.positionCanvas !== undefined && this.props.repositionCanvas ) { this.setState( { panX: this.props.positionCanvas.x, panY: this.props.positionCanvas.y, }, () => { this.props.canvasPositionEnded(); } ); } } dragStart = (e: React.MouseEvent): void => { e.preventDefault(); if ((e.target as SVGSVGElement).classList.contains("draggable")) { this._active = true; const dataId = (e.target as SVGSVGElement).getAttribute("data-id"); if (dataId !== null) { this._currentPointId = dataId; } if ((e.target as SVGSVGElement).classList.contains("control-point")) { const type = (e.target as SVGSVGElement).getAttribute("type"); if (type !== null) { this._isCurrentPointControl = type; } } } if ((e.target as SVGSVGElement).classList.contains("svg-playhead")) { this._active = true; this._playheadSelected = true; this._playheadDrag = e.clientX - e.currentTarget.getBoundingClientRect().left; } if ((e.target as SVGSVGElement).classList.contains("pannable")) { if (this._isControlKeyPress) { this._active = true; this._panStart.set( e.clientX - e.currentTarget.getBoundingClientRect().left, e.clientY - e.currentTarget.getBoundingClientRect().top ); } } }; drag = (e: React.MouseEvent): void => { if (this._active) { e.preventDefault(); var coord = this.getMousePosition(e); if (coord !== undefined) { // Handles the canvas panning if ((e.target as SVGSVGElement).classList.contains("pannable")) { if (this._isControlKeyPress) { if (this._panStart.x !== 0 && this._panStart.y !== 0) { this._panStop.set( e.clientX - e.currentTarget.getBoundingClientRect().left, e.clientY - e.currentTarget.getBoundingClientRect().top ); this.panDirection(); } } } // Handles the playhead dragging if ( e.currentTarget.classList.contains("linear") && this._playheadDrag !== 0 && this._playheadSelected ) { const moving = e.clientX - e.currentTarget.getBoundingClientRect().left; const draggableAreaWidth = e.currentTarget.clientWidth; const initialFrame = this.props.framesInCanvasView.from; const lastFrame = this.props.framesInCanvasView.to; const framesInCanvas = lastFrame - initialFrame; const unit = draggableAreaWidth / framesInCanvas; const newFrame = Math.round(moving / unit) + initialFrame; this.props.setCurrentFrame(newFrame); } else { // Handles the control point dragging var newPoints = [...this.props.keyframeSvgPoints]; let point = newPoints.find((kf) => kf.id === this._currentPointId); if (point) { if (this._isCurrentPointControl === "left") { point.leftControlPoint = coord; point.isLeftActive = true; point.isRightActive = false; } else if (this._isCurrentPointControl === "right") { point.rightControlPoint = coord; point.isRightActive = true; point.isLeftActive = false; } else { point.keyframePoint = coord; point.isRightActive = false; point.isLeftActive = false; } this.props.updatePosition(point, this._currentPointId); } } } } }; dragEnd = (e: React.MouseEvent): 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; this._movedX = 0; this._movedY = 0; }; getMousePosition = (e: React.MouseEvent): Vector2 | undefined => { 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; } }; /** * Handles the canvas panning direction and sets the X and Y values to move the * SVG canvas */ panDirection() { let directionX = 1; if (this._movedX < this._panStop.x) { directionX = -1; //left } else { directionX = 1; //right } let directionY = 1; if (this._movedY < this._panStop.y) { directionY = -1; //top } else { directionY = 1; //bottom } const bufferX = this._movedX === 0 ? 1 : Math.abs(this._movedX - this._panStop.x); const bufferY = this._movedY === 0 ? 1 : Math.abs(this._movedY - this._panStop.y); let xMulti = 0; if (bufferX >= this._dragBuffer) { xMulti = Math.round(Math.abs(bufferX - this._dragBuffer) / 2.5); } let yMulti = 0; if (bufferY >= this._dragBuffer) { yMulti = Math.round(Math.abs(bufferY - this._dragBuffer) / 2.5); } this._movedX = this._panStop.x; this._movedY = this._panStop.y; let newX = this.state.panX + directionX * xMulti; let newY = this.state.panY + directionY * yMulti; this.setState({ panX: Math.round(newX), panY: Math.round(newY), }); this.props.panningY(Math.round(newY)); this.props.panningX(Math.round(newX)); } keyDown(e: KeyboardEvent) { e.preventDefault(); if (e.keyCode === 17 || e.keyCode === 32) { this._draggableArea.current?.style.setProperty("cursor", "grab"); this._isControlKeyPress = true; } } keyUp(e: KeyboardEvent) { e.preventDefault(); if (e.keyCode === 17 || e.keyCode === 32) { this._draggableArea.current?.style.setProperty("cursor", "initial"); this._isControlKeyPress = false; this._active = false; this._panStart.set(0, 0); this._panStop.set(0, 0); this._movedX = 0; this._movedY = 0; } if (e.keyCode === 8 || e.keyCode === 46) { 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() { const viewBoxScaling = `${this.props.positionCanvas?.x} ${this.props.positionCanvas?.y} ${Math.round( this.props.scale * 200 )} ${Math.round(this.props.scale * 100)}`; return ( <> {this.props.children} ); } }