animationCurveEditorComponent.tsx 97 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419
  1. import * as React from "react";
  2. import { Animation } from "babylonjs/Animations/animation";
  3. import { Vector2, Vector3, Quaternion } from "babylonjs/Maths/math.vector";
  4. import { Color3, Color4 } from "babylonjs/Maths/math.color";
  5. import { Size } from "babylonjs/Maths/math.size";
  6. import { EasingFunction } from "babylonjs/Animations/easing";
  7. import { IAnimationKey } from "babylonjs/Animations/animationKey";
  8. import { IKeyframeSvgPoint } from "./keyframeSvgPoint";
  9. import { SvgDraggableArea } from "./svgDraggableArea";
  10. import { Timeline } from "./timeline";
  11. import { Notification } from "./notification";
  12. import { GraphActionsBar } from "./graphActionsBar";
  13. import { Scene } from "babylonjs/scene";
  14. import { IAnimatable } from "babylonjs/Animations/animatable.interface";
  15. import { Animatable } from "babylonjs/Animations/animatable";
  16. import { TargetedAnimation } from "babylonjs/Animations/animationGroup";
  17. import { EditorControls } from "./editorControls";
  18. import { SelectedCoordinate } from "./animationListTree";
  19. import { LockObject } from "../lockObject";
  20. import { GlobalState } from "../../../../globalState";
  21. import { Nullable } from "babylonjs/types";
  22. import { Observer } from "babylonjs/Misc/observable";
  23. import { ScaleLabel } from "./scale-label";
  24. import { KeyframeSvgPoint } from "./keyframeSvgPoint";
  25. require("./curveEditor.scss");
  26. interface IAnimationCurveEditorComponentProps {
  27. playOrPause?: () => void;
  28. scene: Scene;
  29. entity: IAnimatable | TargetedAnimation;
  30. lockObject: LockObject;
  31. globalState: GlobalState;
  32. }
  33. interface ICanvasAxis {
  34. value: number;
  35. label: number;
  36. }
  37. export enum CurveScale {
  38. float,
  39. radians,
  40. degrees,
  41. integers,
  42. default,
  43. }
  44. export interface IActionableKeyFrame {
  45. frame?: number | string;
  46. value?: any;
  47. }
  48. interface ICurveData {
  49. pathData: string;
  50. pathLength: number;
  51. domCurve: React.RefObject<SVGPathElement>;
  52. color: string;
  53. id: string;
  54. }
  55. export class AnimationCurveEditorComponent extends React.Component<
  56. IAnimationCurveEditorComponentProps,
  57. {
  58. isOpen: boolean;
  59. selected: Animation | null;
  60. svgKeyframes: IKeyframeSvgPoint[] | undefined;
  61. currentFrame: number;
  62. currentValue: number;
  63. frameAxisLength: ICanvasAxis[];
  64. valueAxisLength: ICanvasAxis[];
  65. isFlatTangentMode: boolean;
  66. isTangentMode: boolean;
  67. isBrokenMode: boolean;
  68. lerpMode: boolean;
  69. scale: number;
  70. playheadOffset: number;
  71. notification: string;
  72. currentPoint: SVGPoint | undefined;
  73. playheadPos: number;
  74. isPlaying: boolean;
  75. selectedPathData: ICurveData[] | undefined;
  76. selectedCoordinate: number;
  77. animationLimit: number;
  78. fps: number;
  79. isLooping: boolean;
  80. panningY: number;
  81. panningX: number;
  82. repositionCanvas: boolean;
  83. actionableKeyframe: IActionableKeyFrame;
  84. valueScaleType: CurveScale;
  85. valueScale: number;
  86. canvasLength: number;
  87. lastKeyframeCreated: Nullable<string>;
  88. canvasWidthScale: number;
  89. valuesPositionResize: number;
  90. framesInCanvasView: { from: number; to: number };
  91. maxFrame: number | undefined;
  92. minFrame: number | undefined;
  93. }
  94. > {
  95. private _snippetUrl = "https://snippet.babylonjs.com";
  96. // Height scale *Review this functionaliy
  97. private _heightScale: number = 100;
  98. private _scaleFactor: number = 2;
  99. private _currentScale: number = 10;
  100. // Canvas Length *Review this functionality
  101. readonly _entityName: string;
  102. //private _canvasLength: number;
  103. private _svgKeyframes: IKeyframeSvgPoint[] = [];
  104. private _isPlaying: boolean = false;
  105. private _graphCanvas: React.RefObject<HTMLDivElement>;
  106. private _editor: React.RefObject<HTMLDivElement>;
  107. private _editorWindow: Window;
  108. //private _selectedCurve: React.RefObject<SVGPathElement>;
  109. private _svgCanvas: React.RefObject<SvgDraggableArea>;
  110. private _isTargetedAnimation: boolean;
  111. private _pixelFrameUnit: number;
  112. private _resizedTimeline: number;
  113. private _onBeforeRenderObserver: Nullable<Observer<Scene>>;
  114. private _mainAnimatable: Nullable<Animatable>;
  115. constructor(props: IAnimationCurveEditorComponentProps) {
  116. super(props);
  117. this._entityName = (this.props.entity as any).id;
  118. this._editor = React.createRef();
  119. this._graphCanvas = React.createRef();
  120. this._svgCanvas = React.createRef();
  121. this._pixelFrameUnit = 10;
  122. let initialSelection;
  123. let initialPathData;
  124. let initialLerpMode;
  125. if (this.props.entity instanceof TargetedAnimation) {
  126. this._isTargetedAnimation = true;
  127. initialSelection = this.props.entity.animation;
  128. initialLerpMode = initialSelection !== undefined ? this.analizeAnimationForLerp(initialSelection) : false;
  129. initialPathData = initialSelection !== undefined ? this.getPathData(initialSelection) : undefined;
  130. } else {
  131. this._isTargetedAnimation = false;
  132. let hasAnimations =
  133. this.props.entity.animations !== undefined || this.props.entity.animations !== null
  134. ? this.props.entity.animations
  135. : false;
  136. initialSelection = hasAnimations !== false ? hasAnimations && hasAnimations[0] : null;
  137. initialLerpMode =
  138. initialSelection !== undefined
  139. ? this.analizeAnimationForLerp(this.props.entity.animations && initialSelection)
  140. : false;
  141. initialPathData = initialSelection && this.getPathData(initialSelection);
  142. initialPathData = initialPathData === null || initialPathData === undefined ? undefined : initialPathData;
  143. }
  144. const _canvasLength = 240;
  145. this.stopAnimation();
  146. // will update this until we have a top scroll/zoom feature
  147. let valueInd = [2, 1.8, 1.6, 1.4, 1.2, 1, 0.8, 0.6, 0.4, 0.2, 0];
  148. this.state = {
  149. selected: initialSelection,
  150. isOpen: true,
  151. svgKeyframes: this._svgKeyframes,
  152. currentFrame: 0,
  153. currentValue: 1,
  154. isFlatTangentMode: false,
  155. isTangentMode: false,
  156. isBrokenMode: false,
  157. lerpMode: initialLerpMode,
  158. playheadOffset: this._graphCanvas.current
  159. ? this._graphCanvas.current.children[0].clientWidth / (_canvasLength * 10)
  160. : 0,
  161. frameAxisLength: this.setFrameAxis(_canvasLength),
  162. valueAxisLength: new Array(10).fill(0).map((s, i) => {
  163. return { value: i * 10, label: valueInd[i] };
  164. }),
  165. notification: "",
  166. currentPoint: undefined,
  167. scale: 1,
  168. playheadPos: 0,
  169. isPlaying: false,
  170. selectedPathData: initialPathData,
  171. selectedCoordinate: 0,
  172. animationLimit: _canvasLength / 2,
  173. canvasLength: _canvasLength,
  174. fps: 60,
  175. isLooping: true,
  176. panningY: 0,
  177. panningX: 0,
  178. repositionCanvas: false,
  179. actionableKeyframe: { frame: undefined, value: undefined },
  180. valueScaleType: CurveScale.default,
  181. valueScale: 2,
  182. lastKeyframeCreated: null,
  183. canvasWidthScale: 200,
  184. valuesPositionResize: 2,
  185. framesInCanvasView: { from: 0, to: 20 },
  186. maxFrame: undefined,
  187. minFrame: undefined,
  188. };
  189. }
  190. componentDidMount() {
  191. this.state.selected && this.selectAnimation(this.state.selected);
  192. if (
  193. this._editor.current &&
  194. this._editor.current.ownerDocument &&
  195. this._editor.current.ownerDocument.defaultView
  196. ) {
  197. this._editorWindow = this._editor.current.ownerDocument.defaultView;
  198. this._editorWindow.addEventListener("resize", this.onWindowResizeWidth.bind(this));
  199. }
  200. }
  201. componentDidUpdate(prevProps: IAnimationCurveEditorComponentProps, prevState: any) {
  202. if (prevState.currentFrame !== this.state.currentFrame) {
  203. this.onCurrentFrameChangeChangeScene(this.state.currentFrame);
  204. }
  205. }
  206. onCurrentFrameChangeChangeScene(value: number) {
  207. if (!this._mainAnimatable) {
  208. return;
  209. }
  210. this._mainAnimatable.goToFrame(value);
  211. }
  212. /**
  213. * Notifications
  214. * To add notification we set the state and clear to make the notification bar hide.
  215. */
  216. clearNotification = () => {
  217. this.setState({ notification: "" });
  218. };
  219. /**
  220. * Zoom and Scroll
  221. * This section handles zoom and scroll
  222. * of the graph area.
  223. */
  224. zoom = (e: React.WheelEvent<HTMLDivElement>) => {
  225. e.nativeEvent.stopImmediatePropagation();
  226. let scaleX = 1;
  227. if (Math.sign(e.deltaY) === -1) {
  228. scaleX = this.state.scale; //- 0.01; //+ 0.01;
  229. }
  230. this.setState({ scale: scaleX });
  231. };
  232. setFrameAxis(currentLength: number) {
  233. //const factor = 10 / this._pixelFrameUnit;
  234. let halfNegative = new Array(currentLength).fill(0).map((s, i) => {
  235. return { value: -i * 10, label: -i };
  236. });
  237. let halfPositive = new Array(currentLength).fill(0).map((s, i) => {
  238. return { value: i * 10, label: i };
  239. });
  240. return [...halfNegative, ...halfPositive];
  241. }
  242. setValueLines() {
  243. const lineV = this._heightScale / 10;
  244. const initialValues = new Array(this._currentScale).fill(0).map((_, i) => {
  245. return {
  246. value: i * lineV,
  247. label: (this._scaleFactor * ((this._currentScale - i) / this._currentScale)).toFixed(2),
  248. };
  249. });
  250. initialValues.shift();
  251. const valueHeight = Math.abs(Math.round(this.state.panningY / this._currentScale));
  252. const sign = Math.sign(this.state.panningY);
  253. const pannedValues = new Array(valueHeight).fill(0).map((s, i) => {
  254. return sign === -1
  255. ? {
  256. value: -i * lineV,
  257. label: ((i + this._currentScale) / (this._currentScale / this._scaleFactor)).toFixed(2),
  258. }
  259. : {
  260. value: (i + lineV) * this._currentScale,
  261. label: ((i * -1) / (this._currentScale / this._scaleFactor)).toFixed(2),
  262. };
  263. });
  264. return [...initialValues, ...pannedValues];
  265. }
  266. getValueLabel(i: number) {
  267. // Need to update this when Y axis grows
  268. let label = 0;
  269. if (i === 0) {
  270. label = 2;
  271. }
  272. if (i === 50) {
  273. label = 1;
  274. } else {
  275. label = (100 - i * 2) * 0.01 + 1;
  276. }
  277. return label;
  278. }
  279. resetPlayheadOffset() {
  280. if (this._graphCanvas && this._graphCanvas.current) {
  281. this.setState({
  282. playheadOffset:
  283. this._graphCanvas.current.children[0].clientWidth /
  284. (this.state.canvasLength * 10 * this.state.scale),
  285. });
  286. }
  287. }
  288. encodeCurveId(animationName: string, keyframeIndex: number) {
  289. return animationName + "_" + keyframeIndex;
  290. }
  291. decodeCurveId(id: string) {
  292. const order = parseInt(id.split("_")[3]);
  293. const coordinate = parseInt(id.split("_")[2]);
  294. return { order, coordinate };
  295. }
  296. getKeyframeValueFromAnimation(id: string) {
  297. const animation = this.state.selected as Animation;
  298. const { order, coordinate } = this.decodeCurveId(id);
  299. const keys = [...animation.getKeys()];
  300. const key = keys.find((_, i) => i === order);
  301. if (key) {
  302. const valueAsArray = this.getValueAsArray(animation.dataType, key.value);
  303. return { frame: key?.frame, value: valueAsArray[coordinate] };
  304. } else {
  305. return undefined;
  306. }
  307. }
  308. /**
  309. * Keyframe Manipulation
  310. * This section handles events from SvgDraggableArea.
  311. */
  312. selectKeyframe = (id: string, multiselect: boolean) => {
  313. let frameValue: IActionableKeyFrame | undefined;
  314. const selectedKeyframe = this.state.svgKeyframes?.find((kf) => kf.id === id);
  315. const isKeyFrameSelected = selectedKeyframe?.selected;
  316. const hasCollinearPoints = this.hasCollinearPoints(selectedKeyframe);
  317. if (!multiselect) {
  318. frameValue = this.getKeyframeValueFromAnimation(id);
  319. this.deselectKeyframes();
  320. } else {
  321. frameValue = { frame: undefined, value: undefined };
  322. }
  323. if (isKeyFrameSelected) {
  324. frameValue = { frame: undefined, value: undefined };
  325. }
  326. const updatedKeyframes = this.state.svgKeyframes?.map((kf) => {
  327. if (kf.id === id) {
  328. kf.selected = !isKeyFrameSelected;
  329. }
  330. return kf;
  331. });
  332. let maxFrame = undefined;
  333. let minFrame = undefined;
  334. if (frameValue && typeof frameValue.frame === "number") {
  335. let { prev, next } = this.getPreviousAndNextKeyframe(frameValue.frame);
  336. maxFrame = next;
  337. minFrame = prev;
  338. }
  339. this.setState({
  340. svgKeyframes: updatedKeyframes,
  341. actionableKeyframe: frameValue ?? this.state.actionableKeyframe,
  342. maxFrame: maxFrame,
  343. minFrame: minFrame,
  344. isBrokenMode: !hasCollinearPoints,
  345. });
  346. };
  347. hasCollinearPoints = (kf: IKeyframeSvgPoint | undefined) => {
  348. const left = kf?.leftControlPoint;
  349. const right = kf?.rightControlPoint;
  350. if (left === undefined || right === undefined || left === null || right === null) {
  351. return false;
  352. } else {
  353. if (left.y === right.y) {
  354. return true;
  355. } else {
  356. return false;
  357. }
  358. }
  359. };
  360. getPreviousAndNextKeyframe = (frame: number) => {
  361. let prev,
  362. next = undefined;
  363. const animation = this.state.selected;
  364. if (animation) {
  365. const keys = animation.getKeys();
  366. if (keys) {
  367. const index = keys.findIndex((x) => x.frame === frame);
  368. prev = keys[index - 1] && keys[index - 1].frame + 1;
  369. next = keys[index + 1] && keys[index + 1].frame - 1;
  370. }
  371. }
  372. return { prev, next };
  373. };
  374. selectKeyframeFromId = (id: string, actionableKeyframe: IActionableKeyFrame) => {
  375. this.deselectKeyframes();
  376. const updatedKeyframes = this.state.svgKeyframes?.map((kf) => {
  377. if (kf.id === id) {
  378. kf.selected = true;
  379. }
  380. return kf;
  381. });
  382. let { prev, next } = this.getPreviousAndNextKeyframe(actionableKeyframe.frame as number);
  383. this.setState({
  384. svgKeyframes: updatedKeyframes,
  385. actionableKeyframe: actionableKeyframe ?? this.state.actionableKeyframe,
  386. maxFrame: next,
  387. minFrame: prev,
  388. });
  389. };
  390. resetActionableKeyframe = () => {
  391. this.setState({
  392. actionableKeyframe: { frame: undefined, value: undefined },
  393. maxFrame: undefined,
  394. minFrame: undefined,
  395. });
  396. };
  397. selectedControlPoint = (type: string, id: string) => {
  398. const controlPoint = this.state.svgKeyframes?.find((x) => x.id === id);
  399. if (controlPoint) {
  400. let isSelected;
  401. if (type === "left") {
  402. isSelected = controlPoint.isLeftActive;
  403. controlPoint.isLeftActive = !isSelected;
  404. controlPoint.isRightActive = false;
  405. }
  406. if (type === "right") {
  407. isSelected = controlPoint.isRightActive;
  408. controlPoint.isRightActive = !isSelected;
  409. controlPoint.isLeftActive = false;
  410. }
  411. }
  412. let updatedKeyframes = this.state.svgKeyframes?.map((kf) => {
  413. if (kf.id === id) {
  414. kf === controlPoint;
  415. }
  416. return kf;
  417. });
  418. this.setState({ svgKeyframes: updatedKeyframes });
  419. };
  420. deselectKeyframes = () => {
  421. let updatedKeyframes = this.state.svgKeyframes?.map((kf) => {
  422. kf.isLeftActive = false;
  423. kf.isRightActive = false;
  424. kf.selected = false;
  425. return kf;
  426. });
  427. this.setState({
  428. svgKeyframes: updatedKeyframes,
  429. actionableKeyframe: { frame: undefined, value: undefined },
  430. maxFrame: undefined,
  431. minFrame: undefined,
  432. });
  433. };
  434. updateValuePerCoordinate(
  435. dataType: number,
  436. value: number | Vector2 | Vector3 | Color3 | Color4 | Size | Quaternion,
  437. newValue: number,
  438. coordinate?: number
  439. ) {
  440. if (dataType === Animation.ANIMATIONTYPE_FLOAT) {
  441. value = newValue;
  442. }
  443. if (dataType === Animation.ANIMATIONTYPE_VECTOR2) {
  444. switch (coordinate) {
  445. case SelectedCoordinate.x:
  446. (value as Vector2).x = newValue;
  447. break;
  448. case SelectedCoordinate.y:
  449. (value as Vector2).y = newValue;
  450. break;
  451. }
  452. }
  453. if (dataType === Animation.ANIMATIONTYPE_VECTOR3) {
  454. switch (coordinate) {
  455. case SelectedCoordinate.x:
  456. (value as Vector3).x = newValue;
  457. break;
  458. case SelectedCoordinate.y:
  459. (value as Vector3).y = newValue;
  460. break;
  461. case SelectedCoordinate.z:
  462. (value as Vector3).z = newValue;
  463. break;
  464. }
  465. }
  466. if (dataType === Animation.ANIMATIONTYPE_QUATERNION) {
  467. switch (coordinate) {
  468. case SelectedCoordinate.x:
  469. (value as Quaternion).x = newValue;
  470. break;
  471. case SelectedCoordinate.y:
  472. (value as Quaternion).y = newValue;
  473. break;
  474. case SelectedCoordinate.z:
  475. (value as Quaternion).z = newValue;
  476. break;
  477. case SelectedCoordinate.w:
  478. (value as Quaternion).w = newValue;
  479. break;
  480. }
  481. }
  482. if (dataType === Animation.ANIMATIONTYPE_COLOR3) {
  483. switch (coordinate) {
  484. case SelectedCoordinate.r:
  485. (value as Color3).r = newValue;
  486. break;
  487. case SelectedCoordinate.g:
  488. (value as Color3).g = newValue;
  489. break;
  490. case SelectedCoordinate.b:
  491. (value as Color3).b = newValue;
  492. break;
  493. }
  494. }
  495. if (dataType === Animation.ANIMATIONTYPE_COLOR4) {
  496. switch (coordinate) {
  497. case SelectedCoordinate.r:
  498. (value as Color4).r = newValue;
  499. break;
  500. case SelectedCoordinate.g:
  501. (value as Color4).g = newValue;
  502. break;
  503. case SelectedCoordinate.b:
  504. (value as Color4).b = newValue;
  505. break;
  506. case SelectedCoordinate.a:
  507. (value as Color4).a = newValue;
  508. break;
  509. }
  510. }
  511. if (dataType === Animation.ANIMATIONTYPE_SIZE) {
  512. switch (coordinate) {
  513. case SelectedCoordinate.width:
  514. (value as Size).width = newValue;
  515. break;
  516. case SelectedCoordinate.g:
  517. (value as Size).height = newValue;
  518. break;
  519. }
  520. }
  521. return value;
  522. }
  523. forceFrameZeroToExist(keys: IAnimationKey[]) {
  524. const zeroFrame = keys.find((x) => Math.abs(x.frame) === 0);
  525. if (zeroFrame === undefined) {
  526. const prevToZero = keys.filter((x) => Math.sign(x.frame) === -1).sort((a, b) => b.frame - a.frame);
  527. let value;
  528. if (prevToZero.length !== 0) {
  529. value = prevToZero[0].value;
  530. } else {
  531. value = 1;
  532. }
  533. const frame: IAnimationKey = { frame: 0, value };
  534. keys.push(frame);
  535. keys.sort((a, b) => a.frame - b.frame);
  536. }
  537. }
  538. renderPoints = (updatedSvgKeyFrame: IKeyframeSvgPoint, id: string) => {
  539. let animation = this.state.selected as Animation;
  540. const { order: index, coordinate } = this.decodeCurveId(id);
  541. let keys = [...animation.getKeys()];
  542. let newFrame = 0;
  543. if (updatedSvgKeyFrame.keyframePoint.x !== 0) {
  544. if (updatedSvgKeyFrame.keyframePoint.x > 0 && updatedSvgKeyFrame.keyframePoint.x < 1) {
  545. newFrame = 0;
  546. } else {
  547. newFrame = Math.round(updatedSvgKeyFrame.keyframePoint.x / this._pixelFrameUnit);
  548. }
  549. }
  550. if (newFrame > keys[index].frame) {
  551. if (index === keys.length - 1) {
  552. keys[index].frame = newFrame;
  553. } else {
  554. const nextKf = keys[index + 1];
  555. if (nextKf) {
  556. if (nextKf.frame <= newFrame) {
  557. keys[index].frame = keys[index].frame;
  558. } else {
  559. keys[index].frame = newFrame;
  560. }
  561. }
  562. }
  563. }
  564. if (newFrame < keys[index].frame) {
  565. if (index === 0) {
  566. keys[index].frame = newFrame;
  567. } else {
  568. const prevKf = keys[index - 1];
  569. if (prevKf) {
  570. if (prevKf.frame >= newFrame) {
  571. keys[index].frame = keys[index].frame;
  572. } else {
  573. keys[index].frame = newFrame;
  574. }
  575. }
  576. }
  577. }
  578. let updatedValue =
  579. ((this._heightScale - updatedSvgKeyFrame.keyframePoint.y) / this._heightScale) * this._scaleFactor;
  580. const updatedValueInCoordinate = this.updateValuePerCoordinate(
  581. animation.dataType,
  582. keys[index].value,
  583. updatedValue,
  584. coordinate
  585. );
  586. keys[index].value = updatedValueInCoordinate;
  587. this.updateLeftControlPoint(updatedSvgKeyFrame, keys[index], animation.dataType, coordinate);
  588. this.updateRightControlPoint(updatedSvgKeyFrame, keys[index], animation.dataType, coordinate);
  589. this.forceFrameZeroToExist(keys);
  590. animation.setKeys(keys);
  591. let { prev, next } = this.getPreviousAndNextKeyframe(newFrame);
  592. this.setState({
  593. actionableKeyframe: { frame: newFrame, value: updatedValueInCoordinate },
  594. maxFrame: next,
  595. minFrame: prev,
  596. });
  597. this.selectAnimation(animation, coordinate);
  598. };
  599. updateLeftControlPoint(
  600. updatedSvgKeyFrame: IKeyframeSvgPoint,
  601. key: IAnimationKey,
  602. dataType: number,
  603. coordinate: number
  604. ) {
  605. if (updatedSvgKeyFrame.isLeftActive) {
  606. if (updatedSvgKeyFrame.leftControlPoint !== null) {
  607. // Rotate Control Points
  608. // Get the previous svgKeyframe and measure distance between these two points
  609. let distanceWithPreviousKeyframe = this.getControlPointWeight(updatedSvgKeyFrame);
  610. // Get a quarter of that value for amplitude
  611. let distanceAmplitudeOfX = updatedSvgKeyFrame.leftControlPoint.x - distanceWithPreviousKeyframe;
  612. let slope =
  613. (updatedSvgKeyFrame.leftControlPoint.y - updatedSvgKeyFrame.keyframePoint.y) /
  614. (updatedSvgKeyFrame.leftControlPoint.x - updatedSvgKeyFrame.keyframePoint.x);
  615. let newValueOfY =
  616. (distanceAmplitudeOfX - updatedSvgKeyFrame.leftControlPoint.x) * slope +
  617. updatedSvgKeyFrame.keyframePoint.y;
  618. let updatedValue =
  619. ((newValueOfY - updatedSvgKeyFrame.keyframePoint.y) * this._scaleFactor) / this._heightScale;
  620. if (updatedValue > -100 && updatedValue < 100) {
  621. key.inTangent = slope; //this.updateValuePerCoordinate(dataType, key.inTangent, updatedValue, coordinate);
  622. if (!this.state.isBrokenMode) {
  623. if (updatedSvgKeyFrame.rightControlPoint !== null) {
  624. // Sets opposite value
  625. // get angle between control points and keep angle...
  626. key.outTangent = key.inTangent * -1;
  627. }
  628. }
  629. }
  630. }
  631. }
  632. }
  633. updateRightControlPoint(
  634. updatedSvgKeyFrame: IKeyframeSvgPoint,
  635. key: IAnimationKey,
  636. dataType: number,
  637. coordinate: number
  638. ) {
  639. if (updatedSvgKeyFrame.isRightActive) {
  640. if (updatedSvgKeyFrame.rightControlPoint !== null) {
  641. // Rotate Control Points
  642. // Get the next svgKeyframe and measure distance between these two points
  643. let distanceWithNextKeyframe = this.getControlPointWeight(updatedSvgKeyFrame);
  644. let distanceAmplitudeOfX = updatedSvgKeyFrame.rightControlPoint.x + distanceWithNextKeyframe;
  645. let slope =
  646. (updatedSvgKeyFrame.rightControlPoint.y - updatedSvgKeyFrame.keyframePoint.y) /
  647. (updatedSvgKeyFrame.rightControlPoint.x - updatedSvgKeyFrame.keyframePoint.x);
  648. let newValueOfY =
  649. (distanceAmplitudeOfX - updatedSvgKeyFrame.rightControlPoint.x) * slope +
  650. updatedSvgKeyFrame.keyframePoint.y;
  651. let updatedValue =
  652. ((newValueOfY - updatedSvgKeyFrame.keyframePoint.y) * this._scaleFactor) / this._heightScale;
  653. if (updatedValue > -100 && updatedValue < 100) {
  654. key.outTangent = slope * -1; //this.updateValuePerCoordinate(dataType, key.outTangent, updatedValue, coordinate);
  655. if (!this.state.isBrokenMode) {
  656. if (updatedSvgKeyFrame.leftControlPoint !== null) {
  657. // Sets opposite value
  658. key.inTangent = key.outTangent * -1;
  659. }
  660. }
  661. }
  662. }
  663. }
  664. }
  665. getControlPointWeight(updatedSvgKeyFrame: IKeyframeSvgPoint) {
  666. let distanceWithPreviousKeyframe = this.state.canvasWidthScale / 4;
  667. if (this.state.svgKeyframes) {
  668. let indexOfKeyframe = this.state.svgKeyframes.indexOf(updatedSvgKeyFrame);
  669. let previousKeyframe = this.state.svgKeyframes[indexOfKeyframe - 1];
  670. if (previousKeyframe?.keyframePoint) {
  671. distanceWithPreviousKeyframe =
  672. Vector2.Distance(updatedSvgKeyFrame.keyframePoint, previousKeyframe.keyframePoint) / 2;
  673. }
  674. }
  675. let distanceWithNextKeyframe = this.state.canvasWidthScale / 4;
  676. if (this.state.svgKeyframes) {
  677. let indexOfKeyframe = this.state.svgKeyframes.indexOf(updatedSvgKeyFrame);
  678. let nextKeyframe = this.state.svgKeyframes[indexOfKeyframe + 1];
  679. if (nextKeyframe?.keyframePoint) {
  680. distanceWithNextKeyframe =
  681. Vector2.Distance(nextKeyframe.keyframePoint, updatedSvgKeyFrame.keyframePoint) / 2;
  682. }
  683. }
  684. if (distanceWithPreviousKeyframe < distanceWithNextKeyframe) {
  685. return distanceWithPreviousKeyframe;
  686. } else {
  687. return distanceWithNextKeyframe;
  688. }
  689. }
  690. handleFrameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  691. event.preventDefault();
  692. let frame;
  693. let maxFrame = undefined;
  694. let minFrame = undefined;
  695. if (event.target.value === "") {
  696. frame = "";
  697. } else {
  698. frame = parseInt(event.target.value);
  699. const { prev, next } = this.getPreviousAndNextKeyframe(frame);
  700. maxFrame = next;
  701. minFrame = prev;
  702. }
  703. this.setState({
  704. actionableKeyframe: {
  705. frame: frame,
  706. value: this.state.actionableKeyframe.value,
  707. },
  708. maxFrame,
  709. minFrame,
  710. });
  711. };
  712. handleValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
  713. event.preventDefault();
  714. let value;
  715. if (event.target.value !== undefined) {
  716. if (event.target.value !== "") {
  717. value = parseFloat(event.target.value);
  718. } else {
  719. value = "";
  720. }
  721. this.setState({
  722. actionableKeyframe: {
  723. frame: this.state.actionableKeyframe.frame,
  724. value: value,
  725. },
  726. maxFrame: undefined,
  727. minFrame: undefined,
  728. });
  729. }
  730. };
  731. setKeyframeValueFromInput = (actionableKeyframe: IActionableKeyFrame) => {
  732. this.setState(
  733. {
  734. actionableKeyframe,
  735. },
  736. this.setKeyframeValue
  737. );
  738. };
  739. setKeyframeValue = () => {
  740. if (
  741. this.state.actionableKeyframe.frame !== "" &&
  742. this.state.actionableKeyframe.frame !== undefined &&
  743. this.state.actionableKeyframe.value !== "" &&
  744. this.state.actionableKeyframe.value !== undefined
  745. ) {
  746. if (this.state.selected !== null) {
  747. let currentSelected = this.state.svgKeyframes?.find((kf) => kf.selected);
  748. if (currentSelected) {
  749. let { order, coordinate } = this.decodeCurveId(currentSelected.id);
  750. let animation = this.state.selected;
  751. let keys = animation.getKeys();
  752. let isKeyframe = keys.find((_, i) => i === order);
  753. if (isKeyframe) {
  754. let updatedKeys = keys.map((k, i) => {
  755. if (i === order) {
  756. k.frame = this.state.actionableKeyframe.frame as number;
  757. const currentValue = this.getValueAsArray(animation.dataType, k.value);
  758. currentValue[coordinate] = this.state.actionableKeyframe.value;
  759. k.value = currentValue.length === 1 ? currentValue[0] : currentValue;
  760. }
  761. return k;
  762. });
  763. this.forceFrameZeroToExist(updatedKeys);
  764. this.state.selected.setKeys(updatedKeys);
  765. this.selectAnimation(animation);
  766. this.setCanvasPosition({
  767. frame: this.state.actionableKeyframe.frame as number,
  768. value: this.state.actionableKeyframe.value,
  769. });
  770. const { prev, next } = this.getPreviousAndNextKeyframe(
  771. this.state.actionableKeyframe.frame as number
  772. );
  773. this.setState({ maxFrame: next, minFrame: prev });
  774. }
  775. }
  776. }
  777. }
  778. };
  779. setFlatTangent = () => {
  780. if (this.state.selected !== null) {
  781. const keyframes = this.state.svgKeyframes?.filter((kf) => kf.selected).map((k) => this.decodeCurveId(k.id));
  782. const currentAnimation = this.state.selected;
  783. const keys = currentAnimation.getKeys();
  784. if (this.state.isBrokenMode) {
  785. const keyframeWithControlPointSelected = this.state.svgKeyframes?.find((kf) => kf.selected);
  786. if (keyframeWithControlPointSelected) {
  787. keyframes?.forEach((k) => {
  788. const keyframe = keys[k.order];
  789. if (keyframeWithControlPointSelected.isLeftActive) {
  790. keyframe.inTangent = this.returnZero(currentAnimation.dataType);
  791. }
  792. if (keyframeWithControlPointSelected.isRightActive) {
  793. keyframe.outTangent = this.returnZero(currentAnimation.dataType);
  794. }
  795. });
  796. }
  797. } else {
  798. keyframes?.forEach((k) => {
  799. const keyframe = keys[k.order];
  800. keyframe.inTangent = this.returnZero(currentAnimation.dataType);
  801. keyframe.outTangent = this.returnZero(currentAnimation.dataType);
  802. });
  803. }
  804. currentAnimation.setKeys(keys);
  805. this.selectAnimation(currentAnimation, this.state.selectedCoordinate);
  806. }
  807. };
  808. setBrokenMode = () => {
  809. if (this.state.selected !== null) {
  810. let animation = this.state.selected;
  811. this.setState({ isBrokenMode: !this.state.isBrokenMode }, () =>
  812. this.selectAnimation(animation, this.state.selectedCoordinate)
  813. );
  814. }
  815. };
  816. setLerpToActiveControlPoint = () => {
  817. const animation = this.state.selected;
  818. if (this.state.svgKeyframes && animation) {
  819. const keys = animation.getKeys();
  820. const selectedKeyframe = this.state.svgKeyframes.find(
  821. (keyframe: IKeyframeSvgPoint) => keyframe.selected && (keyframe.isLeftActive || keyframe.isRightActive)
  822. );
  823. if (selectedKeyframe !== null && selectedKeyframe) {
  824. const { order, coordinate } = this.decodeCurveId(selectedKeyframe.id);
  825. const key = keys[order];
  826. if (selectedKeyframe.isLeftActive && selectedKeyframe.leftControlPoint !== null) {
  827. const start = new Vector2(key.frame, key.value);
  828. const prev = new Vector2(keys[order - 1].frame, keys[order - 1].value);
  829. //const pointHalf = Vector2.Lerp(prev, start, 0.5); // Explore the distance of the point
  830. //const inTangent = pointHalf.y - start.y;
  831. let slope = (start.y - prev.y) / (start.x - prev.x);
  832. key.inTangent = slope * -1;
  833. } else if (selectedKeyframe.isRightActive && selectedKeyframe.rightControlPoint !== null) {
  834. const start = new Vector2(key.frame, key.value);
  835. const next = new Vector2(keys[order + 1].frame, keys[order + 1].value);
  836. //const pointHalf = Vector2.Lerp(start, next, 0.5); // Explore the distance of the point
  837. let slope = (next.y - start.y) / (next.x - start.x);
  838. // const outTangent = pointHalf.y - start.y;
  839. key.outTangent = slope;
  840. }
  841. this.setState({ isBrokenMode: true }, () => {
  842. this.selectAnimation(animation, coordinate);
  843. });
  844. }
  845. }
  846. };
  847. addKeyframeClick = () => {
  848. if (this.state.selected !== null) {
  849. let currentAnimation = this.state.selected;
  850. let keys = currentAnimation.getKeys();
  851. let x = this.state.currentFrame;
  852. let existValue = keys.find((k) => k.frame === x);
  853. if (existValue === undefined) {
  854. let y = this.state.actionableKeyframe.value ?? 1;
  855. // check if value exists...
  856. let arrayValue: any = [];
  857. let emptyValue = this.returnZero(currentAnimation.dataType);
  858. if (emptyValue) {
  859. arrayValue = this.getValueAsArray(currentAnimation.dataType, emptyValue);
  860. }
  861. // calculate point between prevkeyframe and nextkeyframe.
  862. const previousKFs = keys.filter((kf) => kf.frame < x);
  863. const nextKFs = keys.filter((kf) => kf.frame > x);
  864. const prev = previousKFs.slice(-1)[0];
  865. const next = nextKFs[0];
  866. if (prev === undefined && next) {
  867. y = next.value;
  868. }
  869. if (prev && next === undefined) {
  870. y = prev.value;
  871. }
  872. if (prev && next) {
  873. const value1 = new Vector2(prev.frame, prev.value);
  874. const tangent1 = new Vector2(prev.outTangent, prev.outTangent);
  875. const value2 = new Vector2(next.frame, next.value);
  876. const tangent2 = new Vector2(next.inTangent, next.inTangent);
  877. const amount = (x - prev.frame) / (next.frame - prev.frame);
  878. const newV = Vector2.Hermite(value1, tangent1, value2, tangent2, amount);
  879. y = newV.y;
  880. }
  881. arrayValue[this.state.selectedCoordinate] = y;
  882. let actualValue = this.setValueAsType(currentAnimation.dataType, arrayValue);
  883. const recentlyCreated = {
  884. frame: x,
  885. value: actualValue,
  886. inTangent: this.state.isFlatTangentMode ? 0 : 0, // check if flat mode can be turn off
  887. outTangent: this.state.isFlatTangentMode ? 0 : 0, // check if flat mode can be turn off
  888. };
  889. keys.push(recentlyCreated);
  890. keys.sort((a, b) => a.frame - b.frame);
  891. const newIndex = keys.findIndex((kf) => kf.frame === x);
  892. const id = `${currentAnimation.name}_${currentAnimation.targetProperty}_${this.state.selectedCoordinate}`;
  893. const curvedId = this.encodeCurveId(id, newIndex);
  894. this.setState({ lastKeyframeCreated: curvedId });
  895. this.forceFrameZeroToExist(keys);
  896. currentAnimation.setKeys(keys);
  897. this.selectAnimation(currentAnimation, this.state.selectedCoordinate);
  898. }
  899. }
  900. };
  901. removeKeyframeClick = () => {
  902. if (this.state.selected !== null) {
  903. let currentAnimation = this.state.selected;
  904. if (currentAnimation.dataType === Animation.ANIMATIONTYPE_FLOAT) {
  905. let keys = currentAnimation.getKeys();
  906. let x = this.state.currentFrame;
  907. let filteredKeys = keys.filter((kf) => kf.frame !== x);
  908. currentAnimation.setKeys(filteredKeys);
  909. this.selectAnimation(currentAnimation, this.state.selectedCoordinate);
  910. }
  911. }
  912. };
  913. removeKeyframes = (points: IKeyframeSvgPoint[]) => {
  914. if (this.state.selected !== null) {
  915. let currentAnimation = this.state.selected;
  916. const indexesToRemove = points.map((p) => {
  917. return {
  918. index: parseInt(p.id.split("_")[3]),
  919. coordinate: parseInt(p.id.split("_")[2]),
  920. };
  921. });
  922. if (currentAnimation.dataType === Animation.ANIMATIONTYPE_FLOAT) {
  923. let keys = currentAnimation.getKeys();
  924. let filteredKeys = keys.filter((_, i) => {
  925. return !indexesToRemove.find((x) => x.index === i);
  926. });
  927. currentAnimation.setKeys(filteredKeys);
  928. this.deselectKeyframes();
  929. this.selectAnimation(currentAnimation, this.state.selectedCoordinate);
  930. }
  931. }
  932. };
  933. addKeyFrame(event: React.MouseEvent<SVGSVGElement>) {
  934. event.preventDefault();
  935. if (this.state.selected !== null) {
  936. var svg = event.target as SVGSVGElement;
  937. var pt = svg.createSVGPoint();
  938. pt.x = event.clientX;
  939. pt.y = event.clientY;
  940. var inverse = svg.getScreenCTM()?.inverse();
  941. var cursorpt = pt.matrixTransform(inverse);
  942. var currentAnimation = this.state.selected;
  943. var keys = currentAnimation.getKeys();
  944. var height = 100;
  945. var middle = height / 2;
  946. var keyValue;
  947. if (cursorpt.y < middle) {
  948. keyValue = 1 + (100 / cursorpt.y) * 0.1;
  949. }
  950. if (cursorpt.y > middle) {
  951. keyValue = 1 - (100 / cursorpt.y) * 0.1;
  952. }
  953. keys.push({ frame: cursorpt.x, value: keyValue });
  954. currentAnimation.setKeys(keys);
  955. this.selectAnimation(currentAnimation);
  956. }
  957. }
  958. /**
  959. * Curve Rendering Functions
  960. * This section handles how to render curves.
  961. */
  962. setKeyframePointLinear(point: Vector2, index: number) {
  963. // here set the ID to a unique id
  964. let svgKeyframe = {
  965. keyframePoint: point,
  966. rightControlPoint: null,
  967. leftControlPoint: null,
  968. id: index.toString(),
  969. selected: false,
  970. isLeftActive: false,
  971. isRightActive: false,
  972. };
  973. this._svgKeyframes.push(svgKeyframe);
  974. }
  975. flatTangents(keyframes: IAnimationKey[], dataType: number) {
  976. // Checks if Flat Tangent is active (tangents are set to zero)
  977. // only flat the selected control point
  978. // if multiple selected then flat all...
  979. let flattened;
  980. if (this.state && this.state.isFlatTangentMode) {
  981. flattened = keyframes.map((kf) => {
  982. if (kf.inTangent !== undefined) {
  983. kf.inTangent = this.returnZero(dataType);
  984. }
  985. if (kf.outTangent !== undefined) {
  986. kf.outTangent = this.returnZero(dataType);
  987. }
  988. return kf;
  989. });
  990. } else {
  991. flattened = keyframes;
  992. }
  993. this.setState({ isFlatTangentMode: false });
  994. return flattened;
  995. }
  996. returnZero(dataType: number) {
  997. switch (dataType) {
  998. case Animation.ANIMATIONTYPE_FLOAT:
  999. return 0;
  1000. case Animation.ANIMATIONTYPE_VECTOR3:
  1001. return Vector3.Zero();
  1002. case Animation.ANIMATIONTYPE_VECTOR2:
  1003. return Vector2.Zero();
  1004. case Animation.ANIMATIONTYPE_QUATERNION:
  1005. return Quaternion.Zero();
  1006. case Animation.ANIMATIONTYPE_COLOR3:
  1007. return new Color3(0, 0, 0);
  1008. case Animation.ANIMATIONTYPE_COLOR4:
  1009. return new Color4(0, 0, 0, 0);
  1010. case Animation.ANIMATIONTYPE_SIZE:
  1011. return new Size(0, 0);
  1012. default:
  1013. return 0;
  1014. }
  1015. }
  1016. getValueAsArray(valueType: number, value: number | Vector2 | Vector3 | Color3 | Color4 | Size | Quaternion) {
  1017. switch (valueType) {
  1018. case Animation.ANIMATIONTYPE_FLOAT:
  1019. return [value as number];
  1020. case Animation.ANIMATIONTYPE_VECTOR3:
  1021. return (value as Vector3).asArray();
  1022. case Animation.ANIMATIONTYPE_VECTOR2:
  1023. return (value as Vector2).asArray();
  1024. case Animation.ANIMATIONTYPE_QUATERNION:
  1025. return (value as Quaternion).asArray();
  1026. case Animation.ANIMATIONTYPE_COLOR3:
  1027. return (value as Color3).asArray();
  1028. case Animation.ANIMATIONTYPE_COLOR4:
  1029. return (value as Color4).asArray();
  1030. case Animation.ANIMATIONTYPE_SIZE:
  1031. return [(value as Size).width, (value as Size).height];
  1032. default:
  1033. return [];
  1034. }
  1035. }
  1036. setValueAsType(valueType: number, arrayValue: number[]) {
  1037. switch (valueType) {
  1038. case Animation.ANIMATIONTYPE_FLOAT:
  1039. return arrayValue[0];
  1040. case Animation.ANIMATIONTYPE_VECTOR3:
  1041. return new Vector3(arrayValue[0], arrayValue[1], arrayValue[2]);
  1042. case Animation.ANIMATIONTYPE_VECTOR2:
  1043. return new Vector2(arrayValue[0], arrayValue[1]);
  1044. case Animation.ANIMATIONTYPE_QUATERNION:
  1045. return new Quaternion(arrayValue[0], arrayValue[1], arrayValue[2], arrayValue[3]);
  1046. case Animation.ANIMATIONTYPE_COLOR3:
  1047. return new Color3(arrayValue[0], arrayValue[1], arrayValue[2]);
  1048. case Animation.ANIMATIONTYPE_COLOR4:
  1049. return new Color4(arrayValue[0], arrayValue[1], arrayValue[2], arrayValue[3]);
  1050. case Animation.ANIMATIONTYPE_SIZE:
  1051. return new Size(arrayValue[0], arrayValue[1]);
  1052. default:
  1053. return arrayValue[0];
  1054. }
  1055. }
  1056. getPathData(animation: Animation | null) {
  1057. if (animation === null) {
  1058. return undefined;
  1059. }
  1060. var keyframes = animation.getKeys();
  1061. if (keyframes === undefined || keyframes.length === 0) {
  1062. return undefined;
  1063. } else {
  1064. const {
  1065. easingMode,
  1066. easingType,
  1067. usesTangents,
  1068. valueType,
  1069. highestFrame,
  1070. name,
  1071. targetProperty,
  1072. } = this.getAnimationData(animation);
  1073. //keyframes = this.flatTangents(keyframes, valueType); // This will break because we are using setState before mounted...
  1074. const startKey = keyframes[0];
  1075. let middle = this._heightScale / this._scaleFactor; //?
  1076. let collection: ICurveData[] = [];
  1077. const colors = ["red", "green", "blue", "white", "#7a4ece"];
  1078. const startValue = this.getValueAsArray(valueType, startKey.value);
  1079. for (var d = 0; d < startValue.length; d++) {
  1080. const id = `${name}_${targetProperty}_${d}`;
  1081. const curveColor = valueType === Animation.ANIMATIONTYPE_FLOAT ? colors[4] : colors[d];
  1082. // START OF LINE/CURVE
  1083. let data: string | undefined = `M${startKey.frame * this._pixelFrameUnit}, ${
  1084. this._heightScale - startValue[d] * middle
  1085. }`; //
  1086. if (this.state) {
  1087. if (usesTangents) {
  1088. data = this.curvePathWithTangents(keyframes, data, middle, valueType, d, id);
  1089. } else {
  1090. if (easingType !== undefined && easingMode !== undefined) {
  1091. let easingFunction = animation.getEasingFunction();
  1092. data = this.curvePath(keyframes, data, middle, easingFunction as EasingFunction);
  1093. } else {
  1094. if (this.state !== undefined) {
  1095. data = this.curvePathWithoutTangents(keyframes, data, middle, valueType, d, id);
  1096. }
  1097. }
  1098. }
  1099. }
  1100. collection.push({
  1101. pathData: data,
  1102. pathLength: highestFrame,
  1103. domCurve: React.createRef(),
  1104. color: curveColor,
  1105. id: id,
  1106. });
  1107. }
  1108. return collection;
  1109. }
  1110. }
  1111. getAnimationData(animation: Animation) {
  1112. // General Props
  1113. let loopMode = animation.loopMode;
  1114. let name = animation.name;
  1115. let blendingSpeed = animation.blendingSpeed;
  1116. let targetProperty = animation.targetProperty;
  1117. let targetPropertyPath = animation.targetPropertyPath;
  1118. let framesPerSecond = animation.framePerSecond;
  1119. let highestFrame = animation.getHighestFrame();
  1120. //let serialized = animation.serialize();
  1121. let usesTangents =
  1122. animation.getKeys().find((kf) => kf.hasOwnProperty("inTangent") || kf.hasOwnProperty("outTangent")) !==
  1123. undefined
  1124. ? true
  1125. : false;
  1126. let valueType = animation.dataType;
  1127. // easing properties
  1128. let easingType, easingMode;
  1129. let easingFunction: EasingFunction = animation.getEasingFunction() as EasingFunction;
  1130. if (easingFunction === undefined) {
  1131. easingType = undefined;
  1132. easingMode = undefined;
  1133. } else {
  1134. easingType = easingFunction.constructor.name;
  1135. easingMode = easingFunction.getEasingMode();
  1136. }
  1137. return {
  1138. loopMode,
  1139. name,
  1140. blendingSpeed,
  1141. targetPropertyPath,
  1142. targetProperty,
  1143. framesPerSecond,
  1144. highestFrame,
  1145. usesTangents,
  1146. easingType,
  1147. easingMode,
  1148. valueType,
  1149. };
  1150. }
  1151. calculateLinearTangents(keyframes: IAnimationKey[]) {
  1152. const updatedKeyframes: IAnimationKey[] = keyframes.map((kf, i) => {
  1153. if (keyframes[i + 1] !== undefined) {
  1154. const start = new Vector2(keyframes[i].frame, keyframes[i].value);
  1155. const next = new Vector2(keyframes[i + 1].frame, keyframes[i + 1].value);
  1156. let slope = (next.y - start.y) / (next.x - start.x);
  1157. kf.outTangent = slope;
  1158. }
  1159. if (keyframes[i - 1] !== undefined) {
  1160. const start = new Vector2(keyframes[i].frame, keyframes[i].value);
  1161. const prev = new Vector2(keyframes[i - 1].frame, keyframes[i - 1].value);
  1162. let slope = (prev.y - start.y) / (prev.x - start.x);
  1163. kf.inTangent = slope * -1;
  1164. }
  1165. if (i === keyframes.length - 1) {
  1166. kf.outTangent = null;
  1167. }
  1168. return kf;
  1169. });
  1170. return updatedKeyframes;
  1171. }
  1172. curvePathWithoutTangents(
  1173. keyframes: IAnimationKey[],
  1174. data: string,
  1175. middle: number,
  1176. type: number,
  1177. coordinate: number,
  1178. animationName: string
  1179. ) {
  1180. const updatedKeyframes = this.calculateLinearTangents(keyframes);
  1181. return this.curvePathWithTangents(updatedKeyframes, data, middle, type, coordinate, animationName);
  1182. }
  1183. curvePathWithTangents(
  1184. keyframes: IAnimationKey[],
  1185. data: string,
  1186. middle: number,
  1187. type: number,
  1188. coordinate: number,
  1189. animationName: string
  1190. ) {
  1191. keyframes.forEach((key, i) => {
  1192. // Create a unique id for curve
  1193. const curveId = this.encodeCurveId(animationName, i);
  1194. // identify type of value and split...
  1195. const keyframe_valueAsArray = this.getValueAsArray(type, key.value)[coordinate];
  1196. let svgKeyframe;
  1197. let outTangent;
  1198. let inTangent;
  1199. let defaultWeight = this.state.canvasWidthScale / 2; // Get weight depending on prev and next frame distance
  1200. //distanceWithPreviousKeyframe = Vector2.Distance(updatedSvgKeyFrame.keyframePoint, previousKeyframe.keyframePoint) / 4;
  1201. // For inTangent
  1202. // has prev frame?
  1203. let weightIn = 0;
  1204. if (keyframes[i - 1] !== undefined) {
  1205. let prevIn = new Vector2(keyframes[i - 1].frame, keyframes[i - 1].value);
  1206. let currIn = new Vector2(key.frame, key.value);
  1207. weightIn = (Vector2.Distance(prevIn, currIn) / 2) * this._pixelFrameUnit;
  1208. }
  1209. // For outTangent
  1210. // has next frame?
  1211. let weightOut = 0;
  1212. if (keyframes[i + 1] !== undefined) {
  1213. let prevOut = new Vector2(keyframes[i + 1].frame, keyframes[i + 1].value);
  1214. let currOut = new Vector2(key.frame, key.value);
  1215. weightOut = (Vector2.Distance(prevOut, currOut) / 2) * this._pixelFrameUnit;
  1216. }
  1217. if (weightIn !== 0 && weightOut !== 0) {
  1218. if (weightIn < weightOut) {
  1219. defaultWeight = weightIn > defaultWeight ? defaultWeight : weightIn;
  1220. } else {
  1221. defaultWeight = weightOut > defaultWeight ? defaultWeight : weightOut;
  1222. }
  1223. }
  1224. if (weightIn === 0 && weightOut !== 0) {
  1225. defaultWeight = weightOut > defaultWeight ? defaultWeight : weightOut;
  1226. }
  1227. if (weightIn !== 0 && weightOut === 0) {
  1228. defaultWeight = weightIn > defaultWeight ? defaultWeight : weightIn;
  1229. }
  1230. // if curve doesnt have tangents then must be null to have linear
  1231. // right now has 0 then the linear will show a slight curve as flat tangents...
  1232. let defaultTangent: number | null = null;
  1233. if (i !== 0 || i !== keyframes.length - 1) {
  1234. defaultTangent = null;
  1235. }
  1236. // defaultTangent = 0; Zero or if linear get linear formula (slope of next, prev point)
  1237. var inT =
  1238. key.inTangent === null || key.inTangent === undefined
  1239. ? defaultTangent
  1240. : this.getValueAsArray(type, key.inTangent)[coordinate];
  1241. var outT =
  1242. key.outTangent === null || key.inTangent === undefined
  1243. ? defaultTangent
  1244. : this.getValueAsArray(type, key.outTangent)[coordinate];
  1245. //let y = this._heightScale - keyframe_valueAsArray * middle; // should be half of heightscale
  1246. defaultWeight = 1 * this._pixelFrameUnit;
  1247. if (inT !== null) {
  1248. let valueInY = inT + keyframe_valueAsArray;
  1249. let valueIn = this._heightScale - valueInY * middle;
  1250. inTangent = new Vector2(key.frame * this._pixelFrameUnit - defaultWeight, valueIn);
  1251. } else {
  1252. inTangent = null;
  1253. }
  1254. if (outT !== null) {
  1255. let valueOutY = outT + keyframe_valueAsArray;
  1256. let valueOut = this._heightScale - valueOutY * middle;
  1257. outTangent = new Vector2(key.frame * this._pixelFrameUnit + defaultWeight, valueOut);
  1258. } else {
  1259. outTangent = null;
  1260. }
  1261. if (i === 0) {
  1262. svgKeyframe = {
  1263. keyframePoint: new Vector2(
  1264. key.frame * this._pixelFrameUnit,
  1265. this._heightScale - keyframe_valueAsArray * middle
  1266. ),
  1267. rightControlPoint: outTangent,
  1268. leftControlPoint: null,
  1269. id: curveId,
  1270. selected: false,
  1271. isLeftActive: false,
  1272. isRightActive: false,
  1273. };
  1274. if (outTangent !== null) {
  1275. data += ` C${outTangent.x} ${outTangent.y} `;
  1276. } else {
  1277. data += ` C${svgKeyframe.keyframePoint.x} ${svgKeyframe.keyframePoint.y} `;
  1278. }
  1279. } else {
  1280. svgKeyframe = {
  1281. keyframePoint: new Vector2(
  1282. key.frame * this._pixelFrameUnit,
  1283. this._heightScale - keyframe_valueAsArray * middle
  1284. ),
  1285. rightControlPoint: outTangent,
  1286. leftControlPoint: inTangent,
  1287. id: curveId,
  1288. selected: false,
  1289. isLeftActive: false,
  1290. isRightActive: false,
  1291. };
  1292. if (outTangent !== null && inTangent !== null) {
  1293. data += ` ${inTangent.x} ${inTangent.y} ${svgKeyframe.keyframePoint.x} ${svgKeyframe.keyframePoint.y} C${outTangent.x} ${outTangent.y}`;
  1294. }
  1295. if (outTangent === null && inTangent !== null) {
  1296. data += ` ${inTangent.x} ${inTangent.y} ${svgKeyframe.keyframePoint.x} ${svgKeyframe.keyframePoint.y} C${svgKeyframe.keyframePoint.x} ${svgKeyframe.keyframePoint.y}`;
  1297. }
  1298. if (inTangent === null && outTangent !== null) {
  1299. const prev = this._svgKeyframes[i - 1];
  1300. data += ` ${prev.keyframePoint.x} ${prev.keyframePoint.y} ${svgKeyframe.keyframePoint.x} ${svgKeyframe.keyframePoint.y} C${outTangent.x} ${outTangent.y}`;
  1301. }
  1302. if (inTangent === null && outTangent === null) {
  1303. const prev = this._svgKeyframes[i - 1];
  1304. data += ` ${prev.keyframePoint.x} ${prev.keyframePoint.y} ${svgKeyframe.keyframePoint.x} ${svgKeyframe.keyframePoint.y} C${svgKeyframe.keyframePoint.x} ${svgKeyframe.keyframePoint.y}`;
  1305. }
  1306. }
  1307. if (this.state) {
  1308. let prev = this.state.svgKeyframes?.find((kf) => kf.id === curveId);
  1309. if (prev) {
  1310. svgKeyframe.isLeftActive = prev?.isLeftActive;
  1311. svgKeyframe.isRightActive = prev?.isRightActive;
  1312. svgKeyframe.selected = prev?.selected;
  1313. }
  1314. }
  1315. this._svgKeyframes.push(svgKeyframe);
  1316. }, this);
  1317. const lastCurveEnd = data.lastIndexOf("C");
  1318. const cleanedData = data.substring(0, lastCurveEnd);
  1319. return cleanedData;
  1320. }
  1321. curvePath(keyframes: IAnimationKey[], data: string, middle: number, easingFunction: EasingFunction) {
  1322. // This will get 1/4 and 3/4 of points in eased curve
  1323. const u = 0.25;
  1324. const v = 0.75;
  1325. keyframes.forEach((key, i) => {
  1326. // identify type of value and split...
  1327. // Gets previous initial point of curve segment
  1328. var pointA = new Vector2(0, 0);
  1329. if (i === 0) {
  1330. pointA.x = key.frame;
  1331. pointA.y = this._heightScale - key.value * middle;
  1332. this.setKeyframePoint([pointA], i, keyframes.length);
  1333. } else {
  1334. pointA.x = keyframes[i - 1].frame;
  1335. pointA.y = this._heightScale - keyframes[i - 1].value * middle;
  1336. // Gets the end point of this curve segment
  1337. var pointB = new Vector2(key.frame, this._heightScale - key.value * middle);
  1338. // Get easing value of percentage to get the bezier control points below
  1339. 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)
  1340. let dv = easingFunction.easeInCore(v); // Option 2: Create a easeInCore function and adapt it with the new control points values... needs more revision.
  1341. // Direction of curve up/down
  1342. let yInt25 = 0;
  1343. if (pointB.y > pointA.y) {
  1344. // if pointB.y > pointA.y = goes down
  1345. yInt25 = (pointB.y - pointA.y) * du + pointA.y;
  1346. } else if (pointB.y < pointA.y) {
  1347. // if pointB.y < pointA.y = goes up
  1348. yInt25 = pointA.y - (pointA.y - pointB.y) * du;
  1349. }
  1350. let yInt75 = 0;
  1351. if (pointB.y > pointA.y) {
  1352. yInt75 = (pointB.y - pointA.y) * dv + pointA.y;
  1353. } else if (pointB.y < pointA.y) {
  1354. yInt75 = pointA.y - (pointA.y - pointB.y) * dv;
  1355. }
  1356. // Intermediate points in curve
  1357. let intermediatePoint25 = new Vector2((pointB.x - pointA.x) * u + pointA.x, yInt25);
  1358. let intermediatePoint75 = new Vector2((pointB.x - pointA.x) * v + pointA.x, yInt75);
  1359. // Gets the four control points of bezier curve
  1360. let controlPoints = this.interpolateControlPoints(
  1361. pointA,
  1362. intermediatePoint25,
  1363. u,
  1364. intermediatePoint75,
  1365. v,
  1366. pointB
  1367. );
  1368. if (controlPoints !== undefined) {
  1369. this.setKeyframePoint(controlPoints, i, keyframes.length);
  1370. data += ` C${controlPoints[1].x} ${controlPoints[1].y} ${controlPoints[2].x} ${controlPoints[2].y} ${controlPoints[3].x} ${controlPoints[3].y}`;
  1371. }
  1372. }
  1373. });
  1374. return data;
  1375. }
  1376. setKeyframePoint(controlPoints: Vector2[], index: number, keyframesCount: number) {
  1377. let svgKeyframe;
  1378. if (index === 0) {
  1379. svgKeyframe = {
  1380. keyframePoint: controlPoints[0],
  1381. rightControlPoint: null,
  1382. leftControlPoint: null,
  1383. id: index.toString(),
  1384. selected: false,
  1385. isLeftActive: false,
  1386. isRightActive: false,
  1387. };
  1388. } else {
  1389. this._svgKeyframes[index - 1].rightControlPoint = controlPoints[1];
  1390. svgKeyframe = {
  1391. keyframePoint: controlPoints[3],
  1392. rightControlPoint: null,
  1393. leftControlPoint: controlPoints[2],
  1394. id: index.toString(),
  1395. selected: false,
  1396. isLeftActive: false,
  1397. isRightActive: false,
  1398. };
  1399. }
  1400. this._svgKeyframes.push(svgKeyframe);
  1401. }
  1402. interpolateControlPoints(
  1403. p0: Vector2,
  1404. p1: Vector2,
  1405. u: number,
  1406. p2: Vector2,
  1407. v: number,
  1408. p3: Vector2
  1409. ): Vector2[] | undefined {
  1410. let a = 0.0;
  1411. let b = 0.0;
  1412. let c = 0.0;
  1413. let d = 0.0;
  1414. let det = 0.0;
  1415. let q1: Vector2 = new Vector2();
  1416. let q2: Vector2 = new Vector2();
  1417. let controlA: Vector2 = p0;
  1418. let controlB: Vector2 = new Vector2();
  1419. let controlC: Vector2 = new Vector2();
  1420. let controlD: Vector2 = p3;
  1421. if (u <= 0.0 || u >= 1.0 || v <= 0.0 || v >= 1.0 || u >= v) {
  1422. return undefined;
  1423. }
  1424. a = 3 * (1 - u) * (1 - u) * u;
  1425. b = 3 * (1 - u) * u * u;
  1426. c = 3 * (1 - v) * (1 - v) * v;
  1427. d = 3 * (1 - v) * v * v;
  1428. det = a * d - b * c;
  1429. if (det == 0.0) return undefined;
  1430. q1.x = p1.x - ((1 - u) * (1 - u) * (1 - u) * p0.x + u * u * u * p3.x);
  1431. q1.y = p1.y - ((1 - u) * (1 - u) * (1 - u) * p0.y + u * u * u * p3.y);
  1432. q2.x = p2.x - ((1 - v) * (1 - v) * (1 - v) * p0.x + v * v * v * p3.x);
  1433. q2.y = p2.y - ((1 - v) * (1 - v) * (1 - v) * p0.y + v * v * v * p3.y);
  1434. controlB.x = (d * q1.x - b * q2.x) / det;
  1435. controlB.y = (d * q1.y - b * q2.y) / det;
  1436. controlC.x = (-c * q1.x + a * q2.x) / det;
  1437. controlC.y = (-c * q1.y + a * q2.y) / det;
  1438. return [controlA, controlB, controlC, controlD];
  1439. }
  1440. deselectAnimation = () => {
  1441. const animations = (this.props.entity as IAnimatable).animations;
  1442. if (animations && animations.length === 0) {
  1443. setTimeout(() => this.cleanCanvas(), 0);
  1444. }
  1445. this.cleanCanvas();
  1446. };
  1447. cleanCanvas = () => {
  1448. this.setState({
  1449. selected: null,
  1450. svgKeyframes: [],
  1451. selectedPathData: undefined,
  1452. selectedCoordinate: 0,
  1453. });
  1454. };
  1455. /**
  1456. * Core functions
  1457. * This section handles main Curve Editor Functions.
  1458. */
  1459. selectAnimation = (animation: Animation, coordinate?: SelectedCoordinate) => {
  1460. this._svgKeyframes = [];
  1461. let updatedPath;
  1462. let filteredSvgKeys;
  1463. let selectedCurve = 0;
  1464. this.stopAnimation();
  1465. if (coordinate === undefined) {
  1466. updatedPath = this.getPathData(animation);
  1467. } else {
  1468. let curves = this.getPathData(animation);
  1469. updatedPath = [];
  1470. filteredSvgKeys = this._svgKeyframes?.filter((curve) => {
  1471. let id = parseInt(curve.id.split("_")[2]);
  1472. if (id === coordinate) {
  1473. return true;
  1474. } else {
  1475. return false;
  1476. }
  1477. });
  1478. curves?.map((curve) => {
  1479. let id = parseInt(curve.id.split("_")[2]);
  1480. if (id === coordinate) {
  1481. updatedPath.push(curve);
  1482. }
  1483. });
  1484. selectedCurve = coordinate;
  1485. }
  1486. // check for empty svgKeyframes, lastframe, selected
  1487. this.setState(
  1488. {
  1489. selected: animation,
  1490. svgKeyframes: coordinate !== undefined ? filteredSvgKeys : this._svgKeyframes,
  1491. selectedPathData: updatedPath,
  1492. selectedCoordinate: selectedCurve,
  1493. fps: animation.framePerSecond,
  1494. },
  1495. this.postSelectionEvents
  1496. );
  1497. };
  1498. postSelectionEvents = () => {
  1499. if (this.state.lastKeyframeCreated !== null) {
  1500. this.deselectKeyframes();
  1501. this.selectKeyframe(this.state.lastKeyframeCreated, false);
  1502. this.setState({ lastKeyframeCreated: null });
  1503. }
  1504. this.setMainAnimatable();
  1505. if (this.state.selected) {
  1506. const lastKeyframe = this.state.selected.getHighestFrame();
  1507. const currentLimit = this.state.animationLimit;
  1508. if (currentLimit < lastKeyframe) {
  1509. this.changeAnimationLimit(lastKeyframe);
  1510. }
  1511. }
  1512. };
  1513. setMainAnimatable() {
  1514. if (this.state.selected !== null) {
  1515. let target = this.props.entity;
  1516. if (this.props.entity instanceof TargetedAnimation) {
  1517. target = this.props.entity.target;
  1518. }
  1519. this.props.scene.stopAllAnimations();
  1520. if (this._mainAnimatable?.target !== target) {
  1521. const keys = this.state.selected.getKeys();
  1522. if (keys.length !== 0) {
  1523. const firstFrame = keys[0].frame;
  1524. const LastFrame = this.state.selected.getHighestFrame();
  1525. this._mainAnimatable = this.props.scene.beginAnimation(
  1526. target,
  1527. firstFrame,
  1528. LastFrame,
  1529. this.state.isLooping
  1530. );
  1531. this._mainAnimatable.stop();
  1532. }
  1533. }
  1534. }
  1535. }
  1536. isAnimationPlaying() {
  1537. let target = this.props.entity;
  1538. if (this.props.entity instanceof TargetedAnimation) {
  1539. target = this.props.entity.target;
  1540. }
  1541. return this.props.scene.getAllAnimatablesByTarget(target).length > 0;
  1542. }
  1543. stopAnimation() {
  1544. let target = this.props.entity;
  1545. if (this.props.entity instanceof TargetedAnimation) {
  1546. target = this.props.entity.target;
  1547. }
  1548. this._isPlaying = this.props.scene.getAllAnimatablesByTarget(target).length > 0;
  1549. if (this._isPlaying) {
  1550. this.props.playOrPause && this.props.playOrPause();
  1551. if (this.state !== undefined) {
  1552. this.setState({ isPlaying: false });
  1553. }
  1554. this._isPlaying = false;
  1555. }
  1556. }
  1557. setIsLooping = () => {
  1558. this.setState({ isLooping: !this.state.isLooping, isPlaying: false }, () => this.stopAnimation());
  1559. };
  1560. setFramesPerSecond = (fps: number) => {
  1561. this.setState({ fps: fps, isPlaying: false }, () => this.stopAnimation());
  1562. };
  1563. analizeAnimationForLerp(animation: Animation | null) {
  1564. if (animation !== null) {
  1565. const { easingMode, easingType, usesTangents } = this.getAnimationData(animation);
  1566. if (easingType === undefined && easingMode === undefined && !usesTangents) {
  1567. return true;
  1568. } else {
  1569. return false;
  1570. }
  1571. } else {
  1572. return false;
  1573. }
  1574. }
  1575. /**
  1576. * Timeline
  1577. * This section controls the timeline.
  1578. */
  1579. changeCurrentFrame = (frame: number) => {
  1580. this.stopAnimation();
  1581. const animation = this.state.selected;
  1582. if (animation) {
  1583. const hasKeyframe = animation.getKeys().find((x) => x.frame === frame);
  1584. const currentValue = this.calculateCurrentPointInCurve(frame);
  1585. const value = hasKeyframe
  1586. ? this.getValueAsArray(animation.dataType, hasKeyframe.value)[this.state.selectedCoordinate]
  1587. : currentValue ?? 0;
  1588. const keyframe: IAnimationKey = { frame, value };
  1589. this.setState(
  1590. {
  1591. currentFrame: frame,
  1592. isPlaying: false,
  1593. },
  1594. () => {
  1595. const index = animation.getKeys().findIndex((x) => x.frame === keyframe.frame);
  1596. const animationName = `${animation.name}_${animation.targetProperty}_${this.state.selectedCoordinate}`;
  1597. const id = this.encodeCurveId(animationName, index);
  1598. this.selectKeyframeFromId(id, keyframe);
  1599. this.setCanvasPosition(keyframe);
  1600. }
  1601. );
  1602. }
  1603. };
  1604. calculateCurrentPointInCurve = (frame: number): number | undefined => {
  1605. if (
  1606. this.state.selectedPathData !== undefined &&
  1607. this.state.selectedPathData[this.state.selectedCoordinate] !== undefined
  1608. ) {
  1609. const selectedCurve = this.state.selectedPathData[this.state.selectedCoordinate].domCurve.current;
  1610. if (selectedCurve !== null) {
  1611. const curveLength = selectedCurve.getTotalLength();
  1612. const frameValue = (frame * curveLength) / 100;
  1613. const currentPointInCurve = selectedCurve.getPointAtLength(frameValue);
  1614. const middle = this._heightScale / 2;
  1615. const offset =
  1616. (currentPointInCurve?.y * this._heightScale - this._heightScale ** 2 / 2) /
  1617. middle /
  1618. this._heightScale;
  1619. const unit = Math.sign(offset);
  1620. const currentValue = unit === -1 ? Math.abs(offset + unit) : unit - offset;
  1621. this.setState({
  1622. currentValue: currentValue,
  1623. currentPoint: currentPointInCurve,
  1624. });
  1625. return currentValue;
  1626. }
  1627. }
  1628. return undefined;
  1629. };
  1630. setCanvasPosition = (keyframe: IAnimationKey) => {
  1631. if (this.state.selected) {
  1632. const positionX = (keyframe.frame - this._pixelFrameUnit) * this._pixelFrameUnit;
  1633. let value = 0;
  1634. if (keyframe.value === null) {
  1635. value = this.state.panningY;
  1636. } else {
  1637. value = this.getValueAsArray(this.state.selected.dataType, keyframe.value)[
  1638. this.state.selectedCoordinate
  1639. ];
  1640. }
  1641. const valueScale = this._heightScale / this._scaleFactor;
  1642. const middleCanvas = this._heightScale / 2;
  1643. const positionY = value === 0 ? middleCanvas : middleCanvas - value * valueScale;
  1644. // change initialframe, last frame
  1645. const currentFramesInCanvas = this.state.framesInCanvasView.to - this.state.framesInCanvasView.from;
  1646. const newStartFrameInCanvas = Math.round(positionX / this._pixelFrameUnit);
  1647. this.setState({
  1648. panningX: positionX,
  1649. panningY: positionY,
  1650. repositionCanvas: true,
  1651. framesInCanvasView: { from: newStartFrameInCanvas, to: newStartFrameInCanvas + currentFramesInCanvas },
  1652. });
  1653. }
  1654. };
  1655. setCurrentFrame = (frame: number) => {
  1656. this.setState({
  1657. currentFrame: frame,
  1658. });
  1659. };
  1660. changeAnimationLimit = (limit: number) => {
  1661. this.stopAnimation();
  1662. const doubleLimit = limit * 2;
  1663. this.setState({
  1664. animationLimit: limit,
  1665. canvasLength: doubleLimit,
  1666. frameAxisLength: this.setFrameAxis(doubleLimit),
  1667. });
  1668. };
  1669. updateFrameInKeyFrame = (frame: number, index: number) => {
  1670. if (this.state && this.state.selected) {
  1671. let animation = this.state.selected;
  1672. let keys = [...animation.getKeys()];
  1673. keys[index].frame = frame;
  1674. animation.setKeys(keys);
  1675. this.selectAnimation(animation);
  1676. }
  1677. };
  1678. playPause = (direction: number) => {
  1679. this.registerObs();
  1680. if (this.state.selected) {
  1681. let target = this.props.entity;
  1682. if (this.props.entity instanceof TargetedAnimation) {
  1683. target = this.props.entity.target;
  1684. }
  1685. if (this.state.isPlaying && direction === 0) {
  1686. this.props.scene.stopAnimation(target);
  1687. this.setState({ isPlaying: false });
  1688. this._isPlaying = false;
  1689. this.forceUpdate();
  1690. } else {
  1691. if (this.state.isPlaying) {
  1692. this.props.scene.stopAnimation(target);
  1693. }
  1694. this.props.scene.stopAllAnimations();
  1695. let keys = this.state.selected.getKeys();
  1696. if (keys.length !== 0) {
  1697. let firstFrame = keys[0].frame;
  1698. let LastFrame = this.state.selected.getHighestFrame();
  1699. if (direction === 1) {
  1700. this._mainAnimatable = this.props.scene.beginAnimation(
  1701. target,
  1702. firstFrame,
  1703. LastFrame,
  1704. this.state.isLooping
  1705. );
  1706. }
  1707. if (direction === -1) {
  1708. this._mainAnimatable = this.props.scene.beginAnimation(
  1709. target,
  1710. LastFrame,
  1711. firstFrame,
  1712. this.state.isLooping
  1713. );
  1714. }
  1715. if (!this.state.isLooping && this._mainAnimatable) {
  1716. this._mainAnimatable.onAnimationEnd = () => this.playPause(0);
  1717. }
  1718. }
  1719. const zeroFrames = keys.filter((x) => x.frame === 0);
  1720. if (zeroFrames.length > 1) {
  1721. keys.shift();
  1722. }
  1723. keys.sort((a, b) => a.frame - b.frame);
  1724. this._isPlaying = true;
  1725. this.setState({ isPlaying: true });
  1726. this.forceUpdate();
  1727. }
  1728. }
  1729. };
  1730. moveFrameTo(e: React.MouseEvent<SVGRectElement, MouseEvent>) {
  1731. this.stopAnimation();
  1732. var svg = e.currentTarget as SVGRectElement;
  1733. var CTM = svg.getScreenCTM();
  1734. let position;
  1735. if (CTM) {
  1736. position = new Vector2((e.clientX - CTM.e) / CTM.a, (e.clientY - CTM.f) / CTM.d);
  1737. let selectedFrame = Math.round(position.x / this._pixelFrameUnit);
  1738. this.setState({ currentFrame: selectedFrame, isPlaying: false }, () => {
  1739. if (this.state.selected) {
  1740. const index = this.state.selected.getKeys().findIndex((x) => x.frame === selectedFrame);
  1741. const keyframe = this.state.selected.getKeys().find((x) => x.frame === selectedFrame);
  1742. if (index !== undefined && keyframe !== undefined) {
  1743. const animationName = `${this.state.selected.name}_${this.state.selected.targetProperty}_${this.state.selectedCoordinate}`;
  1744. const id = this.encodeCurveId(animationName, index);
  1745. this.selectKeyframeFromId(id, keyframe);
  1746. this.setCanvasPosition(keyframe);
  1747. }
  1748. }
  1749. });
  1750. }
  1751. }
  1752. registerObs() {
  1753. if (this._onBeforeRenderObserver) {
  1754. this.props.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver);
  1755. this._onBeforeRenderObserver = null;
  1756. }
  1757. this._onBeforeRenderObserver = this.props.scene.onBeforeRenderObservable.add(() => {
  1758. if (!this._isPlaying || !this._mainAnimatable) {
  1759. return;
  1760. }
  1761. this.setState({
  1762. currentFrame: Math.round(this._mainAnimatable.masterFrame),
  1763. });
  1764. });
  1765. }
  1766. componentWillUnmount() {
  1767. this.playPause(0);
  1768. if (this._onBeforeRenderObserver) {
  1769. this.props.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver);
  1770. this._onBeforeRenderObserver = null;
  1771. }
  1772. }
  1773. isCurrentFrame(frame: number) {
  1774. return this.state.currentFrame === frame;
  1775. }
  1776. setPanningY = (panningY: number) => {
  1777. this.setState({ panningY });
  1778. };
  1779. setPanningX = (panningX: number) => {
  1780. const currentFramesInCanvas = this.state.framesInCanvasView.to - this.state.framesInCanvasView.from;
  1781. const newStartFrameInCanvas = Math.round(panningX / this._pixelFrameUnit);
  1782. this.setState({
  1783. panningX,
  1784. framesInCanvasView: { from: newStartFrameInCanvas, to: newStartFrameInCanvas + currentFramesInCanvas },
  1785. });
  1786. };
  1787. canvasPositionEnded = () => {
  1788. this.setState({ repositionCanvas: false });
  1789. };
  1790. setNotificationMessage = (message: string) => {
  1791. this.setState({ notification: message });
  1792. };
  1793. frameSelectedKeyframes = () => {
  1794. const animation = this.state.selected;
  1795. const coordinate = this.state.selectedCoordinate;
  1796. if (animation) {
  1797. let highest, lowest, middleFrame, firstFrame, lastFrame;
  1798. const keysCopy = [...animation.getKeys()];
  1799. // calculate scale factor for Value Axis //
  1800. const selectedKeyframes = this.state.svgKeyframes?.filter((x) => x.selected);
  1801. if (selectedKeyframes?.length === 0) {
  1802. firstFrame = keysCopy[0].frame;
  1803. lastFrame = keysCopy[keysCopy.length - 1].frame;
  1804. // If not selected get all keyframes
  1805. keysCopy.sort(
  1806. (a, b) =>
  1807. this.getValueAsArray(animation.dataType, a.value)[coordinate] -
  1808. this.getValueAsArray(animation.dataType, b.value)[coordinate]
  1809. );
  1810. lowest = keysCopy[0];
  1811. highest = keysCopy[keysCopy.length - 1];
  1812. keysCopy.sort((a, b) => a.frame - b.frame);
  1813. middleFrame =
  1814. Math.round((keysCopy[keysCopy.length - 1].frame - keysCopy[0].frame) / 2) + keysCopy[0].frame;
  1815. } else {
  1816. // If selected get keys
  1817. const keysInRange = keysCopy.filter((kf, i) => {
  1818. return selectedKeyframes?.find((a: IKeyframeSvgPoint) => {
  1819. const { order } = this.decodeCurveId(a.id);
  1820. return i === order ? kf : undefined;
  1821. });
  1822. });
  1823. // Sort to get first and last frame
  1824. keysInRange.sort((a, b) => a.frame - b.frame);
  1825. // Get previous and next non selected keyframe in range
  1826. const prevKey = keysCopy[keysCopy.indexOf(keysInRange[0]) - 1];
  1827. const nextKey = keysCopy[keysCopy.indexOf(keysInRange[keysInRange.length - 1]) + 1];
  1828. firstFrame = prevKey.frame;
  1829. lastFrame = nextKey.frame;
  1830. // Insert keys in range
  1831. if (prevKey) {
  1832. keysInRange.push(prevKey);
  1833. }
  1834. if (nextKey) {
  1835. keysInRange.push(nextKey);
  1836. }
  1837. // Sort to get lowest and highest values for scale
  1838. keysInRange.sort(
  1839. (a, b) =>
  1840. this.getValueAsArray(animation.dataType, a.value)[coordinate] -
  1841. this.getValueAsArray(animation.dataType, b.value)[coordinate]
  1842. );
  1843. lowest = keysInRange[0];
  1844. highest = keysInRange[keysInRange.length - 1];
  1845. keysInRange.sort((a, b) => a.frame - b.frame);
  1846. middleFrame =
  1847. Math.round((keysInRange[keysInRange.length - 1].frame - keysInRange[0].frame) / 2) +
  1848. keysInRange[0].frame;
  1849. }
  1850. // calculate scale...
  1851. const scale =
  1852. this.getValueAsArray(animation.dataType, highest?.value)[coordinate] -
  1853. this.getValueAsArray(animation.dataType, lowest?.value)[coordinate];
  1854. // Scale Frames to fit width of canvas
  1855. // reposition canvas to middle value of scale
  1856. const canvasMargin = 1.5;
  1857. this._scaleFactor = isNaN(scale) || scale === 0 ? 2 : scale * canvasMargin;
  1858. // Set a new scale factor but for Frames
  1859. // ****
  1860. // get client width of canvas
  1861. // how many frames are in canvas ... get the pixelFrameUnit?
  1862. // get frames needed (last frame - first frame)
  1863. let currentSpace = 780;
  1864. const frameUnit = 39;
  1865. if (this._graphCanvas.current) {
  1866. currentSpace = this._graphCanvas.current?.clientWidth;
  1867. }
  1868. const availableSpaceForFrames = currentSpace / frameUnit;
  1869. // with client width divide the number of frames needed
  1870. const frameDistance = lastFrame - firstFrame;
  1871. this._pixelFrameUnit = availableSpaceForFrames / (frameDistance / 10); // Update scale here...
  1872. const canvasValue = isNaN(scale) || scale === 0 ? 1 : scale / 2 + lowest?.value;
  1873. const firstF = firstFrame;
  1874. const centerFrame = middleFrame;
  1875. this.setState(
  1876. {
  1877. framesInCanvasView: { from: firstFrame, to: lastFrame },
  1878. },
  1879. () => {
  1880. // Need to center and reposition canvas
  1881. this.setCanvasPosition({ frame: firstF, value: canvasValue });
  1882. console.log(`Should center canvas at: ${centerFrame}, ${canvasValue}`);
  1883. // Render new points
  1884. this.selectAnimation(animation, coordinate);
  1885. }
  1886. );
  1887. }
  1888. };
  1889. onWindowResizeWidth = () => {
  1890. if (this._graphCanvas.current) {
  1891. const defaultWidth = 781;
  1892. const defaultSvgProportion = 1.8;
  1893. const proportionResized = (defaultSvgProportion / 2) * 10;
  1894. const svgWidth = 200;
  1895. const width = (this._graphCanvas.current.clientWidth / svgWidth) * defaultSvgProportion;
  1896. const percentResize = (this._graphCanvas.current.clientWidth * 100) / defaultWidth;
  1897. const value = (percentResize - 100) * -1;
  1898. this.setState({ valuesPositionResize: value - width + proportionResized });
  1899. this.onTimelineResize();
  1900. }
  1901. };
  1902. onTimelineResize = () => {
  1903. if (this._editor.current) {
  1904. const scrollHandle = this._editor.current.getElementsByClassName("scroll-handle")[0].clientWidth;
  1905. this._resizedTimeline = scrollHandle;
  1906. }
  1907. };
  1908. render() {
  1909. return (
  1910. <div ref={this._editor} id="animation-curve-editor">
  1911. <Notification
  1912. message={this.state.notification}
  1913. open={this.state.notification !== "" ? true : false}
  1914. close={this.clearNotification}
  1915. />
  1916. <GraphActionsBar
  1917. setKeyframeValue={this.setKeyframeValueFromInput}
  1918. enabled={this.state.selected === null || this.state.selected === undefined ? false : true}
  1919. title={this._entityName}
  1920. actionableKeyframe={this.state.actionableKeyframe}
  1921. handleFrameChange={this.handleFrameChange}
  1922. handleValueChange={this.handleValueChange}
  1923. addKeyframe={this.addKeyframeClick}
  1924. removeKeyframe={this.removeKeyframeClick}
  1925. frameSelectedKeyframes={this.frameSelectedKeyframes}
  1926. brokenMode={this.state.isBrokenMode}
  1927. brokeTangents={this.setBrokenMode}
  1928. lerpMode={this.state.lerpMode}
  1929. setLerpToActiveControlPoint={this.setLerpToActiveControlPoint}
  1930. flatTangent={this.setFlatTangent}
  1931. frameRange={{ max: this.state.maxFrame, min: this.state.minFrame }}
  1932. />
  1933. <div className="content">
  1934. <div className="row">
  1935. <EditorControls
  1936. deselectAnimation={this.deselectAnimation}
  1937. selectAnimation={this.selectAnimation}
  1938. isTargetedAnimation={this._isTargetedAnimation}
  1939. entity={this.props.entity}
  1940. selected={this.state.selected}
  1941. lockObject={this.props.lockObject}
  1942. setNotificationMessage={this.setNotificationMessage}
  1943. globalState={this.props.globalState}
  1944. snippetServer={this._snippetUrl}
  1945. fps={this.state.fps}
  1946. setFps={this.setFramesPerSecond}
  1947. setIsLooping={this.setIsLooping}
  1948. />
  1949. <div ref={this._graphCanvas} className="graph-chart" onWheel={this.zoom}>
  1950. {this.state.svgKeyframes && (
  1951. <SvgDraggableArea
  1952. ref={this._svgCanvas}
  1953. viewBoxScale={this.state.frameAxisLength.length}
  1954. scale={this.state.scale}
  1955. removeSelectedKeyframes={this.removeKeyframes}
  1956. deselectKeyframes={this.deselectKeyframes}
  1957. updatePosition={this.renderPoints}
  1958. panningY={this.setPanningY}
  1959. panningX={this.setPanningX}
  1960. setCurrentFrame={this.setCurrentFrame}
  1961. positionCanvas={new Vector2(this.state.panningX, this.state.panningY)}
  1962. repositionCanvas={this.state.repositionCanvas}
  1963. canvasPositionEnded={this.canvasPositionEnded}
  1964. keyframeSvgPoints={this.state.svgKeyframes}
  1965. resetActionableKeyframe={this.resetActionableKeyframe}
  1966. framesInCanvasView={this.state.framesInCanvasView}
  1967. >
  1968. {this.setValueLines().map((line, i) => {
  1969. return (
  1970. <text
  1971. key={`value_inline_${i}`}
  1972. x={this.state.panningX - 5}
  1973. y={line.value}
  1974. dx={this.state.valuesPositionResize}
  1975. textAnchor="middle"
  1976. dy="-1"
  1977. style={{
  1978. fontSize: `${0.18 * this.state.scale}em`,
  1979. fontWeight: "bold",
  1980. textAlign: "center",
  1981. }}
  1982. >
  1983. {line.label}
  1984. </text>
  1985. );
  1986. })}
  1987. {this.setValueLines().map((line, i) => {
  1988. return (
  1989. <line
  1990. key={i}
  1991. x1={-((this.state.frameAxisLength.length * 10) / 2)}
  1992. y1={line.value}
  1993. x2={this.state.frameAxisLength.length * 10}
  1994. y2={line.value}
  1995. ></line>
  1996. );
  1997. })}
  1998. {/* Multiple Curves */}
  1999. {this.state.selectedPathData?.map((curve, i) => (
  2000. <path
  2001. key={i}
  2002. ref={curve.domCurve}
  2003. pathLength={curve.pathLength}
  2004. id="curve"
  2005. d={curve.pathData}
  2006. style={{
  2007. stroke: curve.color,
  2008. fill: "none",
  2009. strokeWidth: "0.5",
  2010. }}
  2011. ></path>
  2012. ))}
  2013. {this.state.svgKeyframes.map((keyframe, i) => (
  2014. <KeyframeSvgPoint
  2015. key={`${keyframe.id}_${i}`}
  2016. id={keyframe.id}
  2017. keyframePoint={keyframe.keyframePoint}
  2018. leftControlPoint={keyframe.leftControlPoint}
  2019. rightControlPoint={keyframe.rightControlPoint}
  2020. isLeftActive={keyframe.isLeftActive}
  2021. isRightActive={keyframe.isRightActive}
  2022. selected={keyframe.selected}
  2023. selectedControlPoint={this.selectedControlPoint}
  2024. selectKeyframe={this.selectKeyframe}
  2025. framesInCanvasView={this.state.framesInCanvasView}
  2026. />
  2027. ))}
  2028. <rect
  2029. onClick={(e) => this.moveFrameTo(e)}
  2030. x={-((this.state.frameAxisLength.length * 10) / 2)}
  2031. y={91 + this.state.panningY + "%"}
  2032. width={this.state.frameAxisLength.length * 10}
  2033. height="9%"
  2034. fill="#222"
  2035. style={{ cursor: "pointer" }}
  2036. ></rect>
  2037. {this.state.frameAxisLength.map((f, i) => (
  2038. <svg key={i} x="0" y={96 + this.state.panningY + "%"} className="frame-contain">
  2039. <text
  2040. x={f.value}
  2041. y="1px"
  2042. dx="2px"
  2043. style={{ fontSize: `${0.2 * this.state.scale}em` }}
  2044. >
  2045. {Math.round((f.label * 10) / this._pixelFrameUnit)}
  2046. </text>
  2047. <line x1={f.value} y1="0" x2={f.value} y2="5%"></line>
  2048. {f.value % this.state.fps === 0 && f.value !== 0 ? (
  2049. <line
  2050. x1={f.value * this._pixelFrameUnit}
  2051. y1="-100%"
  2052. x2={f.value * this._pixelFrameUnit}
  2053. y2="5%"
  2054. ></line>
  2055. ) : null}
  2056. </svg>
  2057. ))}
  2058. {this.state.selected && this.state.currentFrame ? (
  2059. <svg x="0" y={96 + this.state.panningY + "%"}>
  2060. <line
  2061. x1={this.state.currentFrame * this._pixelFrameUnit}
  2062. y1="0"
  2063. x2={this.state.currentFrame * this._pixelFrameUnit}
  2064. y2="-100%"
  2065. style={{
  2066. stroke: "#a4a4a4",
  2067. strokeWidth: 0.4,
  2068. }}
  2069. />
  2070. <svg x={this.state.currentFrame * this._pixelFrameUnit} y="-1">
  2071. <circle className="svg-playhead" cx="0" cy="0" r="2%" fill="white" />
  2072. <text
  2073. x="0"
  2074. y="1%"
  2075. textAnchor="middle"
  2076. style={{
  2077. fontSize: `${0.17 * this.state.scale}em`,
  2078. pointerEvents: "none",
  2079. fontWeight: 600,
  2080. }}
  2081. >
  2082. {this.state.currentFrame}
  2083. </text>
  2084. </svg>
  2085. </svg>
  2086. ) : null}
  2087. </SvgDraggableArea>
  2088. )}
  2089. <div
  2090. style={{
  2091. width: 30,
  2092. height: "inherit",
  2093. position: "absolute",
  2094. zIndex: 1,
  2095. pointerEvents: "none",
  2096. backgroundColor: "rgb(188 188 188 / 11%)",
  2097. top: 40,
  2098. }}
  2099. ></div>
  2100. <ScaleLabel current={this.state.valueScaleType} />
  2101. </div>
  2102. </div>
  2103. <div className="row-bottom">
  2104. <Timeline
  2105. currentFrame={this.state.currentFrame}
  2106. playPause={this.playPause}
  2107. isPlaying={this.state.isPlaying}
  2108. dragKeyframe={this.updateFrameInKeyFrame}
  2109. onCurrentFrameChange={this.changeCurrentFrame}
  2110. onAnimationLimitChange={this.changeAnimationLimit}
  2111. animationLimit={this.state.animationLimit}
  2112. keyframes={this.state.selected && this.state.selected.getKeys()}
  2113. selected={this.state.selected && this.state.selected.getKeys()[0]}
  2114. fps={this.state.fps}
  2115. repositionCanvas={this.setCanvasPosition}
  2116. resizeWindowProportion={this._resizedTimeline}
  2117. ></Timeline>
  2118. </div>
  2119. </div>
  2120. </div>
  2121. );
  2122. }
  2123. }