// 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.Keyframe asObj) data[i] = new LinearCurve.Keyframe(asObj.Time, (T)asObj.Value); } SetKeyframes(data); } /// public override int AddKeyframe(float time, object value) { return AddKeyframe(new LinearCurve.Keyframe(time, (T)value)); } /// public override int AddKeyframe(float time, object value, object tangentIn, object tangentOut) { return AddKeyframe(new LinearCurve.Keyframe(time, (T)value)); } /// public override void GetKeyframe(int index, out float time, out object value, out object tangentIn, out object tangentOut) { var k = _keyframes[index]; time = k.Time; value = k.Value; tangentIn = tangentOut = 0.0f; } /// public override object GetKeyframe(int index) { return _keyframes[index]; } /// public override void SetKeyframeValue(int index, object value) { var k = _keyframes[index]; k.Value = (T)value; _keyframes[index] = k; UpdateKeyframes(); UpdateTooltips(); OnEdited(); } /// public override Float2 GetKeyframePoint(int index, int component) { var k = _keyframes[index]; return new Float2(k.Time, Accessor.GetCurveValue(ref k.Value, component)); } /// public override void OnKeyframesGet(string trackName, Action get) { for (int i = 0; i < _keyframes.Count; i++) { var k = _keyframes[i]; get(trackName, k.Time, k); } } /// public override void OnKeyframesSet(List> keyframes) { OnEditingStart(); _keyframes.Clear(); if (keyframes != null) { foreach (var e in keyframes) { var k = (LinearCurve.Keyframe)e.Value; _keyframes.Add(new LinearCurve.Keyframe(e.Key, k.Value)); } } OnKeyframesChanged(); OnEdited(); OnEditingEnd(); } /// public override int KeyframesCount => _keyframes.Count; /// public override void UpdateKeyframes() { if (_points.Count == 0) { // No keyframes _contents.Bounds = Rectangle.Empty; return; } var wasLocked = _mainPanel.IsLayoutLocked; _mainPanel.IsLayoutLocked = true; // Place keyframes Rectangle curveContentAreaBounds = _mainPanel.GetClientArea(); var viewScale = ViewScale; for (int i = 0; i < _points.Count; i++) { var p = _points[i]; var k = _keyframes[p.Index]; var location = GetKeyframePoint(ref k, p.Component); var point = new Float2 ( location.X * UnitsPerSecond - p.Width * 0.5f, location.Y * -UnitsPerSecond - p.Height * 0.5f + curveContentAreaBounds.Height ); if (_showCollapsed) { point.Y = 1.0f; p.Size = new Float2(KeyframesSize.X / viewScale.X, Height - 2.0f); p.Visible = p.Component == 0; } else { p.Size = KeyframesSize / viewScale; p.Visible = true; } p.Location = point; } // Calculate bounds var bounds = _points[0].Bounds; for (var i = 1; i < _points.Count; i++) { bounds = Rectangle.Union(bounds, _points[i].Bounds); } // Adjust contents bounds to fill the curve area if (EnablePanning != UseMode.Off || !ShowCollapsed) { bounds.Width = Mathf.Max(bounds.Width, 1.0f); bounds.Height = Mathf.Max(bounds.Height, 1.0f); bounds.Location = ApplyUseModeMask(EnablePanning, bounds.Location, _contents.Location); bounds.Size = ApplyUseModeMask(EnablePanning, bounds.Size, _contents.Size); if (!_contents._isMovingSelection) _contents.Bounds = bounds; } else if (_contents.Bounds == Rectangle.Empty) { _contents.Bounds = Rectangle.Union(bounds, new Rectangle(Float2.Zero, Float2.One)); } // Offset the keyframes (parent container changed its location) var posOffset = _contents.Location; for (var i = 0; i < _points.Count; i++) { _points[i].Location -= posOffset; } UpdateTangents(); if (!wasLocked) _mainPanel.UnlockChildrenRecursive(); _mainPanel.PerformLayout(); } /// public override void UpdateTangents() { for (int i = 0; i < _tangents.Length; i++) _tangents[i].Visible = false; } /// protected override void DrawCurve(ref Rectangle viewRect) { var components = Accessor.GetCurveComponents(); for (int component = 0; component < components; component++) { if (ShowStartEndLines) { var start = new LinearCurve.Keyframe { Value = DefaultValue, Time = -10000000.0f, }; var end = new LinearCurve.Keyframe { Value = DefaultValue, Time = 10000000.0f, }; if (_keyframes.Count == 0) { DrawLine(start, end, component, ref viewRect); } else { DrawLine(start, _keyframes[0], component, ref viewRect); DrawLine(_keyframes[_keyframes.Count - 1], end, component, ref viewRect); } } var color = Colors[component]; for (int i = 1; i < _keyframes.Count; i++) { var startK = _keyframes[i - 1]; var endK = _keyframes[i]; var start = GetKeyframePoint(ref startK, component); var end = GetKeyframePoint(ref endK, component); var p1 = PointFromKeyframes(start, ref viewRect); var p2 = PointFromKeyframes(end, ref viewRect); Render2D.DrawLine(p1, p2, color); } } } /// public override void OnDestroy() { _keyframes.Clear(); base.OnDestroy(); } } /// /// The Bezier curve editor control. /// /// The keyframe value type. /// /// /// public class BezierCurveEditor : 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 BezierCurveEditor() { 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(BezierCurve.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 BezierCurve.Keyframe[] ?? keyframes.ToArray(); if (_keyframes.SequenceEqual(keyframesArray)) return; if (keyframesArray.Length > MaxKeyframes) { var tmp = keyframesArray; keyframesArray = new BezierCurve.Keyframe[MaxKeyframes]; Array.Copy(tmp, keyframesArray, MaxKeyframes); } _keyframes.Clear(); _keyframes.AddRange(keyframesArray); _keyframes.Sort((a, b) => a.Time.CompareTo(b.Time)); UpdateFPS(); OnKeyframesChanged(); } private void ResetTangents() { bool edited = false; for (int i = 0; i < _points.Count; i++) { var p = _points[i]; if (!p.IsSelected) continue; if (!edited) OnEditingStart(); edited = true; var k = _keyframes[p.Index]; if (p.Index > 0) { Accessor.SetCurveValue(0.0f, ref k.TangentIn, p.Component); } if (p.Index < _keyframes.Count - 1) { Accessor.SetCurveValue(0.0f, ref k.TangentOut, p.Component); } _keyframes[p.Index] = k; } if (!edited) return; UpdateTangents(); OnEdited(); OnEditingEnd(); } private void SetTangentsLinear() { bool edited = false; for (int i = 0; i < _points.Count; i++) { var p = _points[i]; if (!p.IsSelected) continue; if (!edited) OnEditingStart(); edited = true; var k = _keyframes[p.Index]; var value = Accessor.GetCurveValue(ref k.Value, p.Component); if (p.Index > 0) { var o = _keyframes[p.Index - 1]; var oValue = Accessor.GetCurveValue(ref o.Value, p.Component); var slope = (value - oValue) / (k.Time - o.Time); Accessor.SetCurveValue(slope, ref k.TangentIn, p.Component); } if (p.Index < _keyframes.Count - 1) { var o = _keyframes[p.Index + 1]; var oValue = Accessor.GetCurveValue(ref o.Value, p.Component); var slope = (oValue - value) / (o.Time - k.Time); Accessor.SetCurveValue(slope, ref k.TangentOut, p.Component); } _keyframes[p.Index] = k; } if (!edited) return; UpdateTangents(); OnEdited(); OnEditingEnd(); } // Computes control points given knots k, this is the brain of the operation private static void ComputeControlPoints(float[] k, out float[] p1, out float[] p2) { // Reference: https://www.particleincell.com/2012/bezier-splines/ var n = k.Length - 1; p1 = new float[n]; p2 = new float[n]; // rhs vector var a = new float[n]; var b = new float[n]; var c = new float[n]; var r = new float[n]; // left most segment a[0] = 0; b[0] = 2; c[0] = 1; r[0] = k[0] + 2 * k[1]; // internal segments for (var i = 1; i < n - 1; i++) { a[i] = 1; b[i] = 4; c[i] = 1; r[i] = 4 * k[i] + 2 * k[i + 1]; } // right segment a[n - 1] = 2; b[n - 1] = 7; c[n - 1] = 0; r[n - 1] = 8 * k[n - 1] + k[n]; // solves Ax=b with the Thomas algorithm (from Wikipedia) for (var i = 1; i < n; i++) { var m = a[i] / b[i - 1]; b[i] = b[i] - m * c[i - 1]; r[i] = r[i] - m * r[i - 1]; } p1[n - 1] = r[n - 1] / b[n - 1]; for (var i = n - 2; i >= 0; --i) p1[i] = (r[i] - c[i] * p1[i + 1]) / b[i]; // we have p1, now compute p2 for (var i = 0; i < n - 1; i++) p2[i] = 2 * k[i + 1] - p1[i + 1]; p2[n - 1] = 0.5f * (k[n] + p1[n - 1]); } private void SetTangentsSmooth() { var k1 = new T[_keyframes.Count]; var k2 = new T[_keyframes.Count]; var kk = new float[_keyframes.Count]; var components = Accessor.GetCurveComponents(); for (int component = 0; component < components; component++) { for (int i = 0; i < _keyframes.Count; i++) { var v = _keyframes[i].Value; kk[i] = Accessor.GetCurveValue(ref v, component); } ComputeControlPoints(kk, out var p1, out var p2); for (int i = 0; i < p1.Length; i++) { Accessor.SetCurveValue(p1[i], ref k1[i], component); Accessor.SetCurveValue(p2[i], ref k2[i], component); } } bool edited = false; for (int i = 0; i < _points.Count; i++) { var p = _points[i]; if (!p.IsSelected) continue; if (!edited) OnEditingStart(); edited = true; var k = _keyframes[p.Index]; var value = Accessor.GetCurveValue(ref k.Value, p.Component); if (p.Index > 0) { var o = _keyframes[p.Index - 1]; var slope = (Accessor.GetCurveValue(ref k2[p.Index], p.Component) - value) / (o.Time - k.Time); Accessor.SetCurveValue(slope, ref k.TangentIn, p.Component); } if (p.Index < _keyframes.Count - 1) { var o = _keyframes[p.Index + 1]; var slope = (Accessor.GetCurveValue(ref k1[p.Index], p.Component) - value) / (o.Time - k.Time); Accessor.SetCurveValue(slope, ref k.TangentOut, p.Component); } _keyframes[p.Index] = k; } if (!edited) return; UpdateTangents(); OnEdited(); OnEditingEnd(); } /// 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 BezierCurve.Keyframe k, int component) { return new Float2(k.Time, Accessor.GetCurveValue(ref k.Value, component)); } private void DrawLine(BezierCurve.Keyframe startK, BezierCurve.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 BezierCurve { 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 ((BezierCurve.Keyframe)keyframe).Time; } /// protected override T GetKeyframeValue(object keyframe) { return ((BezierCurve.Keyframe)keyframe).Value; } /// protected override float GetKeyframeValue(object keyframe, int component) { var value = ((BezierCurve.Keyframe)keyframe).Value; return Accessor.GetCurveValue(ref value, component); } /// protected override void AddKeyframe(Float2 keyframesPos) { var k = new BezierCurve.Keyframe { Time = keyframesPos.X, }; var components = Accessor.GetCurveComponents(); for (int component = 0; component < components; component++) { Accessor.SetCurveValue(keyframesPos.Y, ref k.Value, component); Accessor.SetCurveValue(0.0f, ref k.TangentIn, component); Accessor.SetCurveValue(0.0f, ref k.TangentOut, component); } OnEditingStart(); AddKeyframe(k); OnEditingEnd(); } /// protected override void SetKeyframeInternal(int index, object keyframe) { _keyframes[index] = (BezierCurve.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) { var k = _keyframes[index]; var value = isIn ? k.TangentIn : k.TangentOut; return Accessor.GetCurveValue(ref value, component); } /// protected override void SetKeyframeTangentInternal(int index, bool isIn, int component, float value) { var k = _keyframes[index]; if (isIn) Accessor.SetCurveValue(value, ref k.TangentIn, component); else Accessor.SetCurveValue(value, ref k.TangentOut, component); _keyframes[index] = k; } /// 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 BezierCurveEditor Editor; [Collection(CanReorderItems = false, Spacing = 10)] public BezierCurve.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 BezierCurve.Keyframe[keyframes.Length]; for (int i = 0; i < keyframes.Length; i++) { if (keyframes[i] is BezierCurve.Keyframe asT) data[i] = asT; else if (keyframes[i] is BezierCurve.Keyframe asObj) data[i] = new BezierCurve.Keyframe(asObj.Time, (T)asObj.Value, (T)asObj.TangentIn, (T)asObj.TangentOut); } SetKeyframes(data); } /// public override int AddKeyframe(float time, object value) { return AddKeyframe(new BezierCurve.Keyframe(time, (T)value)); } /// public override int AddKeyframe(float time, object value, object tangentIn, object tangentOut) { return AddKeyframe(new BezierCurve.Keyframe(time, (T)value, (T)tangentIn, (T)tangentOut)); } /// public override void GetKeyframe(int index, out float time, out object value, out object tangentIn, out object tangentOut) { var k = _keyframes[index]; time = k.Time; value = k.Value; tangentIn = k.TangentIn; tangentOut = k.TangentOut; } /// public override object GetKeyframe(int index) { return _keyframes[index]; } /// public override void SetKeyframeValue(int index, object value) { var k = _keyframes[index]; k.Value = (T)value; _keyframes[index] = k; UpdateKeyframes(); UpdateTooltips(); OnEdited(); } /// public override Float2 GetKeyframePoint(int index, int component) { var k = _keyframes[index]; return new Float2(k.Time, Accessor.GetCurveValue(ref k.Value, component)); } /// public override void OnKeyframesGet(string trackName, Action get) { for (int i = 0; i < _keyframes.Count; i++) { var k = _keyframes[i]; get(trackName, k.Time, k); } } /// public override void OnKeyframesSet(List> keyframes) { OnEditingStart(); _keyframes.Clear(); if (keyframes != null) { foreach (var e in keyframes) { var k = (BezierCurve.Keyframe)e.Value; _keyframes.Add(new BezierCurve.Keyframe(e.Key, k.Value, k.TangentIn, k.TangentOut)); } } OnKeyframesChanged(); OnEdited(); OnEditingEnd(); } /// public override int KeyframesCount => _keyframes.Count; /// public override void UpdateKeyframes() { if (_points.Count == 0) { // No keyframes _contents.Bounds = Rectangle.Empty; return; } _mainPanel.LockChildrenRecursive(); // Place keyframes Rectangle curveContentAreaBounds = _mainPanel.GetClientArea(); var viewScale = ViewScale; var pointsSize = _showCollapsed ? new Float2(4.0f / viewScale.X, Height - 2.0f) : KeyframesSize / viewScale; for (int i = 0; i < _points.Count; i++) { var p = _points[i]; var k = _keyframes[p.Index]; var point = GetKeyframePoint(ref k, p.Component); var location = new Float2 ( point.X * UnitsPerSecond - pointsSize.X * 0.5f, point.Y * -UnitsPerSecond - pointsSize.Y * 0.5f + curveContentAreaBounds.Height ); if (_showCollapsed) { location.Y = 1.0f; p.Visible = p.Component == 0; } else { p.Visible = true; } p.Bounds = new Rectangle(location, pointsSize); } // Calculate bounds var bounds = _points[0].Bounds; for (int i = 1; i < _points.Count; i++) { bounds = Rectangle.Union(bounds, _points[i].Bounds); } // Adjust contents bounds to fill the curve area if (EnablePanning != UseMode.Off || !ShowCollapsed) { bounds.Width = Mathf.Max(bounds.Width, 1.0f); bounds.Height = Mathf.Max(bounds.Height, 1.0f); bounds.Location = ApplyUseModeMask(EnablePanning, bounds.Location, _contents.Location); bounds.Size = ApplyUseModeMask(EnablePanning, bounds.Size, _contents.Size); if (!_contents._isMovingSelection) _contents.Bounds = bounds; } else if (_contents.Bounds == Rectangle.Empty) { _contents.Bounds = Rectangle.Union(bounds, new Rectangle(Float2.Zero, Float2.One)); } // Offset the keyframes (parent container changed its location) var posOffset = _contents.Location; for (var i = 0; i < _points.Count; i++) { _points[i].Location -= posOffset; } UpdateTangents(); _mainPanel.UnlockChildrenRecursive(); _mainPanel.PerformLayout(); } /// public override void UpdateTangents() { // Find selected keyframe Rectangle curveContentAreaBounds = _mainPanel.GetClientArea(); var selectedCount = 0; var selectedIndex = -1; KeyframePoint selectedKeyframe = null; var selectedComponent = -1; for (int i = 0; i < _points.Count; i++) { var p = _points[i]; if (p.IsSelected) { selectedIndex = p.Index; selectedKeyframe = p; selectedComponent = p.Component; selectedCount++; } } // Place tangents (only for a single selected keyframe) if (selectedCount == 1 && !_showCollapsed) { var posOffset = _contents.Location; var k = _keyframes[selectedIndex]; for (int i = 0; i < _tangents.Length; i++) { var t = _tangents[i]; t.Index = selectedIndex; t.Point = selectedKeyframe; t.Component = selectedComponent; var tangent = t.TangentValue; var direction = t.IsIn ? -1.0f : 1.0f; var offset = 30.0f * direction; var location = GetKeyframePoint(ref k, selectedComponent); t.Size = KeyframesSize / ViewScale; t.Location = new Float2 ( location.X * UnitsPerSecond - t.Width * 0.5f + offset, location.Y * -UnitsPerSecond - t.Height * 0.5f + curveContentAreaBounds.Height - offset * tangent ); var isFirst = selectedIndex == 0 && t.IsIn; var isLast = selectedIndex == _keyframes.Count - 1 && !t.IsIn; t.Visible = !isFirst && !isLast; t.UpdateTooltip(); if (t.Visible) _tangents[i].Location -= posOffset; } } else { for (int i = 0; i < _tangents.Length; i++) { _tangents[i].Visible = false; } } } /// protected override void OnShowContextMenu(ContextMenu.ContextMenu cm, int selectionCount) { if (selectionCount != 0) { cm.AddSeparator(); cm.AddButton("Reset tangents", ResetTangents); cm.AddButton("Linear tangents", SetTangentsLinear); cm.AddButton("Smooth tangents", SetTangentsSmooth); } } /// protected override void DrawCurve(ref Rectangle viewRect) { var components = Accessor.GetCurveComponents(); for (int component = 0; component < components; component++) { if (ShowStartEndLines) { var start = new BezierCurve.Keyframe { Value = DefaultValue, Time = -10000000.0f, }; var end = new BezierCurve.Keyframe { Value = DefaultValue, Time = 10000000.0f, }; if (_keyframes.Count == 0) { DrawLine(start, end, component, ref viewRect); } else { DrawLine(start, _keyframes[0], component, ref viewRect); DrawLine(_keyframes[_keyframes.Count - 1], end, component, ref viewRect); } } var color = Colors[component]; for (int i = 1; i < _keyframes.Count; i++) { var startK = _keyframes[i - 1]; var endK = _keyframes[i]; var start = GetKeyframePoint(ref startK, component); var end = GetKeyframePoint(ref endK, component); var startTangent = Accessor.GetCurveValue(ref startK.TangentOut, component); var endTangent = Accessor.GetCurveValue(ref endK.TangentIn, component); var offset = (end.X - start.X) * 0.5f; var p1 = PointFromKeyframes(start, ref viewRect); var p2 = PointFromKeyframes(start + new Float2(offset, startTangent * offset), ref viewRect); var p3 = PointFromKeyframes(end - new Float2(offset, endTangent * offset), ref viewRect); var p4 = PointFromKeyframes(end, ref viewRect); Render2D.DrawBezier(p1, p2, p3, p4, color); } } } /// public override void OnDestroy() { _keyframes.Clear(); base.OnDestroy(); } } }