前言
视觉效果不仅是用户体验的重要组成部分,也是吸引用户注意力的关键因素之一。特别是在WPF平台上,开发拥有丰富的工具和库来创建引人入胜的界面。星空效果作为一种既美观又富有创意的视觉元素,能够为应用程序增添独特的魅力和深度,使用户仿佛置身于浩瀚宇宙之中。
本文将详细介绍如何使用WPF实现令人惊叹的星空效果。通过结合WPF的强大图形处理能力和动画特性,我们将一步步引导您构建一个逼真的星空背景,让您的应用程序界面焕发出迷人的光彩。
效果
前阵子看到ay的蜘蛛网效果和知乎的登录页背景,觉得效果很酷.自己也想写一个.于是写着写着就变成这样了.少女梦幻的赶脚有木有.我这有着一颗少女心的抠脚大汉。
实现思路
分为两个部分:
1、星星无休止的漫游.
2、星星之间的连线.
星星和连线非别放到两个容器里,以便分开操作.
星星
把星星的运动分解为X轴和Y轴两个不相干的运动,分别操作.操作就是随机生成一个速度,随机生成一个时间.运动完之后再随机生成一个速度,随机生成一个时间......无限循环.
星星的旋转也是同样的道理.
连线
首先解释下连线的规则.两个星星之间连线,每个星星都有一个连线的势力范围,就是宽度乘以连线倍率,这个连线倍率可以在窗体设置.当两个势力范围有交集的时候,就连线.
例:星1宽度5,星2宽度10,连线倍率是3,那么这两个星星的距离小于53+10 3=45时就连线,大于45时断开.如果连线倍率设置为4,则两个星星减的距离小于54+104=60时连线,大于60时断开.
实现与资源占有率
星星运动的实现有两种:
1、基于Grid和TranslateTransform.用DoubleAnimation动画控制星星的位移.
2、基于Canvas.通过帧动画控制Canvas的X,Y.
连线的实现也有两种:
1、简单粗暴.在每一帧都清空连线容器.然后双层循环星星,重新连接所有星星(符合连线规则的).
2、在每一帧循环连线,判断连线规则.符合就改变此连线的X1,Y1,X2,Y2.而不去重新new连线.不符合规则的就移除.然后依然是双层循环星星,看符合规则的两个星星间有没有连线,没有的就new一个.
众所周知,WPF做这种动画资源占有率还是比较高的,写了这么多实现,也是因为这个.
大体上还是基于Canvas的实现占用资源稍低.但也有个问题,如果给星星再加一个模糊效果的话,基于Canvas实现的资源占有率不会飙升,而是帧数明显降低.(也可能是我电脑环境的原因)
并不能说那种实现好与坏,可能具体运行环境不一样,参数设置不一样,每种实现都有不同的表现.
然后关于资源占有率问题,以我目前的水平,就只能到这了.博友们自己取舍吧.
部分代码
StarrySky.cs
代码如下;
c#
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;
}
c#
/// <summary>
/// 星星
/// </summary>
internal class StarInfo
{
/// <summary>
/// X坐标
/// </summary>
public double X { get; set; }
/// <summary>
/// X轴速度(单位距离/帧)
/// </summary>
public double XV { get; set; }
/// <summary>
/// X坐标以X轴速度运行的时间(帧)
/// </summary>
public int XT { get; set; }
/// <summary>
/// Y坐标
/// </summary>
public double Y { get; set; }
/// <summary>
/// Y轴速度(单位距离/帧)
/// </summary>
public double YV { get; set; }
/// <summary>
/// Y坐标以Y轴速度运行的时间(帧)
/// </summary>
public int YT { get; set; }
/// <summary>
/// 对星星的引用
/// </summary>
public Path StarRef { get; set; }
public Dictionary<StarInfo, Line> StarLines { get; set; }
}
c#
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);
}
c#
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(6, 301), //帧
Y = _random.Next(0, (int)_canvas.ActualHeight),
YV = (double)_random.Next(-StarVMax, StarVMax) / 60,
YT = _random.Next(6, 301), //帧
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.5, 0.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(0, 360 * 5); //角度
var t = a / v; //时间
var dur = new Duration(new TimeSpan(0, 0, 0, 0, (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(128, 256);
var g = (byte)_random.Next(128, 256);
var b = (byte)_random.Next(128, 256);
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(100, 1001);
}
//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(100, 1001);
}
}
}
/// <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 }
}
};
}
}
StarrySky.xaml
代码如下;
xml
<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>
StarrySkyExample.xaml代码如下;
xml
<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 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>
c#
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();
}
}
}
StarrySkyExample.xaml.cs代码如下;
源码下载
星空效果的两种实现
基于Canvas的不是粗暴刷新的那个版本优化.当然没有质的改变,只是优化了一点点.主要是在星星对象上添加对连线的引用列表.循环列表,而不是去循环线的容器,逻辑简单了,少层循环.大概100个星减少5%的cup消耗.
优化后的源码下载
源码:files.cnblogs.com/files/tsliw...
Gtihub:github.com/WPFDevelope...
Gitee:gitee.com/WPFDevelope...
总结
通过本文的详细介绍,我们已经成功地探索了如何在WPF中实现令人惊叹的星空效果。从说明到代码实现,再到优化与扩展,每一步都在帮助大家充分利用WPF的强大功能,创造出既美观又高效的星空背景。
最后
如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。
也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!
作者:普通的地球人
出处:cnblogs.com/tsliwei/p/6282183.html
声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!