Skip to content

Commit

Permalink
[ColorPicker] Change zoom animation behavior (#11057)
Browse files Browse the repository at this point in the history
* [ColorPicker] Change zoom animation behavior
Makes the main window large enough to accommodate all zoom levels.

* [ColorPicker] Change zoom window position logic
Use PointFromScreen to calculate mouse position relative to window
This requires a "visible" window, so use opacity to fake-hide window
Window is still fully hidden when color picker closes

* [ColorPicker] Extract and modify resize behavior
Allows easier editing of animation easing/duration

* Update expect.txt

IAnimatable
IEasing

Co-authored-by: Clint Rutkas <clint@rutkas.com>
  • Loading branch information
DoctorNefario and crutkas committed May 7, 2021
1 parent 21247c0 commit 9461909
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 201 deletions.
2 changes: 2 additions & 0 deletions .github/actions/spell-check/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,7 @@ hxx
Hyperlink
IAction
IActivated
IAnimatable
IApp
IApplication
IAppx
Expand Down Expand Up @@ -796,6 +797,7 @@ IDrop
idx
IDXGI
IDYES
IEasing
IEnum
IEnumerable
IEnumerator
Expand Down

This file was deleted.

65 changes: 49 additions & 16 deletions src/modules/colorPicker/ColorPickerUI/Behaviors/ResizeBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,69 @@ namespace ColorPicker.Behaviors
{
public class ResizeBehavior : Behavior<FrameworkElement>
{
// animation behavior variables
// used when size is getting bigger
private static readonly TimeSpan _animationTime = TimeSpan.FromMilliseconds(200);
private static readonly IEasingFunction _easeFunction = new SineEase() { EasingMode = EasingMode.EaseOut };

// used when size is getting smaller
private static readonly TimeSpan _animationTimeSmaller = _animationTime;
private static readonly IEasingFunction _easeFunctionSmaller = new QuadraticEase() { EasingMode = EasingMode.EaseIn };

private static void CustomAnimation(DependencyProperty prop, IAnimatable sender, double fromValue, double toValue)
{
// if the animation is to/from a value of 0, it will cancel the current animation
DoubleAnimation move = null;
if (toValue > 0 && fromValue > 0)
{
// if getting bigger
if (fromValue < toValue)
{
move = new DoubleAnimation(fromValue, toValue, new Duration(_animationTime), FillBehavior.Stop)
{
EasingFunction = _easeFunction,
};
}
else
{
move = new DoubleAnimation(fromValue, toValue, new Duration(_animationTimeSmaller), FillBehavior.Stop)
{
EasingFunction = _easeFunctionSmaller,
};
}
}

// HandoffBehavior must be SnapshotAndReplace
// Compose does not allow cancellation
sender.BeginAnimation(prop, move, HandoffBehavior.SnapshotAndReplace);
}

public static readonly DependencyProperty WidthProperty = DependencyProperty.Register("Width", typeof(double), typeof(ResizeBehavior), new PropertyMetadata(new PropertyChangedCallback(WidthPropertyChanged)));

private static void WidthPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var sender = ((ResizeBehavior)d).AssociatedObject;
var move = new DoubleAnimation(sender.Width, (double)e.NewValue, new Duration(TimeSpan.FromMilliseconds(150)), FillBehavior.Stop);
move.Completed += (s, e1) =>
{
sender.BeginAnimation(FrameworkElement.WidthProperty, null);
sender.Width = (double)e.NewValue;
};

move.EasingFunction = new QuadraticEase() { EasingMode = EasingMode.EaseOut };
sender.BeginAnimation(FrameworkElement.WidthProperty, move, HandoffBehavior.Compose);
var fromValue = sender.Width;
var toValue = (double)e.NewValue;

// setting Width before animation prevents jumping
sender.Width = toValue;
CustomAnimation(FrameworkElement.WidthProperty, sender, fromValue, toValue);
}

public static readonly DependencyProperty HeightProperty = DependencyProperty.Register("Height", typeof(double), typeof(ResizeBehavior), new PropertyMetadata(new PropertyChangedCallback(HeightPropertyChanged)));

private static void HeightPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var sender = ((ResizeBehavior)d).AssociatedObject;
var move = new DoubleAnimation(sender.Height, (double)e.NewValue, new Duration(TimeSpan.FromMilliseconds(150)), FillBehavior.Stop);
move.Completed += (s, e1) =>
{
sender.BeginAnimation(FrameworkElement.HeightProperty, null);
sender.Height = (double)e.NewValue;
};

move.EasingFunction = new QuadraticEase() { EasingMode = EasingMode.EaseOut };
sender.BeginAnimation(FrameworkElement.HeightProperty, move, HandoffBehavior.Compose);
var fromValue = sender.Height;
var toValue = (double)e.NewValue;

// setting Height before animation prevents jumping
sender.Height = toValue;
CustomAnimation(FrameworkElement.HeightProperty, sender, fromValue, toValue);
}

public double Width
Expand Down
136 changes: 51 additions & 85 deletions src/modules/colorPicker/ColorPickerUI/Helpers/ZoomWindowHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,27 @@ namespace ColorPicker.Helpers
[Export(typeof(ZoomWindowHelper))]
public class ZoomWindowHelper
{
private const int ZoomWindowChangeDelayInMS = 50;
private const int ZoomFactor = 2;
private const int BaseZoomImageSize = 50;
private const int MaxZoomLevel = 4;
private const int MinZoomLevel = 0;

private static readonly Bitmap _bmp = new Bitmap(BaseZoomImageSize, BaseZoomImageSize, PixelFormat.Format32bppArgb);
private static readonly Graphics _graphics = Graphics.FromImage(_bmp);

private readonly IZoomViewModel _zoomViewModel;
private readonly AppStateHandler _appStateHandler;
private readonly IThrottledActionInvoker _throttledActionInvoker;

private int _currentZoomLevel;
private int _previousZoomLevel;

private ZoomWindow _zoomWindow;

private double _lastLeft;
private double _lastTop;

private double _previousScaledX;
private double _previousScaledY;

[ImportingConstructor]
public ZoomWindowHelper(IZoomViewModel zoomViewModel, AppStateHandler appStateHandler, IThrottledActionInvoker throttledActionInvoker)
public ZoomWindowHelper(IZoomViewModel zoomViewModel, AppStateHandler appStateHandler)
{
_zoomViewModel = zoomViewModel;
_appStateHandler = appStateHandler;
_throttledActionInvoker = throttledActionInvoker;
_appStateHandler.AppClosed += AppStateHandler_AppClosed;
_appStateHandler.AppHidden += AppStateHandler_AppClosed;
}
Expand Down Expand Up @@ -73,7 +67,7 @@ public void CloseZoomWindow()
{
_currentZoomLevel = 0;
_previousZoomLevel = 0;
HideZoomWindow();
HideZoomWindow(true);
}

private void SetZoomImage(System.Windows.Point point)
Expand All @@ -89,28 +83,17 @@ private void SetZoomImage(System.Windows.Point point)
{
var x = (int)point.X - (BaseZoomImageSize / 2);
var y = (int)point.Y - (BaseZoomImageSize / 2);
var rect = new Rectangle(x, y, BaseZoomImageSize, BaseZoomImageSize);

using (var bmp = new Bitmap(rect.Width, rect.Height, PixelFormat.Format32bppArgb))
{
var g = Graphics.FromImage(bmp);
g.CopyFromScreen(rect.Left, rect.Top, 0, 0, bmp.Size, CopyPixelOperation.SourceCopy);

var bitmapImage = BitmapToImageSource(bmp);
_graphics.CopyFromScreen(x, y, 0, 0, _bmp.Size, CopyPixelOperation.SourceCopy);

_zoomViewModel.ZoomArea = bitmapImage;
_zoomViewModel.ZoomFactor = 1;
}
_zoomViewModel.ZoomArea = BitmapToImageSource(_bmp);
}
else
{
var enlarge = (_currentZoomLevel - _previousZoomLevel) > 0 ? true : false;
var currentZoomFactor = enlarge ? ZoomFactor : 1.0 / ZoomFactor;

_zoomViewModel.ZoomFactor *= currentZoomFactor;
_zoomViewModel.ZoomFactor = Math.Pow(ZoomFactor, _currentZoomLevel - 1);
}

ShowZoomWindow((int)point.X, (int)point.Y);
ShowZoomWindow(point);
}

private static BitmapSource BitmapToImageSource(Bitmap bitmap)
Expand All @@ -130,84 +113,67 @@ private static BitmapSource BitmapToImageSource(Bitmap bitmap)
}
}

private void HideZoomWindow()
private void HideZoomWindow(bool fully = false)
{
if (_zoomWindow != null)
{
_zoomWindow.Hide();
_zoomWindow.Opacity = 0;
_zoomViewModel.DesiredWidth = 0;
_zoomViewModel.DesiredHeight = 0;

if (fully)
{
_zoomWindow.Hide();
}
}
}

private void ShowZoomWindow(int x, int y)
private void ShowZoomWindow(System.Windows.Point point)
{
if (_zoomWindow == null)
{
_zoomWindow = new ZoomWindow();
_zoomWindow.Content = _zoomViewModel;
_zoomWindow.Loaded += ZoomWindow_Loaded;
_zoomWindow.IsVisibleChanged += ZoomWindow_IsVisibleChanged;
}

// we just started zooming, remember where we opened zoom window
if (_currentZoomLevel == 1 && _previousZoomLevel == 0)
_zoomWindow ??= new ZoomWindow
{
var dpi = MonitorResolutionHelper.GetCurrentMonitorDpi();
_previousScaledX = x / dpi.DpiScaleX;
_previousScaledY = y / dpi.DpiScaleY;
}
Content = _zoomViewModel,
Opacity = 0,
};

_lastLeft = Math.Floor(_previousScaledX - (BaseZoomImageSize * Math.Pow(ZoomFactor, _currentZoomLevel - 1) / 2));
_lastTop = Math.Floor(_previousScaledY - (BaseZoomImageSize * Math.Pow(ZoomFactor, _currentZoomLevel - 1) / 2));

var justShown = false;
if (!_zoomWindow.IsVisible)
{
_zoomWindow.Left = _lastLeft;
_zoomWindow.Top = _lastTop;
_zoomViewModel.Height = BaseZoomImageSize;
_zoomViewModel.Width = BaseZoomImageSize;
_zoomWindow.Show();
justShown = true;

// make sure color picker window is on top of just opened zoom window
AppStateHandler.SetTopMost();
}

// dirty hack - sometimes when we just show a window on a second monitor with different DPI,
// window position is not set correctly on a first time, we need to "ping" it again to make it appear on the proper location
if (justShown)
if (_zoomWindow.Opacity < 0.5)
{
_zoomWindow.Left = _lastLeft + 1;
_zoomWindow.Top = _lastTop + 1;
SessionEventHelper.Event.ZoomUsed = true;
}
var halfWidth = _zoomWindow.Width / 2;
var halfHeight = _zoomWindow.Height / 2;

_throttledActionInvoker.ScheduleAction(
() =>
{
_zoomWindow.DesiredLeft = _lastLeft;
_zoomWindow.DesiredTop = _lastTop;
_zoomViewModel.DesiredHeight = BaseZoomImageSize * _zoomViewModel.ZoomFactor;
_zoomViewModel.DesiredWidth = BaseZoomImageSize * _zoomViewModel.ZoomFactor;
},
ZoomWindowChangeDelayInMS);
}
// usually takes 1-3 iterations to converge
// 5 is just an arbitrary limit to prevent infinite loops
for (var i = 0; i < 5; i++)
{
// mouse position relative to top left of _zoomWindow
var scaledPoint = _zoomWindow.PointFromScreen(point);

private void ZoomWindow_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
// need to set at this point again, to avoid issues moving between screens with different scaling
if ((bool)e.NewValue)
{
_zoomWindow.Left = _lastLeft;
_zoomWindow.Top = _lastTop;
var diffX = scaledPoint.X - halfWidth;
var diffY = scaledPoint.Y - halfHeight;

// minimum difference that is considered important
const double minDiff = 0.05;
if (Math.Abs(diffX) < minDiff && Math.Abs(diffY) < minDiff)
{
break;
}

_zoomWindow.Left += diffX;
_zoomWindow.Top += diffY;
}

// make sure color picker window is on top of just opened zoom window
AppStateHandler.SetTopMost();
_zoomWindow.Opacity = 1;
}
}

private void ZoomWindow_Loaded(object sender, RoutedEventArgs e)
{
// need to call it again at load time, because it does was not dpi aware at the first time of Show() call
_zoomWindow.Left = _lastLeft;
_zoomWindow.Top = _lastTop;
_zoomViewModel.DesiredHeight = BaseZoomImageSize * _zoomViewModel.ZoomFactor;
_zoomViewModel.DesiredWidth = BaseZoomImageSize * _zoomViewModel.ZoomFactor;
}

private void AppStateHandler_AppClosed(object sender, EventArgs e)
Expand Down
2 changes: 2 additions & 0 deletions src/modules/colorPicker/ColorPickerUI/Views/ZoomView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
Focusable="False">

<Border x:Name="WindowBorder"
HorizontalAlignment="Center"
VerticalAlignment="Center"
BorderBrush="{DynamicResource WindowBorderBrush}"
Margin="12"
BorderThickness="1"
Expand Down
2 changes: 0 additions & 2 deletions src/modules/colorPicker/ColorPickerUI/ZoomWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
mc:Ignorable="d"
Title="Zoom window"
WindowStyle="None"
SizeToContent="WidthAndHeight"
Topmost="True"
AllowsTransparency="True"
Background="Transparent"
Expand All @@ -17,6 +16,5 @@
Focusable="False">
<e:Interaction.Behaviors>
<behaviors:CloseZoomWindowBehavior/>
<behaviors:MoveWindowBehavior Left="{Binding DesiredLeft, Mode=TwoWay}" Top="{Binding DesiredTop}"/>
</e:Interaction.Behaviors>
</Window>

0 comments on commit 9461909

Please sign in to comment.