diff --git a/DesktopClock.Tests/PixelShifterTests.cs b/DesktopClock.Tests/PixelShifterTests.cs new file mode 100644 index 0000000..8af6e53 --- /dev/null +++ b/DesktopClock.Tests/PixelShifterTests.cs @@ -0,0 +1,55 @@ +using System; +using DesktopClock.Utilities; + +namespace DesktopClock.Tests; + +public class PixelShifterTests +{ + [Theory] + [InlineData(5, 10)] // Evenly divisible. + [InlineData(3, 10)] // Not evenly divisible. + [InlineData(10, 5)] // Amount is larger than total. + public void ShiftX_ShouldNotExceedMaxTotalShift(int shiftAmount, int maxTotalShift) + { + var shifter = new PixelShifter + { + PixelsPerShift = shiftAmount, + MaxPixelOffset = maxTotalShift, + }; + + double totalShiftX = 0; + + // Test 100 times because it's random. + for (var i = 0; i < 100; i++) + { + var shift = shifter.ShiftX(); + totalShiftX += shift; + + Assert.InRange(Math.Abs(totalShiftX), 0, maxTotalShift); + } + } + + [Theory] + [InlineData(5, 10)] // Evenly divisible. + [InlineData(3, 10)] // Not evenly divisible. + [InlineData(10, 5)] // Amount is larger than total. + public void ShiftY_ShouldNotExceedMaxTotalShift(int shiftAmount, int maxTotalShift) + { + var shifter = new PixelShifter + { + PixelsPerShift = shiftAmount, + MaxPixelOffset = maxTotalShift, + }; + + double totalShiftY = 0; + + // Test 100 times because it's random. + for (var i = 0; i < 100; i++) + { + var shift = shifter.ShiftY(); + totalShiftY += shift; + + Assert.InRange(Math.Abs(totalShiftY), 0, maxTotalShift); + } + } +} diff --git a/DesktopClock/MainWindow.xaml.cs b/DesktopClock/MainWindow.xaml.cs index e88365c..84afda8 100644 --- a/DesktopClock/MainWindow.xaml.cs +++ b/DesktopClock/MainWindow.xaml.cs @@ -10,6 +10,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using DesktopClock.Properties; +using DesktopClock.Utilities; using H.NotifyIcon; using H.NotifyIcon.EfficiencyMode; using Humanizer; @@ -27,6 +28,7 @@ public partial class MainWindow : Window private TaskbarIcon _trayIcon; private TimeZoneInfo _timeZone; private SoundPlayer _soundPlayer; + private PixelShifter _pixelShifter; /// /// The date and time to countdown to, or null if regular clock is desired. @@ -40,6 +42,12 @@ public partial class MainWindow : Window [ObservableProperty] private string _currentTimeOrCountdownString; + /// + /// The amount of margin applied in order to shift the clock's pixels and help prevent burn-in. + /// + [ObservableProperty] + private Thickness _pixelShift; + public MainWindow() { InitializeComponent(); @@ -206,6 +214,8 @@ private void SystemClockTimer_SecondChanged(object sender, EventArgs e) { UpdateTimeString(); + TryShiftPixels(); + TryPlaySound(); } @@ -262,6 +272,20 @@ private void TryPlaySound() } } + private void TryShiftPixels() + { + if (!Settings.Default.BurnInMitigation || DateTimeOffset.Now.Second != 0) + return; + + _pixelShifter ??= new(); + + Dispatcher.Invoke(() => + { + Left += _pixelShifter.ShiftX(); + Top += _pixelShifter.ShiftY(); + }); + } + private void UpdateTimeString() { string GetTimeString() diff --git a/DesktopClock/Properties/Settings.cs b/DesktopClock/Properties/Settings.cs index 11e4af8..95ed4ee 100644 --- a/DesktopClock/Properties/Settings.cs +++ b/DesktopClock/Properties/Settings.cs @@ -179,6 +179,11 @@ private Settings() /// public bool RightAligned { get; set; } = false; + /// + /// Experimental: Shifts the clock periodically in order to reduce screen burn-in. + /// + public bool BurnInMitigation { get; set; } = false; + /// /// Path to a WAV file to be played on a specified interval. /// diff --git a/DesktopClock/SettingsWindow.xaml b/DesktopClock/SettingsWindow.xaml index 3862b4f..ffb310c 100644 --- a/DesktopClock/SettingsWindow.xaml +++ b/DesktopClock/SettingsWindow.xaml @@ -195,6 +195,12 @@ FontSize="10" Margin="0,0,0,12" /> + + + diff --git a/DesktopClock/Utilities/PixelShifter.cs b/DesktopClock/Utilities/PixelShifter.cs new file mode 100644 index 0000000..73f78aa --- /dev/null +++ b/DesktopClock/Utilities/PixelShifter.cs @@ -0,0 +1,71 @@ +using System; + +namespace DesktopClock.Utilities; + +public class PixelShifter +{ + private readonly Random _random = new(); + private double _totalShiftX; + private double _totalShiftY; + + /// + /// The number of pixels that will be shifted each time. + /// + public int PixelsPerShift { get; set; } = 1; + + /// + /// The maximum amount of drift that can occur in each direction. + /// + public int MaxPixelOffset { get; set; } = 4; + + /// + /// Returns an amount to shift horizontally by while staying within the specified bounds. + /// + public double ShiftX() + { + double pixelsToMoveBy = GetRandomShift(); + pixelsToMoveBy = GetFinalShiftAmount(_totalShiftX, pixelsToMoveBy, MaxPixelOffset); + _totalShiftX += pixelsToMoveBy; + return pixelsToMoveBy; + } + + /// + /// Returns an amount to shift vertically by while staying within the specified bounds. + /// + public double ShiftY() + { + double pixelsToMoveBy = GetRandomShift(); + pixelsToMoveBy = GetFinalShiftAmount(_totalShiftY, pixelsToMoveBy, MaxPixelOffset); + _totalShiftY += pixelsToMoveBy; + return pixelsToMoveBy; + } + + /// + /// Returns a random amount to shift by within the specified amount. + /// + private int GetRandomShift() => _random.Next(-PixelsPerShift, PixelsPerShift + 1); + + /// + /// Returns a capped amount to shift by. + /// + /// The current total amount of shift that has occurred. + /// The proposed amount to shift by this time. + /// The bounds to stay within in respect to the total shift. + private double GetFinalShiftAmount(double current, double offset, double max) + { + var newTotal = current + offset; + + if (newTotal > max) + { + return max - current; + } + else if (newTotal < -max) + { + return -max - current; + } + else + { + return offset; + } + } +}