Skip to content

Commit bcb8006

Browse files
committed
GradientAnimator
1 parent 183e936 commit bcb8006

File tree

4 files changed

+272
-73
lines changed

4 files changed

+272
-73
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Avalonia.Animation.Easings;
5+
using Avalonia.Media;
6+
7+
namespace vj0.Extensions.Animations;
8+
9+
public class GradientAnimator(LinearGradientBrush targetBrush, Color color1, Color color2, double durationSeconds, int steps) : IDisposable
10+
{
11+
private readonly LinearGradientBrush TargetBrush = targetBrush ?? throw new ArgumentNullException(nameof(targetBrush));
12+
private bool IsReversed;
13+
private CancellationTokenSource? CancellationTokenSource;
14+
15+
public void StartAnimation()
16+
{
17+
if (CancellationTokenSource != null) return;
18+
19+
CancellationTokenSource = new CancellationTokenSource();
20+
_ = LoopGradientAnimation(CancellationTokenSource.Token);
21+
}
22+
23+
public void StopAnimation()
24+
{
25+
CancellationTokenSource?.Cancel();
26+
CancellationTokenSource?.Dispose();
27+
CancellationTokenSource = null;
28+
}
29+
30+
private async Task LoopGradientAnimation(CancellationToken cancellationToken)
31+
{
32+
while (!cancellationToken.IsCancellationRequested)
33+
{
34+
var startColor = IsReversed ? color2 : color1;
35+
var endColor = IsReversed ? color1 : color2;
36+
37+
if (TargetBrush.GradientStops.Count < 2)
38+
{
39+
TargetBrush.GradientStops.Clear();
40+
TargetBrush.GradientStops.Add(new GradientStop(startColor, 0));
41+
TargetBrush.GradientStops.Add(new GradientStop(startColor, 1));
42+
}
43+
else
44+
{
45+
TargetBrush.GradientStops[0].Color = startColor;
46+
TargetBrush.GradientStops[1].Color = startColor;
47+
}
48+
49+
var easing = new SineEaseInOut();
50+
var duration = TimeSpan.FromSeconds(durationSeconds);
51+
var delay = duration.TotalMilliseconds / steps;
52+
53+
for (var i = 0; i <= steps; i++)
54+
{
55+
if (cancellationToken.IsCancellationRequested) return;
56+
var progress = easing.Ease(i / (double)steps);
57+
58+
if (progress <= 0.5)
59+
{
60+
var localProgress = progress * 2;
61+
TargetBrush.GradientStops[0].Color = InterpolateColor(startColor, endColor, localProgress);
62+
TargetBrush.GradientStops[1].Color = startColor;
63+
}
64+
else
65+
{
66+
var localProgress = (progress - 0.5) * 2;
67+
TargetBrush.GradientStops[0].Color = endColor;
68+
TargetBrush.GradientStops[1].Color = InterpolateColor(startColor, endColor, localProgress);
69+
}
70+
71+
await Task.Delay((int)delay, cancellationToken);
72+
}
73+
74+
if (cancellationToken.IsCancellationRequested) return;
75+
TargetBrush.GradientStops[0].Color = endColor;
76+
TargetBrush.GradientStops[1].Color = endColor;
77+
78+
await Task.Delay((int)(durationSeconds / 2 * 1000), cancellationToken);
79+
80+
IsReversed = !IsReversed;
81+
}
82+
}
83+
84+
private static Color InterpolateColor(Color from, Color to, double t)
85+
{
86+
var r = (byte)(from.R + (to.R - from.R) * t);
87+
var g = (byte)(from.G + (to.G - from.G) * t);
88+
var b = (byte)(from.B + (to.B - from.B) * t);
89+
var a = (byte)(from.A + (to.A - from.A) * t);
90+
91+
return Color.FromArgb(a, r, g, b);
92+
}
93+
94+
public void Dispose()
95+
{
96+
StopAnimation();
97+
GC.SuppressFinalize(this);
98+
}
99+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
using System;
2+
using Avalonia;
3+
using Avalonia.Controls;
4+
using Avalonia.Controls.Primitives;
5+
using Avalonia.Media;
6+
using Avalonia.VisualTree;
7+
8+
namespace vj0.Extensions.Animations.Properties;
9+
10+
public static class GradientAnimationService
11+
{
12+
public static readonly AttachedProperty<bool> IsAnimatedProperty =
13+
AvaloniaProperty.RegisterAttached<Control, bool>("IsAnimated", typeof(GradientAnimationService), inherits: true);
14+
15+
public static readonly AttachedProperty<Color> AnimationStartColorProperty =
16+
AvaloniaProperty.RegisterAttached<Control, Color>("AnimationStartColor", typeof(GradientAnimationService), Colors.Black, inherits: true);
17+
18+
public static readonly AttachedProperty<Color> AnimationEndColorProperty =
19+
AvaloniaProperty.RegisterAttached<Control, Color>("AnimationEndColor", typeof(GradientAnimationService), Colors.White, inherits: true);
20+
21+
public static readonly AttachedProperty<double> AnimationDurationProperty =
22+
AvaloniaProperty.RegisterAttached<Control, double>("AnimationDuration", typeof(GradientAnimationService), 0.5, inherits: true);
23+
24+
public static readonly AttachedProperty<int> AnimationStepsProperty =
25+
AvaloniaProperty.RegisterAttached<Control, int>("AnimationSteps", typeof(GradientAnimationService), 100, inherits: true);
26+
27+
private static readonly AttachedProperty<GradientAnimator?> AnimatorInstanceProperty =
28+
AvaloniaProperty.RegisterAttached<Control, GradientAnimator?>("AnimatorInstance", typeof(GradientAnimationService));
29+
30+
static GradientAnimationService()
31+
{
32+
IsAnimatedProperty.Changed.Subscribe(OnIsAnimatedChanged);
33+
AnimationStartColorProperty.Changed.Subscribe(OnAnimationStartColorChanged);
34+
AnimationEndColorProperty.Changed.Subscribe(OnAnimationEndColorChanged);
35+
AnimationDurationProperty.Changed.Subscribe(OnAnimationDurationChanged);
36+
AnimationStepsProperty.Changed.Subscribe(OnAnimationStepsChanged);
37+
}
38+
39+
public static bool GetIsAnimated(Control control) => control.GetValue(IsAnimatedProperty);
40+
public static void SetIsAnimated(Control control, bool value) => control.SetValue(IsAnimatedProperty, value);
41+
42+
public static Color GetAnimationStartColor(Control control) => control.GetValue(AnimationStartColorProperty);
43+
public static void SetAnimationStartColor(Control control, Color value) => control.SetValue(AnimationStartColorProperty, value);
44+
45+
public static Color GetAnimationEndColor(Control control) => control.GetValue(AnimationEndColorProperty);
46+
public static void SetAnimationEndColor(Control control, Color value) => control.SetValue(AnimationEndColorProperty, value);
47+
48+
public static double GetAnimationDuration(Control control) => control.GetValue(AnimationDurationProperty);
49+
public static void SetAnimationDuration(Control control, double value) => control.SetValue(AnimationDurationProperty, value);
50+
51+
public static int GetAnimationSteps(Control control) => control.GetValue(AnimationStepsProperty);
52+
public static void SetAnimationSteps(Control control, int value) => control.SetValue(AnimationStepsProperty, value);
53+
54+
private static GradientAnimator? GetAnimatorInstance(Control control) => control.GetValue(AnimatorInstanceProperty);
55+
private static void SetAnimatorInstance(Control control, GradientAnimator? value) => control.SetValue(AnimatorInstanceProperty, value);
56+
57+
private static void OnIsAnimatedChanged(AvaloniaPropertyChangedEventArgs<bool> e)
58+
{
59+
if (e.Sender is not Control control) return;
60+
61+
if (e.GetNewValue<bool>())
62+
{
63+
EnsureAnimator(control);
64+
}
65+
else
66+
{
67+
StopAndDisposeAnimator(control);
68+
}
69+
}
70+
71+
private static void OnAnimationStartColorChanged(AvaloniaPropertyChangedEventArgs<Color> e)
72+
{
73+
if (e.Sender is not Control control || !GetIsAnimated(control)) return;
74+
75+
StopAndDisposeAnimator(control);
76+
EnsureAnimator(control);
77+
}
78+
79+
private static void OnAnimationEndColorChanged(AvaloniaPropertyChangedEventArgs<Color> e)
80+
{
81+
if (e.Sender is not Control control || !GetIsAnimated(control)) return;
82+
83+
StopAndDisposeAnimator(control);
84+
EnsureAnimator(control);
85+
}
86+
87+
private static void OnAnimationDurationChanged(AvaloniaPropertyChangedEventArgs<double> e)
88+
{
89+
if (e.Sender is not Control control || !GetIsAnimated(control)) return;
90+
91+
StopAndDisposeAnimator(control);
92+
EnsureAnimator(control);
93+
}
94+
95+
private static void OnAnimationStepsChanged(AvaloniaPropertyChangedEventArgs<int> e)
96+
{
97+
if (e.Sender is not Control control || !GetIsAnimated(control)) return;
98+
99+
StopAndDisposeAnimator(control);
100+
EnsureAnimator(control);
101+
}
102+
103+
private static void EnsureAnimator(Control control)
104+
{
105+
if (GetAnimatorInstance(control) != null) return;
106+
107+
LinearGradientBrush? targetBrush = null;
108+
109+
if (control is TextBlock textBlock && textBlock.Foreground is LinearGradientBrush fgBrush)
110+
{
111+
targetBrush = fgBrush;
112+
}
113+
else if (control is TemplatedControl templated && templated.Background is LinearGradientBrush bgBrush)
114+
{
115+
targetBrush = bgBrush;
116+
}
117+
118+
if (targetBrush != null)
119+
{
120+
var startColor = GetAnimationStartColor(control);
121+
var endColor = GetAnimationEndColor(control);
122+
var duration = GetAnimationDuration(control);
123+
var steps = GetAnimationSteps(control);
124+
125+
var animator = new GradientAnimator(targetBrush, startColor, endColor, duration, steps);
126+
SetAnimatorInstance(control, animator);
127+
128+
control.AttachedToVisualTree += Control_AttachedToVisualTree;
129+
control.DetachedFromVisualTree += Control_DetachedFromVisualTree;
130+
131+
if (control.GetVisualRoot() != null)
132+
{
133+
animator.StartAnimation();
134+
}
135+
}
136+
}
137+
138+
private static void StopAndDisposeAnimator(Control control)
139+
{
140+
var animator = GetAnimatorInstance(control);
141+
if (animator != null)
142+
{
143+
animator.Dispose();
144+
SetAnimatorInstance(control, null);
145+
146+
control.AttachedToVisualTree -= Control_AttachedToVisualTree;
147+
control.DetachedFromVisualTree -= Control_DetachedFromVisualTree;
148+
}
149+
}
150+
151+
private static void Control_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
152+
{
153+
if (sender is Control control)
154+
{
155+
GetAnimatorInstance(control)?.StartAnimation();
156+
}
157+
}
158+
159+
private static void Control_DetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
160+
{
161+
if (sender is Control control)
162+
{
163+
GetAnimatorInstance(control)?.StopAnimation();
164+
}
165+
}
166+
}

Source/vj0/Views/HomeView.axaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
xmlns:framework="clr-namespace:vj0.Services.Framework"
99
xmlns:navigation="clr-namespace:vj0.Controls.Navigation"
1010
xmlns:application="clr-namespace:vj0.Application"
11+
xmlns:properties="clr-namespace:vj0.Extensions.Animations.Properties"
1112
x:Class="vj0.Views.HomeView"
1213
x:DataType="viewModels:HomeViewModel"
1314
mc:Ignorable="d"
@@ -79,6 +80,12 @@
7980
LetterSpacing="-0.4"
8081
FontSize="25"
8182
HorizontalAlignment="Center"
83+
84+
properties:GradientAnimationService.IsAnimated="True"
85+
properties:GradientAnimationService.AnimationStartColor="#303030"
86+
properties:GradientAnimationService.AnimationEndColor="White"
87+
properties:GradientAnimationService.AnimationDuration="0.5"
88+
8289
VerticalAlignment="Center"
8390
Text="the ultimate datamining experience"
8491
Opacity="0"

Source/vj0/Views/HomeView.cs

Lines changed: 0 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -16,83 +16,10 @@ public partial class HomeView : ViewBase<HomeViewModel>
1616
public HomeView()
1717
{
1818
InitializeComponent();
19-
StartLoopingGradientAnimation();
2019

2120
ViewModel.StartRotation(ViewModel.TagLines, 2700, RotatingTaglineText, useRandom: true);
2221
ViewModel.StartRotation(ViewModel.Tips, 8000, TipText, TipContainer, true);
2322
}
24-
25-
private LinearGradientBrush? _branchBrush;
26-
private bool _isReversed;
27-
28-
private async void StartLoopingGradientAnimation()
29-
{
30-
if (_branchBrush is null)
31-
{
32-
if (Resources.TryGetValue("AnimatingTextBrush", out var brushObj) && brushObj is LinearGradientBrush brush)
33-
{
34-
_branchBrush = brush;
35-
}
36-
else
37-
{
38-
return;
39-
}
40-
}
41-
42-
var darkColor = Color.Parse("#303030");
43-
var lightColor = Colors.White;
44-
45-
var startColor = _isReversed ? lightColor : darkColor;
46-
var endColor = _isReversed ? darkColor : lightColor;
47-
48-
_branchBrush.GradientStops[0].Color = startColor;
49-
_branchBrush.GradientStops[1].Color = startColor;
50-
51-
const float duration_full = 0.5f;
52-
53-
const int steps = 100;
54-
var easing = new SineEaseInOut();
55-
var duration = TimeSpan.FromSeconds(duration_full);
56-
var delay = duration.TotalMilliseconds / steps;
57-
58-
for (var i = 0; i <= steps; i++)
59-
{
60-
var progress = easing.Ease(i / (double)steps);
61-
62-
if (progress <= 0.5)
63-
{
64-
var localProgress = progress * 2;
65-
_branchBrush.GradientStops[0].Color = InterpolateColor(startColor, endColor, localProgress);
66-
_branchBrush.GradientStops[1].Color = startColor;
67-
}
68-
else
69-
{
70-
var localProgress = (progress - 0.5) * 2;
71-
_branchBrush.GradientStops[0].Color = endColor;
72-
_branchBrush.GradientStops[1].Color = InterpolateColor(startColor, endColor, localProgress);
73-
}
74-
75-
await Task.Delay((int)delay);
76-
}
77-
78-
_branchBrush.GradientStops[0].Color = endColor;
79-
_branchBrush.GradientStops[1].Color = endColor;
80-
81-
await Task.Delay((int)(duration_full / 2 * 1000));
82-
83-
_isReversed = !_isReversed;
84-
StartLoopingGradientAnimation();
85-
}
86-
87-
private static Color InterpolateColor(Color from, Color to, double t)
88-
{
89-
var r = (byte)(from.R + (to.R - from.R) * t);
90-
var g = (byte)(from.G + (to.G - from.G) * t);
91-
var b = (byte)(from.B + (to.B - from.B) * t);
92-
var a = (byte)(from.A + (to.A - from.A) * t);
93-
94-
return Color.FromArgb(a, r, g, b);
95-
}
9623

9724
private void OpenDiscord(object? sender, RoutedEventArgs e)
9825
{

0 commit comments

Comments
 (0)