JD2022-TU1/main/tools/framework/JD.Timeline/View/ClipView/Widgets/ClipWidget.cs

784 lines
28 KiB
C#

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Windows.Forms;
using JD.Application.View;
using JD.Application.View.Misc;
using JD.Application.View.Timeline;
using JD.Application.View.WindowLess;
using JD.Audio.Annotations;
using JD.Core.Collections;
using JD.Kernel.Interfaces;
using JD.Kernel.StaticHelpers;
using JD.Kernel.Structs;
using JD.Sequencers.Times;
using JD.Sequencers.Windowlesses;
using JD.Timeline.Model;
using JD.Timeline.Model.Clips;
using JD.Timeline.View.Actions.Macros;
using JD.Timeline.View.GDI;
using TeaBox;
using JD.Serialization;
using TeaBox.Core.Source.Gui;
using Actor = JD.Application.Plugin.World.Actor;
using Brush = System.Drawing.Brush;
using Color = System.Drawing.Color;
using FontFamily = System.Drawing.FontFamily;
using LinearGradientBrush = System.Drawing.Drawing2D.LinearGradientBrush;
using Pen = System.Drawing.Pen;
namespace JD.Timeline.View.ClipView.Widgets
{
/// <summary>
/// Visual representation of a clip in the ClipView
/// </summary>
public class ClipWidget : LocalizedTrackItem, IResizable, ICloneable<WindowlessControl>
{
#region Constants
protected const int IconPadding = 2;
private static Font DefaultFont = new Font(FontFamily.GenericSansSerif, 7);
protected internal static readonly ClipStyle MissingStyle = new ClipStyle { BackgroundColor = Color.Red, Icon = Properties.Resources.Icon_20x20_imagemissing };
protected internal static readonly ClipStyle CollisionStyle = new ClipStyle { BackgroundColor = Color.FromArgb(255, 222, 0), Icon = Properties.Resources.Icon_20x20_warning };
protected internal static readonly ClipStyle UnresolvedStyle = new ClipStyle { BackgroundColor = Color.FromArgb(255, 132, 0), Icon = Properties.Resources.Icon_20x20_noactor };
protected internal static readonly Dictionary<string, ClipStyle> StylePerType = new Dictionary<string, ClipStyle>();
#endregion
#region Variables
private readonly Clip _model;
private readonly Image _image;
private readonly LinearBrush _backgroundBrush = new LinearBrush();
/// <summary>
/// Clips are always drawn of a minimum size of the icon size, we still keep the real width corresponding
/// to clip duration.
/// </summary>
private int _durationWidth;
#endregion
#region Constructor
static ClipWidget()
{
var materialGraphicColor = Color.FromArgb(25, 122, 135);
RegisterStyle<ColorClip>(Properties.Resources.Icon_20x20_color, Color.FromArgb(121, 75, 123));
RegisterStyle<ActorEnableClip>(Properties.Resources.Icon_20x20_actorEnable, Color.FromArgb(147, 96, 96), "Actor Activation");
RegisterStyle<AlphaClip>(Properties.Resources.Icon_20x20_alpha, Color.FromArgb(147, 147, 147));
RegisterStyle<AlphaClip>("in", Properties.Resources.Icon_20x20_alpha_in, Color.FromArgb(147, 147, 147));
RegisterStyle<AlphaClip>("out", Properties.Resources.Icon_20x20_alpha_out, Color.FromArgb(147, 147, 147));
RegisterStyle<FXClip>(Properties.Resources.Icon_20x20_fx, Color.FromArgb(255, 72, 0));
RegisterStyle<MarkerClip>(Properties.Resources.Icon_20x20_marker, Color.FromArgb(102, 209, 240));
RegisterStyle<MaterialGraphicAlphaThresholdClip>(Properties.Resources.Icon_20x20_alpha_threshold, materialGraphicColor, "AlphaThredshold");
RegisterStyle<MaterialGraphicDiffuseAlphaClip>(Properties.Resources.Icon_20x20_alpha, materialGraphicColor, "DiffuseAlpha");
RegisterStyle<MaterialGraphicDiffuseAlphaClip>("in", Properties.Resources.Icon_20x20_alpha_in, materialGraphicColor, "DiffuseAlpha");
RegisterStyle<MaterialGraphicDiffuseAlphaClip>("out", Properties.Resources.Icon_20x20_alpha_out, materialGraphicColor, "DiffuseAlpha");
RegisterStyle<MaterialGraphicDiffuseColorClip>(Properties.Resources.Icon_20x20_color, materialGraphicColor, "DiffuseColor");
RegisterStyle<MaterialGraphicEnableLayerClip>(Properties.Resources.Icon_20x20_trigger, materialGraphicColor, "EnableLayer");
RegisterStyle<MaterialGraphicUVAnimRotationClip>(Properties.Resources.Icon_20x20_rotate, materialGraphicColor, "UVAnimRotation");
RegisterStyle<MaterialGraphicUVRotationClip>(Properties.Resources.Icon_20x20_rotate, materialGraphicColor, "UVRotation");
RegisterStyle<MaterialGraphicUVScaleClip>(Properties.Resources.Icon_20x20_scale, materialGraphicColor, "UVScale");
RegisterStyle<MaterialGraphicUVScrollClip>(Properties.Resources.Icon_20x20_translation, materialGraphicColor, "UVScroll");
RegisterStyle<MaterialGraphicUVTranslationClip>(Properties.Resources.Icon_20x20_translation, materialGraphicColor, "UVTranslation");
RegisterStyle<MotionClip>(Properties.Resources.Icon_24x24_move, Color.FromArgb(220, 220, 220), "Move");
RegisterStyle<PictogramClip>(Properties.Resources.Icon_24x24_picto, Color.FromArgb(150, 170, 190), "Pictogram");
RegisterStyle<CameraFeedClip>(Properties.Resources.Icon_20x20_camera, Color.FromArgb(255, 153, 204), "CameraFeed");
RegisterStyle<PivotClip>(Properties.Resources.Icon_20x20_pivot, Color.FromArgb(255, 0, 126));
RegisterStyle<SizeClip>(Properties.Resources.Icon_20x20_scale, Color.FromArgb(255, 178, 0));
RegisterStyle<ProportionClip>(Properties.Resources.Icon_20x20_scale, Color.FromArgb(174, 208, 0));
RegisterStyle<Proportion3DClip>(Properties.Resources.Icon_20x20_scale, Color.FromArgb(174, 208, 0));
RegisterStyle<TranslationClip>(Properties.Resources.Icon_20x20_translation, Color.FromArgb(39, 100, 183));
RegisterStyle<RotationClip>(Properties.Resources.Icon_20x20_rotate, Color.FromArgb(39, 183, 46));
RegisterStyle<GameplayEventClip>(Properties.Resources.Icon_20x20_trigger, Color.FromArgb(0, 137, 195), "Gameplay Event");
RegisterStyle<HideUserInterfaceClip>(Properties.Resources.Icon_20x20_hideUI, Color.FromArgb(87, 124, 114), "Hide user interface");
RegisterStyle<AnimationClip>(Properties.Resources.Icon_20x20_animation, Color.FromArgb(205, 0, 0), "Animation");
RegisterStyle<SoundSetClip>(Properties.Resources.Icon_20x20_sound, Color.FromArgb(56, 103, 0));
RegisterStyle<SoundwichClip>(Properties.Resources.Icon_20x20_sound, Color.FromArgb(75, 38, 175), "Soundwich");
RegisterStyle<SoundwichClipWithId>(Properties.Resources.Icon_20x20_sound, Color.FromArgb(75, 38, 175), "SoundwichWithId");
RegisterStyle<SpawnActorClip>(Properties.Resources.Icon_20x20_spawn, Color.FromArgb(134, 184, 0), "Spawner");
RegisterStyle<TapeLauncherClip>(Properties.Resources.Icon_20x20_tape, Color.FromArgb(74, 106, 137));
RegisterStyle<TapeReferenceClip>(Properties.Resources.Icon_20x20_tape, Color.FromArgb(117, 124, 55));
RegisterStyle<TextClip>(Properties.Resources.Icon_20x20_text, Color.FromArgb(161, 121, 200));
RegisterStyle<TextAreaSizeClip>(Properties.Resources.Icon_20x20_text, Color.FromArgb(161, 121, 200), "TextArea ReSize Clip");
RegisterStyle<GoldEffectClip>(Properties.Resources.Icon_20x20_fx, Color.FromArgb(223, 84, 60), "Gold move effect");
RegisterStyle<ScoreClip>(Properties.Resources.TransparentBackground, Color.FromArgb(223, 84, 60), "Score Clip");
RegisterStyle<MoveExampleClip>(Properties.Resources.TransparentBackground, Color.FromArgb(223, 84, 60), "Move Example Clip");
RegisterStyle<KaraokeClip>(Properties.Resources.Icon_20x20_fx, Color.FromArgb(255, 222, 0), "Karaoke clip");
RegisterStyle<SlotClip>(Properties.Resources.Icon_16x16_slot, Color.FromArgb(147, 147, 147), "Mashup Signature");
RegisterStyle<VideoClip>(Properties.Resources.Icon_16_16_plug, Color.FromArgb(147, 147, 147), "Mashup Video");
RegisterStyle<CommunityDancerClip>(Properties.Resources.Icon_20x20_motion, Color.FromArgb(220, 220, 220), "Community clip");
RegisterStyle<VibrationClip>(Properties.Resources.Icon_24x24_move, Color.FromArgb(220, 220, 220), "Vibration");
}
public ClipWidget(Clip model)
{
Throw.IfNull("@event", "event");
_model = model;
_model.PropertyChanged += ModelChanged;
_image = Icon;
if (_model.Curves.Any())
{
Controls.Add(new CurveKeysWidget(this));
}
}
protected override void Dispose(bool disposing)
{
_model.PropertyChanged -= ModelChanged;
_backgroundBrush.Dispose();
base.Dispose(disposing);
}
#endregion
#region Properties
public Clip Model
{
get { return _model; }
}
public virtual bool Finite
{
get
{
return true;
}
}
public override float Duration
{
get
{
return Timeline.TimeFromFrameDuration(Model.Duration);
}
}
/// <summary>
/// Start position for the text.
/// </summary>
protected int TextLeft
{
get { return IconPadding * 2 + _image.Width; }
}
protected Rectangle ImageBounds
{
get
{
return new Rectangle(new Point(Bounds.Left + IconPadding, Bounds.Top + (Bounds.Height - _image.Height) / 2 + 1), _image.Size);
}
}
public new TapeTrackWidget Track
{
get { return (TapeTrackWidget)base.Track; }
}
public override string ToolTipText
{
get
{
var timeline = Timeline;
// Can happen when clips are removed from the track view. ...
if (timeline == null)
{
return string.Empty;
}
var collision = Model.CollisionsWith;
if (collision != null)
{
return collision.ToString();
}
var beatText = Beat.ToTimeString(timeline.TimeFromTimeUnit(Time));
var durationText = Beat.ToTimeString(Model.TimeDuration());
return $"Time: {beatText} (beat {Model.StartBeat:0.00})\nDuration: {durationText} ({Model.BeatDuration:0.00} beats)";
}
}
protected virtual int MinimumWidth
{
get { return IconPadding + _image.Width; }
}
public override bool SupportAutoLoop
{
get { return true; }
}
public virtual ClipStyle Style
{
get
{
if (Model.CollisionsWithOther)
{
return CollisionStyle;
}
if (!Model.Tape.IsOffline && Model.IsUnresolved())
{
return UnresolvedStyle;
}
else if (Model is AlphaClip)
{
var alphaClip = Model as AlphaClip;
bool? fadesToOpaque = AlphaClip.FadesToOpaque(alphaClip.Curve);
if (fadesToOpaque.HasValue)
{
return typeof(AlphaClip).GetStyle(fadesToOpaque.Value ? "in" : "out");
}
}
return Model.GetType().GetStyle();
}
}
#endregion
#region Methods
public virtual void Update()
{
Time = Timeline.TimeFromFrame(Model.StartFrame);
SetupBounds();
}
protected override void SetupBounds()
{
if (Timeline != null)
{
int left = Timeline.PixelFromFrame(Model.StartFrame);
int right = Timeline.PixelFromFrame(Model.StopFrame);
_durationWidth = right - left;
int width = right - left;
// When zoomed out, try to paint the clip with the available space on right side of the clip.
if (_durationWidth < MinimumWidth)
{
int maxRight = NextClipWidgetOnTrack == null ? int.MaxValue : NextClipWidgetOnTrack.Bounds.X;
width = new Range<int>(_durationWidth, maxRight - Bounds.Left - 2).Clamp(MinimumWidth);
}
Bounds = new Rectangle(left, Track.Bounds.Top, width, Track.Height);
}
}
public ClipWidget NextClipWidgetOnTrack
{
get;
set;
}
/// <summary>
/// Update internal model when the track item is moved.
/// </summary>
protected override void OnTimeChanged(System.EventArgs e)
{
base.OnTimeChanged(e);
Model.StartFrame = (int)Math.Round(Timeline.FrameFromTime(Time));
}
public override void Snap_Self(float originalDelta, ref float snappedDelta, ref float diff)
{
float snapTime = DraggingEdges == Edge.Right ? EndTime : Time;
float newValue;
if (SnappingUnit == SnappingUnit.None)
{
// Snap to the frame.
newValue = Timeline.TimeFromFrame((float)Math.Round(Timeline.FrameFromTime(snapTime + snappedDelta)));
}
else
{
newValue = Timeline.Wave().UnitConverter.Snap(new BeatUnit(snapTime + snappedDelta), SnappingUnit).Value;
}
snappedDelta = newValue - snapTime;
}
protected virtual SnappingUnit SnappingUnit
{
get { return EditionOptions.CurrentSnappingUnit; }
}
public override void Move(float delta)
{
Time += delta;
}
public virtual bool IsResizeAllowed
{
get { return true; }
}
public virtual void Resize(Edge resizeEdge, float delta)
{
float diff = 0;
Snap_Self(delta, ref delta, ref diff);
int frameDelta = (int)Math.Round(Timeline.FrameFromTime(delta));
int newDuration = Model.Duration;
if (resizeEdge == Edge.Left)
{
newDuration = Math.Max(1, Model.Duration - frameDelta);
Model.StartFrame -= newDuration - Model.Duration;
}
else if (resizeEdge == Edge.Right)
{
newDuration = Math.Max(1, Model.Duration + frameDelta);
}
Model.Resize(newDuration, resizeCurves: WaveControl.IsStretchKeyPress);
}
public virtual void ResizeByValue(Edge resizeEdge, float value)
{
int frame = (int)Math.Round(Timeline.FrameFromTime(value));
int newDuration = Model.Duration;
if (resizeEdge == Edge.Left)
{
newDuration = Math.Max(1, Model.StopFrame - frame);
Model.StartFrame = frame;
}
else if (resizeEdge == Edge.Right)
{
newDuration = frame - Model.StartFrame;
}
Model.Resize(newDuration, WaveControl.IsStretchKeyPress);
}
/// <summary>
/// Called when the clip is added via a context menu/shortcut.
/// </summary>
public virtual void OnInserted()
{
}
#endregion
#region Painting
protected Brush BackgroundBrush(Rectangle bounds)
{
return BackgroundBrush(bounds, BaseColor);
}
protected Brush BackgroundBrush(Rectangle bounds, Color color)
{
_backgroundBrush.Bounds = bounds;
_backgroundBrush.Color1 = color.Add(10);
_backgroundBrush.Color2 = color.Add(-10);
return _backgroundBrush.Brush;
}
protected virtual Pen EdgePen
{
get { return StyleSheet.Pens[Selected ? Color.White : BaseColor.Add(-30)]; }
}
protected Color BaseColor
{
get;
set;
}
protected Rectangle DurationBounds
{
get
{
var bounds = Bounds;
bounds.Width = _durationWidth;
return bounds;
}
}
/// <summary>
/// RegisterStyle and associates a style to a clip type
/// </summary>
/// <typeparam name="T">A Clip class</typeparam>
/// <param name="styleId">an Id used if you want to associate multiple styles to a clip type</param>
/// <param name="icon">an Icon</param>
/// <param name="color">a Color</param>
/// <param name="displayName">a display name</param>
public static void RegisterStyle<T>(string styleId, Bitmap icon, Color color, string displayName = "")
where T : Clip
{
var style = new ClipStyle { BackgroundColor = color, Icon = icon, DisplayName = displayName.NonEmpty(typeof(T).Name) };
StylePerType.Add(typeof(T).Name + styleId, style);
}
public static void RegisterStyle<T>(Bitmap icon, Color color, string displayName = "")
where T : Clip
{
RegisterStyle<T>("", icon, color, displayName);
}
protected override void OnPaint(PaintEventArgs e)
{
SetupBounds();
var graphics = e.Graphics;
BaseColor = BackgroundColor;
if (Selected)
{
BaseColor = BaseColor.Add(100);
}
FillBackground(graphics, Bounds);
DrawClipType(Model.Type, graphics);
DrawEdge(graphics, Bounds);
OnPaintChildControls(e);
if (!Model.IsActive)
{
DrawAsInactive(graphics);
}
}
protected void DrawAsInactive(Graphics graphics)
{
var bounds = Bounds;
if (Selected)
{
bounds.Height -= 2;
bounds.Width -= 2;
bounds.X++;
bounds.Y++;
}
if (bounds.Width > 0 && bounds.Height > 0)
{
var color = Color.Black.WithAlpha(200);
var brush = new LinearBrush { Color1 = color, Color2 = color, Bounds = bounds };
graphics.FillRectangle(brush.Brush, bounds);
}
}
/// <summary>
/// Gets the string as displayed in the editor for the event type.
/// </summary>
/// <returns></returns>
public string DisplayName
{
get { return Style.DisplayName; }
}
/// <summary>
/// Icon associated with the event type
/// </summary>
public Bitmap Icon
{
get { return Style.Icon; }
}
/// <summary>
/// Icon associated with the event type when not selected.
/// </summary>
public Bitmap FadeIcon
{
get { return Style.FadeIcon; }
}
/// <summary>
/// Color associated with the event type when selected.
/// </summary>
public Color BackgroundColor
{
get { return Style.BackgroundColor; }
}
protected virtual void DrawClipType(System.Type clipType, Graphics graphics)
{
var image = Selected ? this.Icon : FadeIcon;
var imageBounds = ImageBounds;
int right = Bounds.Right;
if (imageBounds.Right > right)
{
imageBounds.Width -= imageBounds.Right - right;
}
if (image != null)
{
graphics.DrawImageUnscaledAndClipped(image, imageBounds);
}
}
protected void DrawClipTypeSmall(System.Type clipType, Graphics graphics)
{
DrawClipTypes(new[] { clipType }, graphics);
}
protected void DrawClipTypes(IEnumerable<System.Type> clipTypes, Graphics graphics)
{
const int smallIconSize = 12;
int left = Bounds.Left + IconPadding;
int right = Bounds.Right;
foreach (var clipType in clipTypes)
{
var bounds = new Rectangle(left, Bounds.Top + IconPadding, smallIconSize, smallIconSize);
if (bounds.Right > right)
{
bounds.Width -= bounds.Right - right;
}
if (bounds.Width > 0)
{
graphics.DrawImage(clipType.GetStyle().Icon, bounds);
left += smallIconSize + 1;
}
else
{
break;
}
}
}
protected bool DrawActorImages(IEnumerable<Image> actorsImage, Graphics graphics)
{
const int minimumHeightToDrawActorImage = 24;
const int offset = 6;
int position = Bounds.Left + IconPadding;
bool draw = false;
if (Height >= minimumHeightToDrawActorImage)
{
foreach (var image in actorsImage)
{
if (image != null && image != Actor.DefaultImage)
{
int size = Height - IconPadding * 2;
var imageBounds = new Rectangle(position, Bounds.Top + IconPadding + offset, size, size - offset);
int right = Bounds.Right;
if (imageBounds.Right > right)
{
imageBounds.Width -= imageBounds.Right - right;
}
if (imageBounds.Width > 0 && imageBounds.Height > 0)
{
graphics.DrawImage(image, imageBounds);
}
position += imageBounds.Width;
draw = true;
}
}
}
return draw;
}
protected void DrawEdge(Graphics graphics, Rectangle bounds)
{
bounds.Height--; // The pen draws outside the frame otherwise.
bounds.Width--; // The pen draws outside the frame otherwise.
if (bounds.Width > 0)
{
if (IsCurrentControl)
{
ControlPaint.DrawFocusRectangle(graphics, bounds);
}
else
{
graphics.DrawRectangle(EdgePen, bounds);
}
}
}
protected void DrawRectangle(Graphics graphics, Rectangle bounds, Pen pen)
{
int penWidth = (int)pen.Width;
bounds.Height -= penWidth; // The pen draws outside the frame otherwise.
bounds.Width -= penWidth; // The pen draws outside the frame otherwise.
bounds.Offset(penWidth / 2, penWidth / 2);
if (bounds.Width > 0)
{
graphics.DrawRectangle(pen, bounds);
}
}
protected virtual void FillBackground(Graphics graphics, Rectangle bounds)
{
FillBackground(graphics, bounds, BaseColor);
}
protected virtual void FillBackground(Graphics graphics, Rectangle bounds, Color color)
{
if (bounds.Width <= 0)
return;
if (Finite)
{
graphics.FillRectangle(BackgroundBrush(bounds, color), bounds);
}
else
{
var brush = new LinearGradientBrush(bounds, color, color.Add(150).WithAlpha(100), LinearGradientMode.Horizontal);
graphics.FillRectangle(brush, bounds);
}
}
/// <summary>
/// Paints a label in form : "type:value" with the label in a different color.
/// </summary>
protected void DrawText(Graphics graphics, string label, string value, int line = -1, Color? textColor = null)
{
const int lineCount = 2;
var rect = Bounds;
var iconWidth = TextLeft;
rect.Width -= iconWidth;
rect.X += iconWidth;
if (line >= 0)
{
rect.Height /= lineCount;
rect.Y += line * rect.Height;
}
if (rect.Width <= 0)
{
return;
}
var baseColor = Selected ? Color.FromArgb(60, 60, 60) : textColor ?? Color.White;
if (!Model.IsActive)
{
baseColor = baseColor.Add(-200);
}
var brush = StyleSheet.Brushes[baseColor];
graphics.DrawString(label, DefaultFont, brush, rect);
int width = (int)graphics.MeasureString(label, DefaultFont, rect.Width).Width;
rect.Width -= width;
rect.X += width;
if (rect.Width <= 0)
{
return;
}
graphics.DrawString(value, DefaultFont, brush, rect, StyleSheet.EllipsisStringFormat);
}
public override string ToString()
{
return "Clip : " + Model;
}
#endregion
#region Handlers
protected virtual void ModelChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
Invalidate();
}
#endregion
#region Clone on drag
public WindowlessControl Clone()
{
var clone = _model.Clone();
_model.Tape.Clips.Add(clone);
return Parent.Controls.OfType<ClipWidget>().Single(clip => clip.Model == clone);
}
#endregion
#region Nested
/// <summary>
/// Responsible of painting the keys (they can even be outside the clip ...)
/// </summary>
private class CurveKeysWidget : WindowlessControl
{
private static readonly Image CurveKeyImage = Properties.Resources.Icon_16x16_curveKey;
private readonly ClipWidget _parent;
public CurveKeysWidget(ClipWidget parent)
{
_parent = parent;
_parent.BoundsChanged += Parent_BoundsChanged;
_parent.Model.Curves.ItemChanged += Curves_ItemChanged;
UpdateBounds();
}
protected override void Dispose(bool disposing)
{
_parent.Model.Curves.ItemChanged += Curves_ItemChanged;
base.Dispose(disposing);
}
public override string ToolTipText
{
get { return _parent.ToolTipText; }
}
private void UpdateBounds()
{
if (_parent.Timeline != null)
{
int start = _parent.Model.StartFrame;
int stop = _parent.Model.Curves.SelectMany(curve => curve.Keys).Select(key => (int)key.Frame + start).DefaultIfEmpty(start).Max();
int imageWidth = CurveKeyImage.Width;
int left = _parent.Timeline.PixelFromFrame(start) - imageWidth;
int right = _parent.Timeline.PixelFromFrame(stop) + imageWidth;
Bounds = Rectangle.FromLTRB(left, _parent.Bounds.Top, right, _parent.Bounds.Bottom);
}
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
const int minimumWidth = 30;
if (_parent.Width < minimumWidth)
{
return;
}
float halfWidth = CurveKeyImage.Size.Width / 2f;
float y = Bounds.Bottom - CurveKeyImage.Height;
var timeline = _parent.Timeline;
var startFrame = _parent.Model.StartFrame;
foreach (var frame in _parent.Model.Curves.SelectMany(curve => curve.Keys).Select(key => key.Frame).Distinct())
{
float x = timeline.PixelFromFrame(frame + startFrame) - halfWidth;
e.Graphics.DrawImage(CurveKeyImage, x, y);
}
}
private void Curves_ItemChanged(object sender, ItemChangedEventArgs<BezierCurveFloat> args)
{
UpdateBounds();
}
void Parent_BoundsChanged(object sender, EventArgs e)
{
UpdateBounds();
}
}
#endregion
}
}