You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
847 lines
30 KiB
847 lines
30 KiB
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 |
|
} |
|
}
|
|
|