// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using FlaxEditor.CustomEditors;
using FlaxEditor.GUI.ContextMenu;
using FlaxEngine;
using FlaxEngine.GUI;
// ReSharper disable RedundantAssignment
namespace FlaxEditor.GUI
{
///
/// The base class for editors. Allows to use generic curve editor without type information at compile-time.
///
public abstract class CurveEditorBase : ContainerControl
{
///
/// The UI use mode flags.
///
[Flags]
public enum UseMode
{
///
/// Disable usage.
///
Off = 0,
///
/// Allow only vertical usage.
///
Vertical = 1,
///
/// Allow only horizontal usage.
///
Horizontal = 2,
///
/// Allow both vertical and horizontal usage.
///
On = Vertical | Horizontal,
}
///
/// Occurs when curve gets edited.
///
public event Action Edited;
///
/// Occurs when curve data editing starts (via UI).
///
public event Action EditingStart;
///
/// Occurs when curve data editing ends (via UI).
///
public event Action EditingEnd;
///
/// The maximum amount of keyframes to use in a single curve.
///
public int MaxKeyframes = ushort.MaxValue;
///
/// True if enable view zooming. Otherwise user won't be able to zoom in or out.
///
public UseMode EnableZoom = UseMode.On;
///
/// True if enable view panning. Otherwise user won't be able to move the view area.
///
public UseMode EnablePanning = UseMode.On;
///
/// Gets or sets the scroll bars usage.
///
public abstract ScrollBars ScrollBars { get; set; }
///
/// Enables drawing start/end values continuous lines.
///
public bool ShowStartEndLines;
///
/// Enables drawing background.
///
public bool ShowBackground = true;
///
/// Enables drawing time and values axes (lines and labels).
///
public bool ShowAxes = true;
///
/// Gets the type of the curves keyframes value.
///
public abstract Type ValueType { get; }
///
/// The amount of frames per second of the curve animation (optional). Can be sued to restrict the keyframes time values to the given time quantization rate.
///
public abstract float? FPS { get; set; }
///
/// Gets or sets a value indicating whether show curve collapsed as a list of keyframe points rather than a full curve.
///
public abstract bool ShowCollapsed { get; set; }
///
/// Gets or sets the view offset (via scroll bars).
///
public abstract Vector2 ViewOffset { get; set; }
///
/// Gets or sets the view scale.
///
public abstract Vector2 ViewScale { get; set; }
///
/// Gets the amount of keyframes added to the curve.
///
public abstract int KeyframesCount { get; }
///
/// Called when curve gets edited.
///
public void OnEdited()
{
Edited?.Invoke();
}
///
/// Called when curve data editing starts (via UI).
///
public void OnEditingStart()
{
EditingStart?.Invoke();
}
///
/// Called when curve data editing ends (via UI).
///
public void OnEditingEnd()
{
EditingEnd?.Invoke();
}
///
/// Updates the keyframes positioning.
///
public abstract void UpdateKeyframes();
///
/// Updates the tangents positioning.
///
public abstract void UpdateTangents();
///
/// Evaluates the animation curve value at the specified time.
///
/// The interpolated value from the curve at provided time.
/// The time to evaluate the curve at.
/// If true the curve will loop when it goes past the end or beginning. Otherwise the curve value will be clamped.
public abstract void Evaluate(out object result, float time, bool loop = false);
///
/// Gets the keyframes collection (as boxed objects).
///
/// The array of boxed keyframe values of type or .
public abstract object[] GetKeyframes();
///
/// Sets the keyframes collection (as boxed objects).
///
/// The array of boxed keyframe values of type or .
public abstract void SetKeyframes(object[] keyframes);
///
/// Adds the new keyframe (as boxed object).
///
/// The keyframe time.
/// The keyframe value.
public abstract void AddKeyframe(float time, object value);
///
/// Gets the keyframe data (as boxed objects).
///
/// The keyframe index.
/// The keyframe time.
/// The keyframe value (boxed).
/// The keyframe 'In' tangent value (boxed).
/// The keyframe 'Out' tangent value (boxed).
public abstract void GetKeyframe(int index, out float time, out object value, out object tangentIn, out object tangentOut);
///
/// Gets the existing keyframe value (as boxed object).
///
/// The keyframe index.
/// The keyframe value.
public abstract object GetKeyframe(int index);
///
/// Sets the existing keyframe value (as boxed object).
///
/// The keyframe index.
/// The keyframe value.
public abstract void SetKeyframeValue(int index, object value);
///
/// Converts the into the mask.
///
/// The mode.
/// The mask.
protected static Vector2 GetUseModeMask(UseMode mode)
{
return new Vector2((mode & UseMode.Horizontal) == UseMode.Horizontal ? 1.0f : 0.0f, (mode & UseMode.Vertical) == UseMode.Vertical ? 1.0f : 0.0f);
}
///
/// Filters teh given value using the the .
///
/// The mode.
/// The value to process.
/// The default value.
/// The combined value.
protected static Vector2 ApplyUseModeMask(UseMode mode, Vector2 value, Vector2 defaultValue)
{
return new Vector2(
(mode & UseMode.Horizontal) == UseMode.Horizontal ? value.X : defaultValue.X,
(mode & UseMode.Vertical) == UseMode.Vertical ? value.Y : defaultValue.Y
);
}
}
///
/// The generic curve editor control.
///
/// The keyframe value type.
///
public abstract class CurveEditor : CurveEditorBase where T : new()
{
///
/// The generic keyframe value accessor object for curve editor.
///
/// The keyframe value type.
public interface IKeyframeAccess where U : new()
{
///
/// Gets the default value.
///
/// The value.
void GetDefaultValue(out U value);
///
/// Gets the curve components count. Vector types should return amount of component to use for value editing.
///
/// The components count.
int GetCurveComponents();
///
/// Gets the value of the component for the curve.
///
/// The keyframe value.
/// The component index.
/// The curve value.
float GetCurveValue(ref U value, int component);
///
/// Sets the curve value of the component.
///
/// The curve value to assign.
/// The keyframe value.
/// The component index.
void SetCurveValue(float curve, ref U value, int component);
}
private class KeyframeAccess :
IKeyframeAccess,
IKeyframeAccess,
IKeyframeAccess,
IKeyframeAccess,
IKeyframeAccess,
IKeyframeAccess,
IKeyframeAccess,
IKeyframeAccess,
IKeyframeAccess,
IKeyframeAccess
{
void IKeyframeAccess.GetDefaultValue(out bool value)
{
value = false;
}
int IKeyframeAccess.GetCurveComponents()
{
return 1;
}
float IKeyframeAccess.GetCurveValue(ref bool value, int component)
{
return value ? 1 : 0;
}
void IKeyframeAccess.SetCurveValue(float curve, ref bool value, int component)
{
value = curve >= 0.5f;
}
void IKeyframeAccess.GetDefaultValue(out int value)
{
value = 0;
}
int IKeyframeAccess.GetCurveComponents()
{
return 1;
}
float IKeyframeAccess.GetCurveValue(ref int value, int component)
{
return value;
}
void IKeyframeAccess.SetCurveValue(float curve, ref int value, int component)
{
value = (int)curve;
}
void IKeyframeAccess.GetDefaultValue(out double value)
{
value = 0.0;
}
int IKeyframeAccess.GetCurveComponents()
{
return 1;
}
float IKeyframeAccess.GetCurveValue(ref double value, int component)
{
return (float)value;
}
void IKeyframeAccess.SetCurveValue(float curve, ref double value, int component)
{
value = curve;
}
void IKeyframeAccess.GetDefaultValue(out float value)
{
value = 0.0f;
}
int IKeyframeAccess.GetCurveComponents()
{
return 1;
}
float IKeyframeAccess.GetCurveValue(ref float value, int component)
{
return value;
}
void IKeyframeAccess.SetCurveValue(float curve, ref float value, int component)
{
value = curve;
}
void IKeyframeAccess.GetDefaultValue(out Vector2 value)
{
value = Vector2.Zero;
}
int IKeyframeAccess.GetCurveComponents()
{
return 2;
}
float IKeyframeAccess.GetCurveValue(ref Vector2 value, int component)
{
return value[component];
}
void IKeyframeAccess.SetCurveValue(float curve, ref Vector2 value, int component)
{
value[component] = curve;
}
void IKeyframeAccess.GetDefaultValue(out Vector3 value)
{
value = Vector3.Zero;
}
int IKeyframeAccess.GetCurveComponents()
{
return 3;
}
float IKeyframeAccess.GetCurveValue(ref Vector3 value, int component)
{
return value[component];
}
void IKeyframeAccess.SetCurveValue(float curve, ref Vector3 value, int component)
{
value[component] = curve;
}
void IKeyframeAccess.GetDefaultValue(out Vector4 value)
{
value = Vector4.Zero;
}
int IKeyframeAccess.GetCurveComponents()
{
return 4;
}
float IKeyframeAccess.GetCurveValue(ref Vector4 value, int component)
{
return value[component];
}
void IKeyframeAccess.SetCurveValue(float curve, ref Vector4 value, int component)
{
value[component] = curve;
}
public void GetDefaultValue(out Quaternion value)
{
value = Quaternion.Identity;
}
int IKeyframeAccess.GetCurveComponents()
{
return 3;
}
float IKeyframeAccess.GetCurveValue(ref Quaternion value, int component)
{
return value.EulerAngles[component];
}
void IKeyframeAccess.SetCurveValue(float curve, ref Quaternion value, int component)
{
var euler = value.EulerAngles;
euler[component] = curve;
Quaternion.Euler(euler.X, euler.Y, euler.Z, out value);
}
void IKeyframeAccess.GetDefaultValue(out Color value)
{
value = Color.Black;
}
int IKeyframeAccess.GetCurveComponents()
{
return 4;
}
float IKeyframeAccess.GetCurveValue(ref Color value, int component)
{
return value[component];
}
void IKeyframeAccess.SetCurveValue(float curve, ref Color value, int component)
{
value[component] = curve;
}
void IKeyframeAccess.GetDefaultValue(out Color32 value)
{
value = Color32.Black;
}
int IKeyframeAccess.GetCurveComponents()
{
return 4;
}
float IKeyframeAccess.GetCurveValue(ref Color32 value, int component)
{
return value[component];
}
void IKeyframeAccess.SetCurveValue(float curve, ref Color32 value, int component)
{
value[component] = (byte)Mathf.Clamp(curve, 0, 255);
}
}
private class Popup : ContextMenuBase
{
private CustomEditorPresenter _presenter;
private CurveEditor _editor;
private List _keyframeIndices;
private bool _isDirty;
public Popup(CurveEditor editor, object[] selection, List keyframeIndices = null, float height = 140.0f)
: this(editor, height)
{
_presenter.Select(selection);
_presenter.OpenAllGroups();
_keyframeIndices = keyframeIndices;
if (keyframeIndices != null && selection.Length != keyframeIndices.Count)
throw new Exception();
}
private Popup(CurveEditor editor, float height)
{
_editor = editor;
const float width = 340.0f;
Size = new Vector2(width, height);
var panel1 = new Panel(ScrollBars.Vertical)
{
Bounds = new Rectangle(0, 0.0f, width, height),
Parent = this
};
_presenter = new CustomEditorPresenter(null);
_presenter.Panel.AnchorPreset = AnchorPresets.HorizontalStretchTop;
_presenter.Panel.IsScrollable = true;
_presenter.Panel.Parent = panel1;
_presenter.Modified += OnModified;
}
private void OnModified()
{
if (!_isDirty)
{
_editor.OnEditingStart();
}
_isDirty = true;
if (_keyframeIndices != null)
{
for (int i = 0; i < _presenter.SelectionCount; i++)
{
_editor.SetKeyframeInternal(_keyframeIndices[i], _presenter.Selection[i]);
}
}
else if (_presenter.Selection[0] is IAllKeyframesProxy proxy)
{
proxy.Apply();
}
_editor.UpdateFPS();
_editor.UpdateKeyframes();
_editor.UpdateTooltips();
}
///
protected override void OnShow()
{
Focus();
base.OnShow();
}
///
public override void Hide()
{
if (!Visible)
return;
Focus(null);
if (_isDirty)
{
_editor.OnEdited();
_editor.OnEditingEnd();
}
if (_editor._popup == this)
_editor._popup = null;
_presenter = null;
_editor = null;
_keyframeIndices = null;
base.Hide();
}
///
public override bool OnKeyDown(KeyboardKeys key)
{
if (base.OnKeyDown(key))
return true;
if (key == KeyboardKeys.Escape)
{
Hide();
return true;
}
return false;
}
}
///
/// The curve contents container control.
///
///
protected class ContentsBase : ContainerControl
{
private readonly CurveEditor _editor;
internal bool _leftMouseDown;
private bool _rightMouseDown;
internal Vector2 _leftMouseDownPos = Vector2.Minimum;
private Vector2 _rightMouseDownPos = Vector2.Minimum;
internal Vector2 _mousePos = Vector2.Minimum;
private float _mouseMoveAmount;
internal bool _isMovingSelection;
internal bool _isMovingTangent;
private TangentPoint _movingTangent;
private Vector2 _movingSelectionViewPos;
private Vector2 _cmShowPos;
///
/// Initializes a new instance of the class.
///
/// The curve editor.
public ContentsBase(CurveEditor editor)
{
_editor = editor;
}
private void UpdateSelectionRectangle()
{
var selectionRect = Rectangle.FromPoints(_leftMouseDownPos, _mousePos);
// Find controls to select
for (int i = 0; i < Children.Count; i++)
{
if (Children[i] is KeyframePoint p)
{
p.IsSelected = p.Bounds.Intersects(ref selectionRect);
}
}
_editor.UpdateTangents();
}
///
public override bool IntersectsContent(ref Vector2 locationParent, out Vector2 location)
{
// Pass all events
location = PointFromParent(ref locationParent);
return true;
}
///
public override void OnMouseEnter(Vector2 location)
{
_mousePos = location;
base.OnMouseEnter(location);
}
///
public override void OnMouseMove(Vector2 location)
{
_mousePos = location;
// Moving view
if (_rightMouseDown)
{
// Calculate delta
Vector2 delta = location - _rightMouseDownPos;
delta *= GetUseModeMask(_editor.EnablePanning);
if (delta.LengthSquared > 0.01f)
{
// Move view
_mouseMoveAmount += delta.Length;
_editor.ViewOffset += delta * _editor.ViewScale;
_rightMouseDownPos = location;
Cursor = CursorType.SizeAll;
}
return;
}
// Moving selection
else if (_isMovingSelection)
{
// Calculate delta (apply view offset)
Vector2 viewDelta = _editor.ViewOffset - _movingSelectionViewPos;
_movingSelectionViewPos = _editor.ViewOffset;
var viewRect = _editor._mainPanel.GetClientArea();
var delta = location - _leftMouseDownPos - viewDelta;
_mouseMoveAmount += delta.Length;
if (delta.LengthSquared > 0.01f)
{
// Move selected keyframes
var keyframeDelta = PointToKeyframes(location, ref viewRect) - PointToKeyframes(_leftMouseDownPos - viewDelta, ref viewRect);
var accessor = _editor.Accessor;
var components = accessor.GetCurveComponents();
for (var i = 0; i < _editor._points.Count; i++)
{
var p = _editor._points[i];
if (p.IsSelected)
{
var k = _editor.GetKeyframe(p.Index);
float time = _editor.GetKeyframeTime(k);
float value = _editor.GetKeyframeValue(k, p.Component);
float minTime = p.Index != 0 ? _editor.GetKeyframeTime(_editor.GetKeyframe(p.Index - 1)) + Mathf.Epsilon : float.MinValue;
float maxTime = p.Index != _editor.KeyframesCount - 1 ? _editor.GetKeyframeTime(_editor.GetKeyframe(p.Index + 1)) - Mathf.Epsilon : float.MaxValue;
if (!_editor.ShowCollapsed)
{
// Move on value axis
value += keyframeDelta.Y;
}
// Let the first selected point of this keyframe to edit time
bool isFirstSelected = false;
for (var j = 0; j < components; j++)
{
var idx = p.Index * components + j;
if (idx == i)
{
isFirstSelected = true;
break;
}
if (_editor._points[idx].IsSelected)
break;
}
if (isFirstSelected)
{
time += keyframeDelta.X;
if (_editor.FPS.HasValue)
{
float fps = _editor.FPS.Value;
time = Mathf.Floor(time * fps) / fps;
}
time = Mathf.Clamp(time, minTime, maxTime);
}
// TODO: snapping keyframes to grid when moving
_editor.SetKeyframeInternal(p.Index, time, value, p.Component);
}
}
_editor.UpdateKeyframes();
_editor.UpdateTooltips();
if (_editor.EnablePanning == UseMode.On)
{
_editor._mainPanel.ScrollViewTo(PointToParent(location));
}
_leftMouseDownPos = location;
Cursor = CursorType.SizeAll;
}
return;
}
// Moving tangent
else if (_isMovingTangent)
{
// Calculate delta (apply view offset)
Vector2 viewDelta = _editor.ViewOffset - _movingSelectionViewPos;
_movingSelectionViewPos = _editor.ViewOffset;
var viewRect = _editor._mainPanel.GetClientArea();
var delta = location - _leftMouseDownPos - viewDelta;
_mouseMoveAmount += delta.Length;
if (delta.LengthSquared > 0.01f)
{
// Move selected tangent
var keyframeDelta = PointToKeyframes(location, ref viewRect) - PointToKeyframes(_leftMouseDownPos - viewDelta, ref viewRect);
var direction = _movingTangent.IsIn ? -1.0f : 1.0f;
_movingTangent.TangentValue += direction * keyframeDelta.Y;
_editor.UpdateTangents();
_leftMouseDownPos = location;
Cursor = CursorType.SizeNS;
}
return;
}
// Selecting
else if (_leftMouseDown)
{
UpdateSelectionRectangle();
return;
}
base.OnMouseMove(location);
}
///
public override void OnLostFocus()
{
// Clear flags and state
if (_leftMouseDown)
{
_leftMouseDown = false;
}
if (_rightMouseDown)
{
_rightMouseDown = false;
Cursor = CursorType.Default;
}
_isMovingSelection = false;
_isMovingTangent = false;
base.OnLostFocus();
}
///
public override bool OnMouseDown(Vector2 location, MouseButton button)
{
if (base.OnMouseDown(location, button))
{
// Clear flags
_isMovingSelection = false;
_isMovingTangent = false;
_rightMouseDown = false;
_leftMouseDown = false;
return true;
}
// Cache data
_isMovingSelection = false;
_isMovingTangent = false;
_mousePos = location;
if (button == MouseButton.Left)
{
_leftMouseDown = true;
_leftMouseDownPos = location;
}
if (button == MouseButton.Right)
{
_rightMouseDown = true;
_rightMouseDownPos = location;
}
// Check if any node is under the mouse
var underMouse = GetChildAt(location);
if (underMouse is KeyframePoint keyframe)
{
if (_leftMouseDown)
{
// Check if user is pressing control
if (Root.GetKey(KeyboardKeys.Control))
{
// Add to selection
keyframe.Select();
_editor.UpdateTangents();
}
// Check if node isn't selected
else if (!keyframe.IsSelected)
{
// Select node
_editor.ClearSelection();
keyframe.Select();
_editor.UpdateTangents();
}
// Start moving selected nodes
StartMouseCapture();
_mouseMoveAmount = 0;
_isMovingSelection = true;
_movingSelectionViewPos = _editor.ViewOffset;
_editor.OnEditingStart();
Focus();
return true;
}
}
else if (underMouse is TangentPoint tangent && tangent.Visible)
{
if (_leftMouseDown)
{
// Start moving tangent
StartMouseCapture();
_mouseMoveAmount = 0;
_isMovingTangent = true;
_movingTangent = tangent;
_movingSelectionViewPos = _editor.ViewOffset;
_editor.OnEditingStart();
Focus();
return true;
}
}
else
{
if (_leftMouseDown)
{
// Start selecting
StartMouseCapture();
_editor.ClearSelection();
_editor.UpdateTangents();
Focus();
return true;
}
if (_rightMouseDown)
{
// Start navigating
StartMouseCapture();
Focus();
return true;
}
}
Focus();
return true;
}
///
public override bool OnMouseUp(Vector2 location, MouseButton button)
{
_mousePos = location;
if (_leftMouseDown && button == MouseButton.Left)
{
_leftMouseDown = false;
EndMouseCapture();
Cursor = CursorType.Default;
// Editing tangent
if (_isMovingTangent)
{
if (_mouseMoveAmount > 3.0f)
{
_editor.OnEdited();
_editor.OnEditingEnd();
}
}
// Moving keyframes
else if (_isMovingSelection)
{
if (_mouseMoveAmount > 3.0f)
{
_editor.OnEdited();
_editor.OnEditingEnd();
}
}
// Selecting
else
{
UpdateSelectionRectangle();
}
_isMovingSelection = false;
_isMovingTangent = false;
}
if (_rightMouseDown && button == MouseButton.Right)
{
_rightMouseDown = false;
EndMouseCapture();
Cursor = CursorType.Default;
// Check if no move has been made at all
if (_mouseMoveAmount < 3.0f)
{
var selectionCount = _editor.SelectionCount;
var underMouse = GetChildAt(location);
if (selectionCount == 0 && underMouse is KeyframePoint point)
{
// Select node
selectionCount = 1;
point.Select();
_editor.UpdateTangents();
}
var viewRect = _editor._mainPanel.GetClientArea();
_cmShowPos = PointToKeyframes(location, ref viewRect);
var cm = new ContextMenu.ContextMenu();
cm.AddButton("Add keyframe", () => _editor.AddKeyframe(_cmShowPos)).Enabled = _editor.KeyframesCount < _editor.MaxKeyframes;
if (selectionCount == 0)
{
}
else if (selectionCount == 1)
{
cm.AddButton("Edit keyframe", () => _editor.EditKeyframes(this, location));
cm.AddButton("Remove keyframe", _editor.RemoveKeyframes);
}
else
{
cm.AddButton("Edit keyframes", () => _editor.EditKeyframes(this, location));
cm.AddButton("Remove keyframes", _editor.RemoveKeyframes);
}
cm.AddButton("Edit all keyframes", () => _editor.EditAllKeyframes(this, location));
if (_editor.EnableZoom != UseMode.Off || _editor.EnablePanning != UseMode.Off)
{
cm.AddSeparator();
cm.AddButton("Show whole curve", _editor.ShowWholeCurve);
cm.AddButton("Reset view", _editor.ResetView);
}
_editor.OnShowContextMenu(cm, selectionCount);
cm.Show(this, location);
}
_mouseMoveAmount = 0;
}
if (base.OnMouseUp(location, button))
{
// Clear flags
_rightMouseDown = false;
_leftMouseDown = false;
return true;
}
return true;
}
///
public override bool OnMouseWheel(Vector2 location, float delta)
{
if (base.OnMouseWheel(location, delta))
return true;
// Zoom in/out
if (_editor.EnableZoom != UseMode.Off && IsMouseOver && !_leftMouseDown && RootWindow.GetKey(KeyboardKeys.Control))
{
// TODO: preserve the view center point for easier zooming
_editor.ViewScale += GetUseModeMask(_editor.EnableZoom) * (delta * 0.1f);
return true;
}
return false;
}
///
protected override void SetScaleInternal(ref Vector2 scale)
{
base.SetScaleInternal(ref scale);
_editor.UpdateKeyframes();
}
///
/// Converts the input point from curve editor contents control space into the keyframes time/value coordinates.
///
/// The point.
/// The curve contents area bounds.
/// The result.
private Vector2 PointToKeyframes(Vector2 point, ref Rectangle curveContentAreaBounds)
{
// Contents -> Keyframes
return new Vector2(
(point.X + Location.X) / UnitsPerSecond,
(point.Y + Location.Y - curveContentAreaBounds.Height) / -UnitsPerSecond
);
}
}
///
/// The single keyframe control.
///
protected class KeyframePoint : Control
{
///
/// The parent curve editor.
///
public CurveEditor Editor;
///
/// The keyframe index.
///
public int Index;
///
/// The component index.
///
public int Component;
///
/// Flag for selected keyframes.
///
public bool IsSelected;
///
public override void Draw()
{
var rect = new Rectangle(Vector2.Zero, Size);
var color = Editor.ShowCollapsed ? Color.Gray : Editor.Colors[Component];
if (IsSelected)
color = Editor.ContainsFocus ? Color.YellowGreen : Color.Lerp(Color.Gray, Color.YellowGreen, 0.4f);
if (IsMouseOver)
color *= 1.1f;
Render2D.FillRectangle(rect, color);
}
///
public override bool OnMouseDoubleClick(Vector2 location, MouseButton button)
{
if (base.OnMouseDoubleClick(location, button))
return true;
if (button == MouseButton.Left)
{
Editor.EditKeyframes(this, location, new List { Index });
return true;
}
return false;
}
///
/// Adds this keyframe to the selection.
///
public void Select()
{
IsSelected = true;
}
///
/// Removes this keyframe from the selection.
///
public void Deselect()
{
IsSelected = false;
}
///
/// Updates the tooltip.
///
public void UpdateTooltip()
{
var k = Editor.GetKeyframe(Index);
var time = Editor.GetKeyframeTime(k);
var value = Editor.GetKeyframeValue(k);
if (Editor.ShowCollapsed)
TooltipText = string.Format("Time: {0}, Value: {1}", time, value);
else
TooltipText = string.Format("Time: {0}, Value: {1}", time, Editor.Accessor.GetCurveValue(ref value, Component));
}
}
///
/// The single keyframe tangent control.
///
protected class TangentPoint : Control
{
///
/// The parent curve editor.
///
public CurveEditor Editor;
///
/// The keyframe index.
///
public int Index;
///
/// The component index.
///
public int Component;
///
/// True if tangent is `In`, otherwise it's `Out`.
///
public bool IsIn;
///
/// The keyframe.
///
public KeyframePoint Point;
///
/// Gets the tangent value on curve.
///
public float TangentValue
{
get => Editor.GetKeyframeTangentInternal(Index, IsIn, Component);
set => Editor.SetKeyframeTangentInternal(Index, IsIn, Component, value);
}
///
public override void Draw()
{
var pointPos = PointFromParent(Point.Center);
Render2D.DrawLine(Size * 0.5f, pointPos, Color.Gray);
var rect = new Rectangle(Vector2.Zero, Size);
var color = Color.MediumVioletRed;
if (IsMouseOver)
color *= 1.1f;
Render2D.FillRectangle(rect, color);
}
///
/// Updates the tooltip.
///
public void UpdateTooltip()
{
TooltipText = string.Format("Tangent {0}: {1}", IsIn ? "in" : "out", TangentValue);
}
}
///
/// The timeline intervals metric area size (in pixels).
///
protected static readonly float LabelsSize = 10.0f;
///
/// The timeline units per second (on time axis).
///
public static readonly float UnitsPerSecond = 100.0f;
///
/// The keyframes size.
///
protected static readonly Vector2 KeyframesSize = new Vector2(5.0f);
///
/// The colors for the keyframe points.
///
protected Color[] Colors = Utilities.Utils.CurveKeyframesColors;
///
/// The curve time/value axes tick steps.
///
protected float[] TickSteps = Utilities.Utils.CurveTickSteps;
///
/// The curve contents area.
///
protected ContentsBase _contents;
///
/// The main UI panel with scroll bars.
///
protected Panel _mainPanel;
///
/// True if refresh keyframes positioning before drawing.
///
protected bool _refreshAfterEdit;
///
/// True if curve is collapsed.
///
protected bool _showCollapsed;
private float[] _tickStrengths;
private Popup _popup;
private float? _fps;
private Color _contentsColor;
private Color _linesColor;
private Color _labelsColor;
private Font _labelsFont;
///
/// The keyframe UI points.
///
protected readonly List _points = new List();
///
/// The tangents UI points.
///
protected readonly TangentPoint[] _tangents = new TangentPoint[2];
///
public override Vector2 ViewOffset
{
get => _mainPanel.ViewOffset;
set => _mainPanel.ViewOffset = value;
}
///
public override Vector2 ViewScale
{
get => _contents.Scale;
set => _contents.Scale = Vector2.Clamp(value, new Vector2(0.0001f), new Vector2(1000.0f));
}
///
/// The keyframes data accessor.
///
public readonly IKeyframeAccess Accessor = new KeyframeAccess() as IKeyframeAccess;
///
/// Gets a value indicating whether user is editing the curve.
///
public bool IsUserEditing => _popup != null || _contents._leftMouseDown;
///
/// The default value.
///
public T DefaultValue;
///
public override ScrollBars ScrollBars
{
get => _mainPanel.ScrollBars;
set => _mainPanel.ScrollBars = value;
}
///
public override Type ValueType => typeof(T);
///
public override float? FPS
{
get => _fps;
set
{
if (_fps.HasValue == value.HasValue && (!value.HasValue || Mathf.NearEqual(_fps.Value, value.Value)))
return;
_fps = value;
UpdateFPS();
}
}
///
public override bool ShowCollapsed
{
get => _showCollapsed;
set
{
if (_showCollapsed == value)
return;
_showCollapsed = value;
UpdateKeyframes();
UpdateTangents();
if (value)
ShowWholeCurve();
}
}
///
/// Occurs when keyframes collection gets changed (keyframe added or removed).
///
public event Action KeyframesChanged;
///
/// Initializes a new instance of the class.
///
protected CurveEditor()
{
_tickStrengths = new float[TickSteps.Length];
Accessor.GetDefaultValue(out DefaultValue);
var style = Style.Current;
_contentsColor = style.Background.RGBMultiplied(0.7f);
_linesColor = style.ForegroundDisabled.RGBMultiplied(0.7f);
_labelsColor = style.ForegroundDisabled;
_labelsFont = style.FontSmall;
_mainPanel = new Panel(ScrollBars.Both)
{
ScrollMargin = new Margin(150.0f),
AlwaysShowScrollbars = true,
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = this
};
_contents = new ContentsBase(this)
{
ClipChildren = false,
CullChildren = false,
AutoFocus = false,
Parent = _mainPanel,
Bounds = Rectangle.Empty,
};
}
///
/// Updates the keyframes to match the FPS.
///
protected abstract void UpdateFPS();
///
/// Updates the keyframes tooltips.
///
protected void UpdateTooltips()
{
for (var i = 0; i < _points.Count; i++)
_points[i].UpdateTooltip();
}
///
/// Called when keyframes collection gets changed (keyframe added or removed).
///
protected virtual void OnKeyframesChanged()
{
KeyframesChanged?.Invoke();
}
///
/// Evaluates the animation curve value at the specified time.
///
/// The interpolated value from the curve at provided time.
/// The time to evaluate the curve at.
/// If true the curve will loop when it goes past the end or beginning. Otherwise the curve value will be clamped.
public abstract void Evaluate(out T result, float time, bool loop = false);
///
/// Gets the time of the keyframe
///
/// The keyframe object.
/// The keyframe time.
protected abstract float GetKeyframeTime(object keyframe);
///
/// Gets the value of the keyframe.
///
/// The keyframe object.
/// The keyframe value.
protected abstract T GetKeyframeValue(object keyframe);
///
/// Gets the value of the keyframe (single component).
///
/// The keyframe object.
/// The keyframe value component index.
/// The keyframe component value.
protected abstract float GetKeyframeValue(object keyframe, int component);
///
/// Adds a new keyframe at the given location (in keyframes space).
///
/// The new keyframe position (in keyframes space).
protected abstract void AddKeyframe(Vector2 keyframesPos);
///
/// Sets the keyframe data (internally).
///
/// The keyframe index.
/// The keyframe to set.
protected abstract void SetKeyframeInternal(int index, object keyframe);
///
/// Sets the keyframe data (internally).
///
/// The keyframe index.
/// The time to set.
/// The value to set.
/// The value component.
protected abstract void SetKeyframeInternal(int index, float time, float value, int component);
///
/// Gets the keyframe tangent value (internally).
///
/// The keyframe index.
/// True if tangent is `In`, otherwise it's `Out`.
/// The value component index.
/// The tangent component value.
protected abstract float GetKeyframeTangentInternal(int index, bool isIn, int component);
///
/// Sets the keyframe tangent value (internally).
///
/// The keyframe index.
/// True if tangent is `In`, otherwise it's `Out`.
/// The value component index.
/// The tangent component value.
protected abstract void SetKeyframeTangentInternal(int index, bool isIn, int component, float value);
///
/// Removes the keyframes data (internally).
///
/// The list of indices of the keyframes to remove.
protected abstract void RemoveKeyframesInternal(HashSet indicesToRemove);
///
/// Called when showing a context menu. Can be used to add custom buttons with actions.
///
/// The menu.
/// The amount of selected keyframes.
protected virtual void OnShowContextMenu(ContextMenu.ContextMenu cm, int selectionCount)
{
}
///
/// Gets proxy object for all keyframes list.
///
/// The proxy object.
protected abstract IAllKeyframesProxy GetAllKeyframesEditingProxy();
///
/// Interface for keyframes editing proxy objects.
///
protected interface IAllKeyframesProxy
{
///
/// Applies the proxy data to the editor.
///
void Apply();
}
private void EditAllKeyframes(Control control, Vector2 pos)
{
_popup = new Popup(this, new object[] { GetAllKeyframesEditingProxy() }, null, 400.0f);
_popup.Show(control, pos);
}
private void EditKeyframes(Control control, Vector2 pos)
{
var keyframeIndices = new List();
for (int i = 0; i < _points.Count; i++)
{
var p = _points[i];
if (!p.IsSelected || keyframeIndices.Contains(p.Index))
continue;
keyframeIndices.Add(p.Index);
}
EditKeyframes(control, pos, keyframeIndices);
}
private void EditKeyframes(Control control, Vector2 pos, List keyframeIndices)
{
var selection = new object[keyframeIndices.Count];
var keyframes = GetKeyframes();
for (int i = 0; i < keyframeIndices.Count; i++)
selection[i] = keyframes[keyframeIndices[i]];
_popup = new Popup(this, selection, keyframeIndices);
_popup.Show(control, pos);
}
private void RemoveKeyframes()
{
var indicesToRemove = new HashSet();
for (int i = 0; i < _points.Count; i++)
{
var p = _points[i];
if (p.IsSelected)
{
p.Deselect();
indicesToRemove.Add(p.Index);
}
}
if (indicesToRemove.Count == 0)
return;
OnEditingStart();
RemoveKeyframesInternal(indicesToRemove);
OnKeyframesChanged();
OnEdited();
OnEditingEnd();
}
///
/// Shows the whole curve.
///
public void ShowWholeCurve()
{
ViewScale = ApplyUseModeMask(EnableZoom, _mainPanel.Size / _contents.Size, ViewScale);
ViewOffset = ApplyUseModeMask(EnablePanning, -_mainPanel.ControlsBounds.Location, ViewOffset);
UpdateKeyframes();
}
///
/// Resets the view.
///
public void ResetView()
{
ViewScale = ApplyUseModeMask(EnableZoom, Vector2.One, ViewScale);
ViewOffset = ApplyUseModeMask(EnablePanning, Vector2.Zero, ViewOffset);
UpdateKeyframes();
}
///
public override void Evaluate(out object result, float time, bool loop = false)
{
Evaluate(out var value, time, loop);
result = value;
}
private int SelectionCount
{
get
{
int result = 0;
for (int i = 0; i < _points.Count; i++)
if (_points[i].IsSelected)
result++;
return result;
}
}
private void ClearSelection()
{
for (int i = 0; i < _points.Count; i++)
{
_points[i].Deselect();
}
}
private void SelectAll()
{
for (int i = 0; i < _points.Count; i++)
{
_points[i].Select();
}
}
///
/// Converts the input point from curve editor control space into the keyframes time/value coordinates.
///
/// The point.
/// The curve contents area bounds.
/// The result.
protected Vector2 PointToKeyframes(Vector2 point, ref Rectangle curveContentAreaBounds)
{
// Curve Editor -> Main Panel
point = _mainPanel.PointFromParent(point);
// Main Panel -> Contents
point = _contents.PointFromParent(point);
// Contents -> Keyframes
return new Vector2(
(point.X + _contents.Location.X) / UnitsPerSecond,
(point.Y + _contents.Location.Y - curveContentAreaBounds.Height) / -UnitsPerSecond
);
}
///
/// Converts the input point from the keyframes time/value coordinates into the curve editor control space.
///
/// The point.
/// The curve contents area bounds.
/// The result.
protected Vector2 PointFromKeyframes(Vector2 point, ref Rectangle curveContentAreaBounds)
{
// Keyframes -> Contents
point = new Vector2(
point.X * UnitsPerSecond - _contents.Location.X,
point.Y * -UnitsPerSecond + curveContentAreaBounds.Height - _contents.Location.Y
);
// Contents -> Main Panel
point = _contents.PointToParent(point);
// Main Panel -> Curve Editor
return _mainPanel.PointToParent(point);
}
private void DrawAxis(Vector2 axis, ref Rectangle viewRect, float min, float max, float pixelRange)
{
int minDistanceBetweenTicks = 20;
int maxDistanceBetweenTicks = 60;
var range = max - min;
// Find the strength for each modulo number tick marker
int smallestTick = 0;
int biggestTick = TickSteps.Length - 1;
for (int i = TickSteps.Length - 1; i >= 0; i--)
{
// Calculate how far apart these modulo tick steps are spaced
float tickSpacing = TickSteps[i] * pixelRange / range;
// Calculate the strength of the tick markers based on the spacing
_tickStrengths[i] = Mathf.Saturate((tickSpacing - minDistanceBetweenTicks) / (maxDistanceBetweenTicks - minDistanceBetweenTicks));
// Beyond threshold the ticks don't get any bigger or fatter
if (_tickStrengths[i] >= 1)
biggestTick = i;
// Do not show small tick markers
if (tickSpacing <= minDistanceBetweenTicks)
{
smallestTick = i;
break;
}
}
// Draw all tick levels
int tickLevels = biggestTick - smallestTick + 1;
for (int level = 0; level < tickLevels; level++)
{
float strength = _tickStrengths[smallestTick + level];
if (strength <= Mathf.Epsilon)
continue;
// Draw all ticks
int l = Mathf.Clamp(smallestTick + level, 0, TickSteps.Length - 1);
int startTick = Mathf.FloorToInt(min / TickSteps[l]);
int endTick = Mathf.CeilToInt(max / TickSteps[l]);
for (int i = startTick; i <= endTick; i++)
{
if (l < biggestTick && (i % Mathf.RoundToInt(TickSteps[l + 1] / TickSteps[l]) == 0))
continue;
var tick = i * TickSteps[l];
var p = PointFromKeyframes(axis * tick, ref viewRect);
// Draw line
var lineRect = new Rectangle
(
viewRect.Location + (p - 0.5f) * axis,
Vector2.Lerp(viewRect.Size, Vector2.One, axis)
);
Render2D.FillRectangle(lineRect, _linesColor.AlphaMultiplied(strength));
// Draw label
string label = tick.ToString(CultureInfo.InvariantCulture);
var labelRect = new Rectangle
(
viewRect.X + 4.0f + (p.X * axis.X),
viewRect.Y - LabelsSize + (p.Y * axis.Y) + (viewRect.Size.Y * axis.X),
50,
LabelsSize
);
Render2D.DrawText(_labelsFont, label, labelRect, _labelsColor.AlphaMultiplied(strength), TextAlignment.Near, TextAlignment.Center, TextWrapping.NoWrap, 1.0f, 0.7f);
}
}
}
///
/// Draws the curve.
///
/// The main panel client area used as a view bounds.
protected abstract void DrawCurve(ref Rectangle viewRect);
///
public override void Draw()
{
// Hack to refresh UI after keyframes edit
if (_refreshAfterEdit)
{
_refreshAfterEdit = false;
UpdateKeyframes();
}
var style = Style.Current;
var rect = new Rectangle(Vector2.Zero, Size);
var viewRect = _mainPanel.GetClientArea();
// Draw background
if (ShowBackground)
{
Render2D.FillRectangle(rect, _contentsColor);
}
// Draw time and values axes
if (ShowAxes)
{
var upperLeft = PointToKeyframes(viewRect.Location, ref viewRect);
var bottomRight = PointToKeyframes(viewRect.Size, ref viewRect);
var min = Vector2.Min(upperLeft, bottomRight);
var max = Vector2.Max(upperLeft, bottomRight);
var pixelRange = (max - min) * ViewScale * UnitsPerSecond;
Render2D.PushClip(ref viewRect);
DrawAxis(Vector2.UnitX, ref viewRect, min.X, max.X, pixelRange.X);
DrawAxis(Vector2.UnitY, ref viewRect, min.Y, max.Y, pixelRange.Y);
Render2D.PopClip();
}
// Draw curve
if (!_showCollapsed)
{
Render2D.PushClip(ref rect);
DrawCurve(ref viewRect);
Render2D.PopClip();
}
// Draw selection rectangle
if (_contents._leftMouseDown && !_contents._isMovingSelection && !_contents._isMovingTangent)
{
var selectionRect = Rectangle.FromPoints
(
_mainPanel.PointToParent(_contents.PointToParent(_contents._leftMouseDownPos)),
_mainPanel.PointToParent(_contents.PointToParent(_contents._mousePos))
);
Render2D.FillRectangle(selectionRect, Color.Orange * 0.4f);
Render2D.DrawRectangle(selectionRect, Color.Orange);
}
base.Draw();
// Draw border
if (ContainsFocus)
{
Render2D.DrawRectangle(rect, style.BackgroundSelected);
}
}
///
protected override void OnSizeChanged()
{
base.OnSizeChanged();
UpdateKeyframes();
}
///
public override bool OnKeyDown(KeyboardKeys key)
{
if (base.OnKeyDown(key))
return true;
if (key == KeyboardKeys.Delete)
{
RemoveKeyframes();
return true;
}
if (Root.GetKey(KeyboardKeys.Control))
{
switch (key)
{
case KeyboardKeys.A:
SelectAll();
UpdateTangents();
return true;
}
}
return false;
}
///
public override void OnDestroy()
{
_mainPanel = null;
_contents = null;
_popup = null;
_points.Clear();
_labelsFont = null;
Colors = null;
DefaultValue = default;
TickSteps = null;
_tickStrengths = null;
KeyframesChanged = null;
base.OnDestroy();
}
}
///
/// The linear curve editor control.
///
/// The keyframe value type.
///
///
///
public class LinearCurveEditor : CurveEditor where T : new()
{
///
/// The keyframes collection.
///
protected List.Keyframe> _keyframes = new List.Keyframe>();
///
/// Gets the keyframes collection (read-only).
///
public IReadOnlyList.Keyframe> Keyframes => _keyframes;
///
/// Initializes a new instance of the class.
///
public LinearCurveEditor()
{
for (int i = 0; i < _tangents.Length; i++)
{
_tangents[i] = new TangentPoint
{
AutoFocus = false,
Size = KeyframesSize,
Editor = this,
Component = i / 2,
Parent = _contents,
Visible = false,
IsIn = false,
};
}
for (int i = 0; i < _tangents.Length; i += 2)
{
_tangents[i].IsIn = true;
}
}
///
/// Adds the new keyframe.
///
/// The keyframe to add.
public void AddKeyframe(LinearCurve.Keyframe k)
{
if (FPS.HasValue)
{
float fps = FPS.Value;
k.Time = Mathf.Floor(k.Time * fps) / fps;
}
int pos = 0;
while (pos < _keyframes.Count && _keyframes[pos].Time < k.Time)
pos++;
_keyframes.Insert(pos, k);
OnKeyframesChanged();
OnEdited();
}
///
/// Sets the keyframes collection.
///
/// The keyframes.
public void SetKeyframes(IEnumerable.Keyframe> keyframes)
{
if (keyframes == null)
throw new ArgumentNullException(nameof(keyframes));
var keyframesArray = keyframes as LinearCurve.Keyframe[] ?? keyframes.ToArray();
if (_keyframes.SequenceEqual(keyframesArray))
return;
if (keyframesArray.Length > MaxKeyframes)
{
var tmp = keyframesArray;
keyframesArray = new LinearCurve.Keyframe[MaxKeyframes];
Array.Copy(tmp, keyframesArray, MaxKeyframes);
}
_keyframes.Clear();
_keyframes.AddRange(keyframesArray);
_keyframes.Sort((a, b) => a.Time.CompareTo(b.Time));
UpdateFPS();
OnKeyframesChanged();
}
///
protected override void UpdateFPS()
{
if (FPS.HasValue)
{
float fps = FPS.Value;
for (int i = 0; i < _keyframes.Count; i++)
{
var k = _keyframes[i];
k.Time = Mathf.Floor(k.Time * fps) / fps;
_keyframes[i] = k;
}
}
}
///
/// Gets the keyframe point (in keyframes space).
///
/// The keyframe.
/// The keyframe value component index.
/// The point in time/value space.
private Vector2 GetKeyframePoint(ref LinearCurve.Keyframe k, int component)
{
return new Vector2(k.Time, Accessor.GetCurveValue(ref k.Value, component));
}
private void DrawLine(LinearCurve.Keyframe startK, LinearCurve.Keyframe endK, int component, ref Rectangle viewRect)
{
var start = GetKeyframePoint(ref startK, component);
var end = GetKeyframePoint(ref endK, component);
var p1 = PointFromKeyframes(start, ref viewRect);
var p2 = PointFromKeyframes(end, ref viewRect);
var color = Colors[component].RGBMultiplied(0.6f);
Render2D.DrawLine(p1, p2, color, 1.6f);
}
///
protected override void OnKeyframesChanged()
{
var components = Accessor.GetCurveComponents();
while (_points.Count > _keyframes.Count * components)
{
var last = _points.Count - 1;
_points[last].Dispose();
_points.RemoveAt(last);
}
while (_points.Count < _keyframes.Count * components)
{
_points.Add(new KeyframePoint
{
AutoFocus = false,
Size = KeyframesSize,
Editor = this,
Index = _points.Count / components,
Component = _points.Count % components,
Parent = _contents,
});
_refreshAfterEdit = true;
}
UpdateKeyframes();
UpdateTooltips();
base.OnKeyframesChanged();
}
///
public override void Evaluate(out T result, float time, bool loop = false)
{
var curve = new LinearCurve
{
Keyframes = _keyframes.ToArray()
};
curve.Evaluate(out result, time, loop);
}
///
protected override float GetKeyframeTime(object keyframe)
{
return ((LinearCurve.Keyframe)keyframe).Time;
}
///
protected override T GetKeyframeValue(object keyframe)
{
return ((LinearCurve.Keyframe)keyframe).Value;
}
///
protected override float GetKeyframeValue(object keyframe, int component)
{
var value = ((LinearCurve.Keyframe)keyframe).Value;
return Accessor.GetCurveValue(ref value, component);
}
///
protected override void AddKeyframe(Vector2 keyframesPos)
{
var k = new LinearCurve.Keyframe
{
Time = keyframesPos.X,
};
var components = Accessor.GetCurveComponents();
for (int component = 0; component < components; component++)
{
Accessor.SetCurveValue(keyframesPos.Y, ref k.Value, component);
}
OnEditingStart();
AddKeyframe(k);
OnEditingEnd();
}
///
protected override void SetKeyframeInternal(int index, object keyframe)
{
_keyframes[index] = (LinearCurve.Keyframe)keyframe;
}
///
protected override void SetKeyframeInternal(int index, float time, float value, int component)
{
var k = _keyframes[index];
k.Time = time;
Accessor.SetCurveValue(value, ref k.Value, component);
_keyframes[index] = k;
}
///
protected override float GetKeyframeTangentInternal(int index, bool isIn, int component)
{
return 0.0f;
}
///
protected override void SetKeyframeTangentInternal(int index, bool isIn, int component, float value)
{
}
///
protected override void RemoveKeyframesInternal(HashSet indicesToRemove)
{
var keyframes = _keyframes;
_keyframes = new List.Keyframe>();
for (int i = 0; i < keyframes.Count; i++)
{
if (!indicesToRemove.Contains(i))
_keyframes.Add(keyframes[i]);
}
}
sealed class AllKeyframesProxy : IAllKeyframesProxy
{
[HideInEditor, NoSerialize]
public LinearCurveEditor Editor;
[Collection(CanReorderItems = false, Spacing = 10)]
public LinearCurve.Keyframe[] Keyframes;
public void Apply()
{
Editor.SetKeyframes(Keyframes);
}
}
///
protected override IAllKeyframesProxy GetAllKeyframesEditingProxy()
{
return new AllKeyframesProxy
{
Editor = this,
Keyframes = _keyframes.ToArray(),
};
}
///
public override object[] GetKeyframes()
{
var keyframes = new object[_keyframes.Count];
for (int i = 0; i < keyframes.Length; i++)
keyframes[i] = _keyframes[i];
return keyframes;
}
///
public override void SetKeyframes(object[] keyframes)
{
var data = new LinearCurve.Keyframe[keyframes.Length];
for (int i = 0; i < keyframes.Length; i++)
{
if (keyframes[i] is LinearCurve.Keyframe asT)
data[i] = asT;
else if (keyframes[i] is LinearCurve