TimelineGUI.cs 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. #if UNITY_EDITOR && UNITY_IMGUI
  3. using Animancer.Editor.Previews;
  4. using System;
  5. using System.Collections.Generic;
  6. using UnityEditor;
  7. using UnityEngine;
  8. namespace Animancer.Editor
  9. {
  10. /// <summary>[Editor-Only] Draws a GUI box denoting a period of time.</summary>
  11. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/TimelineGUI
  12. ///
  13. public class TimelineGUI : IDisposable
  14. {
  15. /************************************************************************************************************************/
  16. #region Fields
  17. /************************************************************************************************************************/
  18. private static readonly ConversionCache<float, string>
  19. G2Cache = new(value =>
  20. {
  21. if (Math.Abs(value) <= 99)
  22. return value.ToString("G2");
  23. else
  24. return ((int)value).ToString();
  25. });
  26. private static Texture _EventIcon;
  27. /// <summary>The icon used for events.</summary>
  28. public static Texture EventIcon
  29. => AnimancerIcons.Load(ref _EventIcon, "Animation.EventMarker");
  30. private static readonly Color
  31. FadeHighlightColor = new(0.35f, 0.5f, 1, 0.5f),
  32. SelectedEventColor = new(0.3f, 0.55f, 0.95f),
  33. UnselectedEventColor = AnimancerGUI.Grey(0.9f),
  34. PreviewTimeColor = AnimancerStateDrawerColors.FadeLineColor,
  35. BaseTimeColor = AnimancerGUI.Grey(0.5f, 0.75f);
  36. private Rect _Area;
  37. /// <summary>The pixel area in which this <see cref="TimelineGUI"/> is drawing.</summary>
  38. public Rect Area => _Area;
  39. private float _Speed, _Duration, _MinTime, _MaxTime, _StartTime, _EndTime, _FadeInEnd, _FadeOutEnd, _SecondsToPixels;
  40. private bool _HasEndTime;
  41. private readonly List<float>
  42. EventTimes = new();
  43. /// <summary>The height of the time ticks.</summary>
  44. public float TickHeight { get; private set; }
  45. /************************************************************************************************************************/
  46. #endregion
  47. /************************************************************************************************************************/
  48. #region Conversions
  49. /************************************************************************************************************************/
  50. /// <summary>Converts a number of seconds to a horizontal pixel position along the ruler.</summary>
  51. /// <remarks>The value is rounded to the nearest integer.</remarks>
  52. public float SecondsToPixels(float seconds) => AnimancerUtilities.Round((seconds - _MinTime) * _SecondsToPixels);
  53. /// <summary>Converts a horizontal pixel position along the ruler to a number of seconds.</summary>
  54. public float PixelsToSeconds(float pixels) => (pixels / _SecondsToPixels) + _MinTime;
  55. /// <summary>Converts a number of seconds to a normalized time value.</summary>
  56. public float SecondsToNormalized(float seconds) => seconds / _Duration;
  57. /// <summary>Converts a normalized time value to a number of seconds.</summary>
  58. public float NormalizedToSeconds(float normalized) => normalized * _Duration;
  59. /************************************************************************************************************************/
  60. #endregion
  61. /************************************************************************************************************************/
  62. private TimelineGUI() { }
  63. private static readonly TimelineGUI Instance = new();
  64. /// <summary>The currently drawing <see cref="TimelineGUI"/> (or null if none is drawing).</summary>
  65. public static TimelineGUI Current { get; private set; }
  66. /// <summary>Ends the area started by <see cref="BeginGUI"/>.</summary>
  67. void IDisposable.Dispose()
  68. {
  69. Current = null;
  70. GUI.EndClip();
  71. }
  72. /************************************************************************************************************************/
  73. /// <summary>
  74. /// Sets the `area` in which the ruler will be drawn and draws a <see cref="GUI.Box(Rect, string)"/> there.
  75. /// The returned object must have <see cref="IDisposable.Dispose"/> called on it afterwards.
  76. /// </summary>
  77. private static IDisposable BeginGUI(Rect area)
  78. {
  79. if (Current != null)
  80. throw new InvalidOperationException($"{nameof(TimelineGUI)} can't be used recursively.");
  81. if (!EditorGUIUtility.hierarchyMode)
  82. EditorGUI.indentLevel++;
  83. area = EditorGUI.IndentedRect(area);
  84. if (!EditorGUIUtility.hierarchyMode)
  85. EditorGUI.indentLevel--;
  86. GUI.Box(area, "");
  87. GUI.BeginClip(area);
  88. area.x = area.y = 0;
  89. Instance._Area = area;
  90. Instance.TickHeight = Mathf.Ceil(area.height * 0.3f);
  91. return Current = Instance;
  92. }
  93. /************************************************************************************************************************/
  94. /// <summary>Draws the ruler GUI and handles input events for the specified `context`.</summary>
  95. public static void DoGUI(Rect area, SerializableEventSequenceDrawer.Context context, out float addEventNormalizedTime)
  96. {
  97. using (BeginGUI(area))
  98. Current.DoGUI(context, out addEventNormalizedTime);
  99. }
  100. /************************************************************************************************************************/
  101. /// <summary>Draws the ruler GUI and handles input events for the specified `context`.</summary>
  102. private void DoGUI(SerializableEventSequenceDrawer.Context context, out float addEventNormalizedTime)
  103. {
  104. if (context.Property.hasMultipleDifferentValues)
  105. {
  106. GUI.Label(_Area, "Multi-editing events is not supported");
  107. addEventNormalizedTime = float.NaN;
  108. return;
  109. }
  110. var transition = context.TransitionContext.Transition;
  111. _Speed = transition.Speed;
  112. if (float.IsNaN(_Speed))
  113. _Speed = 1;
  114. _Duration = context.TransitionContext.MaximumDuration;
  115. if (_Duration <= 0)
  116. _Duration = 1;
  117. GatherEventTimes(context);
  118. _StartTime = GetStartTime(transition.NormalizedStartTime, _Speed, _Duration);
  119. _FadeInEnd = _StartTime + transition.FadeDuration * _Speed;
  120. _FadeOutEnd = GetFadeOutEnd(_Speed, _EndTime, _Duration);
  121. _MinTime = Mathf.Min(0, _StartTime);
  122. _MinTime = Mathf.Min(_MinTime, _FadeOutEnd);
  123. _MinTime = Mathf.Min(_MinTime, EventTimes[0]);
  124. _MaxTime = Mathf.Max(_StartTime, _FadeOutEnd);
  125. if (EventTimes.Count >= 2)
  126. _MaxTime = Mathf.Max(_MaxTime, EventTimes[^2]);
  127. if (_MaxTime < _Duration)
  128. _MaxTime = _Duration;
  129. _SecondsToPixels = _Area.width / (_MaxTime - _MinTime);
  130. DoFadeHighlightGUI();
  131. if (AnimancerUtilities.TryGetWrappedObject(transition, out ITransitionGUI gui))
  132. gui.OnTimelineBackgroundGUI();
  133. DoEventsGUI(context, out addEventNormalizedTime);
  134. DoRulerGUI();
  135. if (_Speed > 0)
  136. {
  137. if (_StartTime >= _EndTime)
  138. GUI.Label(_Area, "Start Time is not before End Time");
  139. }
  140. else if (_Speed < 0)
  141. {
  142. if (_StartTime <= _EndTime)
  143. GUI.Label(_Area, "Start Time is not after End Time");
  144. }
  145. gui?.OnTimelineForegroundGUI();
  146. }
  147. /************************************************************************************************************************/
  148. /// <summary>Calculates the start time of the transition (in seconds).</summary>
  149. public static float GetStartTime(float normalizedStartTime, float speed, float duration)
  150. {
  151. if (float.IsNaN(normalizedStartTime))
  152. {
  153. return speed < 0 ? duration : 0;
  154. }
  155. else
  156. {
  157. return normalizedStartTime * duration;
  158. }
  159. }
  160. /// <summary>Calculates the end time of the fade out (in seconds).</summary>
  161. public static float GetFadeOutEnd(float speed, float endTime, float duration)
  162. {
  163. if (speed < 0)
  164. return endTime > 0 ? 0 : (endTime - AnimancerGraph.DefaultFadeDuration) * -speed;
  165. else
  166. return endTime < duration ? duration : endTime + AnimancerGraph.DefaultFadeDuration * speed;
  167. }
  168. /************************************************************************************************************************/
  169. private static readonly Vector3[] QuadVertices = new Vector3[4];
  170. /// <summary>Draws a polygon describing the start, end, and fade details.</summary>
  171. private void DoFadeHighlightGUI()
  172. {
  173. if (Event.current.type != EventType.Repaint)
  174. return;
  175. var color = Handles.color;
  176. Handles.color = FadeHighlightColor;
  177. QuadVertices[0] = new(SecondsToPixels(_StartTime), _Area.y);
  178. QuadVertices[1] = new(SecondsToPixels(_FadeInEnd), _Area.yMax + 1);
  179. QuadVertices[2] = new(SecondsToPixels(_FadeOutEnd), _Area.yMax + 1);
  180. QuadVertices[3] = new(SecondsToPixels(_EndTime), _Area.y);
  181. Handles.DrawAAConvexPolygon(QuadVertices);
  182. Handles.color = color;
  183. }
  184. /************************************************************************************************************************/
  185. #region Events
  186. /************************************************************************************************************************/
  187. private void GatherEventTimes(SerializableEventSequenceDrawer.Context context)
  188. {
  189. EventTimes.Clear();
  190. if (context.Times.Count > 0)
  191. {
  192. var depth = context.Times.Property.depth;
  193. var time = context.Times.GetElement(0);
  194. while (time.depth > depth)
  195. {
  196. EventTimes.Add(time.floatValue * _Duration);
  197. time.Next(false);
  198. }
  199. _EndTime = EventTimes[^1];
  200. if (!float.IsNaN(_EndTime))
  201. {
  202. _HasEndTime = true;
  203. return;
  204. }
  205. }
  206. _EndTime = AnimancerEvent.Sequence.GetDefaultNormalizedEndTime(_Speed) * _Duration;
  207. _HasEndTime = false;
  208. if (EventTimes.Count == 0)
  209. EventTimes.Add(_EndTime);
  210. else
  211. EventTimes[^1] = _EndTime;
  212. }
  213. /************************************************************************************************************************/
  214. private static readonly int EventHash = "Event".GetHashCode();
  215. private static readonly List<int> EventControlIDs = new();
  216. /// <summary>Draws the details of the <see cref="SerializableEventSequenceDrawer.Context.Callbacks"/>.</summary>
  217. private void DoEventsGUI(SerializableEventSequenceDrawer.Context context, out float addEventNormalizedTime)
  218. {
  219. addEventNormalizedTime = float.NaN;
  220. var currentEvent = Event.current;
  221. EventControlIDs.Clear();
  222. var selectedEventControlID = -1;
  223. var baseControlID = GUIUtility.GetControlID(EventHash - 1, FocusType.Passive);
  224. for (int i = 0; i < EventTimes.Count; i++)
  225. {
  226. var controlID = GUIUtility.GetControlID(EventHash + i, FocusType.Keyboard);
  227. EventControlIDs.Add(controlID);
  228. if (context.SelectedEvent == i)
  229. selectedEventControlID = controlID;
  230. }
  231. EventControlIDs.Add(baseControlID);
  232. switch (currentEvent.type)
  233. {
  234. case EventType.Repaint:
  235. RepaintEventsGUI(context);
  236. break;
  237. case EventType.MouseDown:
  238. OnMouseDown(currentEvent, context, ref addEventNormalizedTime);
  239. break;
  240. case EventType.MouseUp:
  241. OnMouseUp(currentEvent, context);
  242. break;
  243. case EventType.MouseDrag:
  244. if (_Duration <= 0)
  245. break;
  246. var hotControl = GUIUtility.hotControl;
  247. if (hotControl == baseControlID)
  248. {
  249. SetPreviewTime(context, currentEvent);
  250. GUIUtility.ExitGUI();
  251. }
  252. else
  253. {
  254. for (int i = 0; i < EventTimes.Count; i++)
  255. {
  256. if (hotControl == EventControlIDs[i])
  257. {
  258. if (context.Times.Count < 1)
  259. context.Times.Count = 1;
  260. var seconds = PixelsToSeconds(currentEvent.mousePosition.x);
  261. if (currentEvent.control)
  262. SnapToFrameRate(context, ref seconds);
  263. var timeProperty = context.Times.GetElement(i);
  264. var normalizedTime = seconds / _Duration;
  265. timeProperty.floatValue = normalizedTime;
  266. SerializableEventSequenceDrawer.SyncEventTimeChange(context, i, normalizedTime);
  267. timeProperty.serializedObject.ApplyModifiedProperties();
  268. timeProperty.serializedObject.Update();
  269. GUIUtility.hotControl = EventControlIDs[context.SelectedEvent];
  270. GUI.changed = true;
  271. SetPreviewTime(context, currentEvent);
  272. GUIUtility.ExitGUI();
  273. }
  274. }
  275. }
  276. break;
  277. case EventType.KeyUp:
  278. if (GUIUtility.keyboardControl != selectedEventControlID)
  279. break;
  280. var exitGUI = false;
  281. switch (currentEvent.keyCode)
  282. {
  283. case KeyCode.Delete:
  284. case KeyCode.Backspace:
  285. SerializableEventSequenceDrawer.RemoveEvent(context, context.SelectedEvent);
  286. exitGUI = true;
  287. break;
  288. case KeyCode.LeftArrow:
  289. NudgeEventTime(context, Event.current.shift ? -10 : -1);
  290. break;
  291. case KeyCode.RightArrow:
  292. NudgeEventTime(context, Event.current.shift ? 10 : 1);
  293. break;
  294. case KeyCode.Space:
  295. RoundEventTime(context);
  296. break;
  297. default: return;// Don't call Use.
  298. }
  299. GUI.changed = true;
  300. currentEvent.Use();
  301. if (exitGUI)
  302. GUIUtility.ExitGUI();
  303. break;
  304. }
  305. }
  306. /************************************************************************************************************************/
  307. /// <summary>Snaps the `seconds` value to the nearest multiple of the <see cref="AnimationClip.frameRate"/>.</summary>
  308. public void SnapToFrameRate(SerializableEventSequenceDrawer.Context context, ref float seconds)
  309. {
  310. if (AnimancerUtilities.TryGetFrameRate(context.TransitionContext.Transition, out var frameRate))
  311. {
  312. seconds = AnimancerUtilities.Round(seconds, 1f / frameRate);
  313. }
  314. }
  315. /************************************************************************************************************************/
  316. private void RepaintEventsGUI(SerializableEventSequenceDrawer.Context context)
  317. {
  318. var color = GUI.color;
  319. for (int i = 0; i < EventTimes.Count; i++)
  320. {
  321. var currentColor = color;
  322. // Read Only: currentColor *= new(0.9f, 0.9f, 0.9f, 0.5f * alpha);
  323. if (context.SelectedEvent == i)
  324. {
  325. currentColor *= SelectedEventColor;
  326. }
  327. else
  328. {
  329. currentColor *= UnselectedEventColor;
  330. }
  331. if (i == EventTimes.Count - 1 && !_HasEndTime)
  332. currentColor.a *= 0.65f;
  333. GUI.color = currentColor;
  334. var area = GetEventIconArea(i);
  335. GUI.DrawTexture(area, EventIcon);
  336. }
  337. GUI.color = color;
  338. }
  339. /************************************************************************************************************************/
  340. private void OnMouseDown(Event currentEvent, SerializableEventSequenceDrawer.Context context, ref float addEventNormalizedTime)
  341. {
  342. if (!_Area.Contains(currentEvent.mousePosition))
  343. return;
  344. var selectedEventControlID = 0;
  345. var selectedEvent = -1;
  346. for (int i = 0; i < EventControlIDs.Count; i++)
  347. {
  348. var area = i < EventTimes.Count ? GetEventIconArea(i) : _Area;
  349. if (area.Contains(currentEvent.mousePosition))
  350. {
  351. selectedEventControlID = EventControlIDs[i];
  352. selectedEvent = i;
  353. break;
  354. }
  355. }
  356. if (selectedEvent < 0 || selectedEvent >= EventTimes.Count)
  357. {
  358. SetPreviewTime(context, currentEvent);
  359. selectedEvent = -1;
  360. }
  361. if (currentEvent.type == EventType.MouseDown &&
  362. currentEvent.clickCount == 2)
  363. {
  364. addEventNormalizedTime = PixelsToSeconds(currentEvent.mousePosition.x);
  365. addEventNormalizedTime = SecondsToNormalized(addEventNormalizedTime);
  366. }
  367. context.SelectedEvent = selectedEvent;
  368. GUIUtility.keyboardControl = selectedEventControlID;
  369. currentEvent.Use(selectedEventControlID);
  370. }
  371. /************************************************************************************************************************/
  372. private void OnMouseUp(Event currentEvent, SerializableEventSequenceDrawer.Context context)
  373. {
  374. if (currentEvent.button == 1 &&
  375. _Area.Contains(currentEvent.mousePosition))
  376. {
  377. currentEvent.Use(0);
  378. ShowContextMenu(currentEvent, context);
  379. }
  380. }
  381. /************************************************************************************************************************/
  382. private void ShowContextMenu(Event currentEvent, SerializableEventSequenceDrawer.Context context)
  383. {
  384. context = context.Copy();
  385. var time = SecondsToNormalized(PixelsToSeconds(currentEvent.mousePosition.x));
  386. var hasSelectedEvent = context.SelectedEvent >= 0;
  387. var menu = new GenericMenu();
  388. AddContextFunction(menu, context, "Add Event (Double Click)", true,
  389. () => SerializableEventSequenceDrawer.AddEvent(context, time));
  390. AddContextFunction(menu, context, "Remove Event (Delete)", hasSelectedEvent,
  391. () => SerializableEventSequenceDrawer.RemoveEvent(context, context.SelectedEvent));
  392. const string NudgePrefix = "Nudge Event Time/";
  393. AddContextFunction(menu, context, NudgePrefix + "Left 1 Pixel (Left Arrow)", hasSelectedEvent,
  394. () => NudgeEventTime(context, -1));
  395. AddContextFunction(menu, context, NudgePrefix + "Left 10 Pixels (Shift + Left Arrow)", hasSelectedEvent,
  396. () => NudgeEventTime(context, -10));
  397. AddContextFunction(menu, context, NudgePrefix + "Right 1 Pixel (Right Arrow)", hasSelectedEvent,
  398. () => NudgeEventTime(context, 1));
  399. AddContextFunction(menu, context, NudgePrefix + "Right 10 Pixels (Shift + Right Arrow)", hasSelectedEvent,
  400. () => NudgeEventTime(context, 10));
  401. var canRoundTime = hasSelectedEvent;
  402. if (canRoundTime)
  403. {
  404. time = context.Times.GetElement(context.SelectedEvent).floatValue;
  405. canRoundTime = TryRoundValue(ref time);
  406. }
  407. AddContextFunction(menu, context, $"Round Event Time to {time}x (Space)", canRoundTime,
  408. () => RoundEventTime(context));
  409. menu.ShowAsContext();
  410. }
  411. /************************************************************************************************************************/
  412. private static void AddContextFunction(
  413. GenericMenu menu, SerializableEventSequenceDrawer.Context context, string label, bool enabled, Action function)
  414. {
  415. menu.AddFunction(label, enabled, () =>
  416. {
  417. using (context.SetAsCurrent())
  418. {
  419. function();
  420. GUI.changed = true;
  421. }
  422. });
  423. }
  424. /************************************************************************************************************************/
  425. private void SetPreviewTime(SerializableEventSequenceDrawer.Context context, Event currentEvent)
  426. {
  427. if (_Duration > 0)
  428. {
  429. var seconds = PixelsToSeconds(currentEvent.mousePosition.x);
  430. if (currentEvent.control)
  431. SnapToFrameRate(context, ref seconds);
  432. TransitionPreviewWindow.PreviewNormalizedTime = seconds / _Duration;
  433. }
  434. }
  435. /************************************************************************************************************************/
  436. private Rect GetEventIconArea(int index)
  437. {
  438. var width = EventIcon.width;
  439. var x = SecondsToPixels(EventTimes[index]) - width * 0.5f;
  440. x = Mathf.Clamp(x, 0, _Area.width - width);
  441. return new(x, _Area.y, width, EventIcon.height);
  442. }
  443. /************************************************************************************************************************/
  444. private void NudgeEventTime(SerializableEventSequenceDrawer.Context context, float offsetPixels)
  445. {
  446. var index = context.SelectedEvent;
  447. var time = context.Times.GetElement(index);
  448. var value = time.floatValue;
  449. value = NormalizedToSeconds(value);
  450. value = SecondsToPixels(value);
  451. value += offsetPixels;
  452. value = PixelsToSeconds(value);
  453. value = SecondsToNormalized(value);
  454. time.floatValue = value;
  455. SerializableEventSequenceDrawer.SyncEventTimeChange(context, index, value);
  456. }
  457. /************************************************************************************************************************/
  458. private static void RoundEventTime(SerializableEventSequenceDrawer.Context context)
  459. {
  460. var index = context.SelectedEvent;
  461. var time = context.Times.GetElement(index);
  462. var value = time.floatValue;
  463. if (TryRoundValue(ref value))
  464. {
  465. time.floatValue = value;
  466. SerializableEventSequenceDrawer.SyncEventTimeChange(context, index, value);
  467. }
  468. }
  469. private static bool TryRoundValue(ref float value)
  470. {
  471. var format = System.Globalization.NumberFormatInfo.InvariantInfo;
  472. var text = value.ToString(format);
  473. var dot = text.IndexOf('.');
  474. if (dot < 0)
  475. return false;
  476. Round:
  477. var newValue = (float)Math.Round(value, text.Length - dot - 2, MidpointRounding.AwayFromZero);
  478. if (newValue == value)
  479. {
  480. dot--;
  481. if (dot > 0)
  482. goto Round;
  483. }
  484. if (value != newValue)
  485. {
  486. value = newValue;
  487. return true;
  488. }
  489. else return false;
  490. }
  491. /************************************************************************************************************************/
  492. #endregion
  493. /************************************************************************************************************************/
  494. #region Ticks
  495. /************************************************************************************************************************/
  496. private static readonly List<float> TickTimes = new();
  497. /// <summary>Draws ticks and labels for important times throughout the area.</summary>
  498. private void DoRulerGUI()
  499. {
  500. if (Event.current.type != EventType.Repaint)
  501. return;
  502. var area = new Rect(SecondsToPixels(0), _Area.yMax - TickHeight, 0, TickHeight)
  503. {
  504. xMax = SecondsToPixels(_Duration)
  505. };
  506. EditorGUI.DrawRect(area, BaseTimeColor);
  507. TickTimes.Clear();
  508. TickTimes.Add(0);
  509. TickTimes.Add(_StartTime);
  510. TickTimes.Add(_FadeInEnd);
  511. TickTimes.Add(_Duration);
  512. TickTimes.AddRange(EventTimes);
  513. TickTimes.Sort();
  514. var previousTime = float.NaN;
  515. area.x = float.NegativeInfinity;
  516. for (int i = 0; i < TickTimes.Count; i++)
  517. {
  518. var time = TickTimes[i];
  519. if (previousTime != time)
  520. {
  521. previousTime = time;
  522. DoRulerLabelGUI(ref area, time);
  523. }
  524. }
  525. DrawPreviewTime();
  526. }
  527. /************************************************************************************************************************/
  528. private void DrawPreviewTime()
  529. {
  530. var state = TransitionPreviewWindow.GetCurrentState();
  531. if (state == null)
  532. return;
  533. var normalizedTime = TransitionPreviewWindow.PreviewNormalizedTime;
  534. DrawPreviewTime(normalizedTime, alpha: 1);
  535. // Looping states show faded indicators at every other multiple of the loop.
  536. if (!state.IsLooping)
  537. return;
  538. // Make sure the area is actually wide enough for it to not just be a solid bar.
  539. if ((int)SecondsToPixels(0) > (int)SecondsToPixels(_Duration) - 4)
  540. return;
  541. // Go back to the first visible increment.
  542. while (normalizedTime * _Duration >= _MinTime + _Duration)
  543. normalizedTime -= 1;
  544. // Draw every visible increment from there on.
  545. while (normalizedTime * _Duration <= _MaxTime)
  546. {
  547. DrawPreviewTime(normalizedTime, alpha: 0.2f);
  548. normalizedTime += 1;
  549. }
  550. }
  551. private void DrawPreviewTime(float normalizedTime, float alpha)
  552. {
  553. var time = NormalizedToSeconds(normalizedTime);
  554. var x = SecondsToPixels(time);
  555. if (x >= 0 && x <= _Area.width)
  556. {
  557. var color = PreviewTimeColor;
  558. color.a = alpha;
  559. EditorGUI.DrawRect(new(x - 1, _Area.y, 2, _Area.height), color);
  560. }
  561. }
  562. /************************************************************************************************************************/
  563. private static GUIStyle _RulerLabelStyle;
  564. private static ConversionCache<string, float> _TimeLabelWidthCache;
  565. private void DoRulerLabelGUI(ref Rect previousArea, float time)
  566. {
  567. _RulerLabelStyle ??= new(GUI.skin.label)
  568. {
  569. padding = new(),
  570. contentOffset = new(0, -2),
  571. alignment = TextAnchor.UpperLeft,
  572. fontSize = Mathf.CeilToInt(AnimancerGUI.LineHeight * 0.6f),
  573. };
  574. var text = G2Cache.Convert(time);
  575. _TimeLabelWidthCache ??= ConversionCache.CreateWidthCache(_RulerLabelStyle);
  576. var area = new Rect(
  577. SecondsToPixels(time),
  578. _Area.y,
  579. _TimeLabelWidthCache.Convert(text),
  580. _Area.height);
  581. if (area.x > _Area.x)
  582. {
  583. var tickY = _Area.yMax - TickHeight;
  584. EditorGUI.DrawRect(new(area.x, tickY, 1, TickHeight), AnimancerGUI.TextColor);
  585. }
  586. if (area.xMax > _Area.xMax)
  587. area.x = _Area.xMax - area.width;
  588. if (area.x < 0)
  589. area.x = 0;
  590. if (area.x > previousArea.xMax + 2)
  591. {
  592. GUI.Label(area, text, _RulerLabelStyle);
  593. previousArea = area;
  594. }
  595. }
  596. /************************************************************************************************************************/
  597. #endregion
  598. /************************************************************************************************************************/
  599. }
  600. }
  601. #endif