svgDraggableArea.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. import * as React from "react";
  2. import { Vector2 } from "babylonjs/Maths/math.vector";
  3. import { IKeyframeSvgPoint } from "./keyframeSvgPoint";
  4. interface ISvgDraggableAreaProps {
  5. keyframeSvgPoints: IKeyframeSvgPoint[];
  6. updatePosition: (updatedKeyframe: IKeyframeSvgPoint, id: string) => void;
  7. scale: number;
  8. viewBoxScale: number;
  9. deselectKeyframes: () => void;
  10. removeSelectedKeyframes: (points: IKeyframeSvgPoint[]) => void;
  11. panningY: (panningY: number) => void;
  12. panningX: (panningX: number) => void;
  13. setCurrentFrame: (direction: number) => void;
  14. positionCanvas?: Vector2;
  15. repositionCanvas?: boolean;
  16. canvasPositionEnded: () => void;
  17. resetActionableKeyframe: () => void;
  18. framesInCanvasView: { from: number; to: number };
  19. framesResized: number;
  20. }
  21. /**
  22. * The SvgDraggableArea is a wrapper for SVG Canvas the interaction
  23. *
  24. * Here we control the drag and key behavior for the SVG components.
  25. */
  26. export class SvgDraggableArea extends React.Component<ISvgDraggableAreaProps, { panX: number; panY: number }> {
  27. private _active: boolean;
  28. private _isCurrentPointControl: string;
  29. private _currentPointId: string;
  30. private _draggableArea: React.RefObject<SVGSVGElement>;
  31. private _panStart: Vector2;
  32. private _panStop: Vector2;
  33. private _playheadDrag: number;
  34. private _playheadSelected: boolean;
  35. private _movedX: number;
  36. private _movedY: number;
  37. private _isControlKeyPress: boolean;
  38. readonly _dragBuffer: number;
  39. readonly _draggingMultiplier: number;
  40. constructor(props: ISvgDraggableAreaProps) {
  41. super(props);
  42. this._currentPointId = "";
  43. this._isCurrentPointControl = "";
  44. this._draggableArea = React.createRef();
  45. this._panStart = new Vector2(0, 0);
  46. this._panStop = new Vector2(0, 0);
  47. this._playheadDrag = 0;
  48. this._playheadSelected = false;
  49. this._movedX = 0;
  50. this._movedY = 0;
  51. this._dragBuffer = 3;
  52. this._draggingMultiplier = 10;
  53. this._isControlKeyPress = false;
  54. this.state = { panX: 0, panY: 0 };
  55. }
  56. componentDidMount() {
  57. this._draggableArea.current?.addEventListener("keydown", this.keyDown.bind(this));
  58. this._draggableArea.current?.addEventListener("keyup", this.keyUp.bind(this));
  59. setTimeout(() => {
  60. this._draggableArea.current?.clientWidth !== undefined ? this._draggableArea.current?.clientWidth : 0;
  61. }, 500);
  62. }
  63. componentDidUpdate(prevProps: ISvgDraggableAreaProps) {
  64. if (
  65. this.props.positionCanvas !== prevProps.positionCanvas &&
  66. this.props.positionCanvas !== undefined &&
  67. this.props.repositionCanvas
  68. ) {
  69. this.setState(
  70. {
  71. panX: this.props.positionCanvas.x,
  72. panY: this.props.positionCanvas.y,
  73. },
  74. () => {
  75. this.props.canvasPositionEnded();
  76. }
  77. );
  78. }
  79. }
  80. dragStart = (e: React.MouseEvent<SVGSVGElement, MouseEvent>): void => {
  81. e.preventDefault();
  82. if ((e.target as SVGSVGElement).classList.contains("draggable")) {
  83. this._active = true;
  84. const dataId = (e.target as SVGSVGElement).getAttribute("data-id");
  85. if (dataId !== null) {
  86. this._currentPointId = dataId;
  87. }
  88. if ((e.target as SVGSVGElement).classList.contains("control-point")) {
  89. const type = (e.target as SVGSVGElement).getAttribute("type");
  90. if (type !== null) {
  91. this._isCurrentPointControl = type;
  92. }
  93. }
  94. }
  95. if ((e.target as SVGSVGElement).classList.contains("svg-playhead")) {
  96. this._active = true;
  97. this._playheadSelected = true;
  98. this._playheadDrag = e.clientX - e.currentTarget.getBoundingClientRect().left;
  99. }
  100. if ((e.target as SVGSVGElement).classList.contains("pannable")) {
  101. if (this._isControlKeyPress) {
  102. this._active = true;
  103. this._panStart.set(
  104. e.clientX - e.currentTarget.getBoundingClientRect().left,
  105. e.clientY - e.currentTarget.getBoundingClientRect().top
  106. );
  107. }
  108. }
  109. };
  110. drag = (e: React.MouseEvent<SVGSVGElement, MouseEvent>): void => {
  111. if (this._active) {
  112. e.preventDefault();
  113. var coord = this.getMousePosition(e);
  114. if (coord !== undefined) {
  115. // Handles the canvas panning
  116. if ((e.target as SVGSVGElement).classList.contains("pannable")) {
  117. if (this._isControlKeyPress) {
  118. if (this._panStart.x !== 0 && this._panStart.y !== 0) {
  119. this._panStop.set(
  120. e.clientX - e.currentTarget.getBoundingClientRect().left,
  121. e.clientY - e.currentTarget.getBoundingClientRect().top
  122. );
  123. this.panDirection();
  124. }
  125. }
  126. }
  127. // Handles the playhead dragging
  128. if (
  129. e.currentTarget.classList.contains("linear") &&
  130. this._playheadDrag !== 0 &&
  131. this._playheadSelected
  132. ) {
  133. const moving = e.clientX - e.currentTarget.getBoundingClientRect().left;
  134. const draggableAreaWidth = e.currentTarget.clientWidth;
  135. const initialFrame = this.props.framesInCanvasView.from;
  136. const lastFrame = this.props.framesInCanvasView.to;
  137. const framesInCanvas = lastFrame - initialFrame;
  138. const unit = draggableAreaWidth / framesInCanvas;
  139. const newFrame = Math.round(moving / unit) + initialFrame;
  140. this.props.setCurrentFrame(newFrame);
  141. } else {
  142. // Handles the control point dragging
  143. var newPoints = [...this.props.keyframeSvgPoints];
  144. let point = newPoints.find((kf) => kf.id === this._currentPointId);
  145. if (point) {
  146. if (this._isCurrentPointControl === "left") {
  147. point.leftControlPoint = coord;
  148. point.isLeftActive = true;
  149. point.isRightActive = false;
  150. } else if (this._isCurrentPointControl === "right") {
  151. point.rightControlPoint = coord;
  152. point.isRightActive = true;
  153. point.isLeftActive = false;
  154. } else {
  155. point.keyframePoint = coord;
  156. point.isRightActive = false;
  157. point.isLeftActive = false;
  158. }
  159. this.props.updatePosition(point, this._currentPointId);
  160. }
  161. }
  162. }
  163. }
  164. };
  165. dragEnd = (e: React.MouseEvent<SVGSVGElement, MouseEvent>): void => {
  166. e.preventDefault();
  167. this._active = false;
  168. this._currentPointId = "";
  169. this._isCurrentPointControl = "";
  170. this._panStart.set(0, 0);
  171. this._panStop.set(0, 0);
  172. this._playheadDrag = 0;
  173. this._playheadSelected = false;
  174. this._movedX = 0;
  175. this._movedY = 0;
  176. };
  177. getMousePosition = (e: React.MouseEvent<SVGSVGElement, MouseEvent>): Vector2 | undefined => {
  178. if (this._draggableArea.current) {
  179. var svg = this._draggableArea.current as SVGSVGElement;
  180. var CTM = svg.getScreenCTM();
  181. if (CTM) {
  182. return new Vector2((e.clientX - CTM.e) / CTM.a, (e.clientY - CTM.f) / CTM.d);
  183. } else {
  184. return undefined;
  185. }
  186. } else {
  187. return undefined;
  188. }
  189. };
  190. /**
  191. * Handles the canvas panning direction and sets the X and Y values to move the
  192. * SVG canvas
  193. */
  194. panDirection() {
  195. let directionX = 1;
  196. if (this._movedX < this._panStop.x) {
  197. directionX = -1; //left
  198. } else {
  199. directionX = 1; //right
  200. }
  201. let directionY = 1;
  202. if (this._movedY < this._panStop.y) {
  203. directionY = -1; //top
  204. } else {
  205. directionY = 1; //bottom
  206. }
  207. const bufferX = this._movedX === 0 ? 1 : Math.abs(this._movedX - this._panStop.x);
  208. const bufferY = this._movedY === 0 ? 1 : Math.abs(this._movedY - this._panStop.y);
  209. let xMulti = 0;
  210. if (bufferX >= this._dragBuffer) {
  211. xMulti = Math.round(Math.abs(bufferX - this._dragBuffer) / 2.5);
  212. }
  213. let yMulti = 0;
  214. if (bufferY >= this._dragBuffer) {
  215. yMulti = Math.round(Math.abs(bufferY - this._dragBuffer) / 2.5);
  216. }
  217. this._movedX = this._panStop.x;
  218. this._movedY = this._panStop.y;
  219. let newX = this.state.panX + directionX * xMulti;
  220. let newY = this.state.panY + directionY * yMulti;
  221. this.setState({
  222. panX: Math.round(newX),
  223. panY: Math.round(newY),
  224. });
  225. this.props.panningY(Math.round(newY));
  226. this.props.panningX(Math.round(newX));
  227. }
  228. keyDown(e: KeyboardEvent) {
  229. e.preventDefault();
  230. if (e.keyCode === 17 || e.keyCode === 32) {
  231. this._draggableArea.current?.style.setProperty("cursor", "grab");
  232. this._isControlKeyPress = true;
  233. }
  234. }
  235. keyUp(e: KeyboardEvent) {
  236. e.preventDefault();
  237. if (e.keyCode === 17 || e.keyCode === 32) {
  238. this._draggableArea.current?.style.setProperty("cursor", "initial");
  239. this._isControlKeyPress = false;
  240. this._active = false;
  241. this._panStart.set(0, 0);
  242. this._panStop.set(0, 0);
  243. this._movedX = 0;
  244. this._movedY = 0;
  245. }
  246. if (e.keyCode === 8 || e.keyCode === 46) {
  247. const pointsToDelete = this.props.keyframeSvgPoints.filter((kf) => kf.selected);
  248. this.props.removeSelectedKeyframes(pointsToDelete);
  249. }
  250. }
  251. focus = (e: React.MouseEvent<SVGSVGElement>) => {
  252. e.preventDefault();
  253. this._draggableArea.current?.focus();
  254. if ((e.target as SVGSVGElement).className.baseVal == "linear pannable") {
  255. if (this.isNotControlPointActive()) {
  256. this.props.deselectKeyframes();
  257. }
  258. this.props.resetActionableKeyframe();
  259. }
  260. };
  261. isNotControlPointActive() {
  262. const activeControlPoints = this.props.keyframeSvgPoints.filter((x) => x.isLeftActive || x.isRightActive);
  263. if (activeControlPoints.length !== 0) {
  264. return false;
  265. } else {
  266. return true;
  267. }
  268. }
  269. render() {
  270. const viewBoxScaling = `${this.props.positionCanvas?.x} ${this.props.positionCanvas?.y} ${Math.round(
  271. this.props.scale * 200
  272. )} ${Math.round(this.props.scale * 100)}`;
  273. return (
  274. <>
  275. <svg
  276. className="linear pannable"
  277. ref={this._draggableArea}
  278. tabIndex={0}
  279. onMouseMove={this.drag}
  280. onMouseDown={this.dragStart}
  281. onMouseUp={this.dragEnd}
  282. onMouseLeave={this.dragEnd}
  283. onClick={this.focus}
  284. viewBox={viewBoxScaling}
  285. >
  286. {this.props.children}
  287. </svg>
  288. </>
  289. );
  290. }
  291. }