GUICurveDrawing.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. //********************************** Banshee Engine (www.banshee3d.com) **************************************************//
  2. //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
  3. using System;
  4. using System.Collections.Generic;
  5. using BansheeEngine;
  6. namespace BansheeEditor
  7. {
  8. /** @addtogroup AnimationEditor
  9. * @{
  10. */
  11. /// <summary>
  12. /// Draws one or multiple curves over the specified physical area. User can specify horizontal and vertical range to
  13. /// display, as well as physical size of the GUI area.
  14. /// </summary>
  15. internal class GUICurveDrawing
  16. {
  17. private const int LINE_SPLIT_WIDTH = 2;
  18. private static readonly Color COLOR_MID_GRAY = new Color(90.0f / 255.0f, 90.0f / 255.0f, 90.0f / 255.0f, 1.0f);
  19. private static readonly Color COLOR_DARK_GRAY = new Color(40.0f / 255.0f, 40.0f / 255.0f, 40.0f / 255.0f, 1.0f);
  20. private EdAnimationCurve[] curves;
  21. private bool[][] selectedKeyframes;
  22. private int width;
  23. private int height;
  24. private float xRange = 60.0f;
  25. private float yRange = 20.0f;
  26. private int fps = 1;
  27. private int markedFrameIdx = 0;
  28. private int drawableWidth;
  29. private GUICanvas canvas;
  30. private GUIGraphTicks tickHandler;
  31. /// <summary>
  32. /// Creates a new curve drawing GUI element.
  33. /// </summary>
  34. /// <param name="layout">Layout into which to add the GUI element.</param>
  35. /// <param name="width">Width of the element in pixels.</param>
  36. /// <param name="height">Height of the element in pixels.</param>
  37. /// <param name="curves">Initial set of curves to display. </param>
  38. public GUICurveDrawing(GUILayout layout, int width, int height, EdAnimationCurve[] curves)
  39. {
  40. canvas = new GUICanvas();
  41. layout.AddElement(canvas);
  42. tickHandler = new GUIGraphTicks(GUITickStepType.Time);
  43. this.curves = curves;
  44. SetSize(width, height);
  45. ClearSelectedKeyframes(); // Makes sure the array is initialized
  46. Rebuild();
  47. }
  48. /// <summary>
  49. /// Change the set of curves to display.
  50. /// </summary>
  51. /// <param name="curves">New set of curves to draw on the GUI element.</param>
  52. public void SetCurves(EdAnimationCurve[] curves)
  53. {
  54. this.curves = curves;
  55. }
  56. /// <summary>
  57. /// Change the physical size of the GUI element.
  58. /// </summary>
  59. /// <param name="width">Width of the element in pixels.</param>
  60. /// <param name="height">Height of the element in pixels.</param>
  61. public void SetSize(int width, int height)
  62. {
  63. this.width = width;
  64. this.height = height;
  65. canvas.SetWidth(width);
  66. canvas.SetHeight(height);
  67. drawableWidth = Math.Max(0, width - GUIGraphTime.PADDING * 2);
  68. }
  69. /// <summary>
  70. /// Changes the visible range that the GUI element displays.
  71. /// </summary>
  72. /// <param name="xRange">Range of the horizontal area. Displayed area will range from [0, xRange].</param>
  73. /// <param name="yRange">Range of the vertical area. Displayed area will range from
  74. /// [-yRange * 0.5, yRange * 0.5]</param>
  75. public void SetRange(float xRange, float yRange)
  76. {
  77. this.xRange = xRange;
  78. this.yRange = yRange;
  79. }
  80. /// <summary>
  81. /// Number of frames per second, used for frame selection and marking.
  82. /// </summary>
  83. /// <param name="fps">Number of prames per second.</param>
  84. public void SetFPS(int fps)
  85. {
  86. this.fps = Math.Max(1, fps);
  87. }
  88. /// <summary>
  89. /// Sets the frame at which to display the frame marker.
  90. /// </summary>
  91. /// <param name="frameIdx">Index of the frame to display the marker on, or -1 to clear the marker.</param>
  92. public void SetMarkedFrame(int frameIdx)
  93. {
  94. markedFrameIdx = frameIdx;
  95. }
  96. /// <summary>
  97. /// Marks the specified key-frame as selected, changing the way it is displayed.
  98. /// </summary>
  99. /// <param name="curveIdx">Index of the curve the keyframe is on.</param>
  100. /// <param name="keyIdx">Index of the keyframe.</param>
  101. /// <param name="selected">True to select it, false to deselect it.</param>
  102. public void SelectKeyframe(int curveIdx, int keyIdx, bool selected)
  103. {
  104. if (selectedKeyframes == null)
  105. return;
  106. if (curveIdx < 0 || curveIdx >= selectedKeyframes.Length)
  107. return;
  108. if (keyIdx < 0 || keyIdx >= selectedKeyframes[curveIdx].Length)
  109. return;
  110. selectedKeyframes[curveIdx][keyIdx] = selected;
  111. }
  112. /// <summary>
  113. /// Clears any key-frames that were marked as selected.
  114. /// </summary>
  115. public void ClearSelectedKeyframes()
  116. {
  117. selectedKeyframes = new bool[curves.Length][];
  118. for (int i = 0; i < curves.Length; i++)
  119. {
  120. KeyFrame[] keyframes = curves[i].Native.KeyFrames;
  121. selectedKeyframes[i] = new bool[keyframes.Length];
  122. }
  123. }
  124. /// <summary>
  125. /// Returns time for a frame with the specified index. Depends on set range and FPS.
  126. /// </summary>
  127. /// <param name="frameIdx">Index of the frame (not a key-frame) to get the time for.</param>
  128. /// <returns>Time of the frame with the provided index. </returns>
  129. public float GetTimeForFrame(int frameIdx)
  130. {
  131. float range = GetRange();
  132. int numFrames = (int)range * fps;
  133. float timePerFrame = range / numFrames;
  134. return frameIdx* timePerFrame;
  135. }
  136. /// <summary>
  137. /// Retrieves information under the provided window coordinates. This involves coordinates of the curve, as well
  138. /// as curve and key-frame indices that were under the coordinates (if any).
  139. /// </summary>
  140. /// <param name="windowCoords">Coordinate relative to the window the GUI element is on.</param>
  141. /// <param name="curveCoords">Curve coordinates within the range as specified by <see cref="SetRange"/>. Only
  142. /// Valid when function returns true.</param>
  143. /// <param name="curveIdx">Sequential index of the curve that's under the coordinates. -1 if no curve. Index
  144. /// corresponds to the curve index as provided by the curve array in the constructor or
  145. /// <see cref="SetCurves"/>.</param>
  146. /// <param name="keyIdx">Index of a keyframe that that's under the coordinates, on the curve as referenced by
  147. /// <paramref name="curveIdx"/>. -1 if no keyframe.</param>
  148. /// <returns>True if the window coordinates were within the curve area, false otherwise.</returns>
  149. public bool GetCoordInfo(Vector2I windowCoords, out Vector2 curveCoords, out int curveIdx, out int keyIdx)
  150. {
  151. Rect2I bounds = canvas.Bounds;
  152. // Check if outside of curve drawing bounds
  153. if (windowCoords.x < (bounds.x + GUIGraphTime.PADDING) || windowCoords.x >= (bounds.x + bounds.width - GUIGraphTime.PADDING) ||
  154. windowCoords.y < bounds.y || windowCoords.y >= (bounds.y + bounds.height))
  155. {
  156. curveCoords = new Vector2();
  157. curveIdx = -1;
  158. keyIdx = -1;
  159. return false;
  160. }
  161. // Find time and value of the place under the coordinates
  162. Vector2I relativeCoords = windowCoords - new Vector2I(bounds.x + GUIGraphTime.PADDING, bounds.y);
  163. float lengthPerPixel = xRange / drawableWidth;
  164. float heightPerPixel = yRange / height;
  165. float yOffset = yRange/2.0f;
  166. float t = relativeCoords.x*lengthPerPixel;
  167. float value = yOffset - relativeCoords.y*heightPerPixel;
  168. curveCoords = new Vector2();
  169. curveCoords.x = t;
  170. curveCoords.y = value;
  171. // Find nearest keyframe, if any
  172. keyIdx = -1;
  173. curveIdx = -1;
  174. float nearestDistance = float.MaxValue;
  175. for (int i = 0; i < curves.Length; i++)
  176. {
  177. EdAnimationCurve curve = curves[i];
  178. KeyFrame[] keyframes = curve.Native.KeyFrames;
  179. for (int j = 0; j < keyframes.Length; j++)
  180. {
  181. Vector2I keyframeCoords = new Vector2I((int)(keyframes[j].time / lengthPerPixel),
  182. (int)((yOffset - keyframes[j].value) / heightPerPixel));
  183. float distanceToKey = Vector2I.Distance(relativeCoords, keyframeCoords);
  184. if (distanceToKey < nearestDistance)
  185. {
  186. nearestDistance = distanceToKey;
  187. keyIdx = j;
  188. curveIdx = i;
  189. }
  190. }
  191. // We're not near any keyframe
  192. if (nearestDistance > 5.0f)
  193. keyIdx = -1;
  194. }
  195. // Find nearest curve, if any
  196. if (keyIdx == -1)
  197. {
  198. // Note: This will not detect a curve if coordinate is over a step, and in general this works poorly with large slopes
  199. curveIdx = -1;
  200. nearestDistance = float.MaxValue;
  201. for (int i = 0; i < curves.Length; i++)
  202. {
  203. EdAnimationCurve curve = curves[i];
  204. KeyFrame[] keyframes = curve.Native.KeyFrames;
  205. if (keyframes.Length == 0)
  206. continue;
  207. if (t < keyframes[0].time || t > keyframes[keyframes.Length - 1].time)
  208. continue;
  209. float curveValue = curves[i].Native.Evaluate(t);
  210. float distanceToCurve = Math.Abs(curveValue - value);
  211. if (distanceToCurve < nearestDistance)
  212. {
  213. nearestDistance = distanceToCurve;
  214. curveIdx = i;
  215. }
  216. }
  217. // We're not near any curve
  218. float nearestDistancePx = nearestDistance/heightPerPixel;
  219. if (nearestDistancePx > 15.0f)
  220. curveIdx = -1;
  221. }
  222. return true;
  223. }
  224. /// <summary>
  225. /// Draws a vertical frame marker on the curve area.
  226. /// </summary>
  227. /// <param name="t">Time at which to draw the marker.</param>
  228. /// <param name="color">Color with which to draw the marker.</param>
  229. private void DrawFrameMarker(float t, Color color)
  230. {
  231. int xPos = (int)((t / GetRange()) * drawableWidth) + GUIGraphTime.PADDING;
  232. Vector2I start = new Vector2I(xPos, 0);
  233. Vector2I end = new Vector2I(xPos, height);
  234. canvas.DrawLine(start, end, color);
  235. }
  236. /// <summary>
  237. /// Draws a horizontal line representing the line at y = 0.
  238. /// </summary>
  239. private void DrawCenterLine()
  240. {
  241. int heightOffset = height / 2; // So that y = 0 is at center of canvas
  242. Vector2I start = new Vector2I(0, heightOffset);
  243. Vector2I end = new Vector2I(width, heightOffset);
  244. canvas.DrawLine(start, end, COLOR_DARK_GRAY);
  245. }
  246. /// <summary>
  247. /// Draws a keyframe a the specified time and value.
  248. /// </summary>
  249. /// <param name="t">Time to draw the keyframe at.</param>
  250. /// <param name="y">Y value to draw the keyframe at.</param>
  251. /// <param name="selected">Determines should the keyframe be drawing using the selected color scheme, or normally.
  252. /// </param>
  253. private void DrawKeyframe(float t, float y, bool selected)
  254. {
  255. int heightOffset = height / 2; // So that y = 0 is at center of canvas
  256. int xPos = (int)((t / GetRange()) * drawableWidth) + GUIGraphTime.PADDING;
  257. int yPos = heightOffset - (int)((y/yRange)*height);
  258. Vector2I a = new Vector2I(xPos - 3, yPos);
  259. Vector2I b = new Vector2I(xPos, yPos - 3);
  260. Vector2I c = new Vector2I(xPos + 3, yPos);
  261. Vector2I d = new Vector2I(xPos, yPos + 3);
  262. // Draw diamond shape
  263. Vector2I[] linePoints = new Vector2I[] { a, b, c, d, a };
  264. Vector2I[] trianglePoints = new Vector2I[] { b, c, a, d };
  265. canvas.DrawTriangleStrip(trianglePoints, Color.White, 101);
  266. if (selected)
  267. canvas.DrawPolyLine(linePoints, Color.BansheeOrange, 100);
  268. else
  269. canvas.DrawPolyLine(linePoints, Color.Black, 100);
  270. }
  271. /// <summary>
  272. /// Returns the range of times displayed by the timeline rounded to the multiple of FPS.
  273. /// </summary>
  274. /// <param name="padding">If true, extra range will be included to cover the right-most padding.</param>
  275. /// <returns>Time range rounded to a multiple of FPS.</returns>
  276. private float GetRange(bool padding = false)
  277. {
  278. float spf = 1.0f / fps;
  279. float range = xRange;
  280. if (padding)
  281. {
  282. float lengthPerPixel = xRange / drawableWidth;
  283. range += lengthPerPixel * GUIGraphTime.PADDING;
  284. }
  285. return ((int)range / spf) * spf;
  286. }
  287. /// <summary>
  288. /// Rebuilds the internal GUI elements. Should be called whenever timeline properties change.
  289. /// </summary>
  290. public void Rebuild()
  291. {
  292. canvas.Clear();
  293. if (curves == null)
  294. return;
  295. tickHandler.SetRange(0.0f, GetRange(true), drawableWidth + GUIGraphTime.PADDING);
  296. // Draw vertical frame markers
  297. int numTickLevels = tickHandler.NumLevels;
  298. for (int i = numTickLevels - 1; i >= 0; i--)
  299. {
  300. float[] ticks = tickHandler.GetTicks(i);
  301. float strength = tickHandler.GetLevelStrength(i);
  302. for (int j = 0; j < ticks.Length; j++)
  303. {
  304. Color color = COLOR_DARK_GRAY;
  305. color.a *= strength;
  306. DrawFrameMarker(ticks[j], color);
  307. }
  308. }
  309. // Draw center line
  310. DrawCenterLine();
  311. // Draw curves
  312. int curveIdx = 0;
  313. foreach (var curve in curves)
  314. {
  315. Color color = GetUniqueColor(curveIdx);
  316. DrawCurve(curve, color);
  317. // Draw keyframes
  318. KeyFrame[] keyframes = curve.Native.KeyFrames;
  319. for (int i = 0; i < keyframes.Length; i++)
  320. DrawKeyframe(keyframes[i].time, keyframes[i].value, IsSelected(curveIdx, i));
  321. curveIdx++;
  322. }
  323. // Draw selected frame marker
  324. if (markedFrameIdx != -1)
  325. DrawFrameMarker(GetTimeForFrame(markedFrameIdx), Color.BansheeOrange);
  326. }
  327. /// <summary>
  328. /// Generates a unique color based on the provided index.
  329. /// </summary>
  330. /// <param name="idx">Index to use for generating a color. Should be less than 30 in order to guarantee reasonably
  331. /// different colors.</param>
  332. /// <returns>Unique color.</returns>
  333. private Color GetUniqueColor(int idx)
  334. {
  335. const int COLOR_SPACING = 359 / 15;
  336. float hue = ((idx * COLOR_SPACING) % 359) / 359.0f;
  337. return Color.HSV2RGB(new Color(hue, 175.0f / 255.0f, 175.0f / 255.0f));
  338. }
  339. /// <summary>
  340. /// Checks is the provided key-frame currently marked as selected.
  341. /// </summary>
  342. /// <param name="curveIdx">Index of the curve the keyframe is on.</param>
  343. /// <param name="keyIdx">Index of the keyframe.</param>
  344. /// <returns>True if selected, false otherwise.</returns>
  345. private bool IsSelected(int curveIdx, int keyIdx)
  346. {
  347. if (selectedKeyframes == null)
  348. return false;
  349. if (curveIdx < 0 || curveIdx >= selectedKeyframes.Length)
  350. return false;
  351. if (keyIdx < 0 || keyIdx >= selectedKeyframes[curveIdx].Length)
  352. return false;
  353. return selectedKeyframes[curveIdx][keyIdx];
  354. }
  355. /// <summary>
  356. /// Draws the curve using the provided color.
  357. /// </summary>
  358. /// <param name="curve">Curve to draw within the currently set range. </param>
  359. /// <param name="color">Color to draw the curve with.</param>
  360. private void DrawCurve(EdAnimationCurve curve, Color color)
  361. {
  362. float lengthPerPixel = xRange/drawableWidth;
  363. float pixelsPerHeight = height/yRange;
  364. int heightOffset = height/2; // So that y = 0 is at center of canvas
  365. KeyFrame[] keyframes = curve.Native.KeyFrames;
  366. if (keyframes.Length < 0)
  367. return;
  368. // Draw start line
  369. {
  370. float start = MathEx.Clamp(keyframes[0].time, 0.0f, xRange);
  371. int startPixel = (int)(start / lengthPerPixel);
  372. int xPosStart = 0;
  373. int xPosEnd = GUIGraphTime.PADDING + startPixel;
  374. int yPos = (int)(curve.Native.Evaluate(0.0f, false) * pixelsPerHeight);
  375. yPos = heightOffset - yPos; // Offset and flip height (canvas Y goes down)
  376. Vector2I a = new Vector2I(xPosStart, yPos);
  377. Vector2I b = new Vector2I(xPosEnd, yPos);
  378. canvas.DrawLine(a, b, COLOR_MID_GRAY);
  379. }
  380. List<Vector2I> linePoints = new List<Vector2I>();
  381. // Draw in between keyframes
  382. for (int i = 0; i < keyframes.Length - 1; i++)
  383. {
  384. float start = MathEx.Clamp(keyframes[i].time, 0.0f, xRange);
  385. float end = MathEx.Clamp(keyframes[i + 1].time, 0.0f, xRange);
  386. int startPixel = (int)(start / lengthPerPixel);
  387. int endPixel = (int)(end / lengthPerPixel);
  388. bool isStep = keyframes[i].outTangent == float.PositiveInfinity ||
  389. keyframes[i + 1].inTangent == float.PositiveInfinity;
  390. // If step tangent, draw the required lines without sampling, as the sampling will miss the step
  391. if (isStep)
  392. {
  393. // Line from left to right frame
  394. int xPos = startPixel;
  395. int yPosStart = (int)(curve.Native.Evaluate(start, false) * pixelsPerHeight);
  396. yPosStart = heightOffset - yPosStart; // Offset and flip height (canvas Y goes down)
  397. linePoints.Add(new Vector2I(GUIGraphTime.PADDING + xPos, yPosStart));
  398. xPos = endPixel;
  399. linePoints.Add(new Vector2I(GUIGraphTime.PADDING + xPos, yPosStart));
  400. // Line representing the step
  401. int yPosEnd = (int)(curve.Native.Evaluate(end, false) * pixelsPerHeight);
  402. yPosEnd = heightOffset - yPosEnd; // Offset and flip height (canvas Y goes down)
  403. linePoints.Add(new Vector2I(GUIGraphTime.PADDING + xPos, yPosEnd));
  404. }
  405. else // Draw normally
  406. {
  407. int numSplits;
  408. float timeIncrement;
  409. if (startPixel != endPixel)
  410. {
  411. float fNumSplits = (endPixel - startPixel)/(float) LINE_SPLIT_WIDTH;
  412. numSplits = MathEx.FloorToInt(fNumSplits);
  413. float remainder = fNumSplits - numSplits;
  414. float lengthRounded = (end - start)*(numSplits/fNumSplits);
  415. timeIncrement = lengthRounded/numSplits;
  416. numSplits += MathEx.CeilToInt(remainder) + 1;
  417. }
  418. else
  419. {
  420. numSplits = 1;
  421. timeIncrement = 0.0f;
  422. }
  423. for (int j = 0; j < numSplits; j++)
  424. {
  425. int xPos = Math.Min(startPixel + j * LINE_SPLIT_WIDTH, endPixel);
  426. float t = Math.Min(start + j * timeIncrement, end);
  427. int yPos = (int)(curve.Native.Evaluate(t, false) * pixelsPerHeight);
  428. yPos = heightOffset - yPos; // Offset and flip height (canvas Y goes down)
  429. linePoints.Add(new Vector2I(GUIGraphTime.PADDING + xPos, yPos));
  430. }
  431. }
  432. }
  433. canvas.DrawPolyLine(linePoints.ToArray(), color);
  434. // Draw end line
  435. {
  436. float end = MathEx.Clamp(keyframes[keyframes.Length - 1].time, 0.0f, xRange);
  437. int endPixel = (int)(end / lengthPerPixel);
  438. int xPosStart = GUIGraphTime.PADDING + endPixel;
  439. int xPosEnd = width;
  440. int yPos = (int)(curve.Native.Evaluate(xRange, false) * pixelsPerHeight);
  441. yPos = heightOffset - yPos; // Offset and flip height (canvas Y goes down)
  442. Vector2I a = new Vector2I(xPosStart, yPos);
  443. Vector2I b = new Vector2I(xPosEnd, yPos);
  444. canvas.DrawLine(a, b, COLOR_MID_GRAY);
  445. }
  446. }
  447. }
  448. /** }@ */
  449. }