實現效果:canvas
代碼組織:app
文件解釋:框架
界面xaml:dom
<Canvas x:Name="PageCanvas"> <ae:MagnitismCanvas x:Name="MagCanvas" Drag="0.8" BorderForce="1000" ChildForce="100" DataContext="{Binding ElementName=PageCanvas}" Width="{Binding Path=ActualWidth}" Height="{Binding Path=ActualHeight}"> <ae:FireworkEffect Canvas.Top="100" Canvas.Left="100"> <ae:FireworkEffect.Child> <Button>Click For Fireworks</Button> </ae:FireworkEffect.Child> </ae:FireworkEffect> <ae:SonicEffect Canvas.Top="200" Canvas.Left="200" RingThickness="10" RingRadius="15" RingDelay="0:0:2" RingColor="#22553366"> <ae:SonicEffect.Child> <Button>Click For Sonic burst</Button> </ae:SonicEffect.Child> </ae:SonicEffect>
OverlayRenderDecoratorOverlayVisual:ide
/// <summary> /// This class is used internally to delegate it's rendering to the parent OverlayRenderDecorator. /// </summary> internal class OverlayRenderDecoratorOverlayVisual : DrawingVisual { internal bool IsHitTestVisible { get; set; } = false; //dont hit test, these are just overlay graphics protected override GeometryHitTestResult HitTestCore(GeometryHitTestParameters hitTestParameters) { if (IsHitTestVisible) return base.HitTestCore(hitTestParameters); return null; } //dont hit test, these are just overlay graphics protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters) { if (IsHitTestVisible) return base.HitTestCore(hitTestParameters); return null; } }
/// <summary> /// OverlayRenderDecorator provides a simple overlay graphics mechanism similar /// to OnRender called OnOverlayRender /// </summary> [ContentProperty("Child")] public class OverlayRenderDecorator : FrameworkElement { //this will be a visual child, but not a logical child. as the last child, it can 'overlay' graphics // by calling back to us with OnOverlayRender private readonly OverlayRenderDecoratorOverlayVisual _overlayVisual; private readonly VisualCollection _vc; //the only logical child private UIElement _child; //------------------------------------------------------------------- // // Constructors // //------------------------------------------------------------------- #region Constructors /// <summary> /// Default constructor /// </summary> public OverlayRenderDecorator() { _overlayVisual = new OverlayRenderDecoratorOverlayVisual(); _vc = new VisualCollection(this) {_overlayVisual}; //insert the overlay element into the visual tree } #endregion //------------------------------------------------------------------- // // Public Properties // //------------------------------------------------------------------- #region Public Properties /// <summary> /// Enables/Disables hit testing on the overlay visual /// </summary> public bool IsOverlayHitTestVisible { get { if (_overlayVisual != null) return _overlayVisual.IsHitTestVisible; return false; } set { if (_overlayVisual != null) _overlayVisual.IsHitTestVisible = value; } } /// <summary> /// The single child of an <see cref="System.Windows.Media.Animation.OverlayRenderDecorator" /> /// </summary> [DefaultValue(null)] public virtual UIElement Child { get { return _child; } set { if (_child != value) { //need to remove old element from logical tree if (_child != null) { OnDetachChild(_child); RemoveLogicalChild(_child); } _vc.Clear(); if (value != null) { //add to visual _vc.Add(value); //add to logical AddLogicalChild(value); } //always add the overlay child back into the visual tree if its set if (_overlayVisual != null) _vc.Add(_overlayVisual); //cache the new child _child = value; //notify derived types of the new child if (value != null) OnAttachChild(_child); InvalidateMeasure(); } } } /// <summary> /// Returns enumerator to logical children. /// </summary> protected override IEnumerator LogicalChildren { get { if (_child == null) { return new List<UIElement>().GetEnumerator(); } var l = new List<UIElement> {_child}; return l.GetEnumerator(); } } #endregion //------------------------------------------------------------------- // // Protected Methods // //------------------------------------------------------------------- #region Protected Methods /// <summary> /// Updates DesiredSize of the OverlayRenderDecorator. Called by parent UIElement. This is the first pass of layout. /// </summary> /// <remarks> /// OverlayRenderDecorator determines a desired size it needs from the child's sizing properties, margin, and requested /// size. /// </remarks> /// <param name="constraint">Constraint size is an "upper limit" that the return value should not exceed.</param> /// <returns>The OverlayRenderDecorator's desired size.</returns> protected override Size MeasureOverride(Size constraint) { var child = Child; if (child != null) { child.Measure(constraint); return (child.DesiredSize); } return (new Size()); } /// <summary> /// OverlayRenderDecorator computes the position of its single child inside child's Margin and calls Arrange /// on the child. /// </summary> /// <param name="arrangeSize">Size the OverlayRenderDecorator will assume.</param> protected override Size ArrangeOverride(Size arrangeSize) { var child = Child; child?.Arrange(new Rect(arrangeSize)); //Our OnRender gets called in Arrange, but // we dont have access to that, so update the // overlay here. if (_overlayVisual != null) { using (var dc = _overlayVisual.RenderOpen()) { //delegate to derived types OnOverlayRender(dc); } } return (arrangeSize); } /// <summary> /// render method for overlay graphics. /// </summary> /// <param name="dc"></param> protected virtual void OnOverlayRender(DrawingContext dc) { } /// <summary> /// gives derives types a simple way to respond to a new child being added /// </summary> /// <param name="child"></param> protected virtual void OnAttachChild(UIElement child) { } /// <summary> /// gives derives types a simple way to respond to a child being removed /// </summary> /// <param name="child"></param> protected virtual void OnDetachChild(UIElement child) { } protected override int VisualChildrenCount => _vc.Count; protected override Visual GetVisualChild(int index) => _vc[index]; #endregion }
FireworkEffect:測試
//uielement //visual operations //animation effect stuff //LayoutOverrideDecorator public class FireworkEffect : OverlayRenderDecorator { private readonly Vector _gravity = new Vector(0.0, 10.0); private readonly List<Particle> _particles = new List<Particle>(); private Point _lastMousePosition = new Point(0, 0); private double _secondsUntilDrop; private ParticleEffectsTimeTracker _timeTracker; protected Random Random = new Random(); protected override void OnAttachChild(UIElement child) { CompositionTarget.Rendering += OnFrameCallback; child.PreviewMouseLeftButtonUp += OnMouseLeftButtonUp; child.PreviewMouseMove += OnMouseMove; _timeTracker = new ParticleEffectsTimeTracker(); } protected override void OnDetachChild(UIElement child) { CompositionTarget.Rendering -= OnFrameCallback; child.PreviewMouseLeftButtonUp -= OnMouseLeftButtonUp; child.PreviewMouseMove -= OnMouseMove; _timeTracker = null; } protected void OnFrameCallback(object sender, EventArgs e) { UpdateParticles(_timeTracker.Update()); } protected virtual void UpdateParticles(double deltatime) { //drop particles from mouse position if (MouseDropsParticles && IsMouseOver) { _secondsUntilDrop -= deltatime; if (_secondsUntilDrop < 0.0) { AddRandomBurst(_lastMousePosition - new Vector(Radius/2.0, Radius/2.0), 1); _secondsUntilDrop = MouseDropDelay + (Random.NextDouble()*2.0 - 1.0)*MouseDropDelayVariation; } } if (_particles.Count > 0) InvalidateVisual(); //update all particles for (var i = 0; i < _particles.Count;) { //_particles[i] var p = _particles[i]; if (GravitateToMouse) { p.Velocity += (_lastMousePosition - p.Location)*GravitateScale; } else { p.Velocity += _gravity; } p.Location += p.Velocity*deltatime; if (BounceOffContainer) { var radius = p.Diameter/2.0; if (p.Location.X - radius < 0.0) { p.Location.X = radius; p.Velocity.X *= -0.9; } else if (p.Location.X > ActualWidth - radius) { p.Location.X = ActualWidth - radius; p.Velocity.X *= -0.9; } if (p.Location.Y - radius < 0.0) { p.Location.Y = radius; p.Velocity.Y *= -0.9; } else if (p.Location.Y > ActualHeight - radius) { p.Location.Y = ActualHeight - radius; p.Velocity.Y *= -0.9; } } //only increment counter for live particles if (_timeTracker.ElapsedTime > p.DeathTime) _particles.RemoveAt(i); else i++; } } protected override void OnOverlayRender(DrawingContext drawingContext) { foreach (var p in _particles) { //figure out where in the particles life we are var particlelife = (_timeTracker.ElapsedTime - p.LifeTime).TotalSeconds/ (p.DeathTime - p.LifeTime).TotalSeconds; var currentcolor = Color.Multiply(p.StartColor, (float) (1.0 - particlelife)) + Color.Multiply(p.EndColor, (float) particlelife); Brush brush = new RadialGradientBrush(currentcolor, Color.FromArgb(0, currentcolor.R, currentcolor.G, currentcolor.B)); var rect = new RectangleGeometry( new Rect(new Point(p.Location.X - p.Diameter/2.0, p.Location.Y - p.Diameter/2.0), new Size(p.Diameter, p.Diameter))); drawingContext.DrawGeometry(brush, null, rect); } } private void OnMouseMove(object sender, MouseEventArgs e) { _lastMousePosition = e.GetPosition(this); } private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { AddRandomBurst(e.GetPosition(this)); } public void AddRandomBurst() { var point = new Point(Random.NextDouble()*ActualWidth, Random.NextDouble()*ActualHeight); AddRandomBurst(point, ClickBurstSize); } public void AddRandomBurst(Point location) { AddRandomBurst(location, ClickBurstSize); } public void AddRandomBurst(Point location, int count) { for (var i = 0; i < count; i++) { var p = new Particle(); var radius = Radius + (Random.NextDouble()*2.0 - 1.0)*RadiusVariation; var lifetime = Random.NextDouble()*3.0 + 1.0; p.Location = location; p.Velocity = new Vector(Random.NextDouble()*200.0 - 100.0, Random.NextDouble()*-200 + 100.0); p.LifeTime = _timeTracker.ElapsedTime; p.DeathTime = _timeTracker.ElapsedTime + TimeSpan.FromSeconds(lifetime); p.Diameter = 2.0*radius; var startColor = StartColor; var startColorVariation = StartColorVariation; var endColor = EndColor; var endColorVariation = EndColorVariation; var startRandColor = Color.FromScRgb(startColorVariation.ScA*(float) (Random.NextDouble()*2.0 - 1.0), startColorVariation.ScR*(float) (Random.NextDouble()*2.0 - 1.0), startColorVariation.ScG*(float) (Random.NextDouble()*2.0 - 1.0), startColorVariation.ScB*(float) (Random.NextDouble()*2.0 - 1.0)); var endRandColor = Color.FromScRgb(endColorVariation.ScA*(float) (Random.NextDouble()*2.0 - 1.0), endColorVariation.ScR*(float) (Random.NextDouble()*2.0 - 1.0), endColorVariation.ScG*(float) (Random.NextDouble()*2.0 - 1.0), endColorVariation.ScB*(float) (Random.NextDouble()*2.0 - 1.0)); p.StartColor = startColor + startRandColor; p.EndColor = endColor + endRandColor; _particles.Add(p); } } #region Dependency Properties public static readonly DependencyProperty RadiusProperty = DependencyProperty.Register( "RadiusBase", typeof (double), typeof (FireworkEffect), new FrameworkPropertyMetadata(15.0) ); public static readonly DependencyProperty RadiusVariationProperty = DependencyProperty.Register( "RadiusVariation", typeof (double), typeof (FireworkEffect), new FrameworkPropertyMetadata(5.0) ); public static readonly DependencyProperty StartColorProperty = DependencyProperty.Register( "StartColor", typeof (Color), typeof (FireworkEffect), new FrameworkPropertyMetadata(Color.FromArgb(245, 200, 50, 20)) ); public static readonly DependencyProperty EndColorProperty = DependencyProperty.Register( "EndColor", typeof (Color), typeof (FireworkEffect), new FrameworkPropertyMetadata(Color.FromArgb(100, 200, 255, 20)) ); public static readonly DependencyProperty StartColorVariationProperty = DependencyProperty.Register( "StartColorVariation", typeof (Color), typeof (FireworkEffect), new FrameworkPropertyMetadata(Color.FromArgb(10, 55, 50, 20)) ); public static readonly DependencyProperty EndColorVariationProperty = DependencyProperty.Register( "EndColorVariation", typeof (Color), typeof (FireworkEffect), new FrameworkPropertyMetadata(Color.FromArgb(50, 20, 100, 20)) ); public static readonly DependencyProperty MouseDropDelayProperty = DependencyProperty.Register( "MouseDropDelay", typeof (double), typeof (FireworkEffect), new FrameworkPropertyMetadata(0.1) ); public static readonly DependencyProperty MouseDropDelayVariationProperty = DependencyProperty.Register( "MouseDropDelayVariation", typeof (double), typeof (FireworkEffect), new FrameworkPropertyMetadata(0.05) ); public static readonly DependencyProperty ClickBurstSizeProperty = DependencyProperty.Register( "ClickBurstSize", typeof (int), typeof (FireworkEffect), new FrameworkPropertyMetadata(30) ); #endregion #region Properties public double Radius { get { return (double) GetValue(RadiusProperty); } set { SetValue(RadiusProperty, value); } } public double RadiusVariation { get { return (double) GetValue(RadiusVariationProperty); } set { SetValue(RadiusVariationProperty, value); } } public Color StartColor { get { return (Color) GetValue(StartColorProperty); } set { SetValue(StartColorProperty, value); } } public Color EndColor { get { return (Color) GetValue(EndColorProperty); } set { SetValue(EndColorProperty, value); } } public Color StartColorVariation { get { return (Color) GetValue(StartColorVariationProperty); } set { SetValue(StartColorVariationProperty, value); } } public Color EndColorVariation { get { return (Color) GetValue(EndColorVariationProperty); } set { SetValue(EndColorVariationProperty, value); } } public bool BounceOffContainer { get; set; } = false; public bool GravitateToMouse { get; set; } = false; public double GravitateScale { get; set; } = 0.1; public bool MouseDropsParticles { get; set; } = false; public double MouseDropDelay { get { return (double) GetValue(MouseDropDelayProperty); } set { SetValue(MouseDropDelayProperty, value); } } public double MouseDropDelayVariation { get { return (double) GetValue(MouseDropDelayVariationProperty); } set { SetValue(MouseDropDelayVariationProperty, value); } } public int ClickBurstSize { get { return (int) GetValue(ClickBurstSizeProperty); } set { SetValue(ClickBurstSizeProperty, value); } } #endregion }
SonicEffect:ui
//uielement //visual operations public class SonicEffect : OverlayRenderDecorator { protected override void OnAttachChild(UIElement child) { child.PreviewMouseLeftButtonUp += OnMouseLeftButtonUp; } protected override void OnDetachChild(UIElement child) { CompositionTarget.Rendering -= OnFrameCallback; child.PreviewMouseLeftButtonUp -= OnMouseLeftButtonUp; _timeTracker = null; } private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (_timeTracker != null) { _timeTracker.TimerFired -= OnTimerFired; _timeTracker = null; } CompositionTarget.Rendering += OnFrameCallback; _timeTracker = new ParticleEffectsTimeTracker {TimerInterval = _ringDelayInSeconds}; _timeTracker.TimerFired += OnTimerFired; _lowerRing = _upperRing = 0; _clickPosition = e.GetPosition(this); } private void OnFrameCallback(object sender, EventArgs e) { if (_timeTracker != null) { _timeTracker.Update(); InvalidateVisual(); } } private void OnTimerFired(object sender, EventArgs e) { if (_upperRing < RingCount) { _upperRing++; } else { _lowerRing++; if (_lowerRing >= _upperRing) { _timeTracker.TimerFired -= OnTimerFired; _timeTracker = null; CompositionTarget.Rendering -= OnFrameCallback; } } } protected override void OnOverlayRender(DrawingContext dc) { if (_timeTracker != null) { for (var i = _lowerRing; i < _upperRing; i++) { var radius = RingRadius*(i + 1); dc.DrawEllipse(Brushes.Transparent, new Pen(new SolidColorBrush(RingColor), RingThickness), _clickPosition, radius, radius); } } } #region Private Members private ParticleEffectsTimeTracker _timeTracker; private double _ringDelayInSeconds = 3; private Point _clickPosition; private int _lowerRing, _upperRing; #endregion #region Properties public int RingCount { get; set; } = 5; public TimeSpan RingDelay { get { return TimeSpan.FromSeconds(_ringDelayInSeconds); } set { _ringDelayInSeconds = value.TotalSeconds; } } public double RingRadius { get; set; } = 7.0; public double RingThickness { get; set; } = 5.0; public Color RingColor { get; set; } = Color.FromArgb(128, 128, 128, 128); #endregion }
MagnitismCanvas:this
public class MagnitismCanvas : Canvas { public MagnitismCanvas() { // suppress movement in the visual studio designer. if (Process.GetCurrentProcess().ProcessName != "devenv") CompositionTarget.Rendering += UpdateChildren; _timeTracker = new ParticleEffectsTimeTracker(); } private void UpdateChildren(object sender, EventArgs e) { //update time delta _timeTracker.Update(); foreach (UIElement child in LogicalTreeHelper.GetChildren(this)) { var velocity = _childrenVelocities.ContainsKey(child) ? _childrenVelocities[child] : new Vector(0, 0); //compute velocity dampening velocity = velocity*Drag; var truePosition = GetTruePosition(child); var childRect = new Rect(truePosition, child.RenderSize); //accumulate forces var forces = new Vector(0, 0); //add wall repulsion forces.X += BorderForce/Math.Max(1, childRect.Left); forces.X -= BorderForce/Math.Max(1, RenderSize.Width - childRect.Right); forces.Y += BorderForce/Math.Max(1, childRect.Top); forces.Y -= BorderForce/Math.Max(1, RenderSize.Height - childRect.Bottom); //each other child pushes away based on the square distance foreach (UIElement otherchild in LogicalTreeHelper.GetChildren(this)) { //dont push against itself if (otherchild == child) continue; var otherchildtruePosition = GetTruePosition(otherchild); var otherchildRect = new Rect(otherchildtruePosition, otherchild.RenderSize); //make sure rects aren't the same if (otherchildRect == childRect) continue; //ignore children with a size of 0,0 if (otherchildRect.Width == 0 && otherchildRect.Height == 0 || childRect.Width == 0 && childRect.Height == 0) continue; //vector from current other child to current child //are they overlapping? if so, distance is 0 var toChild = AreRectsOverlapping(childRect, otherchildRect) ? new Vector(0, 0) : VectorBetweenRects(childRect, otherchildRect); var length = toChild.Length; if (length < 1) { length = 1; var childCenter = GetCenter(childRect); var otherchildCenter = GetCenter(otherchildRect); //compute toChild from the center of both rects toChild = childCenter - otherchildCenter; } var childpush = ChildForce/length; toChild.Normalize(); forces += toChild*childpush; } //add forces to velocity and store it for next iteration velocity += forces; _childrenVelocities[child] = velocity; //move the object based on it's velocity SetTruePosition(child, truePosition + _timeTracker.DeltaSeconds*velocity); } } private bool AreRectsOverlapping(Rect r1, Rect r2) { if (r1.Bottom < r2.Top) return false; if (r1.Top > r2.Bottom) return false; if (r1.Right < r2.Left) return false; if (r1.Left > r2.Right) return false; return true; } private Point IntersectInsideRect(Rect r, Point raystart, Vector raydir) { var xtop = raystart.X + raydir.X*(r.Top - raystart.Y)/raydir.Y; var xbottom = raystart.X + raydir.X*(r.Bottom - raystart.Y)/raydir.Y; var yleft = raystart.Y + raydir.Y*(r.Left - raystart.X)/raydir.X; var yright = raystart.Y + raydir.Y*(r.Right - raystart.X)/raydir.X; var top = new Point(xtop, r.Top); var bottom = new Point(xbottom, r.Bottom); var left = new Point(r.Left, yleft); var right = new Point(r.Right, yright); var tv = raystart - top; var bv = raystart - bottom; var lv = raystart - left; var rv = raystart - right; //classify ray direction if (raydir.Y < 0) { if (raydir.X < 0) //top left { if (tv.LengthSquared < lv.LengthSquared) return top; return left; } if (tv.LengthSquared < rv.LengthSquared) return top; return right; } if (raydir.X < 0) //bottom left { if (bv.LengthSquared < lv.LengthSquared) return bottom; return left; } if (bv.LengthSquared < rv.LengthSquared) return bottom; return right; } private Vector VectorBetweenRects(Rect r1, Rect r2) { //find the edge points and use these to measure the distance var r1Center = GetCenter(r1); var r2Center = GetCenter(r2); var between = (r1Center - r2Center); between.Normalize(); var edge1 = IntersectInsideRect(r1, r1Center, -between); var edge2 = IntersectInsideRect(r2, r2Center, between); return edge1 - edge2; } private Point GetRenderTransformOffset(UIElement e) { //make sure they object's render transform is a translation var renderTranslation = e.RenderTransform as TranslateTransform; if (renderTranslation == null) { renderTranslation = new TranslateTransform(0, 0); e.RenderTransform = renderTranslation; } return new Point(renderTranslation.X, renderTranslation.Y); } private void SetRenderTransformOffset(UIElement e, Point offset) { //make sure they object's render transform is a translation var renderTranslation = e.RenderTransform as TranslateTransform; if (renderTranslation == null) { renderTranslation = new TranslateTransform(0, 0); e.RenderTransform = renderTranslation; } //set new offset renderTranslation.X = offset.X; renderTranslation.Y = offset.Y; } private Point GetTruePosition(UIElement e) { var renderTranslation = GetRenderTransformOffset(e); return new Point(GetLeft(e) + renderTranslation.X, GetTop(e) + renderTranslation.Y); } private void SetTruePosition(UIElement e, Point p) { var canvasOffset = new Vector(GetLeft(e), GetTop(e)); var renderTranslation = p - canvasOffset; SetRenderTransformOffset(e, renderTranslation); } private Point GetCenter(Rect r) => new Point((r.Left + r.Right) / 2.0, (r.Top + r.Bottom) / 2.0); #region Private Members private readonly ParticleEffectsTimeTracker _timeTracker; private readonly Dictionary<UIElement, Vector> _childrenVelocities = new Dictionary<UIElement, Vector>(); #endregion #region Properties public double BorderForce { get; set; } = 1000.0; public double ChildForce { get; set; } = 200.0; public double Drag { get; set; } = 0.9; #endregion }
ParticleEffectsTimeTracker:spa
//uielement //visual operations //animation effect stuff public class ParticleEffectsTimeTracker { #region Constructors public ParticleEffectsTimeTracker() { ElapsedTime = DateTime.Now; } #endregion #region Events public event EventHandler TimerFired; #endregion public double Update() { var currentTime = DateTime.Now; //get the difference in time var diffTime = currentTime - ElapsedTime; DeltaSeconds = diffTime.TotalSeconds; //does the user want a callback on regular intervals? if (TimerInterval > 0.0) { /* //compute the intervals for this and previous update int currInterval = (int)(currentTime / TimeSpan.FromSeconds(_timerInterval)); int prevInterval = (int)(_lastTime / TimeSpan.FromSeconds(_timerInterval)); //has the interval changed since last update? if (currInterval != prevInterval) { //fire interval event //note that this will only be called once per frame at most // so if they interval is too small, you wont get 2+ fires per frame TimerFired(this, null); } */ if (currentTime != ElapsedTime) { TimerFired?.Invoke(this, null); } } //cycle old time ElapsedTime = currentTime; return DeltaSeconds; } #region Private Memebers #endregion #region Properties public double TimerInterval { get; set; } = -1; public DateTime ElapsedTime { get; private set; } public double DeltaSeconds { get; private set; } #endregion }
Particle:code
internal class Particle { public DateTime DeathTime; public double Diameter; public Color EndColor; public DateTime LifeTime; public Point Location; public Color StartColor; public Vector Velocity; }
代碼解析:
OverlayRenderDecoratorOverlayVisual屬性IsHitTestVisible 爲何設置爲False?
3.此屬性受繼承類的IsOverlayHitTestVisible屬性關聯開關管理。
後續補充詳細---