27

I have created an ellipse in Windows Phone 8.1 Silverlight App and UWP both and I wanted to fill it with animating waves, For this purpose, I am following this solution

but it is for WPF so I am unable to use some control like "Visual Brush".

I wanted to fill ellipse with wave similar to this (ignore 50% in the image) -

enter image description here

And here is my eliipse

<Ellipse Name="WaveEllipse" Grid.Column="1" Grid.Row="0" VerticalAlignment="Top"
         Stroke="{StaticResource PhoneAccentBrush}"
         StrokeThickness="4"
         Width="225"
         Height="225">
</Ellipse>

any alternate on the visual brush? mainly I wanted to implement it in Windows Phone 8.1 Silverlight, but I will switch to UWP if it is not available on WP platform

3 Answers 3

93
+50

Before giving you the code, have a look at this animated gif below to try to understand how this animation could be created.

enter image description here

Make sense, right? All we need to do is to create a shape like this, animate its offset X(endlessly) and Y(water level), and finally just clip it with an ellipse.

So first you will need to use Adobe Illustrator or similar tools to create this shape. In AI, there's a Zig Zag effect(see screenshot below) that's perfectly for this. You just need to make sure the starting point is at the same position as the ending one, so when you repeat the animation, it will feel like it's never ending.

enter image description here

What's currently missing in UWP is the ability to clip a UIElement with a non-rectangular shape, so here we have to export this as a png (otherwise we would export it as a svg and use Path to display it).

Also for the same reason, the clipping part requires a lot of work. Like in Jet Chopper's answer, that's tons of code to just get a surfaceBrush! Not to mention that you will also need to manually handle device lost and app lifecycle.

Thankfully, in Creators Update(i.e. 15063), there's a new API called LoadedImageSurface that creates a CompositionSurfaceBrush by an image uri with a couple of lines' code. In my code example below, you will see that I use this, which means, if you want to support older versions of Windows 10, you will need to replace it with what's in Jet's answer.

Code

The idea is to create a UserControl called WaveProgressControl which encapsulates all the animation logic and exposes a dependency property called Percent that controls the water level.

The WaveProgressControl control - XAML

<UserControl x:Class="WaveProgressControlRepo.WaveProgressControl"
             Height="160"
             Width="160">

    <Grid x:Name="Root">
        <Ellipse x:Name="ClippedImageContainer"
                 Fill="White"
                 Margin="6" />
        <Ellipse x:Name="CircleBorder"
                 Stroke="#FF0289CD"
                 StrokeThickness="3" />
        <TextBlock Foreground="#FF0289CD"
                   FontSize="36"
                   FontWeight="SemiBold"
                   TextAlignment="Right"
                   VerticalAlignment="Center"
                   Width="83"
                   Margin="0,0,12,0">
            <Run Text="{x:Bind Percent, Mode=OneWay}" />
            <Run Text="%"
                 FontSize="22" />
        </TextBlock>
    </Grid>
</UserControl>

The WaveProgressControl control - Code-behind

private readonly Compositor _compositor;
private readonly CompositionPropertySet _percentPropertySet;

public WaveProgressControl()
{
    InitializeComponent();

    _compositor = Window.Current.Compositor;

    _percentPropertySet = _compositor.CreatePropertySet();
    _percentPropertySet.InsertScalar("Value", 0.0f);

    Loaded += OnLoaded;
}

public double Percent
{
    get => (double)GetValue(PercentProperty);
    set => SetValue(PercentProperty, value);
}
public static readonly DependencyProperty PercentProperty =
    DependencyProperty.Register("Percent", typeof(double), typeof(WaveProgressControl),
        new PropertyMetadata(0.0d, (s, e) =>
        {
            var self = (WaveProgressControl)s;
            var propertySet = self._percentPropertySet;
            propertySet.InsertScalar("Value", Convert.ToSingle(e.NewValue) / 100);
        }));

private void OnLoaded(object sender, RoutedEventArgs e)
{
    CompositionSurfaceBrush imageSurfaceBrush;

    SetupClippedWaveImage();
    SetupEndlessWaveAnimationOnXAxis();
    SetupExpressionAnimationOnYAxisBasedOnPercentValue();

    void SetupClippedWaveImage()
    {
        // Note LoadedImageSurface is only available in 15063 onward.
        var imageSurface = LoadedImageSurface.StartLoadFromUri(new Uri(BaseUri, "/Assets/wave.png"));
        imageSurfaceBrush = _compositor.CreateSurfaceBrush(imageSurface);
        imageSurfaceBrush.Stretch = CompositionStretch.None;
        imageSurfaceBrush.Offset = new Vector2(120, 248);

        var maskBrush = _compositor.CreateMaskBrush();
        var maskSurfaceBrush = ClippedImageContainer.GetAlphaMask(); // CompositionSurfaceBrush
        maskBrush.Mask = maskSurfaceBrush;
        maskBrush.Source = imageSurfaceBrush;

        var imageVisual = _compositor.CreateSpriteVisual();
        imageVisual.RelativeSizeAdjustment = Vector2.One;
        ElementCompositionPreview.SetElementChildVisual(ClippedImageContainer, imageVisual);

        imageVisual.Brush = maskBrush;
    }

    void SetupEndlessWaveAnimationOnXAxis()
    {
        var waveOffsetXAnimation = _compositor.CreateScalarKeyFrameAnimation();
        waveOffsetXAnimation.InsertKeyFrame(1.0f, -80.0f, _compositor.CreateLinearEasingFunction());
        waveOffsetXAnimation.Duration = TimeSpan.FromSeconds(1);
        waveOffsetXAnimation.IterationBehavior = AnimationIterationBehavior.Forever;
        imageSurfaceBrush.StartAnimation("Offset.X", waveOffsetXAnimation);
    }

    void SetupExpressionAnimationOnYAxisBasedOnPercentValue()
    {
        var waveOffsetYExpressionAnimation = _compositor.CreateExpressionAnimation("Lerp(248.0f, 120.0f, Percent.Value)");
        waveOffsetYExpressionAnimation.SetReferenceParameter("Percent", _percentPropertySet);
        imageSurfaceBrush.StartAnimation("Offset.Y", waveOffsetYExpressionAnimation);
    }
}

The MainPage

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <local:WaveProgressControl x:Name="WaveProgressControl" />

    <Slider Grid.Row="1"
            Margin="24"
            Value="{x:Bind WaveProgressControl.Percent, Mode=TwoWay}" />
</Grid>

I have put everything into this sample project and below is a live demo. Enjoy! :)

enter image description here

Sign up to request clarification or add additional context in comments.

8 Comments

Thanks for the LoadedImageSurface. It's been so hard to load images.
Thanks you both answer in combined help me very much
Thx, Can I use it in business?
@lindexi absolutely!
@lindexi, the image is placed at the center of the screen, then it's pushed down by 248 and right by 120.
|
12

Here's the UWP sample. You may adjust it as you wish:

<Canvas>
    <Ellipse x:Name="Ellipse" Width="256" Height="256" Fill="DarkViolet" Stroke="DeepSkyBlue" StrokeThickness="8"/>
    <Border x:Name="VisualBorder" Opacity="0.5"/>
</Canvas>

And code behind:

    private async void CreateVisuals()
    {
        var compositor = ElementCompositionPreview.GetElementVisual(this).Compositor;

        var bitmap = await CanvasBitmap.LoadAsync(CanvasDevice.GetSharedDevice(),
            new Uri("ms-appx:///Assets/Wave-PNG-Transparent-Picture.png"));

        var drawingSurface =
            CanvasComposition.CreateCompositionGraphicsDevice(compositor, CanvasDevice.GetSharedDevice())
                .CreateDrawingSurface(bitmap.Size,
                    DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied);
        using (var ds = CanvasComposition.CreateDrawingSession(drawingSurface))
        {
            ds.Clear(Colors.Transparent);
            ds.DrawImage(bitmap);
        }

        var surfaceBrush = compositor.CreateSurfaceBrush(drawingSurface);
        surfaceBrush.Stretch = CompositionStretch.None;

        var maskedBrush = compositor.CreateMaskBrush();
        maskedBrush.Mask = Ellipse.GetAlphaMask();
        maskedBrush.Source = surfaceBrush;

        var sprite = compositor.CreateSpriteVisual();
        sprite.Size = new Vector2((float)Ellipse.Width, (float)Ellipse.Height);
        sprite.Brush = maskedBrush;
        sprite.CenterPoint = new Vector3(sprite.Size / 2, 0);
        sprite.Scale = new Vector3(0.9f);

        ElementCompositionPreview.SetElementChildVisual(VisualBorder, sprite);

        var offsetAnimation = compositor.CreateScalarKeyFrameAnimation();
        offsetAnimation.InsertKeyFrame(0, 0);
        offsetAnimation.InsertKeyFrame(1, 256, compositor.CreateLinearEasingFunction());
        offsetAnimation.Duration = TimeSpan.FromMilliseconds(1000);
        offsetAnimation.IterationBehavior = AnimationIterationBehavior.Forever;

        surfaceBrush.StartAnimation("Offset.X", offsetAnimation);
    }
}

Here's how it looks like:

enter image description here

4 Comments

Thank's, will it work for windows phone 8.1 silverlight , i am checking now .....
something is missed in code ?? Thanks for your effort but it is not as i want it fully filled with "DarkViolet" i want fill upto that wave and fill according to progress as i mention in the given solution link. And it is targeted for Anniversary 14393 SDK some api is not available in 10240 sdk , still some users mobile is not upgraded to anniversary update , so i don't want to target 14393 upgrade directly. But still thanks for your efforts. ............................ :)
Minimum SDK is 10586. You can move Offset.Y to make it fill the whole ellipse. And change a wave so it could hide content below the wave
Thanks you both answer in combined help me very much
1

I have achieved this using a simple solution:

Wave2.png is a extended ( copy pasted the image and added to the end of the first image ) to make it longer.

The solution works on WP8/Store apps/UWP/Silverlight

enter image description here

<Border
        Background="White"
        VerticalAlignment="Center"
        HorizontalAlignment="Center"
        CornerRadius="10000"
        BorderBrush="Black"
        BorderThickness="5">
        <Grid>
            <Ellipse
                x:Name="ellipse"
                VerticalAlignment="Center"
                HorizontalAlignment="Center"
                Height="200"
                Width="200">
                <Ellipse.Fill>
                    <ImageBrush
                        x:Name="WaveImage"
                        Stretch="None"
                        ImageSource="wave2.png">
                        <ImageBrush.Transform>
                            <CompositeTransform
                                TranslateY="200"
                                TranslateX="299" />
                        </ImageBrush.Transform>
                    </ImageBrush>
                </Ellipse.Fill>
            </Ellipse>
            <TextBlock
                VerticalAlignment="Center"
                HorizontalAlignment="Center"
                Text="HUJ" />
        </Grid>
    </Border>

And here is the animation code:

<Storyboard
        x:Name="AnimateWave">
        <DoubleAnimationUsingKeyFrames
            RepeatBehavior="Forever"
            EnableDependentAnimation="True"
            Storyboard.TargetProperty="(Shape.Fill).(Brush.Transform).(CompositeTransform.TranslateX)"
            Storyboard.TargetName="ellipse">
            <EasingDoubleKeyFrame
                KeyTime="0:0:5"
                Value="-299" />
        </DoubleAnimationUsingKeyFrames>
    </Storyboard>

2 Comments

Awesome idea 🙂
@ShubhamSahu just checked if can be done without image extension. Yup, just change the X translations from 299 and -299 to 99 and -99. And accordingly adjust the Y translations

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.