| | |
| | | using System; |
| | | using System.Collections.Generic; |
| | | using UnityEngine; |
| | | |
| | | namespace UnityEditor.PostProcessing |
| | | { |
| | | public sealed class CurveEditor |
| | | { |
| | | #region Enums |
| | | |
| | | enum EditMode |
| | | { |
| | | None, |
| | | Moving, |
| | | TangentEdit |
| | | } |
| | | |
| | | enum Tangent |
| | | { |
| | | In, |
| | | Out |
| | | } |
| | | #endregion |
| | | |
| | | #region Structs |
| | | public struct Settings |
| | | { |
| | | public Rect bounds; |
| | | public RectOffset padding; |
| | | public Color selectionColor; |
| | | public float curvePickingDistance; |
| | | public float keyTimeClampingDistance; |
| | | |
| | | public static Settings defaultSettings |
| | | { |
| | | get |
| | | { |
| | | return new Settings |
| | | { |
| | | bounds = new Rect(0f, 0f, 1f, 1f), |
| | | padding = new RectOffset(10, 10, 10, 10), |
| | | selectionColor = Color.yellow, |
| | | curvePickingDistance = 6f, |
| | | keyTimeClampingDistance = 1e-4f |
| | | }; |
| | | } |
| | | } |
| | | } |
| | | |
| | | public struct CurveState |
| | | { |
| | | public bool visible; |
| | | public bool editable; |
| | | public uint minPointCount; |
| | | public float zeroKeyConstantValue; |
| | | public Color color; |
| | | public float width; |
| | | public float handleWidth; |
| | | public bool showNonEditableHandles; |
| | | public bool onlyShowHandlesOnSelection; |
| | | public bool loopInBounds; |
| | | |
| | | public static CurveState defaultState |
| | | { |
| | | get |
| | | { |
| | | return new CurveState |
| | | { |
| | | visible = true, |
| | | editable = true, |
| | | minPointCount = 2, |
| | | zeroKeyConstantValue = 0f, |
| | | color = Color.white, |
| | | width = 2f, |
| | | handleWidth = 2f, |
| | | showNonEditableHandles = true, |
| | | onlyShowHandlesOnSelection = false, |
| | | loopInBounds = false |
| | | }; |
| | | } |
| | | } |
| | | } |
| | | |
| | | public struct Selection |
| | | { |
| | | public SerializedProperty curve; |
| | | public int keyframeIndex; |
| | | public Keyframe? keyframe; |
| | | |
| | | public Selection(SerializedProperty curve, int keyframeIndex, Keyframe? keyframe) |
| | | { |
| | | this.curve = curve; |
| | | this.keyframeIndex = keyframeIndex; |
| | | this.keyframe = keyframe; |
| | | } |
| | | } |
| | | |
| | | internal struct MenuAction |
| | | { |
| | | internal SerializedProperty curve; |
| | | internal int index; |
| | | internal Vector3 position; |
| | | |
| | | internal MenuAction(SerializedProperty curve) |
| | | { |
| | | this.curve = curve; |
| | | this.index = -1; |
| | | this.position = Vector3.zero; |
| | | } |
| | | |
| | | internal MenuAction(SerializedProperty curve, int index) |
| | | { |
| | | this.curve = curve; |
| | | this.index = index; |
| | | this.position = Vector3.zero; |
| | | } |
| | | |
| | | internal MenuAction(SerializedProperty curve, Vector3 position) |
| | | { |
| | | this.curve = curve; |
| | | this.index = -1; |
| | | this.position = position; |
| | | } |
| | | } |
| | | #endregion |
| | | |
| | | #region Fields & properties |
| | | public Settings settings { get; private set; } |
| | | |
| | | Dictionary<SerializedProperty, CurveState> m_Curves; |
| | | Rect m_CurveArea; |
| | | |
| | | SerializedProperty m_SelectedCurve; |
| | | int m_SelectedKeyframeIndex = -1; |
| | | |
| | | EditMode m_EditMode = EditMode.None; |
| | | Tangent m_TangentEditMode; |
| | | |
| | | bool m_Dirty; |
| | | #endregion |
| | | |
| | | #region Constructors & destructors |
| | | public CurveEditor() |
| | | : this(Settings.defaultSettings) |
| | | {} |
| | | |
| | | public CurveEditor(Settings settings) |
| | | { |
| | | this.settings = settings; |
| | | m_Curves = new Dictionary<SerializedProperty, CurveState>(); |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region Public API |
| | | public void Add(params SerializedProperty[] curves) |
| | | { |
| | | foreach (var curve in curves) |
| | | Add(curve, CurveState.defaultState); |
| | | } |
| | | |
| | | public void Add(SerializedProperty curve) |
| | | { |
| | | Add(curve, CurveState.defaultState); |
| | | } |
| | | |
| | | public void Add(SerializedProperty curve, CurveState state) |
| | | { |
| | | // Make sure the property is in fact an AnimationCurve |
| | | var animCurve = curve.animationCurveValue; |
| | | if (animCurve == null) |
| | | throw new ArgumentException("curve"); |
| | | |
| | | if (m_Curves.ContainsKey(curve)) |
| | | Debug.LogWarning("Curve has already been added to the editor"); |
| | | |
| | | m_Curves.Add(curve, state); |
| | | } |
| | | |
| | | public void Remove(SerializedProperty curve) |
| | | { |
| | | m_Curves.Remove(curve); |
| | | } |
| | | |
| | | public void RemoveAll() |
| | | { |
| | | m_Curves.Clear(); |
| | | } |
| | | |
| | | public CurveState GetCurveState(SerializedProperty curve) |
| | | { |
| | | CurveState state; |
| | | if (!m_Curves.TryGetValue(curve, out state)) |
| | | throw new KeyNotFoundException("curve"); |
| | | |
| | | return state; |
| | | } |
| | | |
| | | public void SetCurveState(SerializedProperty curve, CurveState state) |
| | | { |
| | | if (!m_Curves.ContainsKey(curve)) |
| | | throw new KeyNotFoundException("curve"); |
| | | |
| | | m_Curves[curve] = state; |
| | | } |
| | | |
| | | public Selection GetSelection() |
| | | { |
| | | Keyframe? key = null; |
| | | if (m_SelectedKeyframeIndex > -1) |
| | | { |
| | | var curve = m_SelectedCurve.animationCurveValue; |
| | | |
| | | if (m_SelectedKeyframeIndex >= curve.length) |
| | | m_SelectedKeyframeIndex = -1; |
| | | else |
| | | key = curve[m_SelectedKeyframeIndex]; |
| | | } |
| | | |
| | | return new Selection(m_SelectedCurve, m_SelectedKeyframeIndex, key); |
| | | } |
| | | |
| | | public void SetKeyframe(SerializedProperty curve, int keyframeIndex, Keyframe keyframe) |
| | | { |
| | | var animCurve = curve.animationCurveValue; |
| | | SetKeyframe(animCurve, keyframeIndex, keyframe); |
| | | SaveCurve(curve, animCurve); |
| | | } |
| | | |
| | | public bool OnGUI(Rect rect) |
| | | { |
| | | if (Event.current.type == EventType.Repaint) |
| | | m_Dirty = false; |
| | | |
| | | GUI.BeginClip(rect); |
| | | { |
| | | var area = new Rect(Vector2.zero, rect.size); |
| | | m_CurveArea = settings.padding.Remove(area); |
| | | |
| | | foreach (var curve in m_Curves) |
| | | OnCurveGUI(area, curve.Key, curve.Value); |
| | | |
| | | OnGeneralUI(area); |
| | | } |
| | | GUI.EndClip(); |
| | | |
| | | return m_Dirty; |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region UI & events |
| | | |
| | | void OnCurveGUI(Rect rect, SerializedProperty curve, CurveState state) |
| | | { |
| | | // Discard invisible curves |
| | | if (!state.visible) |
| | | return; |
| | | |
| | | var animCurve = curve.animationCurveValue; |
| | | var keys = animCurve.keys; |
| | | var length = keys.Length; |
| | | |
| | | // Curve drawing |
| | | // Slightly dim non-editable curves |
| | | var color = state.color; |
| | | if (!state.editable) |
| | | color.a *= 0.5f; |
| | | |
| | | Handles.color = color; |
| | | var bounds = settings.bounds; |
| | | |
| | | if (length == 0) |
| | | { |
| | | var p1 = CurveToCanvas(new Vector3(bounds.xMin, state.zeroKeyConstantValue)); |
| | | var p2 = CurveToCanvas(new Vector3(bounds.xMax, state.zeroKeyConstantValue)); |
| | | Handles.DrawAAPolyLine(state.width, p1, p2); |
| | | } |
| | | else if (length == 1) |
| | | { |
| | | var p1 = CurveToCanvas(new Vector3(bounds.xMin, keys[0].value)); |
| | | var p2 = CurveToCanvas(new Vector3(bounds.xMax, keys[0].value)); |
| | | Handles.DrawAAPolyLine(state.width, p1, p2); |
| | | } |
| | | else |
| | | { |
| | | var prevKey = keys[0]; |
| | | for (int k = 1; k < length; k++) |
| | | { |
| | | var key = keys[k]; |
| | | var pts = BezierSegment(prevKey, key); |
| | | |
| | | if (float.IsInfinity(prevKey.outTangent) || float.IsInfinity(key.inTangent)) |
| | | { |
| | | var s = HardSegment(prevKey, key); |
| | | Handles.DrawAAPolyLine(state.width, s[0], s[1], s[2]); |
| | | } |
| | | else Handles.DrawBezier(pts[0], pts[3], pts[1], pts[2], color, null, state.width); |
| | | |
| | | prevKey = key; |
| | | } |
| | | |
| | | // Curve extents & loops |
| | | if (keys[0].time > bounds.xMin) |
| | | { |
| | | if (state.loopInBounds) |
| | | { |
| | | var p1 = keys[length - 1]; |
| | | p1.time -= settings.bounds.width; |
| | | var p2 = keys[0]; |
| | | var pts = BezierSegment(p1, p2); |
| | | |
| | | if (float.IsInfinity(p1.outTangent) || float.IsInfinity(p2.inTangent)) |
| | | { |
| | | var s = HardSegment(p1, p2); |
| | | Handles.DrawAAPolyLine(state.width, s[0], s[1], s[2]); |
| | | } |
| | | else Handles.DrawBezier(pts[0], pts[3], pts[1], pts[2], color, null, state.width); |
| | | } |
| | | else |
| | | { |
| | | var p1 = CurveToCanvas(new Vector3(bounds.xMin, keys[0].value)); |
| | | var p2 = CurveToCanvas(keys[0]); |
| | | Handles.DrawAAPolyLine(state.width, p1, p2); |
| | | } |
| | | } |
| | | |
| | | if (keys[length - 1].time < bounds.xMax) |
| | | { |
| | | if (state.loopInBounds) |
| | | { |
| | | var p1 = keys[length - 1]; |
| | | var p2 = keys[0]; |
| | | p2.time += settings.bounds.width; |
| | | var pts = BezierSegment(p1, p2); |
| | | |
| | | if (float.IsInfinity(p1.outTangent) || float.IsInfinity(p2.inTangent)) |
| | | { |
| | | var s = HardSegment(p1, p2); |
| | | Handles.DrawAAPolyLine(state.width, s[0], s[1], s[2]); |
| | | } |
| | | else Handles.DrawBezier(pts[0], pts[3], pts[1], pts[2], color, null, state.width); |
| | | } |
| | | else |
| | | { |
| | | var p1 = CurveToCanvas(keys[length - 1]); |
| | | var p2 = CurveToCanvas(new Vector3(bounds.xMax, keys[length - 1].value)); |
| | | Handles.DrawAAPolyLine(state.width, p1, p2); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Make sure selection is correct (undo can break it) |
| | | bool isCurrentlySelectedCurve = curve == m_SelectedCurve; |
| | | |
| | | if (isCurrentlySelectedCurve && m_SelectedKeyframeIndex >= length) |
| | | m_SelectedKeyframeIndex = -1; |
| | | |
| | | // Handles & keys |
| | | for (int k = 0; k < length; k++) |
| | | { |
| | | bool isCurrentlySelectedKeyframe = k == m_SelectedKeyframeIndex; |
| | | var e = Event.current; |
| | | |
| | | var pos = CurveToCanvas(keys[k]); |
| | | var hitRect = new Rect(pos.x - 8f, pos.y - 8f, 16f, 16f); |
| | | var offset = isCurrentlySelectedCurve |
| | | ? new RectOffset(5, 5, 5, 5) |
| | | : new RectOffset(6, 6, 6, 6); |
| | | |
| | | var outTangent = pos + CurveTangentToCanvas(keys[k].outTangent).normalized * 40f; |
| | | var inTangent = pos - CurveTangentToCanvas(keys[k].inTangent).normalized * 40f; |
| | | var inTangentHitRect = new Rect(inTangent.x - 7f, inTangent.y - 7f, 14f, 14f); |
| | | var outTangentHitrect = new Rect(outTangent.x - 7f, outTangent.y - 7f, 14f, 14f); |
| | | |
| | | // Draw |
| | | if (state.showNonEditableHandles) |
| | | { |
| | | if (e.type == EventType.repaint) |
| | | { |
| | | var selectedColor = (isCurrentlySelectedCurve && isCurrentlySelectedKeyframe) |
| | | ? settings.selectionColor |
| | | : state.color; |
| | | |
| | | // Keyframe |
| | | EditorGUI.DrawRect(offset.Remove(hitRect), selectedColor); |
| | | |
| | | // Tangents |
| | | if (isCurrentlySelectedCurve && (!state.onlyShowHandlesOnSelection || (state.onlyShowHandlesOnSelection && isCurrentlySelectedKeyframe))) |
| | | { |
| | | Handles.color = selectedColor; |
| | | |
| | | if (k > 0 || state.loopInBounds) |
| | | { |
| | | Handles.DrawAAPolyLine(state.handleWidth, pos, inTangent); |
| | | EditorGUI.DrawRect(offset.Remove(inTangentHitRect), selectedColor); |
| | | } |
| | | |
| | | if (k < length - 1 || state.loopInBounds) |
| | | { |
| | | Handles.DrawAAPolyLine(state.handleWidth, pos, outTangent); |
| | | EditorGUI.DrawRect(offset.Remove(outTangentHitrect), selectedColor); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Events |
| | | if (state.editable) |
| | | { |
| | | // Keyframe move |
| | | if (m_EditMode == EditMode.Moving && e.type == EventType.MouseDrag && isCurrentlySelectedCurve && isCurrentlySelectedKeyframe) |
| | | { |
| | | EditMoveKeyframe(animCurve, keys, k); |
| | | } |
| | | |
| | | // Tangent editing |
| | | if (m_EditMode == EditMode.TangentEdit && e.type == EventType.MouseDrag && isCurrentlySelectedCurve && isCurrentlySelectedKeyframe) |
| | | { |
| | | bool alreadyBroken = !(Mathf.Approximately(keys[k].inTangent, keys[k].outTangent) || (float.IsInfinity(keys[k].inTangent) && float.IsInfinity(keys[k].outTangent))); |
| | | EditMoveTangent(animCurve, keys, k, m_TangentEditMode, e.shift || !(alreadyBroken || e.control)); |
| | | } |
| | | |
| | | // Keyframe selection & context menu |
| | | if (e.type == EventType.mouseDown && rect.Contains(e.mousePosition)) |
| | | { |
| | | if (hitRect.Contains(e.mousePosition)) |
| | | { |
| | | if (e.button == 0) |
| | | { |
| | | SelectKeyframe(curve, k); |
| | | m_EditMode = EditMode.Moving; |
| | | e.Use(); |
| | | } |
| | | else if (e.button == 1) |
| | | { |
| | | // Keyframe context menu |
| | | var menu = new GenericMenu(); |
| | | menu.AddItem(new GUIContent("Delete Key"), false, (x) => |
| | | { |
| | | var action = (MenuAction)x; |
| | | var curveValue = action.curve.animationCurveValue; |
| | | action.curve.serializedObject.Update(); |
| | | RemoveKeyframe(curveValue, action.index); |
| | | m_SelectedKeyframeIndex = -1; |
| | | SaveCurve(action.curve, curveValue); |
| | | action.curve.serializedObject.ApplyModifiedProperties(); |
| | | }, new MenuAction(curve, k)); |
| | | menu.ShowAsContext(); |
| | | e.Use(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Tangent selection & edit mode |
| | | if (e.type == EventType.mouseDown && rect.Contains(e.mousePosition)) |
| | | { |
| | | if (inTangentHitRect.Contains(e.mousePosition) && (k > 0 || state.loopInBounds)) |
| | | { |
| | | SelectKeyframe(curve, k); |
| | | m_EditMode = EditMode.TangentEdit; |
| | | m_TangentEditMode = Tangent.In; |
| | | e.Use(); |
| | | } |
| | | else if (outTangentHitrect.Contains(e.mousePosition) && (k < length - 1 || state.loopInBounds)) |
| | | { |
| | | SelectKeyframe(curve, k); |
| | | m_EditMode = EditMode.TangentEdit; |
| | | m_TangentEditMode = Tangent.Out; |
| | | e.Use(); |
| | | } |
| | | } |
| | | |
| | | // Mouse up - clean up states |
| | | if (e.rawType == EventType.MouseUp && m_EditMode != EditMode.None) |
| | | { |
| | | m_EditMode = EditMode.None; |
| | | } |
| | | |
| | | // Set cursors |
| | | { |
| | | EditorGUIUtility.AddCursorRect(hitRect, MouseCursor.MoveArrow); |
| | | |
| | | if (k > 0 || state.loopInBounds) |
| | | EditorGUIUtility.AddCursorRect(inTangentHitRect, MouseCursor.RotateArrow); |
| | | |
| | | if (k < length - 1 || state.loopInBounds) |
| | | EditorGUIUtility.AddCursorRect(outTangentHitrect, MouseCursor.RotateArrow); |
| | | } |
| | | } |
| | | } |
| | | |
| | | Handles.color = Color.white; |
| | | SaveCurve(curve, animCurve); |
| | | } |
| | | |
| | | void OnGeneralUI(Rect rect) |
| | | { |
| | | var e = Event.current; |
| | | |
| | | // Selection |
| | | if (e.type == EventType.mouseDown) |
| | | { |
| | | GUI.FocusControl(null); |
| | | m_SelectedCurve = null; |
| | | m_SelectedKeyframeIndex = -1; |
| | | bool used = false; |
| | | |
| | | var hit = CanvasToCurve(e.mousePosition); |
| | | float curvePickValue = CurveToCanvas(hit).y; |
| | | |
| | | // Try and select a curve |
| | | foreach (var curve in m_Curves) |
| | | { |
| | | if (!curve.Value.editable || !curve.Value.visible) |
| | | continue; |
| | | |
| | | var prop = curve.Key; |
| | | var state = curve.Value; |
| | | var animCurve = prop.animationCurveValue; |
| | | float hitY = animCurve.length == 0 |
| | | ? state.zeroKeyConstantValue |
| | | : animCurve.Evaluate(hit.x); |
| | | |
| | | var curvePos = CurveToCanvas(new Vector3(hit.x, hitY)); |
| | | |
| | | if (Mathf.Abs(curvePos.y - curvePickValue) < settings.curvePickingDistance) |
| | | { |
| | | m_SelectedCurve = prop; |
| | | |
| | | if (e.clickCount == 2 && e.button == 0) |
| | | { |
| | | // Create a keyframe on double-click on this curve |
| | | EditCreateKeyframe(animCurve, hit, true, state.zeroKeyConstantValue); |
| | | SaveCurve(prop, animCurve); |
| | | } |
| | | else if (e.button == 1) |
| | | { |
| | | // Curve context menu |
| | | var menu = new GenericMenu(); |
| | | menu.AddItem(new GUIContent("Add Key"), false, (x) => |
| | | { |
| | | var action = (MenuAction)x; |
| | | var curveValue = action.curve.animationCurveValue; |
| | | action.curve.serializedObject.Update(); |
| | | EditCreateKeyframe(curveValue, hit, true, 0f); |
| | | SaveCurve(action.curve, curveValue); |
| | | action.curve.serializedObject.ApplyModifiedProperties(); |
| | | }, new MenuAction(prop, hit)); |
| | | menu.ShowAsContext(); |
| | | e.Use(); |
| | | used = true; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (e.clickCount == 2 && e.button == 0 && m_SelectedCurve == null) |
| | | { |
| | | // Create a keyframe on every curve on double-click |
| | | foreach (var curve in m_Curves) |
| | | { |
| | | if (!curve.Value.editable || !curve.Value.visible) |
| | | continue; |
| | | |
| | | var prop = curve.Key; |
| | | var state = curve.Value; |
| | | var animCurve = prop.animationCurveValue; |
| | | EditCreateKeyframe(animCurve, hit, e.alt, state.zeroKeyConstantValue); |
| | | SaveCurve(prop, animCurve); |
| | | } |
| | | } |
| | | else if (!used && e.button == 1) |
| | | { |
| | | // Global context menu |
| | | var menu = new GenericMenu(); |
| | | menu.AddItem(new GUIContent("Add Key At Position"), false, () => ContextMenuAddKey(hit, false)); |
| | | menu.AddItem(new GUIContent("Add Key On Curves"), false, () => ContextMenuAddKey(hit, true)); |
| | | menu.ShowAsContext(); |
| | | } |
| | | |
| | | e.Use(); |
| | | } |
| | | |
| | | // Delete selected key(s) |
| | | if (e.type == EventType.keyDown && (e.keyCode == KeyCode.Delete || e.keyCode == KeyCode.Backspace)) |
| | | { |
| | | if (m_SelectedKeyframeIndex != -1 && m_SelectedCurve != null) |
| | | { |
| | | var animCurve = m_SelectedCurve.animationCurveValue; |
| | | var length = animCurve.length; |
| | | |
| | | if (m_Curves[m_SelectedCurve].minPointCount < length && length >= 0) |
| | | { |
| | | EditDeleteKeyframe(animCurve, m_SelectedKeyframeIndex); |
| | | m_SelectedKeyframeIndex = -1; |
| | | SaveCurve(m_SelectedCurve, animCurve); |
| | | } |
| | | |
| | | e.Use(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | void SaveCurve(SerializedProperty prop, AnimationCurve curve) |
| | | { |
| | | prop.animationCurveValue = curve; |
| | | } |
| | | |
| | | void Invalidate() |
| | | { |
| | | m_Dirty = true; |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region Keyframe manipulations |
| | | |
| | | void SelectKeyframe(SerializedProperty curve, int keyframeIndex) |
| | | { |
| | | m_SelectedKeyframeIndex = keyframeIndex; |
| | | m_SelectedCurve = curve; |
| | | Invalidate(); |
| | | } |
| | | |
| | | void ContextMenuAddKey(Vector3 hit, bool createOnCurve) |
| | | { |
| | | SerializedObject serializedObject = null; |
| | | |
| | | foreach (var curve in m_Curves) |
| | | { |
| | | if (!curve.Value.editable || !curve.Value.visible) |
| | | continue; |
| | | |
| | | var prop = curve.Key; |
| | | var state = curve.Value; |
| | | |
| | | if (serializedObject == null) |
| | | { |
| | | serializedObject = prop.serializedObject; |
| | | serializedObject.Update(); |
| | | } |
| | | |
| | | var animCurve = prop.animationCurveValue; |
| | | EditCreateKeyframe(animCurve, hit, createOnCurve, state.zeroKeyConstantValue); |
| | | SaveCurve(prop, animCurve); |
| | | } |
| | | |
| | | if (serializedObject != null) |
| | | serializedObject.ApplyModifiedProperties(); |
| | | |
| | | Invalidate(); |
| | | } |
| | | |
| | | void EditCreateKeyframe(AnimationCurve curve, Vector3 position, bool createOnCurve, float zeroKeyConstantValue) |
| | | { |
| | | float tangent = EvaluateTangent(curve, position.x); |
| | | |
| | | if (createOnCurve) |
| | | { |
| | | position.y = curve.length == 0 |
| | | ? zeroKeyConstantValue |
| | | : curve.Evaluate(position.x); |
| | | } |
| | | |
| | | AddKeyframe(curve, new Keyframe(position.x, position.y, tangent, tangent)); |
| | | } |
| | | |
| | | void EditDeleteKeyframe(AnimationCurve curve, int keyframeIndex) |
| | | { |
| | | RemoveKeyframe(curve, keyframeIndex); |
| | | } |
| | | |
| | | void AddKeyframe(AnimationCurve curve, Keyframe newValue) |
| | | { |
| | | curve.AddKey(newValue); |
| | | Invalidate(); |
| | | } |
| | | |
| | | void RemoveKeyframe(AnimationCurve curve, int keyframeIndex) |
| | | { |
| | | curve.RemoveKey(keyframeIndex); |
| | | Invalidate(); |
| | | } |
| | | |
| | | void SetKeyframe(AnimationCurve curve, int keyframeIndex, Keyframe newValue) |
| | | { |
| | | var keys = curve.keys; |
| | | |
| | | if (keyframeIndex > 0) |
| | | newValue.time = Mathf.Max(keys[keyframeIndex - 1].time + settings.keyTimeClampingDistance, newValue.time); |
| | | |
| | | if (keyframeIndex < keys.Length - 1) |
| | | newValue.time = Mathf.Min(keys[keyframeIndex + 1].time - settings.keyTimeClampingDistance, newValue.time); |
| | | |
| | | curve.MoveKey(keyframeIndex, newValue); |
| | | Invalidate(); |
| | | } |
| | | |
| | | void EditMoveKeyframe(AnimationCurve curve, Keyframe[] keys, int keyframeIndex) |
| | | { |
| | | var key = CanvasToCurve(Event.current.mousePosition); |
| | | float inTgt = keys[keyframeIndex].inTangent; |
| | | float outTgt = keys[keyframeIndex].outTangent; |
| | | SetKeyframe(curve, keyframeIndex, new Keyframe(key.x, key.y, inTgt, outTgt)); |
| | | } |
| | | |
| | | void EditMoveTangent(AnimationCurve curve, Keyframe[] keys, int keyframeIndex, Tangent targetTangent, bool linkTangents) |
| | | { |
| | | var pos = CanvasToCurve(Event.current.mousePosition); |
| | | |
| | | float time = keys[keyframeIndex].time; |
| | | float value = keys[keyframeIndex].value; |
| | | |
| | | pos -= new Vector3(time, value); |
| | | |
| | | if (targetTangent == Tangent.In && pos.x > 0f) |
| | | pos.x = 0f; |
| | | |
| | | if (targetTangent == Tangent.Out && pos.x < 0f) |
| | | pos.x = 0f; |
| | | |
| | | float tangent; |
| | | |
| | | if (Mathf.Approximately(pos.x, 0f)) |
| | | tangent = pos.y < 0f ? float.PositiveInfinity : float.NegativeInfinity; |
| | | else |
| | | tangent = pos.y / pos.x; |
| | | |
| | | float inTangent = keys[keyframeIndex].inTangent; |
| | | float outTangent = keys[keyframeIndex].outTangent; |
| | | |
| | | if (targetTangent == Tangent.In || linkTangents) |
| | | inTangent = tangent; |
| | | if (targetTangent == Tangent.Out || linkTangents) |
| | | outTangent = tangent; |
| | | |
| | | SetKeyframe(curve, keyframeIndex, new Keyframe(time, value, inTangent, outTangent)); |
| | | } |
| | | |
| | | #endregion |
| | | |
| | | #region Maths utilities |
| | | |
| | | Vector3 CurveToCanvas(Keyframe keyframe) |
| | | { |
| | | return CurveToCanvas(new Vector3(keyframe.time, keyframe.value)); |
| | | } |
| | | |
| | | Vector3 CurveToCanvas(Vector3 position) |
| | | { |
| | | var bounds = settings.bounds; |
| | | var output = new Vector3((position.x - bounds.x) / (bounds.xMax - bounds.x), (position.y - bounds.y) / (bounds.yMax - bounds.y)); |
| | | output.x = output.x * (m_CurveArea.xMax - m_CurveArea.xMin) + m_CurveArea.xMin; |
| | | output.y = (1f - output.y) * (m_CurveArea.yMax - m_CurveArea.yMin) + m_CurveArea.yMin; |
| | | return output; |
| | | } |
| | | |
| | | Vector3 CanvasToCurve(Vector3 position) |
| | | { |
| | | var bounds = settings.bounds; |
| | | var output = position; |
| | | output.x = (output.x - m_CurveArea.xMin) / (m_CurveArea.xMax - m_CurveArea.xMin); |
| | | output.y = (output.y - m_CurveArea.yMin) / (m_CurveArea.yMax - m_CurveArea.yMin); |
| | | output.x = Mathf.Lerp(bounds.x, bounds.xMax, output.x); |
| | | output.y = Mathf.Lerp(bounds.yMax, bounds.y, output.y); |
| | | return output; |
| | | } |
| | | |
| | | Vector3 CurveTangentToCanvas(float tangent) |
| | | { |
| | | if (!float.IsInfinity(tangent)) |
| | | { |
| | | var bounds = settings.bounds; |
| | | float ratio = (m_CurveArea.width / m_CurveArea.height) / ((bounds.xMax - bounds.x) / (bounds.yMax - bounds.y)); |
| | | return new Vector3(1f, -tangent / ratio).normalized; |
| | | } |
| | | |
| | | return float.IsPositiveInfinity(tangent) ? Vector3.up : Vector3.down; |
| | | } |
| | | |
| | | Vector3[] BezierSegment(Keyframe start, Keyframe end) |
| | | { |
| | | var segment = new Vector3[4]; |
| | | |
| | | segment[0] = CurveToCanvas(new Vector3(start.time, start.value)); |
| | | segment[3] = CurveToCanvas(new Vector3(end.time, end.value)); |
| | | |
| | | float middle = start.time + ((end.time - start.time) * 0.333333f); |
| | | float middle2 = start.time + ((end.time - start.time) * 0.666666f); |
| | | |
| | | segment[1] = CurveToCanvas(new Vector3(middle, ProjectTangent(start.time, start.value, start.outTangent, middle))); |
| | | segment[2] = CurveToCanvas(new Vector3(middle2, ProjectTangent(end.time, end.value, end.inTangent, middle2))); |
| | | |
| | | return segment; |
| | | } |
| | | |
| | | Vector3[] HardSegment(Keyframe start, Keyframe end) |
| | | { |
| | | var segment = new Vector3[3]; |
| | | |
| | | segment[0] = CurveToCanvas(start); |
| | | segment[1] = CurveToCanvas(new Vector3(end.time, start.value)); |
| | | segment[2] = CurveToCanvas(end); |
| | | |
| | | return segment; |
| | | } |
| | | |
| | | float ProjectTangent(float inPosition, float inValue, float inTangent, float projPosition) |
| | | { |
| | | return inValue + ((projPosition - inPosition) * inTangent); |
| | | } |
| | | |
| | | float EvaluateTangent(AnimationCurve curve, float time) |
| | | { |
| | | int prev = -1, next = 0; |
| | | for (int i = 0; i < curve.keys.Length; i++) |
| | | { |
| | | if (time > curve.keys[i].time) |
| | | { |
| | | prev = i; |
| | | next = i + 1; |
| | | } |
| | | else break; |
| | | } |
| | | |
| | | if (next == 0) |
| | | return 0f; |
| | | |
| | | if (prev == curve.keys.Length - 1) |
| | | return 0f; |
| | | |
| | | const float kD = 1e-3f; |
| | | float tp = Mathf.Max(time - kD, curve.keys[prev].time); |
| | | float tn = Mathf.Min(time + kD, curve.keys[next].time); |
| | | |
| | | float vp = curve.Evaluate(tp); |
| | | float vn = curve.Evaluate(tn); |
| | | |
| | | if (Mathf.Approximately(tn, tp)) |
| | | return (vn - vp > 0f) ? float.PositiveInfinity : float.NegativeInfinity; |
| | | |
| | | return (vn - vp) / (tn - tp); |
| | | } |
| | | |
| | | #endregion |
| | | } |
| | | } |
| | | using System;
|
| | | using System.Collections.Generic;
|
| | | using UnityEngine;
|
| | |
|
| | | namespace UnityEditor.PostProcessing
|
| | | {
|
| | | public sealed class CurveEditor
|
| | | {
|
| | | #region Enums
|
| | |
|
| | | enum EditMode
|
| | | {
|
| | | None,
|
| | | Moving,
|
| | | TangentEdit
|
| | | }
|
| | |
|
| | | enum Tangent
|
| | | {
|
| | | In,
|
| | | Out
|
| | | }
|
| | | #endregion
|
| | |
|
| | | #region Structs
|
| | | public struct Settings
|
| | | {
|
| | | public Rect bounds;
|
| | | public RectOffset padding;
|
| | | public Color selectionColor;
|
| | | public float curvePickingDistance;
|
| | | public float keyTimeClampingDistance;
|
| | |
|
| | | public static Settings defaultSettings
|
| | | {
|
| | | get
|
| | | {
|
| | | return new Settings
|
| | | {
|
| | | bounds = new Rect(0f, 0f, 1f, 1f),
|
| | | padding = new RectOffset(10, 10, 10, 10),
|
| | | selectionColor = Color.yellow,
|
| | | curvePickingDistance = 6f,
|
| | | keyTimeClampingDistance = 1e-4f
|
| | | };
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | public struct CurveState
|
| | | {
|
| | | public bool visible;
|
| | | public bool editable;
|
| | | public uint minPointCount;
|
| | | public float zeroKeyConstantValue;
|
| | | public Color color;
|
| | | public float width;
|
| | | public float handleWidth;
|
| | | public bool showNonEditableHandles;
|
| | | public bool onlyShowHandlesOnSelection;
|
| | | public bool loopInBounds;
|
| | |
|
| | | public static CurveState defaultState
|
| | | {
|
| | | get
|
| | | {
|
| | | return new CurveState
|
| | | {
|
| | | visible = true,
|
| | | editable = true,
|
| | | minPointCount = 2,
|
| | | zeroKeyConstantValue = 0f,
|
| | | color = Color.white,
|
| | | width = 2f,
|
| | | handleWidth = 2f,
|
| | | showNonEditableHandles = true,
|
| | | onlyShowHandlesOnSelection = false,
|
| | | loopInBounds = false
|
| | | };
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | public struct Selection
|
| | | {
|
| | | public SerializedProperty curve;
|
| | | public int keyframeIndex;
|
| | | public Keyframe? keyframe;
|
| | |
|
| | | public Selection(SerializedProperty curve, int keyframeIndex, Keyframe? keyframe)
|
| | | {
|
| | | this.curve = curve;
|
| | | this.keyframeIndex = keyframeIndex;
|
| | | this.keyframe = keyframe;
|
| | | }
|
| | | }
|
| | |
|
| | | internal struct MenuAction
|
| | | {
|
| | | internal SerializedProperty curve;
|
| | | internal int index;
|
| | | internal Vector3 position;
|
| | |
|
| | | internal MenuAction(SerializedProperty curve)
|
| | | {
|
| | | this.curve = curve;
|
| | | this.index = -1;
|
| | | this.position = Vector3.zero;
|
| | | }
|
| | |
|
| | | internal MenuAction(SerializedProperty curve, int index)
|
| | | {
|
| | | this.curve = curve;
|
| | | this.index = index;
|
| | | this.position = Vector3.zero;
|
| | | }
|
| | |
|
| | | internal MenuAction(SerializedProperty curve, Vector3 position)
|
| | | {
|
| | | this.curve = curve;
|
| | | this.index = -1;
|
| | | this.position = position;
|
| | | }
|
| | | }
|
| | | #endregion
|
| | |
|
| | | #region Fields & properties
|
| | | public Settings settings { get; private set; }
|
| | |
|
| | | Dictionary<SerializedProperty, CurveState> m_Curves;
|
| | | Rect m_CurveArea;
|
| | |
|
| | | SerializedProperty m_SelectedCurve;
|
| | | int m_SelectedKeyframeIndex = -1;
|
| | |
|
| | | EditMode m_EditMode = EditMode.None;
|
| | | Tangent m_TangentEditMode;
|
| | |
|
| | | bool m_Dirty;
|
| | | #endregion
|
| | |
|
| | | #region Constructors & destructors
|
| | | public CurveEditor()
|
| | | : this(Settings.defaultSettings)
|
| | | {}
|
| | |
|
| | | public CurveEditor(Settings settings)
|
| | | {
|
| | | this.settings = settings;
|
| | | m_Curves = new Dictionary<SerializedProperty, CurveState>();
|
| | | }
|
| | |
|
| | | #endregion
|
| | |
|
| | | #region Public API
|
| | | public void Add(params SerializedProperty[] curves)
|
| | | {
|
| | | foreach (var curve in curves)
|
| | | Add(curve, CurveState.defaultState);
|
| | | }
|
| | |
|
| | | public void Add(SerializedProperty curve)
|
| | | {
|
| | | Add(curve, CurveState.defaultState);
|
| | | }
|
| | |
|
| | | public void Add(SerializedProperty curve, CurveState state)
|
| | | {
|
| | | // Make sure the property is in fact an AnimationCurve
|
| | | var animCurve = curve.animationCurveValue;
|
| | | if (animCurve == null)
|
| | | throw new ArgumentException("curve");
|
| | |
|
| | | if (m_Curves.ContainsKey(curve))
|
| | | Debug.LogWarning("Curve has already been added to the editor");
|
| | |
|
| | | m_Curves.Add(curve, state);
|
| | | }
|
| | |
|
| | | public void Remove(SerializedProperty curve)
|
| | | {
|
| | | m_Curves.Remove(curve);
|
| | | }
|
| | |
|
| | | public void RemoveAll()
|
| | | {
|
| | | m_Curves.Clear();
|
| | | }
|
| | |
|
| | | public CurveState GetCurveState(SerializedProperty curve)
|
| | | {
|
| | | CurveState state;
|
| | | if (!m_Curves.TryGetValue(curve, out state))
|
| | | throw new KeyNotFoundException("curve");
|
| | |
|
| | | return state;
|
| | | }
|
| | |
|
| | | public void SetCurveState(SerializedProperty curve, CurveState state)
|
| | | {
|
| | | if (!m_Curves.ContainsKey(curve))
|
| | | throw new KeyNotFoundException("curve");
|
| | |
|
| | | m_Curves[curve] = state;
|
| | | }
|
| | |
|
| | | public Selection GetSelection()
|
| | | {
|
| | | Keyframe? key = null;
|
| | | if (m_SelectedKeyframeIndex > -1)
|
| | | {
|
| | | var curve = m_SelectedCurve.animationCurveValue;
|
| | |
|
| | | if (m_SelectedKeyframeIndex >= curve.length)
|
| | | m_SelectedKeyframeIndex = -1;
|
| | | else
|
| | | key = curve[m_SelectedKeyframeIndex];
|
| | | }
|
| | |
|
| | | return new Selection(m_SelectedCurve, m_SelectedKeyframeIndex, key);
|
| | | }
|
| | |
|
| | | public void SetKeyframe(SerializedProperty curve, int keyframeIndex, Keyframe keyframe)
|
| | | {
|
| | | var animCurve = curve.animationCurveValue;
|
| | | SetKeyframe(animCurve, keyframeIndex, keyframe);
|
| | | SaveCurve(curve, animCurve);
|
| | | }
|
| | |
|
| | | public bool OnGUI(Rect rect)
|
| | | {
|
| | | if (Event.current.type == EventType.Repaint)
|
| | | m_Dirty = false;
|
| | |
|
| | | GUI.BeginClip(rect);
|
| | | {
|
| | | var area = new Rect(Vector2.zero, rect.size);
|
| | | m_CurveArea = settings.padding.Remove(area);
|
| | |
|
| | | foreach (var curve in m_Curves)
|
| | | OnCurveGUI(area, curve.Key, curve.Value);
|
| | |
|
| | | OnGeneralUI(area);
|
| | | }
|
| | | GUI.EndClip();
|
| | |
|
| | | return m_Dirty;
|
| | | }
|
| | |
|
| | | #endregion
|
| | |
|
| | | #region UI & events
|
| | |
|
| | | void OnCurveGUI(Rect rect, SerializedProperty curve, CurveState state)
|
| | | {
|
| | | // Discard invisible curves
|
| | | if (!state.visible)
|
| | | return;
|
| | |
|
| | | var animCurve = curve.animationCurveValue;
|
| | | var keys = animCurve.keys;
|
| | | var length = keys.Length;
|
| | |
|
| | | // Curve drawing
|
| | | // Slightly dim non-editable curves
|
| | | var color = state.color;
|
| | | if (!state.editable)
|
| | | color.a *= 0.5f;
|
| | |
|
| | | Handles.color = color;
|
| | | var bounds = settings.bounds;
|
| | |
|
| | | if (length == 0)
|
| | | {
|
| | | var p1 = CurveToCanvas(new Vector3(bounds.xMin, state.zeroKeyConstantValue));
|
| | | var p2 = CurveToCanvas(new Vector3(bounds.xMax, state.zeroKeyConstantValue));
|
| | | Handles.DrawAAPolyLine(state.width, p1, p2);
|
| | | }
|
| | | else if (length == 1)
|
| | | {
|
| | | var p1 = CurveToCanvas(new Vector3(bounds.xMin, keys[0].value));
|
| | | var p2 = CurveToCanvas(new Vector3(bounds.xMax, keys[0].value));
|
| | | Handles.DrawAAPolyLine(state.width, p1, p2);
|
| | | }
|
| | | else
|
| | | {
|
| | | var prevKey = keys[0];
|
| | | for (int k = 1; k < length; k++)
|
| | | {
|
| | | var key = keys[k];
|
| | | var pts = BezierSegment(prevKey, key);
|
| | |
|
| | | if (float.IsInfinity(prevKey.outTangent) || float.IsInfinity(key.inTangent))
|
| | | {
|
| | | var s = HardSegment(prevKey, key);
|
| | | Handles.DrawAAPolyLine(state.width, s[0], s[1], s[2]);
|
| | | }
|
| | | else Handles.DrawBezier(pts[0], pts[3], pts[1], pts[2], color, null, state.width);
|
| | |
|
| | | prevKey = key;
|
| | | }
|
| | |
|
| | | // Curve extents & loops
|
| | | if (keys[0].time > bounds.xMin)
|
| | | {
|
| | | if (state.loopInBounds)
|
| | | {
|
| | | var p1 = keys[length - 1];
|
| | | p1.time -= settings.bounds.width;
|
| | | var p2 = keys[0];
|
| | | var pts = BezierSegment(p1, p2);
|
| | |
|
| | | if (float.IsInfinity(p1.outTangent) || float.IsInfinity(p2.inTangent))
|
| | | {
|
| | | var s = HardSegment(p1, p2);
|
| | | Handles.DrawAAPolyLine(state.width, s[0], s[1], s[2]);
|
| | | }
|
| | | else Handles.DrawBezier(pts[0], pts[3], pts[1], pts[2], color, null, state.width);
|
| | | }
|
| | | else
|
| | | {
|
| | | var p1 = CurveToCanvas(new Vector3(bounds.xMin, keys[0].value));
|
| | | var p2 = CurveToCanvas(keys[0]);
|
| | | Handles.DrawAAPolyLine(state.width, p1, p2);
|
| | | }
|
| | | }
|
| | |
|
| | | if (keys[length - 1].time < bounds.xMax)
|
| | | {
|
| | | if (state.loopInBounds)
|
| | | {
|
| | | var p1 = keys[length - 1];
|
| | | var p2 = keys[0];
|
| | | p2.time += settings.bounds.width;
|
| | | var pts = BezierSegment(p1, p2);
|
| | |
|
| | | if (float.IsInfinity(p1.outTangent) || float.IsInfinity(p2.inTangent))
|
| | | {
|
| | | var s = HardSegment(p1, p2);
|
| | | Handles.DrawAAPolyLine(state.width, s[0], s[1], s[2]);
|
| | | }
|
| | | else Handles.DrawBezier(pts[0], pts[3], pts[1], pts[2], color, null, state.width);
|
| | | }
|
| | | else
|
| | | {
|
| | | var p1 = CurveToCanvas(keys[length - 1]);
|
| | | var p2 = CurveToCanvas(new Vector3(bounds.xMax, keys[length - 1].value));
|
| | | Handles.DrawAAPolyLine(state.width, p1, p2);
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | // Make sure selection is correct (undo can break it)
|
| | | bool isCurrentlySelectedCurve = curve == m_SelectedCurve;
|
| | |
|
| | | if (isCurrentlySelectedCurve && m_SelectedKeyframeIndex >= length)
|
| | | m_SelectedKeyframeIndex = -1;
|
| | |
|
| | | // Handles & keys
|
| | | for (int k = 0; k < length; k++)
|
| | | {
|
| | | bool isCurrentlySelectedKeyframe = k == m_SelectedKeyframeIndex;
|
| | | var e = Event.current;
|
| | |
|
| | | var pos = CurveToCanvas(keys[k]);
|
| | | var hitRect = new Rect(pos.x - 8f, pos.y - 8f, 16f, 16f);
|
| | | var offset = isCurrentlySelectedCurve
|
| | | ? new RectOffset(5, 5, 5, 5)
|
| | | : new RectOffset(6, 6, 6, 6);
|
| | |
|
| | | var outTangent = pos + CurveTangentToCanvas(keys[k].outTangent).normalized * 40f;
|
| | | var inTangent = pos - CurveTangentToCanvas(keys[k].inTangent).normalized * 40f;
|
| | | var inTangentHitRect = new Rect(inTangent.x - 7f, inTangent.y - 7f, 14f, 14f);
|
| | | var outTangentHitrect = new Rect(outTangent.x - 7f, outTangent.y - 7f, 14f, 14f);
|
| | |
|
| | | // Draw
|
| | | if (state.showNonEditableHandles)
|
| | | {
|
| | | if (e.type == EventType.repaint)
|
| | | {
|
| | | var selectedColor = (isCurrentlySelectedCurve && isCurrentlySelectedKeyframe)
|
| | | ? settings.selectionColor
|
| | | : state.color;
|
| | |
|
| | | // Keyframe
|
| | | EditorGUI.DrawRect(offset.Remove(hitRect), selectedColor);
|
| | |
|
| | | // Tangents
|
| | | if (isCurrentlySelectedCurve && (!state.onlyShowHandlesOnSelection || (state.onlyShowHandlesOnSelection && isCurrentlySelectedKeyframe)))
|
| | | {
|
| | | Handles.color = selectedColor;
|
| | |
|
| | | if (k > 0 || state.loopInBounds)
|
| | | {
|
| | | Handles.DrawAAPolyLine(state.handleWidth, pos, inTangent);
|
| | | EditorGUI.DrawRect(offset.Remove(inTangentHitRect), selectedColor);
|
| | | }
|
| | |
|
| | | if (k < length - 1 || state.loopInBounds)
|
| | | {
|
| | | Handles.DrawAAPolyLine(state.handleWidth, pos, outTangent);
|
| | | EditorGUI.DrawRect(offset.Remove(outTangentHitrect), selectedColor);
|
| | | }
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | // Events
|
| | | if (state.editable)
|
| | | {
|
| | | // Keyframe move
|
| | | if (m_EditMode == EditMode.Moving && e.type == EventType.MouseDrag && isCurrentlySelectedCurve && isCurrentlySelectedKeyframe)
|
| | | {
|
| | | EditMoveKeyframe(animCurve, keys, k);
|
| | | }
|
| | |
|
| | | // Tangent editing
|
| | | if (m_EditMode == EditMode.TangentEdit && e.type == EventType.MouseDrag && isCurrentlySelectedCurve && isCurrentlySelectedKeyframe)
|
| | | {
|
| | | bool alreadyBroken = !(Mathf.Approximately(keys[k].inTangent, keys[k].outTangent) || (float.IsInfinity(keys[k].inTangent) && float.IsInfinity(keys[k].outTangent)));
|
| | | EditMoveTangent(animCurve, keys, k, m_TangentEditMode, e.shift || !(alreadyBroken || e.control));
|
| | | }
|
| | |
|
| | | // Keyframe selection & context menu
|
| | | if (e.type == EventType.mouseDown && rect.Contains(e.mousePosition))
|
| | | {
|
| | | if (hitRect.Contains(e.mousePosition))
|
| | | {
|
| | | if (e.button == 0)
|
| | | {
|
| | | SelectKeyframe(curve, k);
|
| | | m_EditMode = EditMode.Moving;
|
| | | e.Use();
|
| | | }
|
| | | else if (e.button == 1)
|
| | | {
|
| | | // Keyframe context menu
|
| | | var menu = new GenericMenu();
|
| | | menu.AddItem(new GUIContent("Delete Key"), false, (x) =>
|
| | | {
|
| | | var action = (MenuAction)x;
|
| | | var curveValue = action.curve.animationCurveValue;
|
| | | action.curve.serializedObject.Update();
|
| | | RemoveKeyframe(curveValue, action.index);
|
| | | m_SelectedKeyframeIndex = -1;
|
| | | SaveCurve(action.curve, curveValue);
|
| | | action.curve.serializedObject.ApplyModifiedProperties();
|
| | | }, new MenuAction(curve, k));
|
| | | menu.ShowAsContext();
|
| | | e.Use();
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | // Tangent selection & edit mode
|
| | | if (e.type == EventType.mouseDown && rect.Contains(e.mousePosition))
|
| | | {
|
| | | if (inTangentHitRect.Contains(e.mousePosition) && (k > 0 || state.loopInBounds))
|
| | | {
|
| | | SelectKeyframe(curve, k);
|
| | | m_EditMode = EditMode.TangentEdit;
|
| | | m_TangentEditMode = Tangent.In;
|
| | | e.Use();
|
| | | }
|
| | | else if (outTangentHitrect.Contains(e.mousePosition) && (k < length - 1 || state.loopInBounds))
|
| | | {
|
| | | SelectKeyframe(curve, k);
|
| | | m_EditMode = EditMode.TangentEdit;
|
| | | m_TangentEditMode = Tangent.Out;
|
| | | e.Use();
|
| | | }
|
| | | }
|
| | |
|
| | | // Mouse up - clean up states
|
| | | if (e.rawType == EventType.MouseUp && m_EditMode != EditMode.None)
|
| | | {
|
| | | m_EditMode = EditMode.None;
|
| | | }
|
| | |
|
| | | // Set cursors
|
| | | {
|
| | | EditorGUIUtility.AddCursorRect(hitRect, MouseCursor.MoveArrow);
|
| | |
|
| | | if (k > 0 || state.loopInBounds)
|
| | | EditorGUIUtility.AddCursorRect(inTangentHitRect, MouseCursor.RotateArrow);
|
| | |
|
| | | if (k < length - 1 || state.loopInBounds)
|
| | | EditorGUIUtility.AddCursorRect(outTangentHitrect, MouseCursor.RotateArrow);
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | Handles.color = Color.white;
|
| | | SaveCurve(curve, animCurve);
|
| | | }
|
| | |
|
| | | void OnGeneralUI(Rect rect)
|
| | | {
|
| | | var e = Event.current;
|
| | |
|
| | | // Selection
|
| | | if (e.type == EventType.mouseDown)
|
| | | {
|
| | | GUI.FocusControl(null);
|
| | | m_SelectedCurve = null;
|
| | | m_SelectedKeyframeIndex = -1;
|
| | | bool used = false;
|
| | |
|
| | | var hit = CanvasToCurve(e.mousePosition);
|
| | | float curvePickValue = CurveToCanvas(hit).y;
|
| | |
|
| | | // Try and select a curve
|
| | | foreach (var curve in m_Curves)
|
| | | {
|
| | | if (!curve.Value.editable || !curve.Value.visible)
|
| | | continue;
|
| | |
|
| | | var prop = curve.Key;
|
| | | var state = curve.Value;
|
| | | var animCurve = prop.animationCurveValue;
|
| | | float hitY = animCurve.length == 0
|
| | | ? state.zeroKeyConstantValue
|
| | | : animCurve.Evaluate(hit.x);
|
| | |
|
| | | var curvePos = CurveToCanvas(new Vector3(hit.x, hitY));
|
| | |
|
| | | if (Mathf.Abs(curvePos.y - curvePickValue) < settings.curvePickingDistance)
|
| | | {
|
| | | m_SelectedCurve = prop;
|
| | |
|
| | | if (e.clickCount == 2 && e.button == 0)
|
| | | {
|
| | | // Create a keyframe on double-click on this curve
|
| | | EditCreateKeyframe(animCurve, hit, true, state.zeroKeyConstantValue);
|
| | | SaveCurve(prop, animCurve);
|
| | | }
|
| | | else if (e.button == 1)
|
| | | {
|
| | | // Curve context menu
|
| | | var menu = new GenericMenu();
|
| | | menu.AddItem(new GUIContent("Add Key"), false, (x) =>
|
| | | {
|
| | | var action = (MenuAction)x;
|
| | | var curveValue = action.curve.animationCurveValue;
|
| | | action.curve.serializedObject.Update();
|
| | | EditCreateKeyframe(curveValue, hit, true, 0f);
|
| | | SaveCurve(action.curve, curveValue);
|
| | | action.curve.serializedObject.ApplyModifiedProperties();
|
| | | }, new MenuAction(prop, hit));
|
| | | menu.ShowAsContext();
|
| | | e.Use();
|
| | | used = true;
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | if (e.clickCount == 2 && e.button == 0 && m_SelectedCurve == null)
|
| | | {
|
| | | // Create a keyframe on every curve on double-click
|
| | | foreach (var curve in m_Curves)
|
| | | {
|
| | | if (!curve.Value.editable || !curve.Value.visible)
|
| | | continue;
|
| | |
|
| | | var prop = curve.Key;
|
| | | var state = curve.Value;
|
| | | var animCurve = prop.animationCurveValue;
|
| | | EditCreateKeyframe(animCurve, hit, e.alt, state.zeroKeyConstantValue);
|
| | | SaveCurve(prop, animCurve);
|
| | | }
|
| | | }
|
| | | else if (!used && e.button == 1)
|
| | | {
|
| | | // Global context menu
|
| | | var menu = new GenericMenu();
|
| | | menu.AddItem(new GUIContent("Add Key At Position"), false, () => ContextMenuAddKey(hit, false));
|
| | | menu.AddItem(new GUIContent("Add Key On Curves"), false, () => ContextMenuAddKey(hit, true));
|
| | | menu.ShowAsContext();
|
| | | }
|
| | |
|
| | | e.Use();
|
| | | }
|
| | |
|
| | | // Delete selected key(s)
|
| | | if (e.type == EventType.keyDown && (e.keyCode == KeyCode.Delete || e.keyCode == KeyCode.Backspace))
|
| | | {
|
| | | if (m_SelectedKeyframeIndex != -1 && m_SelectedCurve != null)
|
| | | {
|
| | | var animCurve = m_SelectedCurve.animationCurveValue;
|
| | | var length = animCurve.length;
|
| | |
|
| | | if (m_Curves[m_SelectedCurve].minPointCount < length && length >= 0)
|
| | | {
|
| | | EditDeleteKeyframe(animCurve, m_SelectedKeyframeIndex);
|
| | | m_SelectedKeyframeIndex = -1;
|
| | | SaveCurve(m_SelectedCurve, animCurve);
|
| | | }
|
| | |
|
| | | e.Use();
|
| | | }
|
| | | }
|
| | | }
|
| | |
|
| | | void SaveCurve(SerializedProperty prop, AnimationCurve curve)
|
| | | {
|
| | | prop.animationCurveValue = curve;
|
| | | }
|
| | |
|
| | | void Invalidate()
|
| | | {
|
| | | m_Dirty = true;
|
| | | }
|
| | |
|
| | | #endregion
|
| | |
|
| | | #region Keyframe manipulations
|
| | |
|
| | | void SelectKeyframe(SerializedProperty curve, int keyframeIndex)
|
| | | {
|
| | | m_SelectedKeyframeIndex = keyframeIndex;
|
| | | m_SelectedCurve = curve;
|
| | | Invalidate();
|
| | | }
|
| | |
|
| | | void ContextMenuAddKey(Vector3 hit, bool createOnCurve)
|
| | | {
|
| | | SerializedObject serializedObject = null;
|
| | |
|
| | | foreach (var curve in m_Curves)
|
| | | {
|
| | | if (!curve.Value.editable || !curve.Value.visible)
|
| | | continue;
|
| | |
|
| | | var prop = curve.Key;
|
| | | var state = curve.Value;
|
| | |
|
| | | if (serializedObject == null)
|
| | | {
|
| | | serializedObject = prop.serializedObject;
|
| | | serializedObject.Update();
|
| | | }
|
| | |
|
| | | var animCurve = prop.animationCurveValue;
|
| | | EditCreateKeyframe(animCurve, hit, createOnCurve, state.zeroKeyConstantValue);
|
| | | SaveCurve(prop, animCurve);
|
| | | }
|
| | |
|
| | | if (serializedObject != null)
|
| | | serializedObject.ApplyModifiedProperties();
|
| | |
|
| | | Invalidate();
|
| | | }
|
| | |
|
| | | void EditCreateKeyframe(AnimationCurve curve, Vector3 position, bool createOnCurve, float zeroKeyConstantValue)
|
| | | {
|
| | | float tangent = EvaluateTangent(curve, position.x);
|
| | |
|
| | | if (createOnCurve)
|
| | | {
|
| | | position.y = curve.length == 0
|
| | | ? zeroKeyConstantValue
|
| | | : curve.Evaluate(position.x);
|
| | | }
|
| | |
|
| | | AddKeyframe(curve, new Keyframe(position.x, position.y, tangent, tangent));
|
| | | }
|
| | |
|
| | | void EditDeleteKeyframe(AnimationCurve curve, int keyframeIndex)
|
| | | {
|
| | | RemoveKeyframe(curve, keyframeIndex);
|
| | | }
|
| | |
|
| | | void AddKeyframe(AnimationCurve curve, Keyframe newValue)
|
| | | {
|
| | | curve.AddKey(newValue);
|
| | | Invalidate();
|
| | | }
|
| | |
|
| | | void RemoveKeyframe(AnimationCurve curve, int keyframeIndex)
|
| | | {
|
| | | curve.RemoveKey(keyframeIndex);
|
| | | Invalidate();
|
| | | }
|
| | |
|
| | | void SetKeyframe(AnimationCurve curve, int keyframeIndex, Keyframe newValue)
|
| | | {
|
| | | var keys = curve.keys;
|
| | |
|
| | | if (keyframeIndex > 0)
|
| | | newValue.time = Mathf.Max(keys[keyframeIndex - 1].time + settings.keyTimeClampingDistance, newValue.time);
|
| | |
|
| | | if (keyframeIndex < keys.Length - 1)
|
| | | newValue.time = Mathf.Min(keys[keyframeIndex + 1].time - settings.keyTimeClampingDistance, newValue.time);
|
| | |
|
| | | curve.MoveKey(keyframeIndex, newValue);
|
| | | Invalidate();
|
| | | }
|
| | |
|
| | | void EditMoveKeyframe(AnimationCurve curve, Keyframe[] keys, int keyframeIndex)
|
| | | {
|
| | | var key = CanvasToCurve(Event.current.mousePosition);
|
| | | float inTgt = keys[keyframeIndex].inTangent;
|
| | | float outTgt = keys[keyframeIndex].outTangent;
|
| | | SetKeyframe(curve, keyframeIndex, new Keyframe(key.x, key.y, inTgt, outTgt));
|
| | | }
|
| | |
|
| | | void EditMoveTangent(AnimationCurve curve, Keyframe[] keys, int keyframeIndex, Tangent targetTangent, bool linkTangents)
|
| | | {
|
| | | var pos = CanvasToCurve(Event.current.mousePosition);
|
| | |
|
| | | float time = keys[keyframeIndex].time;
|
| | | float value = keys[keyframeIndex].value;
|
| | |
|
| | | pos -= new Vector3(time, value);
|
| | |
|
| | | if (targetTangent == Tangent.In && pos.x > 0f)
|
| | | pos.x = 0f;
|
| | |
|
| | | if (targetTangent == Tangent.Out && pos.x < 0f)
|
| | | pos.x = 0f;
|
| | |
|
| | | float tangent;
|
| | |
|
| | | if (Mathf.Approximately(pos.x, 0f))
|
| | | tangent = pos.y < 0f ? float.PositiveInfinity : float.NegativeInfinity;
|
| | | else
|
| | | tangent = pos.y / pos.x;
|
| | |
|
| | | float inTangent = keys[keyframeIndex].inTangent;
|
| | | float outTangent = keys[keyframeIndex].outTangent;
|
| | |
|
| | | if (targetTangent == Tangent.In || linkTangents)
|
| | | inTangent = tangent;
|
| | | if (targetTangent == Tangent.Out || linkTangents)
|
| | | outTangent = tangent;
|
| | |
|
| | | SetKeyframe(curve, keyframeIndex, new Keyframe(time, value, inTangent, outTangent));
|
| | | }
|
| | |
|
| | | #endregion
|
| | |
|
| | | #region Maths utilities
|
| | |
|
| | | Vector3 CurveToCanvas(Keyframe keyframe)
|
| | | {
|
| | | return CurveToCanvas(new Vector3(keyframe.time, keyframe.value));
|
| | | }
|
| | |
|
| | | Vector3 CurveToCanvas(Vector3 position)
|
| | | {
|
| | | var bounds = settings.bounds;
|
| | | var output = new Vector3((position.x - bounds.x) / (bounds.xMax - bounds.x), (position.y - bounds.y) / (bounds.yMax - bounds.y));
|
| | | output.x = output.x * (m_CurveArea.xMax - m_CurveArea.xMin) + m_CurveArea.xMin;
|
| | | output.y = (1f - output.y) * (m_CurveArea.yMax - m_CurveArea.yMin) + m_CurveArea.yMin;
|
| | | return output;
|
| | | }
|
| | |
|
| | | Vector3 CanvasToCurve(Vector3 position)
|
| | | {
|
| | | var bounds = settings.bounds;
|
| | | var output = position;
|
| | | output.x = (output.x - m_CurveArea.xMin) / (m_CurveArea.xMax - m_CurveArea.xMin);
|
| | | output.y = (output.y - m_CurveArea.yMin) / (m_CurveArea.yMax - m_CurveArea.yMin);
|
| | | output.x = Mathf.Lerp(bounds.x, bounds.xMax, output.x);
|
| | | output.y = Mathf.Lerp(bounds.yMax, bounds.y, output.y);
|
| | | return output;
|
| | | }
|
| | |
|
| | | Vector3 CurveTangentToCanvas(float tangent)
|
| | | {
|
| | | if (!float.IsInfinity(tangent))
|
| | | {
|
| | | var bounds = settings.bounds;
|
| | | float ratio = (m_CurveArea.width / m_CurveArea.height) / ((bounds.xMax - bounds.x) / (bounds.yMax - bounds.y));
|
| | | return new Vector3(1f, -tangent / ratio).normalized;
|
| | | }
|
| | |
|
| | | return float.IsPositiveInfinity(tangent) ? Vector3.up : Vector3.down;
|
| | | }
|
| | |
|
| | | Vector3[] BezierSegment(Keyframe start, Keyframe end)
|
| | | {
|
| | | var segment = new Vector3[4];
|
| | |
|
| | | segment[0] = CurveToCanvas(new Vector3(start.time, start.value));
|
| | | segment[3] = CurveToCanvas(new Vector3(end.time, end.value));
|
| | |
|
| | | float middle = start.time + ((end.time - start.time) * 0.333333f);
|
| | | float middle2 = start.time + ((end.time - start.time) * 0.666666f);
|
| | |
|
| | | segment[1] = CurveToCanvas(new Vector3(middle, ProjectTangent(start.time, start.value, start.outTangent, middle)));
|
| | | segment[2] = CurveToCanvas(new Vector3(middle2, ProjectTangent(end.time, end.value, end.inTangent, middle2)));
|
| | |
|
| | | return segment;
|
| | | }
|
| | |
|
| | | Vector3[] HardSegment(Keyframe start, Keyframe end)
|
| | | {
|
| | | var segment = new Vector3[3];
|
| | |
|
| | | segment[0] = CurveToCanvas(start);
|
| | | segment[1] = CurveToCanvas(new Vector3(end.time, start.value));
|
| | | segment[2] = CurveToCanvas(end);
|
| | |
|
| | | return segment;
|
| | | }
|
| | |
|
| | | float ProjectTangent(float inPosition, float inValue, float inTangent, float projPosition)
|
| | | {
|
| | | return inValue + ((projPosition - inPosition) * inTangent);
|
| | | }
|
| | |
|
| | | float EvaluateTangent(AnimationCurve curve, float time)
|
| | | {
|
| | | int prev = -1, next = 0;
|
| | | for (int i = 0; i < curve.keys.Length; i++)
|
| | | {
|
| | | if (time > curve.keys[i].time)
|
| | | {
|
| | | prev = i;
|
| | | next = i + 1;
|
| | | }
|
| | | else break;
|
| | | }
|
| | |
|
| | | if (next == 0)
|
| | | return 0f;
|
| | |
|
| | | if (prev == curve.keys.Length - 1)
|
| | | return 0f;
|
| | |
|
| | | const float kD = 1e-3f;
|
| | | float tp = Mathf.Max(time - kD, curve.keys[prev].time);
|
| | | float tn = Mathf.Min(time + kD, curve.keys[next].time);
|
| | |
|
| | | float vp = curve.Evaluate(tp);
|
| | | float vn = curve.Evaluate(tn);
|
| | |
|
| | | if (Mathf.Approximately(tn, tp))
|
| | | return (vn - vp > 0f) ? float.PositiveInfinity : float.NegativeInfinity;
|
| | |
|
| | | return (vn - vp) / (tn - tp);
|
| | | }
|
| | |
|
| | | #endregion
|
| | | }
|
| | | }
|