WPF使用Shape实现复杂线条动画

wpf,shape · 浏览次数 : 21

小编点评

**Polyline动画** * 使用 `LinearGradientBrush` 创建动态渐变色线段的填充颜色。 * 使用 `DoubleAnimationUsingKeyFrames` 创建两个动画,分别控制 `polyline1` 和 `polyline2` 的 translateY。 * 在 `polyline1` 和 `polyline2` 的动画中使用 `EasingDoubleKeyFrame` 设置不同的 translateY 值来实现对折线的移动效果。 * 在动画结束后,使用 `Polygon.RenderTransform` 将 `polyline1` 和 `polyline2` 的图形渲染到舞台上。 **三角形动画** * 使用 `DoubleAnimationUsingKeyFrames` 创建两个动画,分别控制 `trigle` 的 translateY 和 translateTransform 的 Y 值。 * 在 `trigle` 的动画中使用 `EasingDoubleKeyFrame` 设置不同的 translateY 和 translateTransform 值来实现三角形沿着轨迹移动的效果。 * 在动画结束后,使用 `Polygon.RenderTransform` 将 `trigle` 的图形渲染到舞台上。 **整体代码** ```csharp // ... // 创建 Polyline var polyline = new Polyline(); polyline.Points.Add(new Point(240, 20)); polyline.Points.Add(new Point(140, 20)); polyline.Points.Add(new Point(140, 100)); // 创建 Triangle var triangle = new Polygon(); triangle.Points.Add(new Point(240, 19)); triangle.Points.Add(new Point(240, 40)); triangle.Points.Add(new Point(220, 19)); // 创建动画 var moveanimation = new Storyboard(); moveanimation.AddSequence(new DoubleAnimationUsingKeyFrames(polyline, "TranslateY", 0, 1, 2)); moveanimation.AddSequence(new DoubleAnimationUsingKeyFrames(triangle, "TranslateY", 0, 1, 2)); // 添加动画到舞台 timeline.Add(moveanimation); timeline.Add(new Storyboard()); // ... ``` **注意** * 代码中使用了 `BeginStoryboard` 和 `EndStoryboard` 来管理动画。 * `IsHidden` 属性用于控制 `polyline` 和 `triangle` 的隐藏状态。

正文

看到巧用 CSS/SVG 实现复杂线条光效动画的文章,便也想尝试用WPF的Shape配合动画实现同样的效果。ChokCoco大佬的文章中介绍了基于SVG的线条动画效果和通过角向渐变配合 MASK 实现渐变线条两种方式。WPF中的Shape与SVG非常相似,因此这种方式也很容易实现。但WPF中仅有的两种渐变画刷不包含角向渐变,本文使用了另外两种方式实现同样的效果。

在Avalonia的API文档中有看到ConicGradientBrush,应该可以用角向渐变的方式来实现。

首先看一下三种方式实现的效果(录制的gif中颜色存在一些偏差,动画有些卡顿,实际效果要好一些):
image

基于Polyline的线条动画效果

这种方式也是利用StrokeDashArray实现虚线样式,然后通过动画设置StrokeDashOffset来实现动画。首先,用Polyline绘制一段折线:

<Polyline Points="240 20 140 20 140 100 0 100" Stroke="#ddd" />

这样,我们就得到一条这样的折线:
image

接下来,利用StrokeDashArray实现与上边折线相同路径的虚线(点划线):

<Polyline Points="240 20 140 20 140 100 0 100" Stroke="red"
StrokeDashArray="20 30" />

image

StrokeDashArray设置了虚线(点划线)中实线段的长度以及间隔,这里和SVG中的stroke-dasharray略有不同,WPF中StrokeDashArray使用的是相对值。例如此处设置的StrokeDashArray="20 30"表示实线段长度为20,间隔为30,这些值是相对于线段的宽度StrokeThickness。如果StrokeThickness=2,那么实线段长度就是40个设备无关单位(Device Independent Units),间隔就是60DIUs。
当我们把间隔设置足够大时,就可以只看到一条实线段,这里折线中三条线段总长是320,因此把实线段设置20,间隔设置300:

<Polyline Points="240 20 140 20 140 100 0 100" Stroke="red"
StrokeDashArray="20 300" />

image
接下来就是借助StrokeDashOffset来实现动画。

<Grid
    Grid.Row="0"
    Grid.Column="0"
    Margin="5">
    <Polyline Points="240 20 140 20 140 100 0 100" Stroke="#ddd" />
    <Polyline
        Points="240 20 140 20 140 100 0 100"
        Stroke="red" StrokeThickness=""
        StrokeDashArray="20 300">
        <Polyline.Triggers>
            <EventTrigger RoutedEvent="Polyline.Loaded">
                <BeginStoryboard>
                    <Storyboard RepeatBehavior="Forever" Storyboard.TargetProperty="StrokeDashOffset">
                        <DoubleAnimation
                            From="0"
                            To="-320"
                            Duration="0:0:3" />
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Polyline.Triggers>
    </Polyline>
</Grid>

与CSS/SVG实现的方式一样,WPF中也只能对整段虚线设置渐变色,无法对其中一段实线设置。要想实现渐变效果只能另寻他法。

基于多条线段的动画

最朴素的想法就是用一条渐变色的线段沿着折线的路径移动,但是最大的问题在于折线拐角处难以处理。最为粗暴简单的思路就是针对折线的三段准备三条线段,第一条线段动画即将结束时,第二条开始,第二条动画即将结束时第三条开始。

<Polyline Points="240 20 140 20 140 100 0 100" Stroke="#ddd" />
<Polyline
    x:Name="polyline1"
    Points="260 20 240 20"
    Stroke="{StaticResource linearBrush}" />
<Polyline
    x:Name="polyline2"
    Points="140 0 140 20"
    Stroke="{StaticResource linearBrush}" />
<Polyline
    x:Name="polyline3"
    Points="160 100 140 100"
    Stroke="{StaticResource linearBrush}" />

image

这里有个细节需要注意,第1条线段向左移动刚好离开折线水平轨迹时,第2条线段才开始向下延垂直轨迹移动,并且移动速度一致,才能保证形成的移动的线段颜色连贯且长度不变。

<Storyboard x:Key="moveLines" RepeatBehavior="Forever">
    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="polyline1" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)">
        <EasingDoubleKeyFrame KeyTime="00:00:00.800" Value="-100" />
        <EasingDoubleKeyFrame KeyTime="00:00:01" Value="-121" />
    </DoubleAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="polyline2" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)">
        <EasingDoubleKeyFrame KeyTime="00:00:00.800" Value="0" />
        <EasingDoubleKeyFrame KeyTime="00:00:01" Value="20" />
        <EasingDoubleKeyFrame KeyTime="00:00:01.8" Value="80" />
        <EasingDoubleKeyFrame KeyTime="00:00:02" Value="101" />
    </DoubleAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="polyline3" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)">
        <EasingDoubleKeyFrame KeyTime="00:00:01.8" Value="0" />
        <EasingDoubleKeyFrame KeyTime="00:00:02" Value="-20" />
        <EasingDoubleKeyFrame KeyTime="00:00:03" Value="-160" />
    </DoubleAnimationUsingKeyFrames>
</Storyboard>

image

这样看效果并不明显,接下来就是需要用一个形状把完成动画的线段遮挡起来:

<Polygon Fill="#fff" Points="240,19 139,19 139,99 0,99 0,79 119,79 119,0 240,0 240,19 240,21 141,21 141,101 0,101 0,99 -20,99 -20,101 -20,122 161,122 161,41 260,41 260,19" />

image

这样基本实现了渐变色线条的动画效果,但终究不够优雅。

基于等腰三角形的动画

上一种方法中,在拐角处由两条线段配合的动画实现的效果,一条线段移出,另一条移入,连接起来刚好是个等腰直角三角形。
image

然后用线性渐变色填充三角形就可以实现移出的线段颜色和移入部分颜色相同。

<LinearGradientBrush x:Key="linearBrush" StartPoint="0 1" EndPoint="1 0">
    <GradientStop Offset="0.25" Color="#399953" />
    <GradientStop Offset="0.5" Color="#fbb300" />
    <GradientStop Offset="0.75" Color="#d53e33" />
    <GradientStop Offset="1" Color="#377af5" />
</LinearGradientBrush>
<Polygon
    x:Name="trigle"
    Fill="{StaticResource linearBrush}"
    Points="240 19 240 40 220 19"/>

接下来就是三角形沿着轨迹移动的动画以及遮挡轨迹以外部分了。

<Storyboard x:Key="moveanimation" RepeatBehavior="Forever">
    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="trigle" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.X)">
        <EasingDoubleKeyFrame KeyTime="00:00:00" Value="20" />
        <EasingDoubleKeyFrame KeyTime="00:00:01" Value="-99" />
        <EasingDoubleKeyFrame KeyTime="00:00:02" Value="-99" />
        <EasingDoubleKeyFrame KeyTime="00:00:03" Value="-240" />
    </DoubleAnimationUsingKeyFrames>
    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="trigle" Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)">
        <EasingDoubleKeyFrame KeyTime="00:00:01" Value="0" />
        <EasingDoubleKeyFrame KeyTime="00:00:02" Value="80" />
        <EasingDoubleKeyFrame KeyTime="00:00:03" Value="80" />
    </DoubleAnimationUsingKeyFrames>
</Storyboard>

<Grid Grid.Row="0" Grid.Column="1">
    <Polyline Points="240 20 140 20 140 100 0 100" Stroke="#ddd" />
    <Polygon
        x:Name="trigle"
        Fill="{StaticResource linearBrush}"
        Points="240 19 240 40 220 19">
        <Polygon.RenderTransform>
            <TranslateTransform />
        </Polygon.RenderTransform>
        <Polygon.Triggers>
            <EventTrigger RoutedEvent="Polygon.Loaded">
                <BeginStoryboard Storyboard="{StaticResource moveanimation}" />
            </EventTrigger>
        </Polygon.Triggers>
    </Polygon>
    <Polygon Fill="#fff" Points="240,19 139,19 139,99 0,99 0,79 119,79 119,0 240,0 240,19 240,21 141,21 141,101 0,101 0,99 -20,99 -20,101 -20,122 161,122 161,41 260,41 260,19" d:IsHidden="True" />
</Grid>

image

小结

基于Polyline的线条StrokeDashOffset的方式最为灵活简洁,不仅适用于直角折线,还适用于各种曲线。如果把此处的Polyline换成一个Ellipse,就可以实现简单的转圈圈等待的动效,但其不足在于线条样式美化空间有限。

基于多条线段的动画可以美化线条,但只适用于Polyline或者直线组成的Path,一旦存在曲线就不适用了。

基于等腰三角形的动画可以看做是基于多条线段的动画的一种特殊场景,局限性较大,仅适用于带直角的折线。

与WPF使用Shape实现复杂线条动画相似的内容:

WPF使用Shape实现复杂线条动画

看到巧用 CSS/SVG 实现复杂线条光效动画的文章,便也想尝试用WPF的Shape配合动画实现同样的效果。ChokCoco大佬的文章中介绍了基于SVG的线条动画效果和通过角向渐变配合 MASK 实现渐变线条两种方式。WPF中的Shape与SVG非常相似,因此这种方式也很容易实现。但WPF中仅有的两

.NET周刊【5月第3期 2024-05-19】

国内文章 WPF使用Shape实现复杂线条动画 https://www.cnblogs.com/czwy/p/18192720 文章介绍了利用WPF的Shape和动画功能,模仿CSS/SVG实现复杂的线条光效动画效果。首先,通过Polyline和StrokeDashArray实现了虚线动画,再通过S

Avalonia中的线性渐变画刷LinearGradientBrush

在WPF中使用Shape实现复杂线条动画后,尝试在Avalonia中也实现同样效果。尽管官方提供了从WPF到Avalonia的快速入门文档,但由于第一次使用Avalonia,体验过程中并不是很顺利,主要是卡在线性渐变画刷LinearGradientBrush的使用上。Avalonia中的线性渐变画刷

WPF使用AppBar实现窗口停靠,适配缩放、全屏响应和多窗口并列(附封装好即开即用的附加属性)

在吕毅大佬的文章中已经详细介绍了什么是AppBar: WPF 使用 AppBar 将窗口停靠在桌面上,让其他程序不占用此窗口的空间(附我封装的附加属性) - walterlv 即让窗口固定在屏幕某一边,并且保证其他窗口最大化后不会覆盖AppBar占据的区域(类似于Windows任务栏)。 但是在我的

WPF使用事件聚合器,实现任意页面跨页通信

前言:最近几天有好几个小伙伴玩WPF,遇到不同页面,不知道要怎么传递消息。于是,我今天就来演示一个事件聚合器的玩法,采用prism框架来实现。作为福利,内容附带了主页面打开对话框时候直接通过参数传递消息的一个小例子,具体请自行围观。 以下内容,创建wpf项目以及引用prism和实现依赖注入等细节,可

WPF使用TextBlock实现查找结果高亮显示

在应用开发过程中,经常遇到这样的需求:通过关键字查找数据,把带有关键字的数据显示出来,同时在结果中高亮显示关键字。在web开发中,只需在关键字上加一层标签,然后设置标签样式就可以轻松实现。 在WPF中显示文本内容通常采用`TextBlock`控件,也可以采用类似的方式,通过内联流内容元素`Run`达

[WPF]使用HLSL实现百叶窗动效

百叶窗动画是制作PPT时常用的动画之一,本文将通过实现百叶窗动画效果的例子介绍在WPF中如何使用ShaderEffect。ShaderEffect使用高级着色器语言(High Level Shading Language,HLSL)事先制作好并且已经编译过的效果。先看下百叶窗动画实现效果: ![im

WPF使用Blazor的快速案例

下面我们将讲解在WPF中使用Blazor,并且使用Blazor做一些文件编辑操作,下面是需要用到的东西 - WPF - Blazor - Masa Blazor - Monaco ## 安装Masa Blazor模板 使用`CMD`指令安装模板 ```shell dotnet new install

WPF/C#:在DataGrid中显示选择框

前言 在使用WPF的过程中可能会经常遇到在DataGrid的最前或者最后添加一列选择框的需求,今天跟大家分享一下,在自己的项目中是如何实现的。 整体实现效果如下: 如果对此感兴趣,可以接下来看具体实现部分。 实践 假设数据库中的模型如下: public class Person { public i

WPF/C#:实现导航功能

前言 在WPF中使用导航功能可以使用Frame控件,这是比较基础的一种方法。前几天分享了wpfui中NavigationView的基本用法,但是如果真正在项目中使用起来,基础的用法是无法满足的。今天通过wpfui中的mvvm例子来说明在wpfui中如何通过依赖注入与MVVM模式使用导航功能。实践起来