// Animancer // Copyright 2020 Kybernetik // #if UNITY_EDITOR using System.Collections.Generic; using UnityEditor; using UnityEngine; namespace Animancer.Editor { /// [Editor-Only] Draws a GUI box denoting a period of time. public sealed class TimeRuler { /************************************************************************************************************************/ #region Fields /************************************************************************************************************************/ private static readonly ConversionCache G2Cache = new ConversionCache((value) => { if (Mathf.Abs(value) <= 99) return value.ToString("G2"); else return ((int)value).ToString(); }); private static readonly Color FadeHighlightColor = new Color(0.35f, 0.5f, 1, 0.5f), SelectedEventColor = new Color(0.3f, 0.55f, 0.95f), UnselectedEventColor = new Color(0.9f, 0.9f, 0.9f), PreviewTimeColor = new Color(1, 0.25f, 0.1f), BaseTimeColor = new Color(0.5f, 0.5f, 0.5f, 0.75f); private Rect _Area; private float _Speed, _Duration, _MinTime, _MaxTime, _StartTime, _FadeInEnd, _FadeOutEnd, _EndTime, _SecondsToPixels; private bool _HasEndTime; private readonly List EventTimes = new List(); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Conversions /************************************************************************************************************************/ /// Converts a number of seconds to a horizontal pixel position along the ruler. public float SecondsToPixels(float seconds) { return (seconds - _MinTime) * _SecondsToPixels; } /// Converts a horizontal pixel position along the ruler to a number of seconds. public float PixelsToSeconds(float pixels) { return (pixels / _SecondsToPixels) + _MinTime; } /// Converts a number of seconds to a normalized time value. public float SecondsToNormalized(float seconds) { return seconds / _Duration; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ /// /// Sets the `area` in which the ruler will be drawn and draws a there. /// Must be followed by a call to . /// public void BeginGUI(Rect area) { area = EditorGUI.IndentedRect(area); GUI.Box(area, ""); #if !UNITY_2019_3_OR_NEWER const float Padding = 1; area.x += Padding; area.y += Padding; area.width -= Padding * 2; area.height -= Padding * 2; #endif GUI.BeginClip(area); area.x = area.y = 0; _Area = area; } /// /// Uses any unused events in the area and ends the area started by /// . /// public void EndGUI() { GUI.EndClip(); } /************************************************************************************************************************/ /// /// Draws the ruler GUI and handles input events for the specified `context`. /// public void DoGUI(Rect area, EventSequenceDrawer.Context context, out float addEventNormalizedTime) { BeginGUI(area); var transition = context.TransitionContext.Transition; _Speed = transition.Speed; var playDirection = _Speed < 0 ? -1 : 1; _Duration = context.TransitionContext.MaximumDuration; if (_Duration <= 0) _Duration = 1; GatherEventTimes(context); _StartTime = transition.NormalizedStartTime; if (float.IsNaN(_StartTime)) { _StartTime = _Speed < 0 ? _Duration : 0; } else { _StartTime *= _Duration; } _FadeInEnd = _StartTime + transition.FadeDuration * playDirection; _FadeOutEnd = GetFadeOutEnd(_Speed, _EndTime, _Duration); _MinTime = Mathf.Min(0, _StartTime); _MinTime = Mathf.Min(_MinTime, _FadeOutEnd); _MinTime = Mathf.Min(_MinTime, EventTimes[0]); _MaxTime = Mathf.Max(_StartTime, _FadeOutEnd); if (EventTimes.Count >= 2) _MaxTime = Mathf.Max(_MaxTime, EventTimes[EventTimes.Count - 2]); if (_MaxTime < _Duration) _MaxTime = _Duration; _SecondsToPixels = _Area.width / (_MaxTime - _MinTime); DoFadeHighlightGUI(); DoEventsGUI(context, out addEventNormalizedTime); DoRulerGUI(); if (_Speed > 0) { if (_StartTime > _EndTime) GUI.Label(_Area, "Start Time is after End Time"); } else if (_Speed < 0) { if (_StartTime < _EndTime) GUI.Label(_Area, "Start Time is before End Time"); } EndGUI(); } /************************************************************************************************************************/ /// Calculates the end time of the fade out. public static float GetFadeOutEnd(float speed, float endTime, float duration) { if (speed < 0) return endTime > 0 ? 0 : endTime - AnimancerPlayable.DefaultFadeDuration; else return endTime < duration ? duration : endTime + AnimancerPlayable.DefaultFadeDuration; } /************************************************************************************************************************/ private static readonly Vector3[] QuadVerts = new Vector3[4]; /// /// Draws a polygon describing the start, end, and fade details. /// public void DoFadeHighlightGUI() { if (Event.current.type != EventType.Repaint) return; var color = Handles.color; Handles.color = FadeHighlightColor; QuadVerts[0] = new Vector3(SecondsToPixels(_StartTime), _Area.y); QuadVerts[1] = new Vector3(SecondsToPixels(_FadeInEnd), _Area.yMax); QuadVerts[2] = new Vector3(SecondsToPixels(_FadeOutEnd), _Area.yMax); QuadVerts[3] = new Vector3(SecondsToPixels(_EndTime), _Area.y); Handles.DrawAAConvexPolygon(QuadVerts); Handles.color = color; } /************************************************************************************************************************/ #region Events /************************************************************************************************************************/ private void GatherEventTimes(EventSequenceDrawer.Context context) { EventTimes.Clear(); if (context.TimeCount > 0) { var depth = context.Times.depth; var time = context.GetTime(0); while (time.depth > depth) { EventTimes.Add(time.floatValue * _Duration); time.Next(false); } _EndTime = EventTimes[EventTimes.Count - 1]; if (!float.IsNaN(_EndTime)) { _HasEndTime = true; return; } } _EndTime = AnimancerEvent.Sequence.GetDefaultNormalizedEndTime(_Speed) * _Duration; _HasEndTime = false; if (EventTimes.Count == 0) EventTimes.Add(_EndTime); else EventTimes[EventTimes.Count - 1] = _EndTime; } /************************************************************************************************************************/ private static readonly int EventHash = "Event".GetHashCode(); private static readonly List EventControlIDs = new List(); /// /// Draws the details of the . /// public void DoEventsGUI(EventSequenceDrawer.Context context, out float addEventNormalizedTime) { addEventNormalizedTime = float.NaN; var currentGUIEvent = Event.current; EventControlIDs.Clear(); var selectedEventControlID = -1; var baseControlID = GUIUtility.GetControlID(EventHash - 1, FocusType.Passive); for (int i = 0; i < EventTimes.Count; i++) { var controlID = GUIUtility.GetControlID(EventHash + i, FocusType.Keyboard); EventControlIDs.Add(controlID); if (context.SelectedEvent == i) selectedEventControlID = controlID; } EventControlIDs.Add(baseControlID); switch (currentGUIEvent.type) { case EventType.Repaint: RepaintEventsGUI(context); break; case EventType.MouseDown: if (_Area.Contains(currentGUIEvent.mousePosition)) OnMouseDown(currentGUIEvent, context, out addEventNormalizedTime); break; case EventType.MouseDrag: if (GUIUtility.hotControl == baseControlID) { SetPreviewTime(currentGUIEvent); GUIUtility.ExitGUI(); } break; case EventType.KeyUp: if (GUIUtility.keyboardControl == selectedEventControlID && (currentGUIEvent.keyCode == KeyCode.Delete || currentGUIEvent.keyCode == KeyCode.Backspace)) { EventSequenceDrawer.RemoveEvent(context, context.SelectedEvent); GUIUtility.ExitGUI(); } break; } } /************************************************************************************************************************/ private void RepaintEventsGUI(EventSequenceDrawer.Context context) { var color = GUI.color; for (int i = 0; i < EventTimes.Count; i++) { var currentColor = color; // Read Only: currentColor *= new Color(0.9f, 0.9f, 0.9f, 0.5f * alpha); if (context.SelectedEvent == i) { currentColor *= SelectedEventColor; } else { currentColor *= UnselectedEventColor; } if (i == EventTimes.Count - 1 && !_HasEndTime) currentColor.a *= 0.65f; GUI.color = currentColor; var area = GetEventIconArea(i); GUI.DrawTexture(area, Styles.EventIcon); } GUI.color = color; } /************************************************************************************************************************/ private void OnMouseDown(Event currentGUIEvent, EventSequenceDrawer.Context context, out float addEventNormalizedTime) { var selectedEventControlID = 0; var selectedEvent = -1; for (int i = 0; i < EventControlIDs.Count; i++) { var area = i < EventTimes.Count ? GetEventIconArea(i) : _Area; if (area.Contains(currentGUIEvent.mousePosition)) { selectedEventControlID = EventControlIDs[i]; selectedEvent = i; break; } } GUIUtility.hotControl = GUIUtility.keyboardControl = selectedEventControlID; if (selectedEvent < 0 || selectedEvent >= EventTimes.Count) { SetPreviewTime(currentGUIEvent); selectedEvent = -1; } if (currentGUIEvent.type == EventType.MouseDown && currentGUIEvent.clickCount == 2) { addEventNormalizedTime = PixelsToSeconds(currentGUIEvent.mousePosition.x); addEventNormalizedTime = SecondsToNormalized(addEventNormalizedTime); } else { addEventNormalizedTime = float.NaN; } context.SelectedEvent = selectedEvent; currentGUIEvent.Use(); } /************************************************************************************************************************/ private void SetPreviewTime(Event currentGUIEvent) { if (_Duration > 0) { var seconds = PixelsToSeconds(currentGUIEvent.mousePosition.x); TransitionPreviewWindow.SetPreviewNormalizedTime(seconds / _Duration); } } /************************************************************************************************************************/ private Rect GetEventIconArea(int index) { var width = Styles.EventIcon.width; var x = SecondsToPixels(EventTimes[index]) - width * 0.5f; x = Mathf.Clamp(x, 0, _Area.width - width); return new Rect(x, _Area.y, width, Styles.EventIcon.height); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Ticks /************************************************************************************************************************/ private const float TickHeight = 0.3f; private static readonly List TickTimes = new List(); /// /// Draws ticks and labels for important times throughout the area. /// public void DoRulerGUI() { if (Event.current.type != EventType.Repaint) return; var tickHeight = _Area.height * TickHeight; var area = new Rect(SecondsToPixels(0), _Area.yMax - tickHeight, 0, tickHeight) { xMax = SecondsToPixels(_Duration) }; EditorGUI.DrawRect(area, BaseTimeColor); TickTimes.Clear(); TickTimes.Add(0); TickTimes.Add(_StartTime); TickTimes.Add(_FadeInEnd); TickTimes.Add(_Duration); for (int i = 0; i < EventTimes.Count; i++) TickTimes.Add(EventTimes[i]); TickTimes.Sort(); var previousTime = float.NaN; area.x = float.NegativeInfinity; for (int i = 0; i < TickTimes.Count; i++) { var time = TickTimes[i]; if (previousTime != time) { previousTime = time; DoRulerLabelGUI(ref area, time); } } var state = TransitionPreviewWindow.GetCurrentState(); if (state != null) { var time = state.Time; DrawPreviewTime(time, 1); if (state.IsLooping) { while (time >= _MinTime + _Duration) time -= _Duration; while (time <= _MaxTime) { DrawPreviewTime(time, 0.25f); time += _Duration; } } } } /************************************************************************************************************************/ private void DrawPreviewTime(float time, float alpha) { time = SecondsToPixels(time); if (time >= 0 && time <= _Area.width) { var color = PreviewTimeColor; color.a = alpha; EditorGUI.DrawRect(new Rect(time - 1, _Area.y, 2, _Area.height), color); } } /************************************************************************************************************************/ private static ConversionCache _TimeLabelWidthCache; private void DoRulerLabelGUI(ref Rect previousArea, float time) { var text = G2Cache.Convert(time); if (_TimeLabelWidthCache == null) _TimeLabelWidthCache = AnimancerGUI.CreateWidthCache(Styles.TimeLabel); var area = new Rect( SecondsToPixels(time), _Area.y, _TimeLabelWidthCache.Convert(text), _Area.height); if (area.x > _Area.x) { var tickHeight = _Area.height * TickHeight; var tickY = _Area.yMax - tickHeight; EditorGUI.DrawRect(new Rect(area.x, tickY, 1, tickHeight), AnimancerGUI.TextColor); } if (area.xMax > _Area.xMax) area.x = _Area.xMax - area.width; if (area.x < 0) area.x = 0; if (area.x > previousArea.xMax + 2) { GUI.Label(area, text, Styles.TimeLabel); previousArea = area; } } /************************************************************************************************************************/ private static class Styles { public static readonly GUIStyle TimeLabel = new GUIStyle(GUI.skin.label) { padding = new RectOffset(), contentOffset = new Vector2(0, -2), alignment = TextAnchor.UpperLeft, fontSize = Mathf.CeilToInt(AnimancerGUI.LineHeight * 0.6f), }; public static readonly Texture EventIcon = EditorGUIUtility.IconContent("Animation.EventMarker").image; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } #endif