Browse Source

Frontend keyframes modification

flabbet 1 year ago
parent
commit
3697b99660

+ 28 - 0
src/PixiEditor.AvaloniaUI/Helpers/Converters/DurationToMarginConverter.cs

@@ -0,0 +1,28 @@
+using System.Globalization;
+using Avalonia;
+
+namespace PixiEditor.AvaloniaUI.Helpers.Converters;
+
+internal class DurationToMarginConverter : SingleInstanceMultiValueConverter<DurationToMarginConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        return Convert(new List<object?> { value }, targetType, parameter, culture);
+    }
+
+    public override object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (values.Count != 2)
+        {
+            return 0;
+            throw new ArgumentException("DurationToWidthConverter requires 2 values");
+        }
+        
+        if(values[0] is int duration && values[1] is double scale)
+        {
+            return new Thickness(duration * scale, 0, 0, 0);
+        }
+        
+        return 0;
+    }
+}

+ 1 - 0
src/PixiEditor.AvaloniaUI/Helpers/Converters/DurationToWidthConverter.cs

@@ -13,6 +13,7 @@ internal class DurationToWidthConverter : SingleInstanceMultiValueConverter<Dura
     {
     {
         if (values.Count != 2)
         if (values.Count != 2)
         {
         {
+            return 0;
             throw new ArgumentException("DurationToWidthConverter requires 2 values");
             throw new ArgumentException("DurationToWidthConverter requires 2 values");
         }
         }
         
         

+ 29 - 0
src/PixiEditor.AvaloniaUI/Helpers/UI/GridDefinitions.cs

@@ -0,0 +1,29 @@
+using Avalonia;
+using Avalonia.Controls;
+
+namespace PixiEditor.AvaloniaUI.Helpers.UI;
+
+public class GridDefinitions : AvaloniaObject
+{
+    public static readonly AttachedProperty<ColumnDefinitions> ColumnDefinitionsBindableProperty =
+        AvaloniaProperty.RegisterAttached<GridDefinitions, Grid, ColumnDefinitions>("ColumnDefinitionsBindable");
+
+    public static void SetColumnDefinitionsBindable(Grid obj, ColumnDefinitions value)
+    {
+        obj.SetValue(ColumnDefinitionsBindableProperty, value);
+        obj.ColumnDefinitions = value;
+    }
+
+    public static ColumnDefinitions GetColumnDefinitionsBindable(Grid obj) => obj.GetValue(ColumnDefinitionsBindableProperty);
+    
+    public static readonly AttachedProperty<RowDefinitions> RowDefinitionsBindableProperty =
+        AvaloniaProperty.RegisterAttached<GridDefinitions, Grid, RowDefinitions>("RowDefinitionsBindable");
+    
+    public static void SetRowDefinitionsBindable(Grid obj, RowDefinitions value)
+    {
+        obj.SetValue(RowDefinitionsBindableProperty, value);
+        obj.RowDefinitions = value;
+    }
+
+    public static RowDefinitions GetRowDefinitionsBindable(Grid obj) => obj.GetValue(RowDefinitionsBindableProperty);
+}

+ 32 - 28
src/PixiEditor.AvaloniaUI/Styles/Templates/KeyFrame.axaml

@@ -5,37 +5,41 @@
                     xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
                     xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
                     xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters">
                     xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters">
     <ControlTheme TargetType="animations:KeyFrame" x:Key="{x:Type animations:KeyFrame}">
     <ControlTheme TargetType="animations:KeyFrame" x:Key="{x:Type animations:KeyFrame}">
-        <Setter Property="ClipToBounds" Value="False"/>
+        <Setter Property="ClipToBounds" Value="False" />
         <Setter Property="Template">
         <Setter Property="Template">
             <ControlTemplate>
             <ControlTemplate>
-                <Border CornerRadius="{DynamicResource ControlCornerRadius}" Background="{DynamicResource ThemeBackgroundBrush1}" Height="20" ClipToBounds="False"
-                      BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderThickness="1">
-                    <Border.Width>
-                        <MultiBinding Converter="{converters:DurationToWidthConverter}">
-                            <Binding Path="Item.Duration" RelativeSource="{RelativeSource TemplatedParent}"/>
-                            <Binding Path="Scale" RelativeSource="{RelativeSource TemplatedParent}"/>
-                        </MultiBinding>
-                    </Border.Width>
-                    <Border CornerRadius="{DynamicResource ControlCornerRadius}" Margin="0, 0, 0, 0" Width="30" Height="30" BorderThickness="1" VerticalAlignment="Center"
-                            HorizontalAlignment="Center" BorderBrush="{DynamicResource ThemeBorderMidBrush}" 
-                            RenderOptions.BitmapInterpolationMode="None">
-                        <Border.Background>
-                            <ImageBrush Source="/Images/CheckerTile.png" TileMode="Tile">
-                                <ImageBrush.Transform>
-                                    <ScaleTransform ScaleX="0.4" ScaleY="0.4"/>
-                                </ImageBrush.Transform>
-                            </ImageBrush>
-                        </Border.Background>
-                        <visuals:SurfaceControl Surface="{Binding Item.PreviewSurface, RelativeSource={RelativeSource TemplatedParent}}" Stretch="Uniform" Width="30" Height="30">
-                            <ui:RenderOptionsBindable.BitmapInterpolationMode>
-                                <MultiBinding Converter="{converters:WidthToBitmapScalingModeConverter}">
-                                    <Binding Path="Item.PreviewSurface.Size.X" RelativeSource="{RelativeSource TemplatedParent}"/>
-                                    <Binding RelativeSource="{RelativeSource Mode=Self}" Path="Bounds.Width"/>
-                                </MultiBinding>
-                            </ui:RenderOptionsBindable.BitmapInterpolationMode>
-                        </visuals:SurfaceControl>
+                <Grid>
+                    <Border CornerRadius="{DynamicResource ControlCornerRadius}"
+                            Background="{DynamicResource ThemeBackgroundBrush1}" Height="20" ClipToBounds="False"
+                            BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderThickness="1">
+                        <Border CornerRadius="{DynamicResource ControlCornerRadius}" Width="30" Height="30"
+                                BorderThickness="1" VerticalAlignment="Center"
+                                HorizontalAlignment="Left" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+                                RenderOptions.BitmapInterpolationMode="None">
+                            <Border.Background>
+                                <ImageBrush Source="/Images/CheckerTile.png" TileMode="Tile">
+                                    <ImageBrush.Transform>
+                                        <ScaleTransform ScaleX="0.4" ScaleY="0.4" />
+                                    </ImageBrush.Transform>
+                                </ImageBrush>
+                            </Border.Background>
+                            <visuals:SurfaceControl
+                                Surface="{Binding Item.PreviewSurface, RelativeSource={RelativeSource TemplatedParent}}"
+                                Stretch="Uniform" Width="30" Height="30">
+                                <ui:RenderOptionsBindable.BitmapInterpolationMode>
+                                    <MultiBinding Converter="{converters:WidthToBitmapScalingModeConverter}">
+                                        <Binding Path="Item.PreviewSurface.Size.X"
+                                                 RelativeSource="{RelativeSource TemplatedParent}" />
+                                        <Binding RelativeSource="{RelativeSource Mode=Self}" Path="Bounds.Width" />
+                                    </MultiBinding>
+                                </ui:RenderOptionsBindable.BitmapInterpolationMode>
+                            </visuals:SurfaceControl>
+                        </Border>
                     </Border>
                     </Border>
-                </Border>
+                    
+                    <Panel HorizontalAlignment="Right" Name="PART_ResizePanelRight" Width="5" Cursor="SizeWestEast" Background="Transparent" ZIndex="1"/>
+                    <Panel HorizontalAlignment="Left" Name="PART_ResizePanelLeft" Width="5" Cursor="SizeWestEast" Background="Transparent" ZIndex="1"/>
+                </Grid>
             </ControlTemplate>
             </ControlTemplate>
         </Setter>
         </Setter>
     </ControlTheme>
     </ControlTheme>

+ 58 - 42
src/PixiEditor.AvaloniaUI/Styles/Templates/Timeline.axaml

@@ -6,7 +6,8 @@
                     xmlns:behaviours="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Behaviours"
                     xmlns:behaviours="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Behaviours"
                     xmlns:commands="clr-namespace:PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands"
                     xmlns:commands="clr-namespace:PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands"
                     xmlns:xaml="clr-namespace:PixiEditor.AvaloniaUI.Models.Commands.XAML"
                     xmlns:xaml="clr-namespace:PixiEditor.AvaloniaUI.Models.Commands.XAML"
-                    xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters">
+                    xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
+                    xmlns:ui="clr-namespace:PixiEditor.AvaloniaUI.Helpers.UI">
     <Design.PreviewWith>
     <Design.PreviewWith>
         <animations:Timeline>
         <animations:Timeline>
             <animations:Timeline.KeyFrames>
             <animations:Timeline.KeyFrames>
@@ -18,7 +19,7 @@
     <ControlTheme TargetType="animations:Timeline" x:Key="{x:Type animations:Timeline}">
     <ControlTheme TargetType="animations:Timeline" x:Key="{x:Type animations:Timeline}">
         <Setter Property="Template">
         <Setter Property="Template">
             <ControlTemplate>
             <ControlTemplate>
-                <Grid  Background="{DynamicResource ThemeBackgroundBrush1}">
+                <Grid Background="{DynamicResource ThemeBackgroundBrush1}">
                     <Grid.RowDefinitions>
                     <Grid.RowDefinitions>
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="Auto" />
@@ -74,46 +75,61 @@
                         </animations:TimelineSlider>
                         </animations:TimelineSlider>
                     </DockPanel>
                     </DockPanel>
 
 
-                    <ScrollViewer Background="{DynamicResource ThemeBackgroundBrush}" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="2" HorizontalScrollBarVisibility="Auto">
-                        <TreeView ItemsSource="{TemplateBinding KeyFrames}">
-                            <TreeView.ItemContainerTheme>
-                                <ControlTheme TargetType="TreeViewItem">
-                                    <Setter Property="ClipToBounds" Value="False" />
-                                    <Setter Property="Template">
-                                        <ControlTemplate>
-                                            <StackPanel Orientation="Horizontal">
-                                                <ContentPresenter Name="PART_HeaderPresenter" Margin="0" Padding="0"
-                                                                  Background="Transparent"
-                                                                  HorizontalContentAlignment="{TemplateBinding HorizontalAlignment}"
-                                                                  Content="{TemplateBinding Header}"
-                                                                  ContentTemplate="{TemplateBinding HeaderTemplate}"
-                                                                  Focusable="False" />
-                                                <ItemsPresenter Name="PART_ItemsPresenter">
-                                                    <ItemsPresenter.ItemsPanel>
-                                                        <ItemsPanelTemplate>
-                                                            <StackPanel Orientation="Horizontal" />
-                                                        </ItemsPanelTemplate>
-                                                    </ItemsPresenter.ItemsPanel>
-                                                </ItemsPresenter>
-                                            </StackPanel>
-                                        </ControlTemplate>
-                                    </Setter>
-                                </ControlTheme>
-                            </TreeView.ItemContainerTheme>
-                            <TreeView.DataTemplates>
-                                <TreeDataTemplate DataType="document:KeyFrameGroupViewModel"
-                                                  ItemsSource="{Binding Children}">
-                                    <Grid Width="200">
-                                        <TextBlock Text="{Binding StartFrame}" />
-                                    </Grid>
-                                </TreeDataTemplate>
-                                <DataTemplate DataType="document:RasterKeyFrameViewModel">
-                                    <animations:KeyFrame Margin="0 10"
-                                        Scale="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
-                                        Item="{Binding}" />
-                                </DataTemplate>
-                            </TreeView.DataTemplates>
-                        </TreeView>
+                    <ScrollViewer Background="{DynamicResource ThemeBackgroundBrush}" Grid.Column="0"
+                                  Grid.ColumnSpan="2" Grid.Row="2" HorizontalScrollBarVisibility="Auto">
+                            <TreeView ItemsSource="{TemplateBinding KeyFrames}">
+                                <TreeView.ItemContainerTheme>
+                                    <ControlTheme TargetType="TreeViewItem">
+                                        <Setter Property="ClipToBounds" Value="False" />
+                                        <Setter Property="Margin">
+                                            <MultiBinding Converter="{converters:DurationToMarginConverter}">
+                                                <Binding Path="DataContext.StartFrame" RelativeSource="{RelativeSource Self}" />
+                                                <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=animations:Timeline}" Path="Scale" />
+                                            </MultiBinding>
+                                        </Setter>
+                                        <Setter Property="Template">
+                                            <ControlTemplate>
+                                                <StackPanel Orientation="Horizontal">
+                                                    <ContentPresenter Name="PART_HeaderPresenter" Margin="0"
+                                                                      Padding="0"
+                                                                      Background="Transparent"
+                                                                      HorizontalContentAlignment="{TemplateBinding HorizontalAlignment}"
+                                                                      Content="{TemplateBinding Header}"
+                                                                      ContentTemplate="{TemplateBinding HeaderTemplate}"
+                                                                      Focusable="False" />
+                                                    <ItemsPresenter Name="PART_ItemsPresenter">
+                                                        <ItemsPresenter.ItemsPanel>
+                                                            <ItemsPanelTemplate>
+                                                                <Grid/>
+                                                            </ItemsPanelTemplate>
+                                                        </ItemsPresenter.ItemsPanel>
+                                                    </ItemsPresenter>
+                                                </StackPanel>
+                                            </ControlTemplate>
+                                        </Setter>
+                                    </ControlTheme>
+                                </TreeView.ItemContainerTheme>
+                                <TreeView.DataTemplates>
+                                    <TreeDataTemplate DataType="document:KeyFrameGroupViewModel"
+                                                      ItemsSource="{Binding Children}">
+                                        <Grid Width="200">
+                                            <TextBlock Text="{Binding StartFrame}" />
+                                        </Grid>
+                                    </TreeDataTemplate>
+                                    <DataTemplate DataType="document:RasterKeyFrameViewModel">
+                                        <animations:KeyFrame Margin="0 10"
+                                                             Scale="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
+                                                             Item="{Binding}">
+                                            <animations:KeyFrame.Width>
+                                                <MultiBinding Converter="{converters:DurationToWidthConverter}">
+                                                    <Binding Path="Duration" />
+                                                    <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType=animations:Timeline}" Path="Scale" />
+                                                </MultiBinding>
+                                            </animations:KeyFrame.Width>
+                                        </animations:KeyFrame>
+                                    </DataTemplate>
+                                </TreeView.DataTemplates>
+                            </TreeView>
                     </ScrollViewer>
                     </ScrollViewer>
                 </Grid>
                 </Grid>
             </ControlTemplate>
             </ControlTemplate>

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameCollection.cs

@@ -9,7 +9,7 @@ public class KeyFrameCollection : ObservableCollection<KeyFrameViewModel>
 {
 {
     public KeyFrameCollection()
     public KeyFrameCollection()
     {
     {
-
+        
     }
     }
 
 
     public void NotifyCollectionChanged()
     public void NotifyCollectionChanged()

+ 30 - 3
src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameViewModel.cs

@@ -7,15 +7,42 @@ namespace PixiEditor.AvaloniaUI.ViewModels.Document;
 public abstract class KeyFrameViewModel(int startFrame, int duration, Guid layerGuid, Guid id) : ObservableObject, IKeyFrameHandler
 public abstract class KeyFrameViewModel(int startFrame, int duration, Guid layerGuid, Guid id) : ObservableObject, IKeyFrameHandler
 {
 {
     private Surface? previewSurface;
     private Surface? previewSurface;
+    private int _startFrame = startFrame;
+    private int _duration = duration;
     
     
     public Surface? PreviewSurface
     public Surface? PreviewSurface
     {
     {
         get => previewSurface;
         get => previewSurface;
         set => SetProperty(ref previewSurface, value);
         set => SetProperty(ref previewSurface, value);
     }
     }
-    
-    public virtual int StartFrame { get; } = startFrame;
-    public virtual int Duration { get; } = duration;
+
+    public virtual int StartFrame
+    {
+        get => _startFrame;
+        set
+        {
+            if (value < 0)
+            {
+                value = 0;
+            }
+            
+            SetProperty(ref _startFrame, value);
+        }
+    }
+    public virtual int Duration
+    {
+        get => _duration;
+        set
+        {
+            if (value < 1)
+            {
+                value = 1;
+            }
+            
+            SetProperty(ref _duration, value);
+        }
+    }
+
     public Guid LayerGuid { get; } = layerGuid;
     public Guid LayerGuid { get; } = layerGuid;
     public Guid Id { get; } = id;
     public Guid Id { get; } = id;
 }
 }

+ 109 - 0
src/PixiEditor.AvaloniaUI/Views/Animations/KeyFrame.cs

@@ -1,9 +1,15 @@
 using Avalonia;
 using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.VisualTree;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 
 
 namespace PixiEditor.AvaloniaUI.Views.Animations;
 namespace PixiEditor.AvaloniaUI.Views.Animations;
 
 
+[TemplatePart("PART_ResizePanelRight", typeof(InputElement))]
+[TemplatePart("PART_ResizePanelLeft", typeof(InputElement))]
 public class KeyFrame : TemplatedControl
 public class KeyFrame : TemplatedControl
 {
 {
     public static readonly StyledProperty<KeyFrameViewModel> ItemProperty = AvaloniaProperty.Register<KeyFrame, KeyFrameViewModel>(
     public static readonly StyledProperty<KeyFrameViewModel> ItemProperty = AvaloniaProperty.Register<KeyFrame, KeyFrameViewModel>(
@@ -22,4 +28,107 @@ public class KeyFrame : TemplatedControl
         get { return (double)GetValue(ScaleProperty); }
         get { return (double)GetValue(ScaleProperty); }
         set { SetValue(ScaleProperty, value); }
         set { SetValue(ScaleProperty, value); }
     }
     }
+    
+    private InputElement _resizePanelRight;
+    private InputElement _resizePanelLeft;
+
+    private int clickFrameOffset;
+
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        base.OnApplyTemplate(e);
+        _resizePanelRight = e.NameScope.Find<InputElement>("PART_ResizePanelRight");
+        _resizePanelLeft = e.NameScope.Find<InputElement>("PART_ResizePanelLeft");
+
+        _resizePanelRight.PointerPressed += CapturePointer;
+        _resizePanelRight.PointerMoved += ResizePanelRightOnPointerMoved;
+
+        _resizePanelLeft.PointerPressed += CapturePointer;
+        _resizePanelLeft.PointerMoved += ResizePanelLeftOnPointerMoved;
+
+        PointerPressed += CapturePointer;
+        PointerMoved += DragOnPointerMoved;
+    }
+    
+    private void CapturePointer(object? sender, PointerPressedEventArgs e)
+    {
+        if (Item is null || e.Handled)
+        {
+            return;
+        }
+        
+        e.Pointer.Capture(sender as IInputElement);
+        clickFrameOffset = Item.StartFrame - (int)Math.Floor(e.GetPosition(this.FindAncestorOfType<Canvas>()).X / Scale);
+        e.Handled = true;
+    }
+
+    private void ResizePanelRightOnPointerMoved(object? sender, PointerEventArgs e)
+    {
+        if (Item is null)
+        {
+            return;
+        }
+        
+        if (e.GetCurrentPoint(_resizePanelRight).Properties.IsLeftButtonPressed)
+        {
+            Item.Duration = MousePosToFrame(e) - Item.StartFrame;
+        }
+        
+        e.Handled = true;
+    }
+    
+    private void ResizePanelLeftOnPointerMoved(object? sender, PointerEventArgs e)
+    {
+        if (Item is null)
+        {
+            return;
+        }
+        
+        if (e.GetCurrentPoint(_resizePanelLeft).Properties.IsLeftButtonPressed)
+        {
+            int frame = MousePosToFrame(e);
+            
+            
+            if (frame >= Item.StartFrame + Item.Duration)
+            {
+                frame = Item.StartFrame + Item.Duration - 1;
+            }
+            
+            int oldStartFrame = Item.StartFrame;
+            Item.StartFrame = frame;
+            Item.Duration += oldStartFrame - Item.StartFrame;
+        }
+        
+        e.Handled = true;
+    }
+    
+    private void DragOnPointerMoved(object? sender, PointerEventArgs e)
+    {
+        if (Item is null)
+        {
+            return;
+        }
+        
+        if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
+        {
+            var frame = MousePosToFrame(e, false);
+            Item.StartFrame = frame + clickFrameOffset;
+        }
+    }
+
+    private int MousePosToFrame(PointerEventArgs e, bool round = true)
+    {
+        double x = e.GetPosition(this.FindAncestorOfType<Canvas>()).X;
+        int frame;
+        if (round)
+        {
+            frame = (int)Math.Round(x / Scale);
+        }
+        else
+        {
+            frame = (int)Math.Floor(x / Scale);
+        }
+        
+        return frame;
+    }
 }
 }