Browse Source

Overlay transitions

Krzysztof Krysiński 1 year ago
parent
commit
424a6f6b07

+ 3 - 2
src/PixiEditor.AvaloniaUI/Views/Main/ViewportControls/ViewportOverlays.cs

@@ -5,6 +5,7 @@ using Avalonia.Data;
 using Avalonia.Input;
 using PixiEditor.AvaloniaUI.Helpers.Converters;
 using PixiEditor.AvaloniaUI.Models.Commands.XAML;
+using PixiEditor.AvaloniaUI.ViewModels;
 using PixiEditor.AvaloniaUI.Views.Overlays;
 using PixiEditor.AvaloniaUI.Views.Overlays.LineToolOverlay;
 using PixiEditor.AvaloniaUI.Views.Overlays.SelectionOverlay;
@@ -79,8 +80,8 @@ internal class ViewportOverlays
 
         Binding fadeOutBinding = new()
         {
-            Source = Viewport,
-            Path = "!Document.ToolsSubViewModel.ColorPickerToolViewModel.PickFromReferenceLayer",
+            Source = ViewModelMain.Current.ToolsSubViewModel,
+            Path = "!ActiveTool.PickFromReferenceLayer",
             Mode = BindingMode.OneWay,
         };
 

+ 58 - 2
src/PixiEditor.AvaloniaUI/Views/Overlays/Overlay.cs

@@ -1,11 +1,16 @@
 using System.Collections.Generic;
 using System.Linq;
 using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Animation.Easings;
 using Avalonia.Controls;
 using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Media;
+using Avalonia.Styling;
+using Avalonia.Threading;
 using PixiEditor.AvaloniaUI.Views.Overlays.Handles;
+using PixiEditor.AvaloniaUI.Views.Overlays.Transitions;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Extensions.UI.Overlays;
 
@@ -33,12 +38,16 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
     public event PointerEvent? PointerPressedOverlay;
     public event PointerEvent? PointerReleasedOverlay;
 
+    private readonly Dictionary<AvaloniaProperty, OverlayTransition> activeTransitions = new();
+
+    private DispatcherTimer? transitionTimer;
+
     public Overlay()
     {
         ZoomScaleProperty.Changed.Subscribe(OnZoomScaleChanged);
     }
 
-    protected virtual void ZoomChanged(double newZoom) { }
+    public abstract void RenderOverlay(DrawingContext context, RectD canvasBounds);
 
     public void Refresh()
     {
@@ -107,8 +116,55 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
         }
     }
 
-    public abstract void RenderOverlay(DrawingContext context, RectD canvasBounds);
+    protected void TransitionTo(AvaloniaProperty property, double durationSeconds, double to, Easing? easing = null)
+    {
+        object? from = GetValue(property);
+
+        if (from is not double fromDouble)
+        {
+            throw new InvalidOperationException("Property must be of type double");
+        }
+
+        activeTransitions[property] = new OverlayDoubleTransition(durationSeconds, fromDouble, to, easing);
+        if (activeTransitions.Count == 1)
+        {
+            StartTransitions();
+        }
+    }
+
+    private void StartTransitions()
+    {
+        transitionTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(16D), DispatcherPriority.Default, (sender, _) =>
+        {
+            ProgressTransitions(sender as DispatcherTimer);
+            Refresh();
+        });
+    }
+
+    private void ProgressTransitions(DispatcherTimer timer)
+    {
+        foreach (var transition in activeTransitions)
+        {
+            transition.Value.Progress += timer.Interval.TotalSeconds / transition.Value.DurationSeconds;
+            transition.Value.Progress = Math.Min(transition.Value.Progress, 1);
+            SetValue(transition.Key, transition.Value.Evaluate());
+        }
 
+        List<KeyValuePair<AvaloniaProperty, OverlayTransition>> transitionsToRemove = activeTransitions
+            .Where(t => t.Value.Progress >= 1).ToList();
+
+        foreach (var transition in transitionsToRemove)
+        {
+            activeTransitions.Remove(transition.Key);
+        }
+
+        if (activeTransitions.Count == 0)
+        {
+            timer.Stop();
+        }
+    }
+
+    protected virtual void ZoomChanged(double newZoom) { }
     protected virtual void OnOverlayPointerReleased(OverlayPointerArgs args)
     {
 

+ 33 - 13
src/PixiEditor.AvaloniaUI/Views/Overlays/ReferenceLayerOverlay.cs

@@ -1,9 +1,13 @@
 using System.ComponentModel;
+using System.Threading;
 using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Animation.Easings;
 using Avalonia.Controls;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
 using Avalonia.Media;
+using Avalonia.Styling;
 using ChunkyImageLib.DataHolders;
 using PixiEditor.AvaloniaUI.Helpers.Converters;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
@@ -14,14 +18,18 @@ namespace PixiEditor.AvaloniaUI.Views.Overlays;
 
 internal class ReferenceLayerOverlay : Overlay
 {
-    public static readonly StyledProperty<ReferenceLayerViewModel> ReferenceLayerProperty = AvaloniaProperty.Register<ReferenceLayerOverlay, ReferenceLayerViewModel>(
-        nameof(ReferenceLayerViewModel));
+    private const float OpacityTransitionDuration = 0.1f;
+    public static readonly StyledProperty<ReferenceLayerViewModel> ReferenceLayerProperty =
+        AvaloniaProperty.Register<ReferenceLayerOverlay, ReferenceLayerViewModel>(
+            nameof(ReferenceLayerViewModel));
 
-    public static readonly StyledProperty<bool> FadeOutProperty = AvaloniaProperty.Register<ReferenceLayerOverlay, bool>(
-        nameof(FadeOut), defaultValue: false);
+    public static readonly StyledProperty<bool> FadeOutProperty =
+        AvaloniaProperty.Register<ReferenceLayerOverlay, bool>(
+            nameof(FadeOut), defaultValue: false);
 
-    public static readonly StyledProperty<ShapeCorners> ReferenceShapeProperty = AvaloniaProperty.Register<ReferenceLayerOverlay, ShapeCorners>(
-        nameof(ReferenceShape));
+    public static readonly StyledProperty<ShapeCorners> ReferenceShapeProperty =
+        AvaloniaProperty.Register<ReferenceLayerOverlay, ShapeCorners>(
+            nameof(ReferenceShape));
 
     public ShapeCorners ReferenceShape
     {
@@ -35,7 +43,6 @@ internal class ReferenceLayerOverlay : Overlay
         set => SetValue(ReferenceLayerProperty, value);
     }
 
-
     public bool FadeOut
     {
         get => GetValue(FadeOutProperty);
@@ -46,7 +53,9 @@ internal class ReferenceLayerOverlay : Overlay
         ? (ReferenceShape.RectSize.X / (double)ReferenceLayer.ReferenceBitmap.Size.X)
         : 1);
 
-    public override OverlayRenderSorting OverlayRenderSorting => ReferenceLayer.IsTopMost ? OverlayRenderSorting.Foreground : OverlayRenderSorting.Background;
+    public override OverlayRenderSorting OverlayRenderSorting => ReferenceLayer.IsTopMost
+        ? OverlayRenderSorting.Foreground
+        : OverlayRenderSorting.Background;
 
     static ReferenceLayerOverlay()
     {
@@ -59,12 +68,16 @@ internal class ReferenceLayerOverlay : Overlay
         //TODO: opacity + animation + border
         if (ReferenceLayer is { ReferenceBitmap: not null })
         {
-            using var renderOptions = context.PushRenderOptions(new RenderOptions { BitmapInterpolationMode = ScaleToBitmapScalingModeConverter.Calculate(ReferenceLayerScale) });
+            using var renderOptions = context.PushRenderOptions(new RenderOptions
+            {
+                BitmapInterpolationMode = ScaleToBitmapScalingModeConverter.Calculate(ReferenceLayerScale)
+            });
             using var matrix = context.PushTransform(ReferenceLayer.ReferenceTransformMatrix);
 
             RectD dirty = new RectD(0, 0, ReferenceLayer.ReferenceBitmap.Size.X, ReferenceLayer.ReferenceBitmap.Size.Y);
             Rect dirtyRect = new Rect(dirty.X, dirty.Y, dirty.Width, dirty.Height);
-            DrawSurfaceOperation drawOperation = new DrawSurfaceOperation(dirtyRect, ReferenceLayer.ReferenceBitmap, Stretch.None, Opacity);
+            DrawSurfaceOperation drawOperation =
+                new DrawSurfaceOperation(dirtyRect, ReferenceLayer.ReferenceBitmap, Stretch.None, Opacity);
             context.Custom(drawOperation);
         }
     }
@@ -86,12 +99,19 @@ internal class ReferenceLayerOverlay : Overlay
     private static void FadeOutChanged(AvaloniaPropertyChangedEventArgs<bool> obj)
     {
         ReferenceLayerOverlay objSender = (ReferenceLayerOverlay)obj.Sender;
-        objSender.PseudoClasses.Set(":fadedOut", obj.NewValue.Value);
+        objSender.ToggleFadeOut(obj.NewValue.Value);
+    }
+
+    private void ToggleFadeOut(bool toggle)
+    {
+        double targetOpaqueOpacity = ReferenceLayer.ShowHighest ? ReferenceLayerViewModel.TopMostOpacity : 1;
+        TransitionTo(OpacityProperty, OpacityTransitionDuration, toggle ? 0 : targetOpaqueOpacity);
     }
 
     private void ReferenceLayerOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
     {
-        PseudoClasses.Set(":showHighest", ReferenceLayer.ShowHighest);
+        double targetOpaqueOpacity = ReferenceLayer.ShowHighest ? ReferenceLayerViewModel.TopMostOpacity : 1;
+        TransitionTo(OpacityProperty, OpacityTransitionDuration, FadeOut ? 0 : targetOpaqueOpacity);
+
     }
 }
-

+ 28 - 0
src/PixiEditor.AvaloniaUI/Views/Overlays/Transitions/OverlayDoubleTransition.cs

@@ -0,0 +1,28 @@
+using Avalonia.Animation.Easings;
+
+namespace PixiEditor.AvaloniaUI.Views.Overlays.Transitions;
+
+internal class OverlayDoubleTransition : OverlayTransition
+{
+    public new double From
+    {
+        get => (double)base.From;
+        set => base.From = value;
+    }
+
+    public new double To
+    {
+        get => (double)base.To;
+        set => base.To = value;
+    }
+
+    protected override object Interpolate(double progress)
+    {
+        return From + (To - From) * progress;
+    }
+
+    public OverlayDoubleTransition(double durationSeconds, double from, double to, Easing? easing = null) : base(durationSeconds, from, to, easing)
+    {
+
+    }
+}

+ 30 - 0
src/PixiEditor.AvaloniaUI/Views/Overlays/Transitions/OverlayTransition.cs

@@ -0,0 +1,30 @@
+using Avalonia.Animation.Easings;
+
+namespace PixiEditor.AvaloniaUI.Views.Overlays.Transitions;
+
+public abstract class OverlayTransition
+{
+    public double DurationSeconds { get; set; }
+    public Easing Easing { get; set; }
+    public object From { get; set; }
+    public object To { get; set; }
+    public double Progress { get; set; }
+
+    protected OverlayTransition(double durationSeconds, object from, object to, Easing? easing = null)
+    {
+        DurationSeconds = durationSeconds;
+        Easing = easing ?? new LinearEasing();
+        From = from;
+        To = to;
+    }
+
+    protected abstract object Interpolate(double progress);
+
+    public object Evaluate() => Evaluate(Progress);
+
+    public object Evaluate(double progress)
+    {
+        double easedT = Easing.Ease(progress);
+        return Interpolate(easedT);
+    }
+}

+ 5 - 2
src/PixiEditor.AvaloniaUI/Views/Rendering/Scene.cs

@@ -82,6 +82,8 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
 
     private List<Overlay> mouseOverOverlays = new();
 
+    private double sceneOpacity = 1;
+
     static Scene()
     {
         AffectsRender<Scene>(BoundsProperty, WidthProperty, HeightProperty, ScaleProperty, AngleRadiansProperty, FlipXProperty,
@@ -123,7 +125,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         using var operation = new DrawSceneOperation(Surface, Document, CanvasPos, Scale, angle, FlipX, FlipY,
             dirtyRect,
             Bounds,
-            Opacity);
+            sceneOpacity);
 
         var matrix = CalculateTransformMatrix();
         context.PushTransform(matrix);
@@ -381,7 +383,8 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
     }
     private static void FadeOutChanged(Scene scene, AvaloniaPropertyChangedEventArgs e)
     {
-        scene.Opacity = e.NewValue is true ? 0 : 1;
+        scene.sceneOpacity = e.NewValue is true ? 0 : 1;
+        scene.InvalidateVisual();
     }
 
     private static void ActiveOverlaysChanged(Scene scene, AvaloniaPropertyChangedEventArgs e)

+ 0 - 1
src/PixiEditor.Extensions/UI/Overlays/IOverlay.cs

@@ -11,7 +11,6 @@ public interface IOverlay
     public void MovePointer(OverlayPointerArgs args);
     public void PressPointer(OverlayPointerArgs args);
     public void ReleasePointer(OverlayPointerArgs args);
-
     public void Refresh();
     public bool TestHit(VecD point);