Browse Source

Key frame visibility

flabbet 1 year ago
parent
commit
f3d9a8745c

+ 8 - 0
src/PixiEditor.AvaloniaUI/Models/DocumentModels/DocumentUpdater.cs

@@ -139,6 +139,9 @@ internal class DocumentUpdater
             case KeyFrameLength_ChangeInfo info:
                 ProcessKeyFrameLength(info);
                 break;
+            case KeyFrameVisibility_ChangeInfo info:
+                ProcessKeyFrameVisibility(info);
+                break;
         }
     }
 
@@ -426,4 +429,9 @@ internal class DocumentUpdater
     {
         doc.AnimationHandler.SetFrameLength(info.KeyFrameGuid, info.StartFrame, info.Duration);
     }
+    
+    private void ProcessKeyFrameVisibility(KeyFrameVisibility_ChangeInfo info)
+    {
+        doc.AnimationHandler.SetKeyFrameVisibility(info.KeyFrameId, info.IsVisible);
+    }
 }

+ 1 - 0
src/PixiEditor.AvaloniaUI/Models/Handlers/IAnimationHandler.cs

@@ -7,6 +7,7 @@ internal interface IAnimationHandler
     public void CreateRasterKeyFrame(Guid targetLayerGuid, int frame, bool cloneFromExisting);
     public void SetActiveFrame(int newFrame);
     public void SetFrameLength(Guid keyFrameId, int newStartFrame, int newDuration);
+    public void SetKeyFrameVisibility(Guid infoKeyFrameId, bool infoIsVisible);
     public bool FindKeyFrame<T>(Guid guid, out T keyFrameHandler) where T : IKeyFrameHandler;
     internal void AddKeyFrame(IKeyFrameHandler keyFrame);
     internal void RemoveKeyFrame(Guid keyFrameId);

+ 1 - 0
src/PixiEditor.AvaloniaUI/Models/Handlers/IKeyFrameHandler.cs

@@ -9,5 +9,6 @@ internal interface IKeyFrameHandler
     public int DurationBindable { get; }
     public Guid LayerGuid { get; }
     public Guid Id { get; }
+    public bool IsVisible { get; }
     public IDocument Document { get; }
 }

+ 4 - 0
src/PixiEditor.AvaloniaUI/Models/Rendering/AffectedAreasGatherer.cs

@@ -116,6 +116,10 @@ internal class AffectedAreasGatherer
                     AddWholeCanvasToMainImage();
                     AddWholeCanvasToEveryImagePreview();
                     break;
+                case KeyFrameVisibility_ChangeInfo info:
+                    AddWholeCanvasToMainImage();
+                    AddWholeCanvasToEveryImagePreview();
+                    break;
             }
         }
     }

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Rendering/MemberPreviewUpdater.cs

@@ -517,7 +517,7 @@ internal class MemberPreviewUpdater
     
     private static bool IsWithinRange(IKeyFrameHandler keyFrame, int frame)
     {
-        return keyFrame.StartFrameBindable <= frame && frame < keyFrame.StartFrameBindable + keyFrame.DurationBindable;
+        return keyFrame.IsVisible && keyFrame.StartFrameBindable <= frame && frame < keyFrame.StartFrameBindable + keyFrame.DurationBindable;
     }
 
     /// <summary>

+ 4 - 0
src/PixiEditor.AvaloniaUI/Styles/Templates/KeyFrame.axaml

@@ -52,6 +52,10 @@
         <Style Selector="^:selected /template/ Border#PreviewBorder">
             <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccent2Color}" />
         </Style>
+        
+        <Style Selector="^:disabled">
+            <Setter Property="Opacity" Value="0.5" />
+        </Style>
     </ControlTheme>
 
 </ResourceDictionary>

+ 3 - 2
src/PixiEditor.AvaloniaUI/Styles/Templates/Timeline.axaml

@@ -36,7 +36,6 @@
                                 Command="{TemplateBinding DeleteKeyFrameCommand}" 
                                 IsEnabled="{Binding !!SelectedKeyFrame, RelativeSource={RelativeSource TemplatedParent}}"
                                 CommandParameter="{Binding SelectedKeyFrame.Id, RelativeSource={RelativeSource TemplatedParent}}"/>
-                        <TextBlock Text="{Binding Scale, RelativeSource={RelativeSource TemplatedParent}}" />
                         <input:NumberInput Min="1"
                                            Value="{Binding Fps, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
                         <Panel>
@@ -84,7 +83,8 @@
                                 ItemsSource="{Binding KeyFrames, RelativeSource={RelativeSource TemplatedParent}}">
                                 <ItemsControl.DataTemplates>
                                     <DataTemplate DataType="document:KeyFrameGroupViewModel">
-                                        <animations:TimelineGroupHeader Height="70" Item="{Binding}" />
+                                        <animations:TimelineGroupHeader Height="70" 
+                                                                        Item="{Binding}" />
                                     </DataTemplate>
                                 </ItemsControl.DataTemplates>
                             </ItemsControl>
@@ -120,6 +120,7 @@
                                         <DataTemplate DataType="document:RasterKeyFrameViewModel">
                                             <animations:KeyFrame
                                                 Scale="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
+                                                IsEnabled="{Binding IsVisible}"
                                                 Item="{Binding}">
                                                 <animations:KeyFrame.Width>
                                                     <MultiBinding Converter="{converters:DurationToWidthConverter}">

+ 4 - 1
src/PixiEditor.AvaloniaUI/Styles/Templates/TimelineGroupHeader.axaml

@@ -8,7 +8,10 @@
         <Setter Property="Template">
             <ControlTemplate>
                 <Border BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderThickness="1">
-                    <TextBlock Text="{Binding Item.LayerName, RelativeSource={RelativeSource TemplatedParent}}"/>
+                    <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
+                        <CheckBox Classes="ImageCheckBox" IsChecked="{Binding Item.IsVisible, RelativeSource={RelativeSource TemplatedParent}}"/>
+                        <TextBlock Text="{Binding Item.LayerName, RelativeSource={RelativeSource TemplatedParent}}" />
+                    </StackPanel>
                 </Border>
             </ControlTemplate>
         </Setter>

+ 9 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Document/AnimationDataViewModel.cs

@@ -69,6 +69,15 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
             keyFrames.NotifyCollectionChanged();
         }
     }
+    
+    public void SetKeyFrameVisibility(Guid keyFrameId, bool isVisible)
+    {
+        if(TryFindKeyFrame(keyFrameId, out KeyFrameViewModel keyFrame))
+        {
+            keyFrame.SetVisibility(isVisible);
+            keyFrames.NotifyCollectionChanged();
+        }
+    }
 
     public void AddKeyFrame(IKeyFrameHandler keyFrame)
     {

+ 13 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameGroupViewModel.cs

@@ -14,6 +14,19 @@ internal class KeyFrameGroupViewModel : KeyFrameViewModel, IKeyFrameGroupHandler
 
     public string LayerName => Document.StructureHelper.Find(LayerGuid).NameBindable;
 
+    public override void SetVisibility(bool isVisible)
+    {
+        foreach (var child in Children)
+        {
+            if(child is KeyFrameViewModel keyFrame)
+            {
+                keyFrame.SetVisibility(isVisible);
+            }
+        }
+        
+        base.SetVisibility(isVisible);
+    }
+
     public KeyFrameGroupViewModel(int startFrame, int duration, Guid layerGuid, Guid id, DocumentViewModel doc, DocumentInternalParts internalParts) 
         : base(startFrame, duration, layerGuid, id, doc, internalParts)
     {

+ 19 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameViewModel.cs

@@ -11,6 +11,7 @@ internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
     private Surface? previewSurface;
     private int startFrameBindable;
     private int durationBindable;
+    private bool isVisibleBindable = true;
 
     public DocumentViewModel Document { get; }
     protected DocumentInternalParts Internals { get; }
@@ -61,6 +62,18 @@ internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
         }
     }
 
+    public virtual bool IsVisible
+    {
+        get => isVisibleBindable;
+        set
+        {
+            if(!Document.UpdateableChangeActive)
+            {
+                Internals.ActionAccumulator.AddFinishedActions(new KeyFrameVisibility_Action(Id, value));
+            }
+        }
+    }
+
     public Guid LayerGuid { get; }
     public Guid Id { get; }
 
@@ -99,4 +112,10 @@ internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
     {
         Internals.ActionAccumulator.AddFinishedActions(new EndKeyFrameLength_Action());
     }
+
+    public virtual void SetVisibility(bool isVisible)
+    {
+        isVisibleBindable = isVisible;
+        OnPropertyChanged(nameof(IsVisible));
+    }
 }

+ 3 - 0
src/PixiEditor.ChangeableDocument/ChangeInfos/Animation/KeyFrameVisibility_ChangeInfo.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.ChangeableDocument.ChangeInfos.Animation;
+
+public record KeyFrameVisibility_ChangeInfo(Guid KeyFrameId, bool IsVisible) : IChangeInfo;

+ 21 - 10
src/PixiEditor.ChangeableDocument/Changeables/Animations/AnimationData.cs

@@ -115,25 +115,36 @@ internal class AnimationData : IReadOnlyAnimationData
     {
         foreach (var keyFrame in root)
         {
-            bool isWithinRange = keyFrame.IsWithinRange(ActiveFrame);
-            if (lastActiveKeyFrames.Contains(keyFrame))
+            if (!keyFrame.IsVisible)
             {
-                if (!isWithinRange)
+                if (lastActiveKeyFrames.Contains(keyFrame))
                 {
                     keyFrame.Deactivated(ActiveFrame);
                     lastActiveKeyFrames.Remove(keyFrame);
                 }
-                else
+            }
+            else
+            {
+                bool isWithinRange = keyFrame.IsWithinRange(ActiveFrame);
+                if (lastActiveKeyFrames.Contains(keyFrame))
+                {
+                    if (!isWithinRange)
+                    {
+                        keyFrame.Deactivated(ActiveFrame);
+                        lastActiveKeyFrames.Remove(keyFrame);
+                    }
+                    else
+                    {
+                        keyFrame.ActiveFrameChanged(ActiveFrame);
+                    }
+                }
+                else if (isWithinRange)
                 {
                     keyFrame.ActiveFrameChanged(ActiveFrame);
+                    lastActiveKeyFrames.Add(keyFrame);
                 }
             }
-            else if (isWithinRange)
-            {
-                keyFrame.ActiveFrameChanged(ActiveFrame);
-                lastActiveKeyFrames.Add(keyFrame);
-            }
-            
+
             if (keyFrame is GroupKeyFrame group)
             {
                 UpdateKeyFrames(group.Children);

+ 9 - 0
src/PixiEditor.ChangeableDocument/Changeables/Animations/GroupKeyFrame.cs

@@ -4,6 +4,7 @@ internal class GroupKeyFrame : KeyFrame
 {
     private ChunkyImage originalLayerImage;
     private Document document;
+    private bool isVisible = true;
     public List<KeyFrame> Children { get; } = new List<KeyFrame>();
     public override int Duration => Children.Count > 0 ? Children.Max(x => x.StartFrame + x.Duration) - StartFrame : 0;
     public override int StartFrame => Children.Count > 0 ? Children.Min(x => x.StartFrame) : 0;
@@ -19,6 +20,14 @@ internal class GroupKeyFrame : KeyFrame
         }
     }
 
+    protected override void OnVisibilityChanged()
+    {
+        foreach (var child in Children)
+        {
+            child.IsVisible = IsVisible;
+        }
+    }
+
     public override void Deactivated(int atFrame)
     {
         //if(atFrame >= EndFrame) return;

+ 16 - 5
src/PixiEditor.ChangeableDocument/Changeables/Animations/KeyFrame.cs

@@ -6,6 +6,7 @@ public abstract class KeyFrame : IReadOnlyKeyFrame, IDisposable
 {
     private int startFrame;
     private int duration;
+    private bool isVisible = true;
     
     public event Action KeyFrameChanged;
 
@@ -44,6 +45,17 @@ public abstract class KeyFrame : IReadOnlyKeyFrame, IDisposable
     public Guid LayerGuid { get; }
     public Guid Id { get; set; }
 
+    public bool IsVisible
+    {
+        get => isVisible;
+        set
+        {
+            isVisible = value;
+            OnVisibilityChanged();
+            KeyFrameChanged?.Invoke();
+        }
+    }
+
     protected KeyFrame(Guid layerGuid, int startFrame)
     {
         LayerGuid = layerGuid;
@@ -51,7 +63,7 @@ public abstract class KeyFrame : IReadOnlyKeyFrame, IDisposable
         duration = 1;
         Id = Guid.NewGuid();
     }
-
+    
     public virtual void ActiveFrameChanged(int atFrame) { }
     public virtual void Deactivated(int atFrame) { }
 
@@ -62,8 +74,7 @@ public abstract class KeyFrame : IReadOnlyKeyFrame, IDisposable
 
     public abstract KeyFrame Clone();
 
-    public virtual void Dispose()
-    {
-        
-    }
+    public virtual void Dispose() { }
+    
+    protected virtual void OnVisibilityChanged() { }
 }

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyKeyFrame.cs

@@ -6,7 +6,8 @@ public interface IReadOnlyKeyFrame
     public int Duration { get; }
     public Guid LayerGuid { get; }
     public Guid Id { get; }
-
+    public bool IsVisible { get; }
+    
     public void ActiveFrameChanged(int atFrame);
     public void Deactivated(int atFrame);
 }

+ 53 - 0
src/PixiEditor.ChangeableDocument/Changes/Animation/KeyFrameVisibility_Change.cs

@@ -0,0 +1,53 @@
+using PixiEditor.ChangeableDocument.Changeables.Animations;
+using PixiEditor.ChangeableDocument.ChangeInfos.Animation;
+
+namespace PixiEditor.ChangeableDocument.Changes.Animation;
+
+internal class KeyFrameVisibility_Change : Change
+{
+    private readonly Guid _keyFrameId;
+    private bool _isVisible;
+    private bool _originalVisibility;
+    
+    [GenerateMakeChangeAction]
+    public KeyFrameVisibility_Change(Guid keyFrameId, bool isVisible)
+    {
+        _keyFrameId = keyFrameId;
+        _isVisible = isVisible;
+    }
+    
+    public override bool InitializeAndValidate(Document target)
+    {
+        if (target.AnimationData.TryFindKeyFrame<KeyFrame>(_keyFrameId, out KeyFrame keyFrame))
+        {
+            _originalVisibility = keyFrame.IsVisible;
+            return true;
+        }
+        
+        return false;
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    {
+        if (target.AnimationData.TryFindKeyFrame<KeyFrame>(_keyFrameId, out var keyFrame))
+        {
+            keyFrame.IsVisible = _isVisible;
+            ignoreInUndo = false;
+            return new KeyFrameVisibility_ChangeInfo(_keyFrameId, _isVisible);
+        }
+        
+        ignoreInUndo = true;
+        return new None();
+    }
+
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
+    {
+        if (target.AnimationData.TryFindKeyFrame<KeyFrame>(_keyFrameId, out var keyFrame))
+        {
+            keyFrame.IsVisible = _originalVisibility;
+            return new KeyFrameVisibility_ChangeInfo(_keyFrameId, !_isVisible);
+        }
+        
+        return new None();
+    }
+}