timeline.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. import * as React from 'react';
  2. import { IAnimationKey } from 'babylonjs/Animations/animationKey';
  3. import { Controls } from './controls';
  4. interface ITimelineProps {
  5. keyframes: IAnimationKey[] | null;
  6. selected: IAnimationKey | null;
  7. currentFrame: number;
  8. onCurrentFrameChange: (frame: number) => void;
  9. dragKeyframe: (frame: number, index: number) => void;
  10. playPause: (direction: number) => void;
  11. isPlaying: boolean;
  12. }
  13. export class Timeline extends React.Component<
  14. ITimelineProps,
  15. { selected: IAnimationKey; activeKeyframe: number | null }
  16. > {
  17. readonly _frames: object[] = Array(300).fill({});
  18. private _scrollable: React.RefObject<HTMLDivElement>;
  19. private _scrollbarHandle: React.RefObject<HTMLDivElement>;
  20. private _direction: number;
  21. private _scrolling: boolean;
  22. private _shiftX: number;
  23. constructor(props: ITimelineProps) {
  24. super(props);
  25. if (this.props.selected !== null) {
  26. this.state = { selected: this.props.selected, activeKeyframe: null };
  27. }
  28. this._scrollable = React.createRef();
  29. this._scrollbarHandle = React.createRef();
  30. this._direction = 0;
  31. this._scrolling = false;
  32. this._shiftX = 0;
  33. }
  34. playBackwards(event: React.MouseEvent<HTMLDivElement>) {
  35. this.props.playPause(-1);
  36. }
  37. play(event: React.MouseEvent<HTMLDivElement>) {
  38. this.props.playPause(1);
  39. }
  40. pause(event: React.MouseEvent<HTMLDivElement>) {
  41. if (this.props.isPlaying) {
  42. this.props.playPause(1);
  43. }
  44. }
  45. handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
  46. this.props.onCurrentFrameChange(parseInt(event.target.value));
  47. event.preventDefault();
  48. }
  49. nextFrame(event: React.MouseEvent<HTMLDivElement>) {
  50. event.preventDefault();
  51. this.props.onCurrentFrameChange(this.props.currentFrame + 1);
  52. (this._scrollable.current as HTMLDivElement).scrollLeft =
  53. this.props.currentFrame * 5;
  54. }
  55. previousFrame(event: React.MouseEvent<HTMLDivElement>) {
  56. event.preventDefault();
  57. if (this.props.currentFrame !== 0) {
  58. this.props.onCurrentFrameChange(this.props.currentFrame - 1);
  59. (this._scrollable.current as HTMLDivElement).scrollLeft = -(
  60. this.props.currentFrame * 5
  61. );
  62. }
  63. }
  64. nextKeyframe(event: React.MouseEvent<HTMLDivElement>) {
  65. event.preventDefault();
  66. if (this.props.keyframes !== null) {
  67. let first = this.props.keyframes.find(
  68. (kf) => kf.frame > this.props.currentFrame
  69. );
  70. if (first) {
  71. this.props.onCurrentFrameChange(first.frame);
  72. this.setState({ selected: first });
  73. (this._scrollable.current as HTMLDivElement).scrollLeft =
  74. first.frame * 5;
  75. }
  76. }
  77. }
  78. previousKeyframe(event: React.MouseEvent<HTMLDivElement>) {
  79. event.preventDefault();
  80. if (this.props.keyframes !== null) {
  81. let keyframes = [...this.props.keyframes];
  82. let first = keyframes
  83. .reverse()
  84. .find((kf) => kf.frame < this.props.currentFrame);
  85. if (first) {
  86. this.props.onCurrentFrameChange(first.frame);
  87. this.setState({ selected: first });
  88. (this._scrollable.current as HTMLDivElement).scrollLeft = -(
  89. first.frame * 5
  90. );
  91. }
  92. }
  93. }
  94. dragStart(e: React.TouchEvent<SVGSVGElement>): void;
  95. dragStart(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
  96. dragStart(e: any): void {
  97. e.preventDefault();
  98. this.setState({ activeKeyframe: parseInt(e.target.id.replace('kf_', '')) });
  99. this._direction = e.clientX;
  100. }
  101. drag(e: React.TouchEvent<SVGSVGElement>): void;
  102. drag(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
  103. drag(e: any): void {
  104. e.preventDefault();
  105. if (this.props.keyframes) {
  106. if (
  107. this.state.activeKeyframe === parseInt(e.target.id.replace('kf_', ''))
  108. ) {
  109. let updatedKeyframe = this.props.keyframes[this.state.activeKeyframe];
  110. if (this._direction > e.clientX) {
  111. console.log(`Dragging left ${this.state.activeKeyframe}`);
  112. let used = this.isFrameBeingUsed(updatedKeyframe.frame - 1, -1);
  113. if (used) {
  114. updatedKeyframe.frame = used;
  115. }
  116. } else {
  117. console.log(`Dragging Right ${this.state.activeKeyframe}`);
  118. let used = this.isFrameBeingUsed(updatedKeyframe.frame + 1, 1);
  119. if (used) {
  120. updatedKeyframe.frame = used;
  121. }
  122. }
  123. this.props.dragKeyframe(
  124. updatedKeyframe.frame,
  125. this.state.activeKeyframe
  126. );
  127. }
  128. }
  129. }
  130. isFrameBeingUsed(frame: number, direction: number) {
  131. let used = this.props.keyframes?.find((kf) => kf.frame === frame);
  132. if (used) {
  133. this.isFrameBeingUsed(used.frame + direction, direction);
  134. return false;
  135. } else {
  136. return frame;
  137. }
  138. }
  139. dragEnd(e: React.TouchEvent<SVGSVGElement>): void;
  140. dragEnd(e: React.MouseEvent<SVGSVGElement, MouseEvent>): void;
  141. dragEnd(e: any): void {
  142. e.preventDefault();
  143. this._direction = 0;
  144. this.setState({ activeKeyframe: null });
  145. }
  146. scrollDragStart(e: React.TouchEvent<HTMLDivElement>): void;
  147. scrollDragStart(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
  148. scrollDragStart(e: any) {
  149. e.preventDefault();
  150. if ((e.target.class = 'scrollbar') && this._scrollbarHandle.current) {
  151. this._scrolling = true;
  152. this._shiftX =
  153. e.clientX - this._scrollbarHandle.current.getBoundingClientRect().left;
  154. this._scrollbarHandle.current.style.left = e.pageX - this._shiftX + 'px';
  155. }
  156. }
  157. scrollDrag(e: React.TouchEvent<HTMLDivElement>): void;
  158. scrollDrag(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
  159. scrollDrag(e: any) {
  160. e.preventDefault();
  161. if (this._scrolling && this._scrollbarHandle.current) {
  162. let moved = e.pageX - this._shiftX;
  163. if (moved > 233 && moved < 630) {
  164. this._scrollbarHandle.current.style.left = moved + 'px';
  165. (this._scrollable.current as HTMLDivElement).scrollLeft = moved + 10;
  166. }
  167. }
  168. }
  169. scrollDragEnd(e: React.TouchEvent<HTMLDivElement>): void;
  170. scrollDragEnd(e: React.MouseEvent<HTMLDivElement, MouseEvent>): void;
  171. scrollDragEnd(e: any) {
  172. e.preventDefault();
  173. this._scrolling = false;
  174. this._shiftX = 0;
  175. }
  176. render() {
  177. return (
  178. <>
  179. <div className='timeline'>
  180. <Controls
  181. keyframes={this.props.keyframes}
  182. selected={this.props.selected}
  183. currentFrame={this.props.currentFrame}
  184. onCurrentFrameChange={this.props.onCurrentFrameChange}
  185. playPause={this.props.playPause}
  186. isPlaying={this.props.isPlaying}
  187. scrollable={this._scrollable}
  188. />
  189. <div className='timeline-wrapper'>
  190. <div ref={this._scrollable} className='display-line'>
  191. <svg
  192. viewBox='0 0 2010 40'
  193. style={{ width: 2000, height: 40, backgroundColor: '#222222' }}
  194. onMouseMove={(e) => this.drag(e)}
  195. onTouchMove={(e) => this.drag(e)}
  196. onTouchStart={(e) => this.dragStart(e)}
  197. onTouchEnd={(e) => this.dragEnd(e)}
  198. onMouseDown={(e) => this.dragStart(e)}
  199. onMouseUp={(e) => this.dragEnd(e)}
  200. onMouseLeave={(e) => this.dragEnd(e)}
  201. onDragStart={() => false}
  202. >
  203. <line
  204. x1={this.props.currentFrame * 10}
  205. y1='0'
  206. x2={this.props.currentFrame * 10}
  207. y2='40'
  208. style={{ stroke: '#12506b', strokeWidth: 6 }}
  209. />
  210. {this._frames.map((frame, i) => {
  211. return (
  212. <svg key={`tl_${i}`}>
  213. {i % 5 === 0 ? (
  214. <>
  215. <text
  216. x={i * 5 - 3}
  217. y='18'
  218. style={{ fontSize: 10, fill: '#555555' }}
  219. >
  220. {i}
  221. </text>
  222. <line
  223. x1={i * 5}
  224. y1='22'
  225. x2={i * 5}
  226. y2='40'
  227. style={{ stroke: '#555555', strokeWidth: 0.5 }}
  228. />
  229. </>
  230. ) : null}
  231. </svg>
  232. );
  233. })}
  234. {this.props.keyframes &&
  235. this.props.keyframes.map((kf, i) => {
  236. return (
  237. <svg
  238. key={`kf_${i}`}
  239. style={{ cursor: 'pointer' }}
  240. tabIndex={i + 40}
  241. >
  242. <line
  243. id={`kf_${i.toString()}`}
  244. x1={kf.frame * 10}
  245. y1='0'
  246. x2={kf.frame * 10}
  247. y2='40'
  248. style={{ stroke: '#ffc017', strokeWidth: 1 }}
  249. />
  250. </svg>
  251. );
  252. })}
  253. </svg>
  254. </div>
  255. <div className='timeline-scroll-handle'>
  256. <div className='scroll-handle'>
  257. <div
  258. className='handle'
  259. ref={this._scrollbarHandle}
  260. style={{ width: 300 }}
  261. >
  262. <div className='left-grabber'>
  263. <div className='grabber'></div>
  264. <div className='grabber'></div>
  265. <div className='grabber'></div>
  266. <div className='text'>20</div>
  267. </div>
  268. <div
  269. className='scrollbar'
  270. onMouseMove={(e) => this.scrollDrag(e)}
  271. onTouchMove={(e) => this.scrollDrag(e)}
  272. onTouchStart={(e) => this.scrollDragStart(e)}
  273. onTouchEnd={(e) => this.scrollDragEnd(e)}
  274. onMouseDown={(e) => this.scrollDragStart(e)}
  275. onMouseUp={(e) => this.scrollDragEnd(e)}
  276. onMouseLeave={(e) => this.scrollDragEnd(e)}
  277. onDragStart={() => false}
  278. ></div>
  279. <div className='right-grabber'>
  280. <div className='text'>100</div>
  281. <div className='grabber'></div>
  282. <div className='grabber'></div>
  283. <div className='grabber'></div>
  284. </div>
  285. </div>
  286. </div>
  287. <div className='input-frame'>
  288. <input
  289. type='number'
  290. value={this.props.currentFrame}
  291. onChange={(e) => this.handleInputChange(e)}
  292. ></input>
  293. </div>
  294. </div>
  295. </div>
  296. </div>
  297. </>
  298. );
  299. }
  300. }