svgDraggableArea.tsx 10 KB

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