animationCurveEditorComponent.tsx 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988
  1. import * as React from "react";
  2. import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
  3. import { faTimes } from "@fortawesome/free-solid-svg-icons";
  4. import { Animation } from 'babylonjs/Animations/animation';
  5. import { Vector2, Vector3, Quaternion } from 'babylonjs/Maths/math.vector';
  6. import { Size } from 'babylonjs/Maths/math.size';
  7. import { Color3, Color4 } from 'babylonjs/Maths/math.color';
  8. import { EasingFunction } from 'babylonjs/Animations/easing';
  9. import { IAnimationKey } from 'babylonjs/Animations/animationKey';
  10. import { IKeyframeSvgPoint } from './keyframeSvgPoint';
  11. import { SvgDraggableArea } from './svgDraggableArea';
  12. import { Timeline } from './timeline';
  13. import { Playhead } from './playhead';
  14. import { Notification } from './notification';
  15. import { GraphActionsBar } from './graphActionsBar';
  16. import { Scene } from "babylonjs/scene";
  17. import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
  18. import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
  19. require("./curveEditor.scss");
  20. interface IAnimationCurveEditorComponentProps {
  21. close: (event: any) => void;
  22. playOrPause: () => void;
  23. title: string;
  24. animations: Animation[];
  25. entityName: string;
  26. scene: Scene;
  27. entity: IAnimatable;
  28. }
  29. interface ICanvasAxis {
  30. value: number;
  31. label: number;
  32. }
  33. export class AnimationCurveEditorComponent extends React.Component<IAnimationCurveEditorComponentProps, {
  34. animations: Animation[],
  35. animationName: string,
  36. animationType: string,
  37. animationTargetProperty: string,
  38. isOpen: boolean, selected: Animation,
  39. currentPathData: string | undefined,
  40. svgKeyframes: IKeyframeSvgPoint[] | undefined,
  41. currentFrame: number,
  42. currentValue: number,
  43. frameAxisLength: ICanvasAxis[],
  44. valueAxisLength: ICanvasAxis[],
  45. flatTangent: boolean,
  46. scale: number,
  47. playheadOffset: number,
  48. notification: string,
  49. currentPoint: SVGPoint | undefined,
  50. lastFrame: number,
  51. playheadPos: number
  52. }> {
  53. private _heightScale: number = 100;
  54. readonly _canvasLength: number = 20;
  55. private _newAnimations: Animation[] = [];
  56. private _svgKeyframes: IKeyframeSvgPoint[] = [];
  57. private _frames: Vector2[] = [];
  58. private _isPlaying: boolean = false;
  59. private _graphCanvas: React.RefObject<HTMLDivElement>;
  60. private _selectedCurve: React.RefObject<SVGPathElement>;
  61. private _svgCanvas: React.RefObject<SvgDraggableArea>;
  62. constructor(props: IAnimationCurveEditorComponentProps) {
  63. super(props);
  64. this._graphCanvas = React.createRef();
  65. this._selectedCurve = React.createRef();
  66. this._svgCanvas = React.createRef();
  67. let valueInd = [2, 1.8, 1.6, 1.4, 1.2, 1, 0.8, 0.6, 0.4, 0.2, 0]; // will update this until we have a top scroll/zoom feature
  68. this.state = {
  69. animations: this._newAnimations,
  70. selected: this.props.animations[0],
  71. isOpen: true,
  72. currentPathData: this.getPathData(this.props.animations[0]),
  73. svgKeyframes: this._svgKeyframes,
  74. animationTargetProperty: 'position.x',
  75. animationName: "",
  76. animationType: "Float",
  77. currentFrame: 0,
  78. currentValue: 1,
  79. flatTangent: false,
  80. playheadOffset: this._graphCanvas.current ? (this._graphCanvas.current.children[1].clientWidth) / (this._canvasLength * 10) : 0,
  81. frameAxisLength: (new Array(this._canvasLength)).fill(0).map((s, i) => { return { value: i * 10, label: i * 10 } }),
  82. valueAxisLength: (new Array(10)).fill(0).map((s, i) => { return { value: i * 10, label: valueInd[i] } }),
  83. notification: "",
  84. lastFrame: 0,
  85. currentPoint: undefined,
  86. scale: 1,
  87. playheadPos: 0,
  88. }
  89. }
  90. componentDidMount() {
  91. setTimeout(() => this.resetPlayheadOffset(), 500);
  92. }
  93. resetPlayheadOffset() {
  94. if (this._graphCanvas && this._graphCanvas.current) {
  95. this.setState({ playheadOffset: (this._graphCanvas.current.children[1].clientWidth) / (this._canvasLength * 10 * this.state.scale) });
  96. }
  97. }
  98. setAxesLength() {
  99. let length = Math.round(this._canvasLength * this.state.scale);
  100. if (length < (this.state.selected.getHighestFrame() * 2) / 10) {
  101. length = (this.state.selected.getHighestFrame() * 2) / 10
  102. }
  103. let valueLines = Math.round((this.state.scale * this._heightScale) / 10);
  104. let newFrameLength = (new Array(length)).fill(0).map((s, i) => { return { value: i * 10, label: i * 10 } });
  105. let newValueLength = (new Array(valueLines)).fill(0).map((s, i) => { return { value: i * 10, label: this.getValueLabel(i * 10) } });
  106. this.setState({ frameAxisLength: newFrameLength, valueAxisLength: newValueLength });
  107. this.resetPlayheadOffset();
  108. }
  109. getValueLabel(i: number) {
  110. // Need to update this when Y axis grows
  111. let label = 0;
  112. if (i === 0) {
  113. label = 2;
  114. }
  115. if (i === 50) {
  116. label = 1;
  117. } else {
  118. label = ((100 - (i * 2)) * 0.01) + 1;
  119. }
  120. return label;
  121. }
  122. handleNameChange(event: React.ChangeEvent<HTMLInputElement>) {
  123. event.preventDefault();
  124. this.setState({ animationName: event.target.value });
  125. }
  126. handleValueChange(event: React.ChangeEvent<HTMLInputElement>) {
  127. event.preventDefault();
  128. this.setState({ currentValue: parseFloat(event.target.value) }, () => {
  129. let animation = this.state.selected;
  130. let keys = animation.getKeys();
  131. let isKeyframe = keys.find(k => k.frame === this.state.currentFrame);
  132. if (isKeyframe){
  133. let updatedKeys = keys.map(k => {
  134. if (k.frame === this.state.currentFrame){
  135. k.value = this.state.currentValue;
  136. }
  137. return k;
  138. });
  139. this.state.selected.setKeys(updatedKeys);
  140. this.selectAnimation(animation);
  141. }
  142. });
  143. }
  144. handleFrameChange(event: React.ChangeEvent<HTMLInputElement>) {
  145. event.preventDefault();
  146. this.changeCurrentFrame(parseInt(event.target.value))
  147. }
  148. handleTypeChange(event: React.ChangeEvent<HTMLSelectElement>) {
  149. event.preventDefault();
  150. this.setState({ animationType: event.target.value });
  151. }
  152. handlePropertyChange(event: React.ChangeEvent<HTMLInputElement>) {
  153. event.preventDefault();
  154. this.setState({ animationTargetProperty: event.target.value });
  155. }
  156. addAnimation() {
  157. if (this.state.animationName != "" && this.state.animationTargetProperty != "") {
  158. let matchTypeTargetProperty = this.state.animationTargetProperty.split('.');
  159. let animationDataType = this.getAnimationTypeofChange(this.state.animationType);
  160. let matched = false;
  161. if (matchTypeTargetProperty.length === 1) {
  162. let match = (this.props.entity as any)[matchTypeTargetProperty[0]];
  163. if (match){
  164. switch (match.constructor.name) {
  165. case "Vector2":
  166. animationDataType === Animation.ANIMATIONTYPE_VECTOR2 ? matched = true : matched = false;
  167. break;
  168. case "Vector3":
  169. animationDataType === Animation.ANIMATIONTYPE_VECTOR3 ? matched = true : matched = false;
  170. break;
  171. case "Quaternion":
  172. animationDataType === Animation.ANIMATIONTYPE_QUATERNION ? matched = true : matched = false;
  173. break;
  174. case "Color3":
  175. animationDataType === Animation.ANIMATIONTYPE_COLOR3 ? matched = true : matched = false;
  176. break;
  177. case "Color4":
  178. animationDataType === Animation.ANIMATIONTYPE_COLOR4 ? matched = true : matched = false;
  179. break;
  180. case "Size":
  181. animationDataType === Animation.ANIMATIONTYPE_SIZE ? matched = true : matched = false;
  182. break;
  183. default: console.log("not recognized");
  184. break;
  185. }
  186. } else {
  187. this.setState({ notification: `The selected entity doesn't have a ${matchTypeTargetProperty[0]} property` });
  188. }
  189. } else if (matchTypeTargetProperty.length > 1) {
  190. let match = (this.props.entity as any)[matchTypeTargetProperty[0]][matchTypeTargetProperty[1]];
  191. if (typeof match === "number") {
  192. animationDataType === Animation.ANIMATIONTYPE_FLOAT ? matched = true : matched = false;
  193. }
  194. }
  195. if (matched) {
  196. let startValue;
  197. let endValue;
  198. // Default start and end values for new animations
  199. switch (animationDataType) {
  200. case Animation.ANIMATIONTYPE_FLOAT:
  201. startValue = 1;
  202. endValue = 1;
  203. break;
  204. case Animation.ANIMATIONTYPE_VECTOR2:
  205. startValue = new Vector2(1,1);
  206. endValue = new Vector2(1,1);
  207. break;
  208. case Animation.ANIMATIONTYPE_VECTOR3:
  209. startValue = new Vector3(1,1,1);
  210. endValue = new Vector3(1,1,1);
  211. break;
  212. case Animation.ANIMATIONTYPE_QUATERNION:
  213. startValue = new Quaternion(1,1,1,1);
  214. endValue = new Quaternion(1,1,1,1);
  215. break;
  216. case Animation.ANIMATIONTYPE_COLOR3:
  217. startValue = new Color3(1,1,1);
  218. endValue = new Color3(1,1,1);
  219. break;
  220. case Animation.ANIMATIONTYPE_COLOR4:
  221. startValue = new Color4(1,1,1,1);
  222. endValue = new Color4(1,1,1,1);
  223. break;
  224. case Animation.ANIMATIONTYPE_SIZE:
  225. startValue = new Size(1,1);
  226. endValue = new Size(1,1);
  227. break;
  228. default: console.log("not recognized");
  229. break;
  230. }
  231. let animation = new Animation(this.state.animationName, this.state.animationTargetProperty, 30, animationDataType);
  232. // Start with two keyframes
  233. var keys = [];
  234. keys.push({
  235. frame: 0,
  236. value: startValue
  237. });
  238. keys.push({
  239. frame: 100,
  240. value: endValue
  241. });
  242. animation.setKeys(keys);
  243. (this.props.entity as IAnimatable).animations?.push(animation);
  244. } else {
  245. this.setState({ notification: `The property "${this.state.animationTargetProperty}" is not a "${this.state.animationType}" type` });
  246. }
  247. } else {
  248. this.setState({ notification: "You need to provide a name and target property." });
  249. }
  250. }
  251. clearNotification() {
  252. this.setState({ notification: "" });
  253. }
  254. addKeyframeClick() {
  255. let currentAnimation = this.state.selected;
  256. if (currentAnimation.dataType === Animation.ANIMATIONTYPE_FLOAT) {
  257. let keys = currentAnimation.getKeys();
  258. let x = this.state.currentFrame;
  259. let y = this.state.currentValue;
  260. keys.push({ frame: x, value: y });
  261. keys.sort((a, b) => a.frame - b.frame);
  262. currentAnimation.setKeys(keys);
  263. this.selectAnimation(currentAnimation);
  264. }
  265. }
  266. removeKeyframeClick() {
  267. let currentAnimation = this.state.selected;
  268. if (currentAnimation.dataType === Animation.ANIMATIONTYPE_FLOAT) {
  269. let keys = currentAnimation.getKeys();
  270. let x = this.state.currentFrame;
  271. let filteredKeys = keys.filter(kf => kf.frame !== x);
  272. currentAnimation.setKeys(filteredKeys);
  273. this.selectAnimation(currentAnimation);
  274. }
  275. }
  276. addKeyFrame(event: React.MouseEvent<SVGSVGElement>) {
  277. event.preventDefault();
  278. var svg = event.target as SVGSVGElement;
  279. var pt = svg.createSVGPoint();
  280. pt.x = event.clientX;
  281. pt.y = event.clientY;
  282. var inverse = svg.getScreenCTM()?.inverse();
  283. var cursorpt = pt.matrixTransform(inverse);
  284. var currentAnimation = this.state.selected;
  285. var keys = currentAnimation.getKeys();
  286. var height = 100;
  287. var middle = (height / 2);
  288. var keyValue;
  289. if (cursorpt.y < middle) {
  290. keyValue = 1 + ((100 / cursorpt.y) * .1)
  291. }
  292. if (cursorpt.y > middle) {
  293. keyValue = 1 - ((100 / cursorpt.y) * .1)
  294. }
  295. keys.push({ frame: cursorpt.x, value: keyValue });
  296. currentAnimation.setKeys(keys);
  297. this.selectAnimation(currentAnimation);
  298. }
  299. updateKeyframe(keyframe: Vector2, index: number) {
  300. let anim = this.state.selected as Animation;
  301. var keys: IAnimationKey[] = [];
  302. var svgKeyframes = this.state.svgKeyframes?.map((k, i) => {
  303. if (i === index) {
  304. k.keyframePoint.x = keyframe.x;
  305. k.keyframePoint.y = keyframe.y;
  306. }
  307. var height = 100;
  308. var middle = (height / 2);
  309. var keyValue;
  310. if (k.keyframePoint.y < middle) {
  311. keyValue = 1 + ((100 / k.keyframePoint.y) * .1)
  312. }
  313. if (k.keyframePoint.y > middle) {
  314. keyValue = 1 - ((100 / k.keyframePoint.y) * .1)
  315. }
  316. keys.push({ frame: k.keyframePoint.x, value: keyValue })
  317. return k;
  318. });
  319. anim.setKeys(keys);
  320. this.setState({ svgKeyframes: svgKeyframes });
  321. }
  322. getAnimationProperties(animation: Animation) {
  323. let easingType, easingMode;
  324. let easingFunction: EasingFunction = animation.getEasingFunction() as EasingFunction;
  325. if (easingFunction === undefined) {
  326. easingType = undefined
  327. easingMode = undefined;
  328. } else {
  329. easingType = easingFunction.constructor.name;
  330. easingMode = easingFunction.getEasingMode();
  331. }
  332. return { easingType, easingMode }
  333. }
  334. getPathData(animation: Animation) {
  335. const { easingMode, easingType } = this.getAnimationProperties(animation);
  336. const keyframes = animation.getKeys();
  337. if (keyframes === undefined) {
  338. return "";
  339. }
  340. const startKey = keyframes[0];
  341. // This assumes the startkey is always 0... beed to change this
  342. let middle = this._heightScale / 2;
  343. // START OF LINE/CURVE
  344. let data: string | undefined = `M${startKey.frame}, ${this._heightScale - (startKey.value * middle)}`;
  345. if (this.state && this.state.flatTangent) {
  346. data = this.curvePathFlat(keyframes, data, middle, animation.dataType);
  347. } else {
  348. if (this.getAnimationData(animation).usesTangents) {
  349. data = this.curvePathWithTangents(keyframes, data, middle, animation.dataType);
  350. } else {
  351. console.log("no tangents in this animation");
  352. if (easingType === undefined && easingMode === undefined) {
  353. data = this.linearInterpolation(keyframes, data, middle);
  354. } else {
  355. let easingFunction = animation.getEasingFunction();
  356. data = this.curvePath(keyframes, data, middle, easingFunction as EasingFunction)
  357. }
  358. }
  359. }
  360. return data;
  361. }
  362. drawAllFrames(initialKey: IAnimationKey, endKey: IAnimationKey, easingFunction: EasingFunction) {
  363. let i = initialKey.frame;
  364. for (i; i < endKey.frame; i++) {
  365. (i * 100 / endKey.frame)
  366. let dy = easingFunction.easeInCore(i);
  367. let value = this._heightScale - (dy * (this._heightScale / 2));
  368. this._frames.push(new Vector2(i, value));
  369. }
  370. }
  371. curvePathFlat(keyframes: IAnimationKey[], data: string, middle: number, dataType: number) {
  372. keyframes.forEach((key, i) => {
  373. if (dataType === Animation.ANIMATIONTYPE_FLOAT) {
  374. var pointA = new Vector2(0, 0);
  375. if (i === 0) {
  376. pointA.set(key.frame, this._heightScale - (key.value * middle));
  377. this.setKeyframePoint([pointA], i, keyframes.length);
  378. } else {
  379. pointA.set(keyframes[i - 1].frame, this._heightScale - (keyframes[i - 1].value * middle));
  380. let tangentA = new Vector2(pointA.x + 10, pointA.y);
  381. let pointB = new Vector2(key.frame, this._heightScale - (key.value * middle));
  382. let tangentB = new Vector2(pointB.x - 10, pointB.y);
  383. this.setKeyframePoint([pointA, tangentA, tangentB, pointB], i, keyframes.length);
  384. data += ` C${tangentA.x} ${tangentA.y} ${tangentB.x} ${tangentB.y} ${pointB.x} ${pointB.y} `
  385. }
  386. }
  387. });
  388. return data;
  389. }
  390. curvePathWithTangents(keyframes: IAnimationKey[], data: string, middle: number, type: number) {
  391. switch (type) {
  392. case Animation.ANIMATIONTYPE_FLOAT:
  393. // value float
  394. break;
  395. case Animation.ANIMATIONTYPE_VECTOR3:
  396. // value float
  397. break;
  398. }
  399. keyframes.forEach((key, i) => {
  400. var inTangent = key.inTangent;
  401. var outTangent = key.outTangent;
  402. let svgKeyframe;
  403. if (i === 0) {
  404. svgKeyframe = { keyframePoint: new Vector2(key.frame, this._heightScale - (key.value * middle)), rightControlPoint: outTangent, leftControlPoint: null, id: i.toString() }
  405. data += ` C${svgKeyframe.keyframePoint.x} ${svgKeyframe.keyframePoint.y} ${outTangent.x} ${outTangent.y}`
  406. } else {
  407. svgKeyframe = { keyframePoint: new Vector2(keyframes[i - 1].frame, this._heightScale - (keyframes[i - 1].value * middle)), rightControlPoint: outTangent, leftControlPoint: inTangent, id: i.toString() }
  408. if (outTangent) {
  409. data += `${inTangent.x} ${inTangent.y} C${svgKeyframe.keyframePoint.x} ${svgKeyframe.keyframePoint.y} ${outTangent.x} ${outTangent.y} `
  410. } else {
  411. data += `${inTangent.x} ${inTangent.y} C${svgKeyframe.keyframePoint.x} ${svgKeyframe.keyframePoint.y} `
  412. }
  413. }
  414. this._svgKeyframes.push(svgKeyframe);
  415. });
  416. return data;
  417. }
  418. curvePath(keyframes: IAnimationKey[], data: string, middle: number, easingFunction: EasingFunction) {
  419. // This will get 1/4 and 3/4 of points in eased curve
  420. const u = .25;
  421. const v = .75;
  422. keyframes.forEach((key, i) => {
  423. // Gets previous initial point of curve segment
  424. var pointA = new Vector2(0, 0);
  425. if (i === 0) {
  426. pointA.x = key.frame;
  427. pointA.y = this._heightScale - (key.value * middle);
  428. this.setKeyframePoint([pointA], i, keyframes.length);
  429. } else {
  430. pointA.x = keyframes[i - 1].frame;
  431. pointA.y = this._heightScale - (keyframes[i - 1].value * middle)
  432. // Gets the end point of this curve segment
  433. var pointB = new Vector2(key.frame, this._heightScale - (key.value * middle));
  434. // Get easing value of percentage to get the bezier control points below
  435. let du = easingFunction.easeInCore(u); // What to do here, when user edits the curve? Option 1: Modify the curve with the new control points as BezierEaseCurve(x,y,z,w)
  436. let dv = easingFunction.easeInCore(v); // Option 2: Create a easeInCore function and adapt it with the new control points values... needs more revision.
  437. // Direction of curve up/down
  438. let yInt25 = 0;
  439. if (pointB.y > pointA.y) { // if pointB.y > pointA.y = goes down
  440. yInt25 = ((pointB.y - pointA.y) * du) + pointA.y
  441. } else if (pointB.y < pointA.y) { // if pointB.y < pointA.y = goes up
  442. yInt25 = pointA.y - ((pointA.y - pointB.y) * du);
  443. }
  444. let yInt75 = 0;
  445. if (pointB.y > pointA.y) {
  446. yInt75 = ((pointB.y - pointA.y) * dv) + pointA.y
  447. } else if (pointB.y < pointA.y) {
  448. yInt75 = pointA.y - ((pointA.y - pointB.y) * dv)
  449. }
  450. // Intermediate points in curve
  451. let intermediatePoint25 = new Vector2(((pointB.x - pointA.x) * u) + pointA.x, yInt25);
  452. let intermediatePoint75 = new Vector2(((pointB.x - pointA.x) * v) + pointA.x, yInt75);
  453. // Gets the four control points of bezier curve
  454. let controlPoints = this.interpolateControlPoints(pointA, intermediatePoint25, u, intermediatePoint75, v, pointB);
  455. if (controlPoints === undefined) {
  456. console.log("error getting bezier control points");
  457. } else {
  458. this.setKeyframePoint(controlPoints, i, keyframes.length);
  459. data += ` C${controlPoints[1].x} ${controlPoints[1].y} ${controlPoints[2].x} ${controlPoints[2].y} ${controlPoints[3].x} ${controlPoints[3].y}`
  460. }
  461. }
  462. });
  463. return data;
  464. }
  465. renderPoints(updatedSvgKeyFrame: IKeyframeSvgPoint, index: number) {
  466. let animation = this.state.selected as Animation;
  467. // Bug: After play/stop we get an extra keyframe at 0
  468. let keys = [...animation.getKeys()];
  469. let newFrame = 0;
  470. if (updatedSvgKeyFrame.keyframePoint.x !== 0) {
  471. if (updatedSvgKeyFrame.keyframePoint.x > 0 && updatedSvgKeyFrame.keyframePoint.x < 1) {
  472. newFrame = 1;
  473. } else {
  474. newFrame = Math.round(updatedSvgKeyFrame.keyframePoint.x);
  475. }
  476. }
  477. keys[index].frame = newFrame; // This value comes as percentage/frame/time
  478. keys[index].value = ((this._heightScale - updatedSvgKeyFrame.keyframePoint.y) / this._heightScale) * 2; // this value comes inverted svg from 0 = 100 to 100 = 0
  479. animation.setKeys(keys);
  480. this.selectAnimation(animation);
  481. }
  482. linearInterpolation(keyframes: IAnimationKey[], data: string, middle: number): string {
  483. keyframes.forEach((key, i) => {
  484. var point = new Vector2(0, 0);
  485. point.x = key.frame;
  486. point.y = this._heightScale - (key.value * middle);
  487. this.setKeyframePointLinear(point, i);
  488. if (i !== 0) {
  489. data += ` L${point.x} ${point.y}`
  490. }
  491. });
  492. return data;
  493. }
  494. setKeyframePointLinear(point: Vector2, index: number) {
  495. let svgKeyframe = { keyframePoint: point, rightControlPoint: null, leftControlPoint: null, id: index.toString() }
  496. this._svgKeyframes.push(svgKeyframe);
  497. }
  498. setKeyframePoint(controlPoints: Vector2[], index: number, keyframesCount: number) {
  499. let svgKeyframe;
  500. if (index === 0) {
  501. svgKeyframe = { keyframePoint: controlPoints[0], rightControlPoint: null, leftControlPoint: null, id: index.toString() }
  502. } else {
  503. this._svgKeyframes[index - 1].rightControlPoint = controlPoints[1];
  504. svgKeyframe = { keyframePoint: controlPoints[3], rightControlPoint: null, leftControlPoint: controlPoints[2], id: index.toString() }
  505. }
  506. this._svgKeyframes.push(svgKeyframe);
  507. }
  508. isAnimationPlaying() {
  509. this._isPlaying = this.props.scene.getAllAnimatablesByTarget(this.props.entity).length > 0;
  510. if (this._isPlaying) {
  511. this.props.playOrPause();
  512. } else {
  513. this._isPlaying = false;
  514. }
  515. }
  516. selectAnimation(animation: Animation) {
  517. this.isAnimationPlaying();
  518. this._svgKeyframes = [];
  519. const pathData = this.getPathData(animation);
  520. let lastFrame = animation.getHighestFrame();
  521. if (pathData === "") {
  522. console.log("no keyframes in this animation");
  523. }
  524. this.setState({ selected: animation, currentPathData: pathData, svgKeyframes: this._svgKeyframes, lastFrame: lastFrame });
  525. }
  526. getAnimationData(animation: Animation) {
  527. // General Props
  528. let loopMode = animation.loopMode;
  529. let name = animation.name;
  530. let blendingSpeed = animation.blendingSpeed;
  531. let targetProperty = animation.targetProperty;
  532. let targetPropertyPath = animation.targetPropertyPath;
  533. let framesPerSecond = animation.framePerSecond;
  534. let highestFrame = animation.getHighestFrame();
  535. // Should we use this for export?
  536. let serialized = animation.serialize();
  537. let usesTangents = animation.getKeys().find(kf => kf.inTangent);
  538. return { loopMode, name, blendingSpeed, targetPropertyPath, targetProperty, framesPerSecond, highestFrame, serialized, usesTangents }
  539. }
  540. getAnimationTypeofChange(selected: string) {
  541. let dataType = 0;
  542. switch (selected) {
  543. case "Float":
  544. dataType = Animation.ANIMATIONTYPE_FLOAT;
  545. break;
  546. case "Quaternion":
  547. dataType = Animation.ANIMATIONTYPE_QUATERNION;
  548. break;
  549. case "Vector3":
  550. dataType = Animation.ANIMATIONTYPE_VECTOR3;
  551. break;
  552. case "Vector2":
  553. dataType = Animation.ANIMATIONTYPE_VECTOR2;
  554. break;
  555. case "Size":
  556. dataType = Animation.ANIMATIONTYPE_SIZE;
  557. break;
  558. case "Color3":
  559. dataType = Animation.ANIMATIONTYPE_COLOR3;
  560. break;
  561. case "Color4":
  562. dataType = Animation.ANIMATIONTYPE_COLOR4;
  563. break;
  564. }
  565. return dataType;
  566. }
  567. interpolateControlPoints(p0: Vector2, p1: Vector2, u: number, p2: Vector2, v: number, p3: Vector2): Vector2[] | undefined {
  568. let a = 0.0;
  569. let b = 0.0;
  570. let c = 0.0;
  571. let d = 0.0;
  572. let det = 0.0;
  573. let q1: Vector2 = new Vector2();
  574. let q2: Vector2 = new Vector2();
  575. let controlA: Vector2 = p0;
  576. let controlB: Vector2 = new Vector2();
  577. let controlC: Vector2 = new Vector2();
  578. let controlD: Vector2 = p3;
  579. if ((u <= 0.0) || (u >= 1.0) || (v <= 0.0) || (v >= 1.0) || (u >= v)) {
  580. return undefined;
  581. }
  582. a = 3 * (1 - u) * (1 - u) * u; b = 3 * (1 - u) * u * u;
  583. c = 3 * (1 - v) * (1 - v) * v; d = 3 * (1 - v) * v * v;
  584. det = a * d - b * c;
  585. if (det == 0.0) return undefined;
  586. q1.x = p1.x - ((1 - u) * (1 - u) * (1 - u) * p0.x + u * u * u * p3.x);
  587. q1.y = p1.y - ((1 - u) * (1 - u) * (1 - u) * p0.y + u * u * u * p3.y);
  588. q2.x = p2.x - ((1 - v) * (1 - v) * (1 - v) * p0.x + v * v * v * p3.x);
  589. q2.y = p2.y - ((1 - v) * (1 - v) * (1 - v) * p0.y + v * v * v * p3.y);
  590. controlB.x = (d * q1.x - b * q2.x) / det;
  591. controlB.y = (d * q1.y - b * q2.y) / det;
  592. controlC.x = ((-c) * q1.x + a * q2.x) / det;
  593. controlC.y = ((-c) * q1.y + a * q2.y) / det;
  594. return [controlA, controlB, controlC, controlD];
  595. }
  596. changeCurrentFrame(frame: number) {
  597. let currentValue;
  598. let selectedCurve = this._selectedCurve.current;
  599. if (selectedCurve) {
  600. var curveLength = selectedCurve.getTotalLength();
  601. let frameValue = (frame * curveLength) / 100;
  602. let currentP = selectedCurve.getPointAtLength(frameValue);
  603. let middle = this._heightScale / 2;
  604. let offset = (((currentP?.y * this._heightScale) - (this._heightScale ** 2) / 2) / middle) / this._heightScale;
  605. let unit = Math.sign(offset);
  606. currentValue = unit === -1 ? Math.abs(offset + unit) : unit - offset;
  607. this.setState({ currentFrame: frame, currentValue: currentValue, currentPoint: currentP });
  608. }
  609. }
  610. setFlatTangent() {
  611. this.setState({ flatTangent: !this.state.flatTangent }, () => this.selectAnimation(this.state.selected));
  612. }
  613. zoom(e: React.WheelEvent<HTMLDivElement>) {
  614. e.nativeEvent.stopImmediatePropagation();
  615. console.log(e.deltaY);
  616. let scaleX = 1;
  617. if (Math.sign(e.deltaY) === -1) {
  618. scaleX = (this.state.scale - 0.01);
  619. } else {
  620. scaleX = (this.state.scale + 0.01);
  621. }
  622. this.setState({ scale: scaleX }, this.setAxesLength);
  623. }
  624. render() {
  625. return (
  626. <div id="animation-curve-editor">
  627. <Notification message={this.state.notification} open={this.state.notification !== "" ? true : false} close={() => this.clearNotification()} />
  628. <div className="header">
  629. <div className="title">{this.props.title}</div>
  630. <div className="close" onClick={(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => this.props.close(event)}>
  631. <FontAwesomeIcon icon={faTimes} />
  632. </div>
  633. </div>
  634. <GraphActionsBar currentValue={this.state.currentValue} currentFrame={this.state.currentFrame} handleFrameChange={(e) => this.handleFrameChange(e)} handleValueChange={(e) => this.handleValueChange(e)} addKeyframe={() => this.addKeyframeClick()} removeKeyframe={() => this.removeKeyframeClick()} flatTangent={() => this.setFlatTangent()} />
  635. <div className="content">
  636. <div className="row">
  637. <div className="animation-list">
  638. <div>
  639. <div className="label-input">
  640. <label>Animation Name</label>
  641. <input type="text" value={this.state.animationName} onChange={(e) => this.handleNameChange(e)}></input>
  642. </div>
  643. <div className="label-input">
  644. <label>Type</label>
  645. <select onChange={(e) => this.handleTypeChange(e)} value={this.state.animationType}>
  646. <option value="Float">Float</option>
  647. <option value="Vector3">Vector3</option>
  648. <option value="Vector2">Vector2</option>
  649. <option value="Quaternion">Quaternion</option>
  650. <option value="Color3">Color3</option>
  651. <option value="Color4">Color4</option>
  652. <option value="Size">Size</option>
  653. </select>
  654. </div>
  655. <div className="label-input">
  656. <label>Target Property</label>
  657. <input type="text" value={this.state.animationTargetProperty} onChange={(e) => this.handlePropertyChange(e)}></input>
  658. </div>
  659. <ButtonLineComponent label={"Add Animation"} onClick={() => this.addAnimation()} />
  660. </div>
  661. <div className="object-tree">
  662. <h2>{this.props.entityName}</h2>
  663. <ul>
  664. {this.props.animations && this.props.animations.map((animation, i) => {
  665. let element;
  666. switch(animation.dataType){
  667. case Animation.ANIMATIONTYPE_FLOAT:
  668. element = <li className={this.state.selected.name === animation.name ? 'active' : ''} key={i} onClick={() => this.selectAnimation(animation)}>
  669. {animation.name}
  670. <strong>{animation.targetProperty}</strong>
  671. </li>
  672. break;
  673. case Animation.ANIMATIONTYPE_VECTOR2:
  674. element = <li className="property" key={i}><p>{animation.targetProperty}</p>
  675. <ul>
  676. <li key={`${i}_x`}>Property <strong>X</strong></li>
  677. <li key={`${i}_y`}>Property <strong>Y</strong></li>
  678. </ul>
  679. </li>
  680. break;
  681. case Animation.ANIMATIONTYPE_VECTOR3:
  682. element = <li className="property" key={i}><p>{animation.targetProperty}</p>
  683. <ul>
  684. <li key={`${i}_x`}>Property <strong>X</strong></li>
  685. <li key={`${i}_y`}>Property <strong>Y</strong></li>
  686. <li key={`${i}_z`}>Property <strong>Z</strong></li>
  687. </ul>
  688. </li>
  689. break;
  690. case Animation.ANIMATIONTYPE_QUATERNION:
  691. element = <li className="property" key={i}><p>{animation.targetProperty}</p>
  692. <ul>
  693. <li key={`${i}_x`}>Property <strong>X</strong></li>
  694. <li key={`${i}_y`}>Property <strong>Y</strong></li>
  695. <li key={`${i}_z`}>Property <strong>Z</strong></li>
  696. <li key={`${i}_w`}>Property <strong>W</strong></li>
  697. </ul>
  698. </li>
  699. break;
  700. case Animation.ANIMATIONTYPE_COLOR3:
  701. element = <li className="property" key={i}><p>{animation.targetProperty}</p>
  702. <ul>
  703. <li key={`${i}_r`}>Property <strong>R</strong></li>
  704. <li key={`${i}_g`}>Property <strong>G</strong></li>
  705. <li key={`${i}_b`}>Property <strong>B</strong></li>
  706. </ul>
  707. </li>
  708. break;
  709. case Animation.ANIMATIONTYPE_COLOR4:
  710. element = <li className="property" key={i}><p>{animation.targetProperty}</p>
  711. <ul>
  712. <li key={`${i}_r`}>Property <strong>R</strong></li>
  713. <li key={`${i}_g`}>Property <strong>G</strong></li>
  714. <li key={`${i}_b`}>Property <strong>B</strong></li>
  715. <li key={`${i}_a`}>Property <strong>A</strong></li>
  716. </ul>
  717. </li>
  718. break;
  719. case Animation.ANIMATIONTYPE_SIZE:
  720. element = <li className="property" key={i}><p>{animation.targetProperty}</p>
  721. <ul>
  722. <li key={`${i}_width`}>Property <strong>Width</strong></li>
  723. <li key={`${i}_height`}>Property <strong>Height</strong></li>
  724. </ul>
  725. </li>
  726. break;
  727. default: console.log("not recognized");
  728. element = null;
  729. break;
  730. }
  731. return element;
  732. })}
  733. </ul>
  734. </div>
  735. </div>
  736. <div ref={this._graphCanvas} className="graph-chart" onWheel={(e) => this.zoom(e)} >
  737. <Playhead frame={this.state.currentFrame} offset={this.state.playheadOffset} />
  738. {this.state.svgKeyframes && <SvgDraggableArea ref={this._svgCanvas} viewBoxScale={this.state.frameAxisLength.length} scale={this.state.scale} keyframeSvgPoints={this.state.svgKeyframes} updatePosition={(updatedSvgKeyFrame: IKeyframeSvgPoint, index: number) => this.renderPoints(updatedSvgKeyFrame, index)}>
  739. {/* Frame Labels */}
  740. { /* Vertical Grid */}
  741. {this.state.frameAxisLength.map((f, i) =>
  742. <svg key={i}>
  743. <text x={f.value} y="-2" dx="-1em" style={{ font: 'italic 0.2em sans-serif', fontSize: `${0.2 * this.state.scale}em` }}>{f.value}</text>
  744. <line x1={f.value} y1="0" x2={f.value} y2="100%"></line>
  745. </svg>
  746. )}
  747. {this.state.valueAxisLength.map((f, i) => {
  748. return <svg key={i}>
  749. <text x="-3" y={f.value} dx="-1em" style={{ font: 'italic 0.2em sans-serif', fontSize: `${0.2 * this.state.scale}em` }}>{f.label.toFixed(1)}</text>
  750. <line x1="0" y1={f.value} x2="100%" y2={f.value}></line>
  751. </svg>
  752. })}
  753. { /* Single Curve -Modify this for multiple selection and view */}
  754. <path ref={this._selectedCurve} pathLength={this.state.lastFrame} id="curve" d={this.state.currentPathData} style={{ stroke: 'red', fill: 'none', strokeWidth: '0.5' }}></path>
  755. {this._frames && this._frames.map(frame =>
  756. <svg x={frame.x} y={frame.y} style={{ overflow: 'visible' }}>
  757. <circle cx="0" cy="0" r="2" stroke="black" strokeWidth="1" fill="white" />
  758. </svg>
  759. )}
  760. </SvgDraggableArea>
  761. }
  762. </div>
  763. </div>
  764. <div className="row">
  765. <Timeline currentFrame={this.state.currentFrame} onCurrentFrameChange={(frame: number) => this.changeCurrentFrame(frame)} keyframes={this.state.selected.getKeys()} selected={this.state.selected.getKeys()[0]}></Timeline>
  766. </div>
  767. </div>
  768. </div>
  769. );
  770. }
  771. }