yanjinhua

V1

2022/08/18阅读:19主题:蓝莹

WPF 实现星空效果

本文经原作者授权以原创方式二次分享,欢迎转载、分享。

原文作者:普通的地球人

原文地址:https://www.cnblogs.com/tsliwei/p/6282183.html

Github地址:https://github.com/WPFDevelopersOrg/WPFDevelopers

效果

  • 前阵子看到ay的蜘蛛网效果和知乎的登录页背景,觉得效果很酷.自己也想写一个.于是写着写着就变成这样了.少女梦幻的赶脚有木有.我这有着一颗少女心的抠脚大汉;

实现思路分为两个部分:

1)星星无休止的漫游;

2)星星之间的连线;

  • 星星和连线非别放到两个容器里,以便分开操作;
星星
  • 把星星的运动分解为X轴和Y轴两个不相干的运动,分别操作.操作就是随机生成一个速度,随机生成一个时间.运动完之后再随机生成一个速度,随机生成一个时间......无限循环;

  • 星星的旋转也是同样的道理;

连线
  • 首先解释下连线的规则.两个星星之间连线,每个星星都有一个连线的势力范围,就是宽度乘以连线倍率,这个连线倍率可以在窗体设置.当两个势力范围有交集的时候,就连线;

  • 例:星1宽度5,星2宽度10,连线倍率是3,那么这两个星星的距离小于5*3+10*3=45时就连线,大于45时断开.如果连线倍率设置为4,则两个星星减的距离小于5*4+10*4=60时连线,大于60时断开;

实现与资源占有率

  • 星星运动的实现有两种:

1)基于GridTranslateTransformDoubleAnimation动画控制星星的位移.

2)基于Canvas通过帧动画控制CanvasX,Y.

连线的实现也有两种:

1)简单粗暴.在每一帧都清空连线容器.然后双层循环星星,重新连接所有星星(符合连线规则的).

2)在每一帧循环连线,判断连线规则.符合就改变此连线的X1,Y1,X2,Y2而不去重新new连线.不符合规则的就移除.然后依然是双层循环星星,看符合规则的两个星星间有没有连线,没有的就new一个.

众所周知,WPF做这种动画资源占有率还是比较高的,写了这么多实现,也是因为这个.

大体上还是基于Canvas的实现占用资源稍低.但也有个问题,如果给星星再加一个模糊效果的话,基于Canvas实现的资源占有率不会飙升,而是帧数明显降低.(也可能是我电脑环境的原因)

并不能说那种实现好与坏,可能具体运行环境不一样,参数设置不一样,每种实现都有不同的表现.

然后关于资源占有率问题,以我目前的水平,就只能到这了.博友们自己取舍吧.

源码如下

1)StarrySky.cs代码如下;

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace WPFDevelopers.Controls
{

    [TemplatePart(Name = GridTemplateName, Type = typeof(Grid))]
    [TemplatePart(Name = CanvasTemplateName, Type = typeof(Canvas))]
    public class StarrySky : Control
    {
        private const string GridTemplateName = "PART_GridLineContainer";

        private const string CanvasTemplateName = "PART_CanvasStarContainer";

        private Grid _grid;
        private Canvas _canvas;

        public static readonly DependencyProperty StarCountProperty =
            DependencyProperty.Register("StarCount"typeof(int), typeof(StarrySky), new UIPropertyMetadata(10));

        public static readonly DependencyProperty StarSizeMinProperty =
            DependencyProperty.Register("StarSizeMin"typeof(int), typeof(StarrySky), new UIPropertyMetadata(5));

        public static readonly DependencyProperty StarSizeMaxProperty =
            DependencyProperty.Register("StarSizeMax"typeof(int), typeof(StarrySky), new UIPropertyMetadata(20));

        public static readonly DependencyProperty StarVMinProperty =
            DependencyProperty.Register("StarVMin"typeof(int), typeof(StarrySky), new UIPropertyMetadata(10));

        public static readonly DependencyProperty StarVMaxProperty =
            DependencyProperty.Register("StarVMax"typeof(int), typeof(StarrySky), new UIPropertyMetadata(20));

        public static readonly DependencyProperty StarRVMinProperty =
            DependencyProperty.Register("StarRVMin"typeof(int), typeof(StarrySky), new UIPropertyMetadata(90));

        public static readonly DependencyProperty StarRVMaxProperty =
            DependencyProperty.Register("StarRVMax"typeof(int), typeof(StarrySky), new UIPropertyMetadata(360));

        public static readonly DependencyProperty LineRateProperty =
            DependencyProperty.Register("LineRate"typeof(int), typeof(StarrySky), new UIPropertyMetadata(3));

        private readonly Random _random = new Random();
        private StarInfo[] _stars;


        static StarrySky()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(StarrySky),
                new FrameworkPropertyMetadata(typeof(StarrySky)));
        }

        public StarrySky()
        {
            Loaded += delegate
            {
                CompositionTarget.Rendering += delegate
                {
                    StarRoamAnimation();
                    AddOrRemoveStarLine();
                    MoveStarLine();
                };
            };
        }
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _grid = GetTemplateChild(GridTemplateName) as Grid;
            _canvas = GetTemplateChild(CanvasTemplateName) as Canvas;
        }

        public int StarCount
        {
            get => (int)GetValue(StarCountProperty);
            set => SetValue(StarCountProperty, value);
        }

        public int StarSizeMin
        {
            get => (int)GetValue(StarSizeMinProperty);
            set => SetValue(StarSizeMinProperty, value);
        }

        public int StarSizeMax
        {
            get => (int)GetValue(StarSizeMaxProperty);
            set => SetValue(StarSizeMaxProperty, value);
        }

        public int StarVMin
        {
            get => (int)GetValue(StarVMinProperty);
            set => SetValue(StarVMinProperty, value);
        }

        public int StarVMax
        {
            get => (int)GetValue(StarVMaxProperty);
            set => SetValue(StarVMaxProperty, value);
        }


        public int StarRVMin
        {
            get => (int)GetValue(StarRVMinProperty);
            set => SetValue(StarRVMinProperty, value);
        }

        public int StarRVMax
        {
            get => (int)GetValue(StarRVMaxProperty);
            set => SetValue(StarRVMaxProperty, value);
        }

        public int LineRate
        {
            get => (int)GetValue(LineRateProperty);
            set => SetValue(LineRateProperty, value);
        }


       
        public void InitStar()
        {
            //清空星星容器
            _stars = new StarInfo[StarCount];
            _canvas.Children.Clear();
            _grid.Children.Clear();
            //生成星星
            for (var i = 0; i < StarCount; i++)
            {
                double size = _random.Next(StarSizeMin, StarSizeMax + 1); //星星尺寸
                var starInfo = new StarInfo
                {
                    X = _random.Next(0, (int)_canvas.ActualWidth),
                    XV = (double)_random.Next(-StarVMax, StarVMax) / 60,
                    XT = _random.Next(6301), //帧
                    Y = _random.Next(0, (int)_canvas.ActualHeight),
                    YV = (double)_random.Next(-StarVMax, StarVMax) / 60,
                    YT = _random.Next(6301), //帧
                    StarLines = new Dictionary<StarInfo, Line>()
                };
                var star = new Path
                {
                    Data = Application.Current.Resources["PathStarrySky"as Geometry,
                    Width = size,
                    Height = size,
                    Stretch = Stretch.Fill,
                    Fill = GetRandomColorBursh(),
                    RenderTransformOrigin = new Point(0.50.5),
                    RenderTransform = new RotateTransform { Angle = 0 }
                };
                Canvas.SetLeft(star, starInfo.X);
                Canvas.SetTop(star, starInfo.Y);
                starInfo.StarRef = star;
                //设置星星旋转动画
                SetStarRotateAnimation(star);
                //添加到容器
                _stars[i] = starInfo;
                _canvas.Children.Add(star);
            }
        }
       
        private void SetStarRotateAnimation(Path star)
        {
            double v = _random.Next(StarRVMin, StarRVMax + 1); //速度
            double a = _random.Next(0360 * 5); //角度
            var t = a / v; //时间
            var dur = new Duration(new TimeSpan(0000, (int)(t * 1000)));

            var sb = new Storyboard
            {
                Duration = dur
            };
            //动画完成事件 再次设置此动画
            sb.Completed += (S, E) => { SetStarRotateAnimation(star); };

            var da = new DoubleAnimation
            {
                To = a,
                Duration = dur
            };
            Storyboard.SetTarget(da, star);
            Storyboard.SetTargetProperty(da, new PropertyPath("(UIElement.RenderTransform).(RotateTransform.Angle)"));
            sb.Children.Add(da);
            sb.Begin(this);
        }
        private SolidColorBrush GetRandomColorBursh()
        {
            var r = (byte)_random.Next(128256);
            var g = (byte)_random.Next(128256);
            var b = (byte)_random.Next(128256);
            return new SolidColorBrush(Color.FromRgb(r, g, b));
        }
        /// <summary>
        ///     星星漫游动画
        /// </summary>
        private void StarRoamAnimation()
        {
            if (_stars == null)
                return;

            foreach (var starInfo in _stars)
            {
                //X轴运动
                if (starInfo.XT > 0)
                {
                    //运动时间大于0,继续运动
                    if (starInfo.X >= _canvas.ActualWidth || starInfo.X <= 0)
                        //碰到边缘,速度取反向
                        starInfo.XV = -starInfo.XV;
                    //位移加,时间减
                    starInfo.X += starInfo.XV;
                    starInfo.XT--;
                    Canvas.SetLeft(starInfo.StarRef, starInfo.X);
                }
                else
                {
                    //运动时间小于0,重新设置速度和时间
                    starInfo.XV = (double)_random.Next(-StarVMax, StarVMax) / 60;
                    starInfo.XT = _random.Next(1001001);
                }

                //Y轴运动
                if (starInfo.YT > 0)
                {
                    //运动时间大于0,继续运动
                    if (starInfo.Y >= _canvas.ActualHeight || starInfo.Y <= 0)
                        //碰到边缘,速度取反向
                        starInfo.YV = -starInfo.YV;
                    //位移加,时间减
                    starInfo.Y += starInfo.YV;
                    starInfo.YT--;
                    Canvas.SetTop(starInfo.StarRef, starInfo.Y);
                }
                else
                {
                    //运动时间小于0,重新设置速度和时间
                    starInfo.YV = (double)_random.Next(-StarVMax, StarVMax) / 60;
                    starInfo.YT = _random.Next(1001001);
                }
            }
        }
        /// <summary>
        ///     添加或者移除星星之间的连线
        /// </summary>
        private void AddOrRemoveStarLine()
        {
            //没有星星 直接返回
            if (_stars == null || StarCount != _stars.Length)
                return;

            //生成星星间的连线
            for (var i = 0; i < StarCount - 1; i++)
                for (var j = i + 1; j < StarCount; j++)
                {
                    var star1 = _stars[i];
                    var x1 = star1.X + star1.StarRef.Width / 2;
                    var y1 = star1.Y + star1.StarRef.Height / 2;
                    var star2 = _stars[j];
                    var x2 = star2.X + star2.StarRef.Width / 2;
                    var y2 = star2.Y + star2.StarRef.Height / 2;
                    var s = Math.Sqrt((y2 - y1) * (y2 - y1) + (x2 - x1) * (x2 - x1)); //两个星星间的距离
                    var threshold = star1.StarRef.Width * LineRate + star2.StarRef.Width * LineRate;
                    if (s <= threshold)
                    {
                        if (!star1.StarLines.ContainsKey(star2))
                        {
                            var line = new Line
                            {
                                X1 = x1,
                                Y1 = y1,
                                X2 = x2,
                                Y2 = y2,
                                Stroke = GetStarLineBrush(star1.StarRef, star2.StarRef)
                            };
                            star1.StarLines.Add(star2, line);
                            _grid.Children.Add(line);
                        }
                    }
                    else
                    {
                        if (star1.StarLines.ContainsKey(star2))
                        {
                            _grid.Children.Remove(star1.StarLines[star2]);
                            star1.StarLines.Remove(star2);
                        }
                    }
                }
        }

        /// <summary>
        ///     移动星星之间的连线
        /// </summary>
        private void MoveStarLine()
        {
            //没有星星 直接返回
            if (_stars == null)
                return;

            foreach (var star in _stars)
                foreach (var starLine in star.StarLines)
                {
                    var line = starLine.Value;
                    line.X1 = star.X + star.StarRef.Width / 2;
                    line.Y1 = star.Y + star.StarRef.Height / 2;
                    line.X2 = starLine.Key.X + starLine.Key.StarRef.Width / 2;
                    line.Y2 = starLine.Key.Y + starLine.Key.StarRef.Height / 2;
                }
        }

        /// <summary>
        ///     获取星星连线颜色画刷
        /// </summary>
        /// <param name="star0">起始星星</param>
        /// <param name="star1">终点星星</param>
        /// <returns>LinearGradientBrush</returns>
        private LinearGradientBrush GetStarLineBrush(Path star0, Path star1)
        {
            return new LinearGradientBrush
            {
                GradientStops = new GradientStopCollection
                {
                    new GradientStop { Offset = 0, Color = (star0.Fill as SolidColorBrush).Color },
                    new GradientStop { Offset = 1, Color = (star1.Fill as SolidColorBrush).Color }
                }
            };
        }
    }

    /// <summary>
    ///     星星
    /// </summary>
    internal class StarInfo
    {
        /// <summary>
        ///     X坐标
        /// </summary>
        public double X { getset; }

        /// <summary>
        ///     X轴速度(单位距离/帧)
        /// </summary>
        public double XV { getset; }

        /// <summary>
        ///     X坐标以X轴速度运行的时间(帧)
        /// </summary>
        public int XT { getset; }

        /// <summary>
        ///     Y坐标
        /// </summary>
        public double Y { getset; }

        /// <summary>
        ///     Y轴速度(单位距离/帧)
        /// </summary>
        public double YV { getset; }

        /// <summary>
        ///     Y坐标以Y轴速度运行的时间(帧)
        /// </summary>
        public int YT { getset; }

        /// <summary>
        ///     对星星的引用
        /// </summary>
        public Path StarRef { getset; }

        public Dictionary<StarInfo, Line> StarLines { getset; }
    }
}

2)StarrySky.xaml代码如下;

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:controls="clr-namespace:WPFDevelopers.Controls">

    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="Basic/ControlBasic.xaml"/>
    </ResourceDictionary.MergedDictionaries>
    <RadialGradientBrush x:Key="StarrySkyRadialGradientBrush" GradientOrigin="0.5,0" Center="0.5,0.3" RadiusX="0.7">
        <GradientStop Color="#FF04040E" Offset="0"/>
        <GradientStop Color="#FF24315D" Offset="1"/>
    </RadialGradientBrush>
    <Style TargetType="{x:Type controls:StarrySky}" 
           BasedOn="{StaticResource ControlBasicStyle}">

        <Setter Property="Background" Value="{StaticResource StarrySkyRadialGradientBrush}"></Setter>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type controls:StarrySky}">
                    <Grid Background="{TemplateBinding Background}">
                        <Grid x:Name="PART_GridLineContainer"/>
                        <Canvas x:Name="PART_CanvasStarContainer"/>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    
</Style>
</ResourceDictionary>

3)StarrySkyExample.xaml代码如下;

<UserControl x:Class="WPFDevelopers.Samples.ExampleViews.StarrySkyExample"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:wpfdev="https://github.com/WPFDevelopersOrg/WPFDevelopers"
             xmlns:ws="https://github.com/WPFDevelopersOrg.WPFDevelopers.Minimal"
             xmlns:local="clr-namespace:WPFDevelopers.Samples.ExampleViews"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">

    <UserControl.Resources>
        <Style TargetType="{x:Type TextBlock}">
            <Setter Property="Foreground" Value="White"></Setter>
            <Setter Property="FontSize" Value="14"></Setter>
            <Setter Property="VerticalAlignment" Value="Center"></Setter>
            <Setter Property="Margin" Value="2"></Setter>
        
</Style>
        <Style TargetType="{x:Type StackPanel}">
            <Setter Property="Margin" Value="2"></Setter>
        
</Style>
        <Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource {x:Type TextBox}}">
            <Setter Property="ws:ElementHelper.Watermark" Value="输入内容"></Setter>
            <Setter Property="Margin" Value="-30,0"></Setter>
            <Setter Property="Width" Value="100"></Setter>
        
</Style>
    </UserControl.Resources>
    <Grid>
        <!--<wpfdev:StarrySky StarCount="{Binding ElementName=tbx_starCount,Path=Text}"
                          StarSizeMin="{Binding ElementName=tbx_starSizeMin,Path=Text}"
                          StarSizeMax="{Binding ElementName=tbx_starSizeMax,Path=Text}"
                          StarVMin="{Binding ElementName=tbx_starVMin,Path=Text}"
                          StarVMax="{Binding ElementName=tbx_starVMax,Path=Text}"
                          StarRVMin="{Binding ElementName=tbx_starRVMin,Path=Text}"
                          StarRVMax="{Binding ElementName=tbx_starRVMax,Path=Text}"
                          LineRate="{Binding ElementName=tbx_lineRate,Path=Text}"
                          Name="myStarrySky">

        </wpfdev:StarrySky>-->

        <wpfdev:StarrySky Name="myStarrySky">

        </wpfdev:StarrySky>
        <StackPanel HorizontalAlignment="Left" VerticalAlignment="Top">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="星星个数:"></TextBlock>
                <TextBox x:Name="tbx_starCount" Text="{Binding ElementName=myStarrySky,Path=StarCount}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="最小尺寸:"></TextBlock>
                <TextBox Name="tbx_starSizeMin"  Text="{Binding ElementName=myStarrySky,Path=StarSizeMin}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="最大尺寸:"></TextBlock>
                <TextBox Name="tbx_starSizeMax" Text="{Binding ElementName=myStarrySky,Path=StarSizeMax}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="最小速度:"></TextBlock>
                <TextBox Name="tbx_starVMin" Text="{Binding ElementName=myStarrySky,Path=StarVMin}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="最大速度:"></TextBlock>
                <TextBox Name="tbx_starVMax" Text="{Binding ElementName=myStarrySky,Path=StarVMax}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="最小转速:"></TextBlock>
                <TextBox Name="tbx_starRVMin" Text="{Binding ElementName=myStarrySky,Path=StarRVMin}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="最大转速:"></TextBlock>
                <TextBox Name="tbx_starRVMax" Text="{Binding ElementName=myStarrySky,Path=StarRVMax}"></TextBox>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="连线倍率:"></TextBlock>
                <TextBox Name="tbx_lineRate" Text="{Binding ElementName=myStarrySky,Path=LineRate}"></TextBox>
            </StackPanel>
            <Button Name="btn_render" Content="生成" Click="btn_render_Click"/>
        </StackPanel>
    </Grid>
</UserControl>

4)StarrySkyExample.xaml.cs代码如下;

using System.Windows.Controls;

namespace WPFDevelopers.Samples.ExampleViews
{
    /// <summary>
    /// StarrySkyExample.xaml 的交互逻辑
    /// </summary>
    public partial class StarrySkyExample : UserControl
    {
        public StarrySkyExample()
        {
            InitializeComponent();
        }

        private void btn_render_Click(object sender, System.Windows.RoutedEventArgs e)
        {
            myStarrySky.InitStar();
        }
    }
}

源码1[1] Gtihub[2] Gitee[3]

参考资料

[1]

源码: https://files.cnblogs.com/files/tsliwei/StarrySkyBasedOnCanvasYOUHUA.zip

[2]

Gtihub: https://github.com/WPFDevelopersOrg/WPFDevelopers

[3]

gitee: https://gitee.com/WPFDevelopersOrg/WPFDevelopers

分类:

后端

标签:

C#

作者介绍

yanjinhua
V1