feat: Add ConfettiCannon control and demo.
This commit is contained in:
1 parent
6c307a6ccc
commit
b36b3b4ed8
8 files changed
+1052
-2
No files matched your search
@@ -1,4 +1,4 @@
|
||||
[
|
||||
[
|
||||
{
|
||||
"title": "Styles",
|
||||
"selectedIndex": 0,
|
||||
@@ -35,6 +35,7 @@
|
||||
"group": true,
|
||||
"demoItemList": [
|
||||
[ "FloatingBlock", "FloatingBlockDemo", "Brush.LikeFilled", "", "Interacting" ],
|
||||
[ "ConfettiCannon", "ConfettiCannonDemo", "Brush.Favorite", "", "Interacting" ],
|
||||
[ "PropertyGrid", "PropertyGridDemo", "Brush.PropertyGridEditorPart", "", "DataEntry" ],
|
||||
[ "ImageSelector", "ImageSelectorDemo", "Brush.Image", "", "DataEntry" ],
|
||||
[ "ImageBlock", "ImageBlockDemo", "Brush.ImageStack", "", "DataDisplay" ],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<DrawingBrush x:Key="CloudDrawingBrush"
|
||||
Stretch="Uniform"
|
||||
@@ -701,6 +701,18 @@
|
||||
</DrawingBrush.Drawing>
|
||||
</DrawingBrush>
|
||||
|
||||
<DrawingBrush x:Key="Brush.Favorite">
|
||||
<DrawingBrush.Drawing>
|
||||
<DrawingGroup>
|
||||
<DrawingGroup.Children>
|
||||
<GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" />
|
||||
<GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M7.2729,-0.000199999999999534L5.6649,4.9998 -9.99999999997669E-05,4.9998 -9.99999999997669E-05,6.5148 4.2109,9.5218 2.1279,15.9998 3.8809,15.9998 7.9999,12.3378 12.1199,15.9998 13.8719,15.9998 11.7899,9.5218 15.9999,6.5148 15.9999,4.9998 10.3369,4.9998 8.7289,-0.000199999999999534z" />
|
||||
<GeometryDrawing Brush="#FF424242" Geometry="F1M10.6143,9.1328L12.5003,14.9998 8.0003,10.9998 3.5003,14.9998 5.3863,9.1328 1.0003,5.9998 6.3933,5.9998 8.0013,0.9998 9.6073,5.9998 15.0003,5.9998z" />
|
||||
</DrawingGroup.Children>
|
||||
</DrawingGroup>
|
||||
</DrawingBrush.Drawing>
|
||||
</DrawingBrush>
|
||||
|
||||
<DrawingBrush x:Key="Brush.LikeFilled">
|
||||
<DrawingBrush.Drawing>
|
||||
<DrawingGroup>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
x:Class="HandyControlDemo.UserControl.ConfettiCannonDemo"
|
||||
hc:ConfettiCannon.Token="ConfettiCannonDemo"
|
||||
Background="{DynamicResource RegionBrush}">
|
||||
<TransitioningContentControl>
|
||||
<ScrollViewer>
|
||||
<hc:UniformSpacingPanel Margin="32"
|
||||
Orientation="Vertical"
|
||||
Spacing="16">
|
||||
<Button Content="Basic Cannon"
|
||||
Click="ButtonBasicCannonOnClick" />
|
||||
<Button Content="Random Direction"
|
||||
Click="ButtonRandomDirectionOnClick" />
|
||||
<Button Content="Realistic Look"
|
||||
Click="ButtonRealisticLookClick" />
|
||||
<Button Content="Fireworks"
|
||||
Click="ButtonFireworksOnClick" />
|
||||
<Button Content="Stars"
|
||||
Click="ButtonStarsOnClick" />
|
||||
<Button Content="Snow"
|
||||
Click="ButtonSnowOnClick" />
|
||||
<Button Content="School Pride"
|
||||
Click="ButtonSchoolPrideOnClick" />
|
||||
<Button Content="Custom Canvas"
|
||||
Click="ButtonCustomCanvasOnClick" />
|
||||
<hc:ConfettiCannonContainer>
|
||||
<Border MinWidth="500"
|
||||
Height="300"
|
||||
Theme="{StaticResource BorderRegion}" />
|
||||
</hc:ConfettiCannonContainer>
|
||||
</hc:UniformSpacingPanel>
|
||||
</ScrollViewer>
|
||||
</TransitioningContentControl>
|
||||
</UserControl>
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
// adapted from https://www.kirilv.com/canvas-confetti
|
||||
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Threading;
|
||||
using HandyControl.Controls;
|
||||
|
||||
namespace HandyControlDemo.UserControl;
|
||||
|
||||
public partial class ConfettiCannonDemo : Avalonia.Controls.UserControl
|
||||
{
|
||||
public ConfettiCannonDemo()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void ButtonBasicCannonOnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
ConfettiCannon.Fire(new ConfettiCannon.Options
|
||||
{
|
||||
ParticleCount = 100,
|
||||
Spread = 70,
|
||||
Origin = new Point(0.5, 0.6)
|
||||
});
|
||||
}
|
||||
|
||||
private void ButtonRandomDirectionOnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var random = new Random();
|
||||
|
||||
ConfettiCannon.Fire(new ConfettiCannon.Options
|
||||
{
|
||||
Angle = random.Next(55, 125),
|
||||
Spread = random.Next(50, 70),
|
||||
ParticleCount = random.Next(50, 100),
|
||||
Origin = new Point(0.5, 0.6)
|
||||
});
|
||||
}
|
||||
|
||||
private void ButtonRealisticLookClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
const int count = 200;
|
||||
|
||||
Fire(0.25, new ConfettiCannon.Options { Spread = 26, StartVelocity = 55 });
|
||||
Fire(0.2, new ConfettiCannon.Options { Spread = 60 });
|
||||
Fire(0.35, new ConfettiCannon.Options { Spread = 100, Decay = 0.91, Scalar = 0.8 });
|
||||
Fire(0.1, new ConfettiCannon.Options { Spread = 120, StartVelocity = 25, Decay = 0.92, Scalar = 1.2 });
|
||||
Fire(0.1, new ConfettiCannon.Options { Spread = 120, StartVelocity = 45 });
|
||||
|
||||
return;
|
||||
|
||||
void Fire(double particleRatio, ConfettiCannon.Options options)
|
||||
{
|
||||
options.ParticleCount = (int) Math.Floor(count * particleRatio);
|
||||
ConfettiCannon.Fire(options);
|
||||
}
|
||||
}
|
||||
|
||||
private void ButtonFireworksOnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
const int duration = 15 * 1000;
|
||||
|
||||
var random = new Random();
|
||||
DateTime animationEnd = DateTime.Now + TimeSpan.FromMilliseconds(duration);
|
||||
|
||||
var timer = new DispatcherTimer(DispatcherPriority.ApplicationIdle)
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(250)
|
||||
};
|
||||
|
||||
timer.Tick += OnTimerOnTick;
|
||||
timer.Start();
|
||||
|
||||
return;
|
||||
|
||||
void OnTimerOnTick(object? o, EventArgs _)
|
||||
{
|
||||
double timeLeft = (animationEnd - DateTime.Now).TotalMilliseconds;
|
||||
|
||||
if (timeLeft <= 0)
|
||||
{
|
||||
timer.Tick -= OnTimerOnTick;
|
||||
timer.Stop();
|
||||
}
|
||||
|
||||
var particleCount = (int) (50 * (timeLeft / duration));
|
||||
ConfettiCannon.Fire(new ConfettiCannon.Options
|
||||
{
|
||||
StartVelocity = 30,
|
||||
Spread = 360,
|
||||
Ticks = 60,
|
||||
ParticleCount = particleCount,
|
||||
Origin = new Point(
|
||||
RandomInRange(0.1, 0.3),
|
||||
random.NextDouble() - 0.2
|
||||
)
|
||||
});
|
||||
ConfettiCannon.Fire(new ConfettiCannon.Options
|
||||
{
|
||||
StartVelocity = 30,
|
||||
Spread = 360,
|
||||
Ticks = 60,
|
||||
ParticleCount = particleCount,
|
||||
Origin = new Point(
|
||||
RandomInRange(0.7, 0.9),
|
||||
random.NextDouble() - 0.2
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ButtonStarsOnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
SetTimeout(Shoot, 0);
|
||||
SetTimeout(Shoot, 100);
|
||||
SetTimeout(Shoot, 200);
|
||||
return;
|
||||
|
||||
void Shoot()
|
||||
{
|
||||
ConfettiCannon.Fire(new ConfettiCannon.Options
|
||||
{
|
||||
Spread = 360,
|
||||
Ticks = 50,
|
||||
Gravity = 0,
|
||||
Decay = 0.94,
|
||||
StartVelocity = 30,
|
||||
Colors = ["#FFE400", "#FFBD00", "#E89400", "#FFCA6C", "#FDFFB8"],
|
||||
ParticleCount = 40,
|
||||
Scalar = 1.2,
|
||||
Shapes = ["star"]
|
||||
});
|
||||
ConfettiCannon.Fire(new ConfettiCannon.Options
|
||||
{
|
||||
Spread = 360,
|
||||
Ticks = 50,
|
||||
Gravity = 0,
|
||||
Decay = 0.94,
|
||||
StartVelocity = 30,
|
||||
Colors = ["#FFE400", "#FFBD00", "#E89400", "#FFCA6C", "#FDFFB8"],
|
||||
ParticleCount = 10,
|
||||
Scalar = 0.75,
|
||||
Shapes = ["circle"]
|
||||
});
|
||||
}
|
||||
|
||||
void SetTimeout(Action action, int delayMilliseconds)
|
||||
{
|
||||
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(delayMilliseconds) };
|
||||
timer.Tick += OnTimerOnTick;
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
void OnTimerOnTick(object? o, EventArgs _)
|
||||
{
|
||||
Shoot();
|
||||
|
||||
if (o is not DispatcherTimer timer)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
timer.Tick -= OnTimerOnTick;
|
||||
timer.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void ButtonSnowOnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
const int duration = 15 * 1000;
|
||||
|
||||
var random = new Random();
|
||||
DateTime animationEnd = DateTime.Now + TimeSpan.FromMilliseconds(duration);
|
||||
double skew = 1;
|
||||
|
||||
new AnimationFrame(this).Start(_ =>
|
||||
{
|
||||
double timeLeft = (animationEnd - DateTime.Now).TotalMilliseconds;
|
||||
|
||||
if (timeLeft <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
int ticks = (int) Math.Max(200, 500 * (timeLeft / duration));
|
||||
// ReSharper disable once AccessToModifiedClosure
|
||||
skew = Math.Max(0.8, skew - 0.001);
|
||||
|
||||
ConfettiCannon.Fire(new ConfettiCannon.Options
|
||||
{
|
||||
ParticleCount = 1,
|
||||
StartVelocity = 0,
|
||||
Ticks = ticks,
|
||||
Origin = new Point(
|
||||
random.NextDouble(),
|
||||
random.NextDouble() * skew - 0.2
|
||||
),
|
||||
Colors = ["#ffffff"],
|
||||
Shapes = ["circle"],
|
||||
Gravity = RandomInRange(0.4, 0.6),
|
||||
Scalar = RandomInRange(0.4, 1),
|
||||
Drift = RandomInRange(-0.4, 0.4),
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void ButtonSchoolPrideOnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
const int duration = 15 * 1000;
|
||||
|
||||
DateTime animationEnd = DateTime.Now + TimeSpan.FromMilliseconds(duration);
|
||||
|
||||
new AnimationFrame(this).Start(_ =>
|
||||
{
|
||||
double timeLeft = (animationEnd - DateTime.Now).TotalMilliseconds;
|
||||
if (timeLeft <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
ConfettiCannon.Fire(new ConfettiCannon.Options
|
||||
{
|
||||
ParticleCount = 2,
|
||||
Angle = 60,
|
||||
Spread = 55,
|
||||
Origin = new Point(0, 0.5),
|
||||
Colors = ["#bb0000", "#ffffff"],
|
||||
});
|
||||
ConfettiCannon.Fire(new ConfettiCannon.Options
|
||||
{
|
||||
ParticleCount = 2,
|
||||
Angle = 120,
|
||||
Spread = 55,
|
||||
Origin = new Point(1, 0.5),
|
||||
Colors = ["#bb0000", "#ffffff"],
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void ButtonCustomCanvasOnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
ConfettiCannon.Fire(new ConfettiCannon.Options
|
||||
{
|
||||
Spread = 70,
|
||||
Origin = new Point(0.5, 1.2),
|
||||
Token = "ConfettiCannonDemo"
|
||||
});
|
||||
}
|
||||
|
||||
private static double RandomInRange(double min, double max)
|
||||
{
|
||||
var random = new Random();
|
||||
return random.NextDouble() * (max - min) + min;
|
||||
}
|
||||
|
||||
private class AnimationFrame
|
||||
{
|
||||
private readonly Visual _host;
|
||||
private Func<double, bool>? _callback;
|
||||
private long _lastTicks;
|
||||
|
||||
public AnimationFrame(Visual host)
|
||||
{
|
||||
_host = host;
|
||||
}
|
||||
|
||||
public void Start(Func<double, bool> callback)
|
||||
{
|
||||
_callback = callback;
|
||||
_lastTicks = DateTime.Now.Ticks;
|
||||
|
||||
var topLevel = TopLevel.GetTopLevel(_host);
|
||||
topLevel?.RequestAnimationFrame(OnFrame);
|
||||
}
|
||||
|
||||
private void OnFrame(TimeSpan time)
|
||||
{
|
||||
long nowTicks = DateTime.Now.Ticks;
|
||||
double deltaTime = (nowTicks - _lastTicks) / 10000000.0;
|
||||
_lastTicks = nowTicks;
|
||||
|
||||
if (_callback?.Invoke(deltaTime) == true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var topLevel = TopLevel.GetTopLevel(_host);
|
||||
topLevel?.RequestAnimationFrame(OnFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
// adapted from https://github.com/catdad/canvas-confetti
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
using HandyControl.Tools;
|
||||
|
||||
namespace HandyControl.Controls;
|
||||
|
||||
public class ConfettiCannon
|
||||
{
|
||||
private static readonly ControlTokenManager<Control> TokenManager =
|
||||
new(unregisterCallback: OnTokenUnregistered);
|
||||
|
||||
public static readonly AttachedProperty<string?> TokenProperty =
|
||||
AvaloniaProperty.RegisterAttached<ConfettiCannon, AvaloniaObject, string?>("Token");
|
||||
|
||||
public static void SetToken(AvaloniaObject element, string? value) => element.SetValue(TokenProperty, value);
|
||||
|
||||
public static string? GetToken(AvaloniaObject element) => element.GetValue(TokenProperty);
|
||||
|
||||
static ConfettiCannon()
|
||||
{
|
||||
TokenProperty.Changed.AddClassHandler<AvaloniaObject>(TokenManager.OnTokenChanged);
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<string, ConfettiCannon> ConfettiCannons = new();
|
||||
private static readonly Lazy<ConfettiCannon> DefaultConfettiCannon = new(() => new ConfettiCannon(), true);
|
||||
private static readonly Random Random = new();
|
||||
|
||||
private readonly ConcurrentQueue<Confetti> _confettis = new();
|
||||
private ConfettiOverlay? _overlay;
|
||||
private Panel? _hostPanel;
|
||||
private Rect _renderRect;
|
||||
private bool _renderingHooked;
|
||||
|
||||
private ConfettiCannon()
|
||||
{
|
||||
}
|
||||
|
||||
public static void Fire()
|
||||
{
|
||||
Fire(new Options());
|
||||
}
|
||||
|
||||
public static void Fire(Options options)
|
||||
{
|
||||
ConfettiCannon confettiCannon = EnsureConfettiCannon(options.Token);
|
||||
confettiCannon.ShowContainer(options.Token);
|
||||
confettiCannon.AddConfettis(options);
|
||||
confettiCannon.StartAnimation();
|
||||
}
|
||||
|
||||
private static void OnTokenUnregistered(string token, Control element)
|
||||
{
|
||||
ConfettiCannons.TryRemove(token, out _);
|
||||
}
|
||||
|
||||
private static ConfettiCannon EnsureConfettiCannon(string? token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
return DefaultConfettiCannon.Value;
|
||||
}
|
||||
|
||||
return ConfettiCannons.GetOrAdd(token!, _ => new ConfettiCannon());
|
||||
}
|
||||
|
||||
private static Window? GetActiveWindow()
|
||||
{
|
||||
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return desktop.Windows.FirstOrDefault(w => w.IsActive) ?? desktop.MainWindow;
|
||||
}
|
||||
|
||||
private static Panel? GetHostPanel(string? token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
var window = GetActiveWindow();
|
||||
return window is null ? null : GetWindowOverlay(window);
|
||||
}
|
||||
|
||||
if (!TokenManager.TryGetControl(token, out Control? element) || element is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (element is Window w)
|
||||
{
|
||||
return GetWindowOverlay(w);
|
||||
}
|
||||
|
||||
return element.GetVisualDescendants().OfType<ConfettiCannonContainer>().FirstOrDefault()
|
||||
?? (element as ConfettiCannonContainer);
|
||||
}
|
||||
|
||||
private static Panel? GetWindowOverlay(Window window)
|
||||
{
|
||||
return OverlayLayer.GetOverlayLayer(window);
|
||||
}
|
||||
|
||||
private static Brush HexToBrush(string hex)
|
||||
{
|
||||
return new SolidColorBrush(Color.Parse(hex));
|
||||
}
|
||||
|
||||
private void StartAnimation()
|
||||
{
|
||||
if (_renderingHooked || _overlay is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var topLevel = TopLevel.GetTopLevel(_overlay);
|
||||
if (topLevel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_renderingHooked = true;
|
||||
topLevel.RequestAnimationFrame(OnRenderingFrame);
|
||||
}
|
||||
|
||||
private void OnRenderingFrame(TimeSpan time)
|
||||
{
|
||||
if (_overlay is null)
|
||||
{
|
||||
_renderingHooked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
SyncOverlaySize();
|
||||
|
||||
RecycleConfettis();
|
||||
|
||||
_overlay.UpdateConfettis(_confettis, _renderRect);
|
||||
_overlay.InvalidateVisual();
|
||||
|
||||
if (_confettis.IsEmpty)
|
||||
{
|
||||
HideContainer();
|
||||
_renderingHooked = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var topLevel = TopLevel.GetTopLevel(_overlay);
|
||||
topLevel?.RequestAnimationFrame(OnRenderingFrame);
|
||||
}
|
||||
|
||||
private void RecycleConfettis()
|
||||
{
|
||||
int initialCount = _confettis.Count;
|
||||
|
||||
for (int i = 0; i < initialCount; i++)
|
||||
{
|
||||
if (_confettis.TryDequeue(out var confetti))
|
||||
{
|
||||
if (!confetti.IsCompleted)
|
||||
{
|
||||
_confettis.Enqueue(confetti);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddConfettis(Options options)
|
||||
{
|
||||
double radAngle = (270 - options.Angle) * (Math.PI / 180);
|
||||
double radSpread = options.Spread * (Math.PI / 180);
|
||||
|
||||
Point origin = options.Origin;
|
||||
double startVelocity = options.StartVelocity;
|
||||
int ticks = options.Ticks;
|
||||
double decay = options.Decay;
|
||||
double draft = options.Drift;
|
||||
double gravity = options.Gravity;
|
||||
double scalar = options.Scalar;
|
||||
bool flat = options.Flat;
|
||||
List<IBrush> brushes = options.GetFinalBrushes();
|
||||
|
||||
for (int i = 0; i < options.ParticleCount; i++)
|
||||
{
|
||||
_confettis.Enqueue(new Confetti
|
||||
{
|
||||
Point = new Point(origin.X * _renderRect.Width, origin.Y * _renderRect.Height),
|
||||
Wobble = Random.NextDouble() * 10,
|
||||
WobbleSpeed = Math.Min(0.11, Random.NextDouble() * 0.1 + 0.05),
|
||||
Velocity = startVelocity * 0.5 + Random.NextDouble() * startVelocity,
|
||||
Angle2D = -radAngle + (0.5 * radSpread - Random.NextDouble() * radSpread),
|
||||
TiltAngle = (Random.NextDouble() * (0.75 - 0.25) + 0.25) * Math.PI,
|
||||
Brush = brushes[i % brushes.Count],
|
||||
Shape = options.Shapes[Random.Next(options.Shapes.Count)],
|
||||
TotalTicks = ticks,
|
||||
Decay = decay,
|
||||
Drift = draft,
|
||||
RandomValue = Random.NextDouble() + 2,
|
||||
Gravity = gravity * 3,
|
||||
OvalScalar = 0.6,
|
||||
Scalar = scalar,
|
||||
Flat = flat,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowContainer(string? token)
|
||||
{
|
||||
Panel? host = GetHostPanel(token);
|
||||
if (host is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Bounds may not be assigned yet on the first hit. Fall back to the
|
||||
// owning TopLevel's client size so particles don't spawn at (0,0).
|
||||
var size = host.Bounds.Size;
|
||||
if (size.Width <= 0 || size.Height <= 0)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(host);
|
||||
if (topLevel is not null)
|
||||
{
|
||||
size = topLevel.ClientSize;
|
||||
}
|
||||
}
|
||||
_renderRect = new Rect(size);
|
||||
|
||||
if (ReferenceEquals(_hostPanel, host) && _overlay is not null && _overlay.GetVisualParent() == host)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HideContainer();
|
||||
|
||||
_hostPanel = host;
|
||||
_overlay = new ConfettiOverlay
|
||||
{
|
||||
IsHitTestVisible = false,
|
||||
Width = size.Width,
|
||||
Height = size.Height,
|
||||
};
|
||||
|
||||
host.Children.Add(_overlay);
|
||||
}
|
||||
|
||||
private void HideContainer()
|
||||
{
|
||||
if (_overlay is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_overlay.GetVisualParent() is Panel parent)
|
||||
{
|
||||
parent.Children.Remove(_overlay);
|
||||
}
|
||||
|
||||
_overlay = null;
|
||||
_hostPanel = null;
|
||||
}
|
||||
|
||||
private void SyncOverlaySize()
|
||||
{
|
||||
if (_overlay is null || _hostPanel is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var size = _hostPanel.Bounds.Size;
|
||||
if (size.Width <= 0 || size.Height <= 0)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(_hostPanel);
|
||||
if (topLevel is not null)
|
||||
{
|
||||
size = topLevel.ClientSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (Math.Abs(_overlay.Width - size.Width) > 0.5 ||
|
||||
Math.Abs(_overlay.Height - size.Height) > 0.5)
|
||||
{
|
||||
_overlay.Width = size.Width;
|
||||
_overlay.Height = size.Height;
|
||||
}
|
||||
|
||||
_renderRect = new Rect(size);
|
||||
}
|
||||
|
||||
public class Options
|
||||
{
|
||||
private static readonly List<string> DefaultColors =
|
||||
[
|
||||
"#26ccff",
|
||||
"#a25afd",
|
||||
"#ff5e7e",
|
||||
"#88ff5a",
|
||||
"#fcff42",
|
||||
"#ffa62d",
|
||||
"#ff36ff"
|
||||
];
|
||||
|
||||
public string? Token { get; set; }
|
||||
|
||||
public int ParticleCount { get; set; } = 50;
|
||||
|
||||
public double Angle { get; set; } = 90;
|
||||
|
||||
public double Spread { get; set; } = 45;
|
||||
|
||||
public double StartVelocity { get; set; } = 45;
|
||||
|
||||
public double Decay { get; set; } = 0.9;
|
||||
|
||||
public double Gravity { get; set; } = 1;
|
||||
|
||||
public double Drift { get; set; }
|
||||
|
||||
public int Ticks { get; set; } = 200;
|
||||
|
||||
public Point Origin { get; set; } = new(0.5, 0.5);
|
||||
|
||||
public List<string> Shapes { get; set; } = ["square", "circle"];
|
||||
|
||||
public List<IBrush> Brushes { get; set; } = [];
|
||||
|
||||
public List<string> Colors { get; set; } = DefaultColors;
|
||||
|
||||
public double Scalar { get; set; } = 1;
|
||||
|
||||
public bool Flat { get; set; }
|
||||
|
||||
internal List<IBrush> GetFinalBrushes()
|
||||
{
|
||||
if (Brushes is { Count: > 0 })
|
||||
{
|
||||
return Brushes;
|
||||
}
|
||||
|
||||
if (Colors is null || Colors.Count == 0)
|
||||
{
|
||||
return DefaultColors.Select(HexToBrush).Cast<IBrush>().ToList();
|
||||
}
|
||||
|
||||
return Colors.Select(HexToBrush).Cast<IBrush>().ToList();
|
||||
}
|
||||
}
|
||||
|
||||
internal class Confetti
|
||||
{
|
||||
public Point Point { get; set; }
|
||||
|
||||
public double Wobble { get; set; }
|
||||
|
||||
public double WobbleSpeed { get; set; }
|
||||
|
||||
public double Velocity { get; set; }
|
||||
|
||||
public double Angle2D { get; set; }
|
||||
|
||||
public double TiltAngle { get; set; }
|
||||
|
||||
public IBrush Brush { get; set; } = Brushes.White;
|
||||
|
||||
public object Shape { get; set; } = "square";
|
||||
|
||||
public int Tick { get; set; }
|
||||
|
||||
public int TotalTicks { get; set; }
|
||||
|
||||
public double Decay { get; set; }
|
||||
|
||||
public double Drift { get; set; }
|
||||
|
||||
public double RandomValue { get; set; }
|
||||
|
||||
public double TiltSin { get; set; }
|
||||
|
||||
public double TiltCos { get; set; }
|
||||
|
||||
public double WobbleX { get; set; }
|
||||
|
||||
public double WobbleY { get; set; }
|
||||
|
||||
public double Gravity { get; set; }
|
||||
|
||||
public double Scalar { get; set; }
|
||||
|
||||
public double OvalScalar { get; set; }
|
||||
|
||||
public bool Flat { get; set; }
|
||||
|
||||
public bool IsCompleted => Tick >= TotalTicks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace HandyControl.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A panel that can host child content and render confetti on top of it
|
||||
/// when targeted by <see cref="ConfettiCannon"/> with a matching token.
|
||||
/// </summary>
|
||||
public class ConfettiCannonContainer : Panel;
|
||||
@@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace HandyControl.Controls;
|
||||
|
||||
internal class ConfettiOverlay : Control
|
||||
{
|
||||
private static readonly Random Random = new();
|
||||
|
||||
private readonly List<ConfettiCannon.Confetti> _renderList = new();
|
||||
private Rect _renderRect;
|
||||
|
||||
public ConfettiOverlay()
|
||||
{
|
||||
IsHitTestVisible = false;
|
||||
}
|
||||
|
||||
public void UpdateConfettis(ConcurrentQueue<ConfettiCannon.Confetti> confettis, Rect renderRect)
|
||||
{
|
||||
_renderRect = renderRect;
|
||||
_renderList.Clear();
|
||||
foreach (var confetti in confettis)
|
||||
{
|
||||
UpdateConfetti(confetti);
|
||||
_renderList.Add(confetti);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
base.Render(context);
|
||||
|
||||
using (context.PushClip(_renderRect))
|
||||
{
|
||||
foreach (var confetti in _renderList)
|
||||
{
|
||||
DrawConfetti(context, confetti);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateConfetti(ConfettiCannon.Confetti confetti)
|
||||
{
|
||||
confetti.Point = new Point(
|
||||
confetti.Point.X + Math.Sin(confetti.Angle2D) * confetti.Velocity + confetti.Drift,
|
||||
confetti.Point.Y + Math.Cos(confetti.Angle2D) * confetti.Velocity + confetti.Gravity
|
||||
);
|
||||
confetti.Velocity *= confetti.Decay;
|
||||
|
||||
if (confetti.Flat)
|
||||
{
|
||||
confetti.Wobble = 0;
|
||||
confetti.WobbleX = confetti.Point.X + 10 * confetti.Scalar;
|
||||
confetti.WobbleY = confetti.Point.Y + 10 * confetti.Scalar;
|
||||
|
||||
confetti.TiltSin = 0;
|
||||
confetti.TiltCos = 0;
|
||||
confetti.RandomValue = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
confetti.Wobble += confetti.WobbleSpeed;
|
||||
confetti.WobbleX = confetti.Point.X + 10 * confetti.Scalar * Math.Cos(confetti.Wobble);
|
||||
confetti.WobbleY = confetti.Point.Y + 10 * confetti.Scalar * Math.Sin(confetti.Wobble);
|
||||
|
||||
confetti.TiltAngle += 0.1;
|
||||
confetti.TiltSin = Math.Sin(confetti.TiltAngle);
|
||||
confetti.TiltCos = Math.Cos(confetti.TiltAngle);
|
||||
confetti.RandomValue = Random.NextDouble() + 2;
|
||||
}
|
||||
|
||||
confetti.Tick++;
|
||||
}
|
||||
|
||||
private static void DrawConfetti(DrawingContext context, ConfettiCannon.Confetti confetti)
|
||||
{
|
||||
double progress = (double) confetti.Tick / confetti.TotalTicks;
|
||||
double opacity = Math.Max(0, 1 - progress);
|
||||
var brush = confetti.Brush;
|
||||
|
||||
double x1 = confetti.Point.X + confetti.RandomValue * confetti.TiltCos;
|
||||
double y1 = confetti.Point.Y + confetti.RandomValue * confetti.TiltSin;
|
||||
double x2 = confetti.WobbleX + confetti.RandomValue * confetti.TiltCos;
|
||||
double y2 = confetti.WobbleY + confetti.RandomValue * confetti.TiltSin;
|
||||
|
||||
using (context.PushOpacity(opacity))
|
||||
{
|
||||
if ("circle".Equals(confetti.Shape))
|
||||
{
|
||||
double radiusX = Math.Abs(x2 - x1) * confetti.OvalScalar;
|
||||
double radiusY = Math.Abs(y2 - y1) * confetti.OvalScalar;
|
||||
double angle = 18 * confetti.Wobble;
|
||||
|
||||
var rotate = new RotateTransform(angle, confetti.Point.X, confetti.Point.Y);
|
||||
using (context.PushTransform(rotate.Value))
|
||||
{
|
||||
context.DrawEllipse(brush, null, confetti.Point, radiusX, radiusY);
|
||||
}
|
||||
}
|
||||
else if ("star".Equals(confetti.Shape))
|
||||
{
|
||||
double rot = Math.PI / 2 * 3;
|
||||
double innerRadius = 4 * confetti.Scalar;
|
||||
double outerRadius = 8 * confetti.Scalar;
|
||||
int spikes = 5;
|
||||
double step = Math.PI / spikes;
|
||||
bool isFirst = true;
|
||||
|
||||
var geometry = new StreamGeometry();
|
||||
using (var sgc = geometry.Open())
|
||||
{
|
||||
while (spikes-- > 0)
|
||||
{
|
||||
double x = confetti.Point.X + Math.Cos(rot) * outerRadius;
|
||||
double y = confetti.Point.Y + Math.Sin(rot) * outerRadius;
|
||||
|
||||
if (isFirst)
|
||||
{
|
||||
sgc.BeginFigure(new Point(x, y), true);
|
||||
isFirst = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
sgc.LineTo(new Point(x, y));
|
||||
}
|
||||
|
||||
rot += step;
|
||||
|
||||
x = confetti.Point.X + Math.Cos(rot) * innerRadius;
|
||||
y = confetti.Point.Y + Math.Sin(rot) * innerRadius;
|
||||
sgc.LineTo(new Point(x, y));
|
||||
rot += step;
|
||||
}
|
||||
|
||||
sgc.EndFigure(true);
|
||||
}
|
||||
|
||||
context.DrawGeometry(brush, null, geometry);
|
||||
}
|
||||
else
|
||||
{
|
||||
var geometry = new StreamGeometry();
|
||||
using (var sgc = geometry.Open())
|
||||
{
|
||||
sgc.BeginFigure(new Point(Math.Floor(confetti.Point.X), Math.Floor(confetti.Point.Y)), true);
|
||||
sgc.LineTo(new Point(Math.Floor(confetti.WobbleX), Math.Floor(y1)));
|
||||
sgc.LineTo(new Point(Math.Floor(x2), Math.Floor(y2)));
|
||||
sgc.LineTo(new Point(Math.Floor(x1), Math.Floor(confetti.WobbleY)));
|
||||
sgc.EndFigure(true);
|
||||
}
|
||||
|
||||
context.DrawGeometry(brush, null, geometry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace HandyControl.Tools;
|
||||
|
||||
internal class ControlTokenManager<T>(
|
||||
Action<string, T>? registerCallback = null,
|
||||
Action<string, T>? unregisterCallback = null
|
||||
)
|
||||
where T : Control
|
||||
{
|
||||
private readonly Dictionary<string, WeakReference<T>> _controlDict = new();
|
||||
|
||||
public void Register(string? token, T? control)
|
||||
{
|
||||
if (string.IsNullOrEmpty(token) || control == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_controlDict[token!] = new WeakReference<T>(control);
|
||||
registerCallback?.Invoke(token!, control);
|
||||
}
|
||||
|
||||
public void Unregister(string? token, T? control)
|
||||
{
|
||||
if (string.IsNullOrEmpty(token) || control == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_controlDict.TryGetValue(token!, out var reference))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reference.TryGetTarget(out var target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ReferenceEquals(target, control))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_controlDict.Remove(token!);
|
||||
unregisterCallback?.Invoke(token!, control);
|
||||
}
|
||||
|
||||
public void Unregister(T? control)
|
||||
{
|
||||
if (control == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string? unregisteredToken = null;
|
||||
foreach (var item in _controlDict)
|
||||
{
|
||||
if (!item.Value.TryGetTarget(out var target) || !ReferenceEquals(control, target))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
unregisteredToken = item.Key;
|
||||
break;
|
||||
}
|
||||
|
||||
if (unregisteredToken == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_controlDict.Remove(unregisteredToken);
|
||||
unregisterCallback?.Invoke(unregisteredToken, control);
|
||||
}
|
||||
|
||||
public void Unregister(string? token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_controlDict.TryGetValue(token!, out var reference))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_controlDict.Remove(token!);
|
||||
|
||||
if (!reference.TryGetTarget(out var target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
unregisterCallback?.Invoke(token!, target);
|
||||
}
|
||||
|
||||
public bool TryGetControl(string? token, out T? control)
|
||||
{
|
||||
control = null;
|
||||
|
||||
if (string.IsNullOrEmpty(token) || !_controlDict.TryGetValue(token!, out var reference))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return reference.TryGetTarget(out control);
|
||||
}
|
||||
|
||||
public void OnTokenChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not T control)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.NewValue == null)
|
||||
{
|
||||
Unregister(control);
|
||||
}
|
||||
else
|
||||
{
|
||||
Register(e.NewValue.ToString(), control);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user