animationCurveEditorComponent.tsx 110 KB

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