// Copyright (c) 2012-2023 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;
namespace FlaxEditor.GUI
{
///
/// The generic curve editor control.
///
/// The keyframe value type.
///
public abstract partial class CurveEditor : CurveEditorBase where T : new()
{
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 Float2(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 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;
///
/// Gets the point time and value on a curve.
///
public Float2 Point => Editor.GetKeyframePoint(Index, Component);
///
/// Gets the time of the keyframe point.
///
public float Time => Editor.GetKeyframeTime(Index);
///
public override void Draw()
{
var rect = new Rectangle(Float2.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(Float2 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;
}
///
protected override bool ShowTooltip => base.ShowTooltip && !Editor._contents._isMovingSelection;
///
/// 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(Float2.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 Float2 KeyframesSize = new Float2(7.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 Float2 ViewOffset
{
get => _mainPanel.ViewOffset;
set
{
_mainPanel.ViewOffset = value;
_mainPanel.FastScroll();
}
}
///
public override Float2 ViewScale
{
get => _contents.Scale;
set => _contents.Scale = Float2.Clamp(value, new Float2(0.0001f), new Float2(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)
{
// Synchronize selection for curve points when collapsed so all points fo the keyframe are selected
for (var i = 0; i < _points.Count; i++)
{
var p = _points[i];
if (p.IsSelected)
{
for (var j = 0; j < _points.Count; j++)
{
var q = _points[j];
if (q.Index == p.Index)
q.IsSelected = true;
}
}
}
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 index.
/// The keyframe time.
protected abstract float GetKeyframeTime(int index);
///
/// 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(Float2 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, Float2 pos)
{
_popup = new Popup(this, new object[] { GetAllKeyframesEditingProxy() }, null, 400.0f);
_popup.Show(control, pos);
}
private void EditKeyframes(Control control, Float2 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, Float2 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()
{
if (KeyframesEditorContext != null)
KeyframesEditorContext.OnKeyframesDelete(this);
else
RemoveKeyframesInner();
}
private void CopyKeyframes(KeyframePoint point = null)
{
float? timeOffset = null;
if (point != null)
{
timeOffset = -point.Time;
}
else
{
for (int i = 0; i < _points.Count; i++)
{
if (_points[i].IsSelected)
{
timeOffset = -_points[i].Time;
break;
}
}
}
KeyframesEditorUtils.Copy(this, timeOffset);
}
private void RemoveKeyframesInner()
{
if (SelectionCount == 0)
return;
var indicesToRemove = new HashSet();
for (int i = 0; i < _points.Count; i++)
{
var p = _points[i];
if (p.IsSelected)
{
p.IsSelected = false;
indicesToRemove.Add(p.Index);
}
}
OnEditingStart();
RemoveKeyframesInternal(indicesToRemove);
OnKeyframesChanged();
OnEdited();
OnEditingEnd();
}
///
public override void ShowWholeCurve()
{
ViewScale = ApplyUseModeMask(EnableZoom, _mainPanel.Size / _contents.Size, ViewScale);
ViewOffset = ApplyUseModeMask(EnablePanning, -_mainPanel.ControlsBounds.Location, 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;
if (ShowCollapsed)
{
for (int i = 0; i < _points.Count; i++)
if (_points[i].Component == 0 && _points[i].IsSelected)
result++;
}
else
{
for (int i = 0; i < _points.Count; i++)
if (_points[i].IsSelected)
result++;
}
return result;
}
}
///
public override void ClearSelection()
{
for (int i = 0; i < _points.Count; i++)
{
_points[i].IsSelected = false;
}
}
///
/// Selects all keyframes.
///
public void SelectAll()
{
for (int i = 0; i < _points.Count; i++)
{
_points[i].IsSelected = true;
}
}
///
/// 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 Float2 PointToKeyframes(Float2 point, ref Rectangle curveContentAreaBounds)
{
// Curve Editor -> Main Panel
point = _mainPanel.PointFromParent(point);
// Main Panel -> Contents
point = _contents.PointFromParent(point);
// Contents -> Keyframes
return new Float2(
(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 Float2 PointFromKeyframes(Float2 point, ref Rectangle curveContentAreaBounds)
{
// Keyframes -> Contents
point = new Float2(
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(Float2 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,
Float2.Lerp(viewRect.Size, Float2.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(Float2.Zero, Size);
var viewRect = _mainPanel.GetClientArea();
// Draw background
if (ShowBackground)
{
Render2D.FillRectangle(rect, _contentsColor);
}
// Draw time and values axes
if (ShowAxes != UseMode.Off)
{
var upperLeft = PointToKeyframes(viewRect.Location, ref viewRect);
var bottomRight = PointToKeyframes(viewRect.Size, ref viewRect);
var min = Float2.Min(upperLeft, bottomRight);
var max = Float2.Max(upperLeft, bottomRight);
var pixelRange = (max - min) * ViewScale * UnitsPerSecond;
Render2D.PushClip(ref viewRect);
if ((ShowAxes & UseMode.Vertical) == UseMode.Vertical)
DrawAxis(Float2.UnitX, ref viewRect, min.X, max.X, pixelRange.X);
if ((ShowAxes & UseMode.Horizontal) == UseMode.Horizontal)
DrawAxis(Float2.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;
switch (key)
{
case KeyboardKeys.Delete:
RemoveKeyframes();
return true;
case KeyboardKeys.A:
if (Root.GetKey(KeyboardKeys.Control))
{
SelectAll();
UpdateTangents();
return true;
}
break;
case KeyboardKeys.C:
if (Root.GetKey(KeyboardKeys.Control))
{
CopyKeyframes();
return true;
}
break;
case KeyboardKeys.V:
if (Root.GetKey(KeyboardKeys.Control))
{
KeyframesEditorUtils.Paste(this);
return true;
}
break;
}
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;
KeyframesEditorContext = 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.
/// The index of the keyframe.
public int AddKeyframe(LinearCurve.Keyframe k)
{
var eps = Mathf.Epsilon;
if (FPS.HasValue)
{
float fps = FPS.Value;
k.Time = Mathf.Floor(k.Time * fps) / fps;
eps = 1.0f / fps;
}
int pos = 0;
while (pos < _keyframes.Count && _keyframes[pos].Time < k.Time)
pos++;
if (_keyframes.Count > pos && Mathf.Abs(_keyframes[pos].Time - k.Time) < eps)
_keyframes[pos] = k;
else
_keyframes.Insert(pos, k);
OnKeyframesChanged();
OnEdited();
return pos;
}
///
/// 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 Float2 GetKeyframePoint(ref LinearCurve.Keyframe k, int component)
{
return new Float2(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(int index)
{
return _keyframes[index].Time;
}
///
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(Float2 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