WPF使用Shape實現複雜線條動畫

czwy發表於2024-05-15

看到巧用 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,一旦存在曲線就不適用了。

基於等腰三角形的動畫可以看做是基於多條線段的動畫的一種特殊場景,侷限性較大,僅適用於帶直角的折線。

相關文章