From 79b32d573c92eeb926d74155edf3412f6ccb8f49 Mon Sep 17 00:00:00 2001
From: NaBian <836904362@qq.com>
Date: Thu, 15 May 2025 23:09:57 +0800
Subject: [PATCH] feat: add ConfettiCannon.
---
README-cn.md | 12 +-
README.md | 8 +-
.../HandyControlDemo_Code.csproj | 7 +
.../Data/DemoInfo.json | 9 +-
.../HandyControlDemo_Shared.projitems | 4 +
.../Resources/Themes/Basic/Brushes.xaml | 18 +-
.../Controls/ConfettiCannonDemo.xaml | 35 ++
.../Controls/ConfettiCannonDemo.xaml.cs | 288 +++++++++++
.../Controls/ConfettiCannon/ConfettiCannon.cs | 473 ++++++++++++++++++
.../ConfettiCannon/ConfettiCannonContainer.cs | 5 +
.../Controls/Other/VisualAdornerContainer.cs | 39 ++
.../HandyControl_Shared.projitems | 4 +
...dyControl.Controls.ConfettiCannon.icon.bmp | Bin 0 -> 824 bytes
13 files changed, 889 insertions(+), 13 deletions(-)
create mode 100644 src/Shared/HandyControlDemo_Shared/UserControl/Controls/ConfettiCannonDemo.xaml
create mode 100644 src/Shared/HandyControlDemo_Shared/UserControl/Controls/ConfettiCannonDemo.xaml.cs
create mode 100644 src/Shared/HandyControl_Shared/Controls/ConfettiCannon/ConfettiCannon.cs
create mode 100644 src/Shared/HandyControl_Shared/Controls/ConfettiCannon/ConfettiCannonContainer.cs
create mode 100644 src/Shared/HandyControl_Shared/Controls/Other/VisualAdornerContainer.cs
create mode 100644 src/Shared/HandyControl_Shared/Resources/Icons/HandyControl.Controls.ConfettiCannon.icon.bmp
diff --git a/README-cn.md b/README-cn.md
index 3643df78..df1eec5d 100644
--- a/README-cn.md
+++ b/README-cn.md
@@ -205,13 +205,17 @@ Step 4: 开始编码
## 最新示例
+### ConfettiCannon
+
+
+
### Watermark
-
+
### AutoCompleteTextBox
-
+
### ElementGroup
@@ -221,12 +225,12 @@ Step 4: 开始编码

+## 历史发布
+
### FlexPanel

-## 历史发布
-
### CheckComboBox

diff --git a/README.md b/README.md
index eb7b8105..4bdc4651 100644
--- a/README.md
+++ b/README.md
@@ -193,6 +193,10 @@ Step 4: Enjoy coding
## Latest examples
+### ConfettiCannon
+
+
+
### Watermark

@@ -209,12 +213,12 @@ Step 4: Enjoy coding

+## History publication
+
### FlexPanel

-## History publication
-
### CheckComboBox

diff --git a/src/Shared/HandyControlDemo_Code/HandyControlDemo_Code.csproj b/src/Shared/HandyControlDemo_Code/HandyControlDemo_Code.csproj
index b18f4c2b..f2ad4ff4 100644
--- a/src/Shared/HandyControlDemo_Code/HandyControlDemo_Code.csproj
+++ b/src/Shared/HandyControlDemo_Code/HandyControlDemo_Code.csproj
@@ -101,6 +101,9 @@
UserControl\Controls\CompareSliderDemo.xaml
+
+ UserControl\Controls\ConfettiCannonDemo.xaml
+
UserControl\Controls\CoverFlowDemo.xaml
@@ -495,6 +498,10 @@
UserControl\Controls\CompareSliderDemo.xaml.cs
CompareSliderDemo.xaml
+
+ UserControl\Controls\ConfettiCannonDemo.xaml.cs
+ ConfettiCannonDemo.xaml
+
UserControl\Controls\CoverFlowDemo.xaml.cs
CoverFlowDemo.xaml
diff --git a/src/Shared/HandyControlDemo_Shared/Data/DemoInfo.json b/src/Shared/HandyControlDemo_Shared/Data/DemoInfo.json
index 68798937..a43073cf 100644
--- a/src/Shared/HandyControlDemo_Shared/Data/DemoInfo.json
+++ b/src/Shared/HandyControlDemo_Shared/Data/DemoInfo.json
@@ -2,7 +2,7 @@
{
"title": "Styles",
"selectedIndex": -1,
- "group": false,
+ "group": false,
"demoItemList": [
[ "Brush", "BrushDemo", "Brush.Brush", "", "" ],
[ "Button", "ButtonDemo", "Brush.ButtonClick", "", "" ],
@@ -43,6 +43,7 @@
"group": true,
"demoItemList": [
[ "FloatingBlock", "FloatingBlockDemo", "Brush.LikeFilled", "", "Interacting" ],
+ [ "ConfettiCannon", "ConfettiCannonDemo", "Brush.Favorite", "1", "Interacting" ],
[ "PropertyGrid", "PropertyGridDemo", "Brush.PropertyGridEditorPart", "", "DataEntry" ],
[ "ImageSelector", "ImageSelectorDemo", "Brush.Image", "", "DataEntry" ],
[ "ImageBlock", "ImageBlockDemo", "Brush.ImageStack", "", "DataDisplay" ],
@@ -79,7 +80,7 @@
[ "Divider", "DividerDemo", "Brush.DottedSplitter", "", "Layout" ],
[ "CirclePanel", "CirclePanelDemo", "Brush.ColorWheel", "", "Layout" ],
[ "Grid", "GridDemo", "Brush.GridPane", "", "Layout" ],
- [ "FlexPanel", "FlexPanelDemo", "Brush.FlowLayoutPanel", "1", "Layout" ],
+ [ "FlexPanel", "FlexPanelDemo", "Brush.FlowLayoutPanel", "", "Layout" ],
[ "UniformSpacingPanel", "UniformSpacingPanelDemo", "Brush.IncreaseHorizontalSpacing", "1", "Layout" ],
[ "RelativePanel", "RelativePanelDemo", "Brush.TransformRelative", "", "Layout" ],
[ "WaterfallPanel", "WaterfallPanelDemo", "Brush.Panel", "", "Layout" ],
@@ -121,8 +122,8 @@
"group": false,
"demoItemList": [
[ "HatchBrushGenerator", "HatchBrushGeneratorDemo", "Brush.DataGenerator", "", "" ],
- [ "Morphing_Animation", "GeometryAnimationDemo", "Brush.Animation", "", "" ],
- [ "Effects", "EffectsDemo", "Brush.Effects", "", "" ]
+ [ "MorphingAnimation", "GeometryAnimationDemo", "Brush.Animation", "", "" ],
+ [ "Effects", "EffectsDemo", "Brush.Effects", "", "" ],
]
}
]
diff --git a/src/Shared/HandyControlDemo_Shared/HandyControlDemo_Shared.projitems b/src/Shared/HandyControlDemo_Shared/HandyControlDemo_Shared.projitems
index 3f7ca4c2..c793f5be 100644
--- a/src/Shared/HandyControlDemo_Shared/HandyControlDemo_Shared.projitems
+++ b/src/Shared/HandyControlDemo_Shared/HandyControlDemo_Shared.projitems
@@ -64,6 +64,9 @@
AutoCompleteTextBoxDemo.xaml
+
+ ConfettiCannonDemo.xaml
+
ElementGroupDemo.xaml
@@ -551,6 +554,7 @@
Designer
MSBuild:Compile
+
Designer
MSBuild:Compile
diff --git a/src/Shared/HandyControlDemo_Shared/Resources/Themes/Basic/Brushes.xaml b/src/Shared/HandyControlDemo_Shared/Resources/Themes/Basic/Brushes.xaml
index 79b21c74..a38157c0 100644
--- a/src/Shared/HandyControlDemo_Shared/Resources/Themes/Basic/Brushes.xaml
+++ b/src/Shared/HandyControlDemo_Shared/Resources/Themes/Basic/Brushes.xaml
@@ -11,7 +11,7 @@
-
+
@@ -57,7 +57,7 @@
-
+
@@ -71,7 +71,7 @@
-
+
@@ -1387,5 +1387,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Shared/HandyControlDemo_Shared/UserControl/Controls/ConfettiCannonDemo.xaml b/src/Shared/HandyControlDemo_Shared/UserControl/Controls/ConfettiCannonDemo.xaml
new file mode 100644
index 00000000..48d5b16e
--- /dev/null
+++ b/src/Shared/HandyControlDemo_Shared/UserControl/Controls/ConfettiCannonDemo.xaml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Shared/HandyControlDemo_Shared/UserControl/Controls/ConfettiCannonDemo.xaml.cs b/src/Shared/HandyControlDemo_Shared/UserControl/Controls/ConfettiCannonDemo.xaml.cs
new file mode 100644
index 00000000..ded59bde
--- /dev/null
+++ b/src/Shared/HandyControlDemo_Shared/UserControl/Controls/ConfettiCannonDemo.xaml.cs
@@ -0,0 +1,288 @@
+// adapted from https://www.kirilv.com/canvas-confetti
+
+using System;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Threading;
+using HandyControl.Controls;
+
+namespace HandyControlDemo.UserControl;
+
+public partial class ConfettiCannonDemo
+{
+ 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
+ {
+ X = RandomInRange(0.1, 0.3),
+ Y = random.NextDouble() - 0.2
+ }
+ });
+ ConfettiCannon.Fire(new ConfettiCannon.Options
+ {
+ StartVelocity = 30,
+ Spread = 360,
+ Ticks = 60,
+ ParticleCount = particleCount,
+ Origin = new Point
+ {
+ X = RandomInRange(0.7, 0.9),
+ Y = 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().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
+ {
+ X = random.NextDouble(),
+ Y = 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().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 Func _callback;
+ private long _lastTicks;
+
+ public void Start(Func callback)
+ {
+ _callback = callback;
+ _lastTicks = DateTime.Now.Ticks;
+ CompositionTarget.Rendering += OnRendering;
+ }
+
+ private void OnRendering(object sender, EventArgs e)
+ {
+ long nowTicks = DateTime.Now.Ticks;
+ double deltaTime = (nowTicks - _lastTicks) / 10000000.0;
+ _lastTicks = nowTicks;
+
+ if (_callback?.Invoke(deltaTime) == true)
+ {
+ CompositionTarget.Rendering -= OnRendering;
+ }
+ }
+ }
+}
diff --git a/src/Shared/HandyControl_Shared/Controls/ConfettiCannon/ConfettiCannon.cs b/src/Shared/HandyControl_Shared/Controls/ConfettiCannon/ConfettiCannon.cs
new file mode 100644
index 00000000..f7e7dc50
--- /dev/null
+++ b/src/Shared/HandyControl_Shared/Controls/ConfettiCannon/ConfettiCannon.cs
@@ -0,0 +1,473 @@
+// adapted from https://github.com/catdad/canvas-confetti
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using System.Windows.Documents;
+using System.Windows.Media;
+using HandyControl.Tools;
+
+namespace HandyControl.Controls;
+
+public class ConfettiCannon : DependencyObject
+{
+ public static readonly DependencyProperty TokenProperty = DependencyProperty.RegisterAttached(
+ "Token", typeof(string), typeof(ConfettiCannon), new PropertyMetadata(null, OnTokenChanged));
+
+ public static void SetToken(DependencyObject element, string value)
+ => element.SetValue(TokenProperty, value);
+
+ public static string GetToken(DependencyObject element)
+ => (string) element.GetValue(TokenProperty);
+
+ private static readonly ConcurrentDictionary ConfettiCannons = new();
+ private static readonly Dictionary ContainerDict = new();
+ private static readonly Lazy DefaultConfettiCannon = new(() => new ConfettiCannon(), true);
+ private static readonly Random Random = new();
+
+ private readonly DrawingVisual _offscreenVisual = new();
+ private readonly ConcurrentQueue _confettis = [];
+
+ private readonly EllipseGeometry _sharedCircleGeometry = new()
+ {
+ Transform = new RotateTransform()
+ };
+
+ private Rect _renderRect;
+ private AdornerLayer _currentAdornerLayer;
+
+ 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();
+ }
+
+ public static void Register(string token, FrameworkElement element)
+ {
+ if (string.IsNullOrEmpty(token) || element is null)
+ {
+ return;
+ }
+
+ ContainerDict[token] = element;
+ }
+
+ public static void Unregister(FrameworkElement element)
+ {
+ if (element is null)
+ {
+ return;
+ }
+
+ var first = ContainerDict.FirstOrDefault(item => ReferenceEquals(element, item.Value));
+ Unregister(first.Key);
+ }
+
+ public static void Unregister(string token)
+ {
+ if (string.IsNullOrEmpty(token))
+ {
+ return;
+ }
+
+ ContainerDict.Remove(token);
+ ConfettiCannons.TryRemove(token, out _);
+ }
+
+ private static ConfettiCannon EnsureConfettiCannon(string token)
+ {
+ if (string.IsNullOrEmpty(token))
+ {
+ return DefaultConfettiCannon.Value;
+ }
+
+ if (ConfettiCannons.TryGetValue(token, out ConfettiCannon confettiCannon))
+ {
+ return confettiCannon;
+ }
+
+ ConfettiCannons[token] = new ConfettiCannon();
+ return ConfettiCannons[token];
+ }
+
+ private static void OnTokenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is not FrameworkElement element)
+ {
+ return;
+ }
+
+ if (e.NewValue is null)
+ {
+ Unregister(element);
+ }
+ else
+ {
+ Register(e.NewValue.ToString(), element);
+ }
+ }
+
+ private static AdornerLayer GetAdornerLayer(string token)
+ {
+ AdornerDecorator decorator;
+
+ if (string.IsNullOrEmpty(token))
+ {
+ decorator = VisualHelper.GetChild(WindowHelper.GetActiveWindow());
+ }
+ else
+ {
+ ContainerDict.TryGetValue(token, out FrameworkElement element);
+ decorator = element is System.Windows.Window
+ ? VisualHelper.GetChild(element)
+ : VisualHelper.GetChild(element);
+ }
+
+ return decorator?.AdornerLayer;
+ }
+
+ private static Brush HexToBrush(string hex)
+ {
+ return new SolidColorBrush((Color) ColorConverter.ConvertFromString(hex));
+ }
+
+ private void UpdateConfetti(DrawingContext context, 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;
+ }
+
+ double progress = (double) confetti.Tick++ / confetti.TotalTicks;
+ confetti.Brush.Opacity = 1 - progress;
+
+ 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;
+
+ 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 geometry = new EllipseGeometry(new Rect(
+ confetti.Point.X - radiusX,
+ confetti.Point.Y - radiusY,
+ radiusX * 2,
+ radiusY * 2
+ ))
+ {
+ Transform = new RotateTransform(angle, confetti.Point.X, confetti.Point.Y)
+ };
+
+ context.DrawGeometry(confetti.Brush, null, geometry);
+ }
+ 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 (StreamGeometryContext 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, true);
+ isFirst = false;
+ }
+
+ sgc.LineTo(new Point(x, y), false, false);
+ rot += step;
+
+ x = confetti.Point.X + Math.Cos(rot) * innerRadius;
+ y = confetti.Point.Y + Math.Sin(rot) * innerRadius;
+ sgc.LineTo(new Point(x, y), false, false);
+ rot += step;
+ }
+ }
+
+ context.DrawGeometry(confetti.Brush, null, geometry);
+ }
+ else
+ {
+ var geometry = new StreamGeometry();
+ using (StreamGeometryContext sgc = geometry.Open())
+ {
+ sgc.BeginFigure(new Point(Math.Floor(confetti.Point.X), Math.Floor(confetti.Point.Y)), true, true);
+ sgc.LineTo(new Point(Math.Floor(confetti.WobbleX), Math.Floor(y1)), false, false);
+ sgc.LineTo(new Point(Math.Floor(x2), Math.Floor(y2)), false, false);
+ sgc.LineTo(new Point(Math.Floor(x1), Math.Floor(confetti.WobbleY)), false, false);
+ }
+
+ context.DrawGeometry(confetti.Brush, null, geometry);
+ }
+ }
+
+ private void StartAnimation()
+ {
+ CompositionTarget.Rendering -= CompositionTargetOnRendering;
+ CompositionTarget.Rendering += CompositionTargetOnRendering;
+ }
+
+ private void CompositionTargetOnRendering(object sender, EventArgs e)
+ {
+ RecycleConfettis();
+
+ using (DrawingContext context = _offscreenVisual.RenderOpen())
+ {
+ context.PushClip(new RectangleGeometry(_renderRect));
+ foreach (Confetti confetti in _confettis)
+ {
+ UpdateConfetti(context, confetti);
+ }
+ }
+
+ if (_confettis.Count == 0)
+ {
+ HideContainer();
+ CompositionTarget.Rendering -= CompositionTargetOnRendering;
+ }
+ }
+
+ 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 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)
+ {
+ AdornerLayer layer = GetAdornerLayer(token);
+ if (layer is null)
+ {
+ return;
+ }
+
+ _renderRect = new Rect(layer.RenderSize);
+
+ if (ReferenceEquals(_currentAdornerLayer, layer))
+ {
+ return;
+ }
+
+ _currentAdornerLayer = layer;
+
+ layer.Add(new VisualAdornerContainer(layer)
+ {
+ Child = _offscreenVisual
+ });
+ }
+
+ private void HideContainer()
+ {
+ var adorner = VisualHelper.GetParent(_offscreenVisual);
+ if (adorner != null)
+ {
+ adorner.Child = null;
+ _currentAdornerLayer?.Remove(adorner);
+ _currentAdornerLayer = null;
+ }
+ }
+
+ public class Options
+ {
+ private static readonly List 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 Shapes { get; set; } = ["square", "circle"];
+
+ public List Brushes { get; set; } = [];
+
+ public List Colors { get; set; } = DefaultColors;
+
+ public double Scalar { get; set; } = 1;
+
+ public bool Flat { get; set; }
+
+ internal List GetFinalBrushes()
+ {
+ if (Brushes is not null && Brushes.Count != 0)
+ {
+ return Brushes;
+ }
+
+ if (Colors is null || Colors.Count == 0)
+ {
+ return DefaultColors.Select(HexToBrush).ToList();
+ }
+
+ return Colors.Select(HexToBrush).ToList();
+ }
+ }
+
+ private 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 Brush Brush { get; set; }
+
+ public object Shape { get; set; }
+
+ 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;
+ }
+}
diff --git a/src/Shared/HandyControl_Shared/Controls/ConfettiCannon/ConfettiCannonContainer.cs b/src/Shared/HandyControl_Shared/Controls/ConfettiCannon/ConfettiCannonContainer.cs
new file mode 100644
index 00000000..d381ab15
--- /dev/null
+++ b/src/Shared/HandyControl_Shared/Controls/ConfettiCannon/ConfettiCannonContainer.cs
@@ -0,0 +1,5 @@
+using System.Windows.Documents;
+
+namespace HandyControl.Controls;
+
+public class ConfettiCannonContainer : AdornerDecorator;
diff --git a/src/Shared/HandyControl_Shared/Controls/Other/VisualAdornerContainer.cs b/src/Shared/HandyControl_Shared/Controls/Other/VisualAdornerContainer.cs
new file mode 100644
index 00000000..7a7e42bf
--- /dev/null
+++ b/src/Shared/HandyControl_Shared/Controls/Other/VisualAdornerContainer.cs
@@ -0,0 +1,39 @@
+using System.Windows;
+using System.Windows.Documents;
+using System.Windows.Media;
+
+namespace HandyControl.Controls;
+
+public class VisualAdornerContainer(UIElement child) : Adorner(child)
+{
+ private Visual _child = child;
+
+ public Visual Child
+ {
+ get => _child;
+ set
+ {
+ if (value == null)
+ {
+ RemoveVisualChild(_child);
+ // ReSharper disable once ExpressionIsAlwaysNull
+ _child = value;
+ return;
+ }
+ AddVisualChild(value);
+ _child = value;
+ }
+ }
+
+ protected override int VisualChildrenCount => _child != null ? 1 : 0;
+
+ protected override Visual GetVisualChild(int index)
+ {
+ if (index == 0 && _child != null)
+ {
+ return _child;
+ }
+
+ return base.GetVisualChild(index);
+ }
+}
diff --git a/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems b/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems
index dea0bb2a..cb73ecf8 100644
--- a/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems
+++ b/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems
@@ -37,6 +37,8 @@
+
+
@@ -56,6 +58,7 @@
+
@@ -953,6 +956,7 @@
+
diff --git a/src/Shared/HandyControl_Shared/Resources/Icons/HandyControl.Controls.ConfettiCannon.icon.bmp b/src/Shared/HandyControl_Shared/Resources/Icons/HandyControl.Controls.ConfettiCannon.icon.bmp
new file mode 100644
index 0000000000000000000000000000000000000000..43cb1225cc15484f077eaaa44dc911e9c2866375
GIT binary patch
literal 824
zcma)(F%p6>5JjW*j-^NN1det39Dq~Ma{zjJZb46DMb9nhZSk4!ESW?{GbFS7_wDin
zKHLs2t}Ew-eR}VmJ4by_{eIMr0>|oBo=38z{M7rtABI7FDU3)7J)1Mnv-jRw)JTd*
zDdXuysj7-<+qQxUND)g|eAjgVEJ(q`h~IcwiXGeW(TwIZCxoz}-jm8(dRf8CvNTPT
z!iwv