[WPF] 用 Effect 实现线条光影效果


1. 前言

几个月前 ChokCoco 大佬发布了一篇文章:

在文章里实现了一个发光的心形线条互相追逐的效果:

现在正好有空就试试用 WPF 实现一下。在实现过程中我用到这些知识和技巧:

  • Segoe Fluent 图标字体
  • 在 Blend 中创建 Path
  • 计算 Path 的长途
  • Path 的边框动画
  • VisualStudio 的设计时数据支持
  • 自定义 Effect

这篇文章将讲解如何使用这些知识和技巧模仿他的动画效果。

2. 图标字体和 Path

虽然 ChokCoco 大佬已经给了一个心形的路径,但总不能每次都期待别人给的东西。对于 WPF 开发者来说,用图标字体和 Blend 可以轻松创建一些简单的路径。

首先要找到一个心形的图标字体,在 Windows 10/11 可以直接使用 Segoe MDL2 和 Segoe Fluent 字体,这两个是随 Windows 10/11 发布的系统内置字体。下面的页面列出了可用的 Segoe Fluent 字体:

https://docs.microsoft.com/en-us/windows/apps/design/style/segoe-fluent-icons-font

找到 HeartFill 的 Unicode 码位 eb52,然后打开 Microsoft Blend for VisualStudio 2019(更新的版本砍掉了这篇文章用到的功能),创建一个 WPF 应用,在 XAML 中输入下面这段 XAML:


这时候应该可以看到一个心形,他就是 HeartFill 的文字图标。在设计视图选中它,右键选择 Path -> Convert to Path(中文版本下应该是 转换为路径):

这样 TextBlock 就被转换为一个相同形状的 Path。接下来将 Fill 设置为空,Stroke 和 StrokeThickness 分别设置为 Black 和 10,Path 的形状就如下图所示,选中左边工具栏的 Pen 工具还可以调整 Path 的形状:

这时候对应的 XAML 如下:

 
     
         
             
             
             
             
         
     
 

3. 计算 Path 的长途

拿到路径后,下一步需要计算它的长度。这个长度不需要太精确,可以用 GetFlattenedPathGeometry 获取 PathGeometry 对象的多边形近似 Geometry,然后计算每条边的长度:

public double GetLength(Geometry geo)
{
    PathGeometry path = geo.GetFlattenedPathGeometry();
    double length = 0.0;
    foreach (PathFigure pf in path.Figures)
    {
        Point start = pf.StartPoint;
        foreach (PolyLineSegment seg in pf.Segments)
        {
            foreach (Point point in seg.Points)
            {
                length += (start - point).Length;
                start = point;
            }
        }
    }
    return length;
}

4. Path 的边框动画

上一步计算出的 Path 长度是 898。

然后通过 StrokeDashArray 和 StrokeDashOffset 对 Path 做边框动画。因为 Path 的 StrokeThickness 是 10 像素,所以做边框动画时所有数值都要除以 10。

第一步,将 StrokeDashArray 设置为 29.9 59.9,它将 Path 的边框分成两部分,第一部分为实线,第二部分为空白。
第二步,然后用 DoubleAnimation 使 StrokeDashOffset 从 0 到 89.8 不断循环,实现线条动画的不断循环。
第三步,添加一个相同的 Path,并让它的动画延迟一秒执行,这样就实现了两个心形线条的追逐动画。








有关边框动画的更多内容,可以参考这两篇文章:


5. VisualStudio 的设计时数据

现在我们只差让这两个 Path 发光了。但在这之前我们需要了解 VisualStudio 的设计时数据的概念。

设计时数据是你设置的模拟数据,使控件更易于在 XAML 设计器中进行可视化。d: 前缀用于设置设计时的属性值,它只影响设计视图,不会编译到正在运行的应用中。具体可以参考这篇文档:

在 Visual Studio 中通过 XAML 设计器使用设计时数据

这是一个很实用的小技巧,由于上面的两个 Path 重叠在一起,在设计视图难以区分,所以用了 d:StrokeDashOffset="45" 让其中一个错开。这段内容只在设计视图起作用,不会有其它副作用。

6. 自定义 Effect

在 WPF 中要做发光效果通常都是用 DropShadowEffect ,例如这样:


    
        
     


    
        
    

但这样颜色实在太淡,太淡了。为了解决这个问题,其中一种做法是叠加多个 Path,这样它们的 Drop Shadow 也会叠加起来,实现一个很亮的发光效果。但是这里会需要对叠加的多个 Path 都做动画,恐怕性能会很有问题。

另一种方式是自定义一个 Effect,它的代码只需要如下几行:

float Amount : register(C0);

float4 main(float2 uv : TEXCOORD) : COLOR
{
    float4 srcColor = tex2D(input, uv);
    srcColor.rgb *= Amount;
    srcColor.a  *= Amount;
    return srcColor;
}

这只是个很简单的 Effect,就是将所有像素的颜色和透明度乘以一个指定值。我不知道这种效果叫什么名字,但因为它最终实现了发光的效果,所以命名为 GlowEffect。使用 GlowEffect 配合 BlurEffect,上面暗淡的颜色就变得明亮起来:


    
        
    
    
        
            
        
        
        
    


    
    

关于自定义 Effect 的更多内容,可以参考 WalterLv 大佬的这篇文章:

WPF 像素着色器入门:使用 Shazzam Shader Editor 编写 HLSL 像素着色器代码

7. 成果

最后的成果如下:

8. 源码

https://github.com/DinoChan/wpf_design_and_animation_lab