Tristan Dubé
Tristan Dubé

Reputation: 740

WPF Clipping with a shape

I am trying to create a 3..2..1 countdown in the form of a user control. Something like this. My Idea was to create two rectangles on top of each other, one light and one dark and have a radial circle as the clipper for the dark rectangle. The radial circle would have Angle property animated so it would turn around.

I found an implementation of the radial circle and bound the Clip property of the rectangle on the RenderedGeometry property of my circle. Here is the result :

Problem Screenshot

The red stroke is the shape of my clipper. This seems to be an odd behavior of the clipping but I sort of understand it but I would like to know if there was a way of going around the fact that my clipped object seems to use the RenderedGeometry in a weird way.

Edit 1 : The effect I am looking for http://www.youtube.com/watch?v=9FPHTo5V2BQ

Upvotes: 2

Views: 4102

Answers (2)

Sphinxxx
Sphinxxx

Reputation: 13017

You can clip your rectangle by using an ArcSegment in the clipping PathGeometry, and animate that ArcSegment's endpoint (Point).

The endpoint can be animated with a PointAnimationUsingPath animation, using an identical ArcSegment as its path. Below is a suggestion based on Charlez Petzold's excellent answer here: Drawing pie slices

<UserControl ... >

    <UserControl.Resources>
        <Point x:Key="SweepCenter" X="100" Y="100" />
        <Size x:Key="SweepRadius" Width="130" Height="130" />

        <!-- Start sweeping at twelve o'clock.. -->
        <Point x:Key="SweepStart" X="100" Y="-30" />
        <!-- ..and keep sweeping clockwise until we're (almost) back at the start point: -->
        <Point x:Key="SweepEnd" X="99.99" Y="-30" />

        <Storyboard x:Key="Sweeper" RepeatBehavior="Forever" AutoReverse="False" >

            <PointAnimationUsingPath Storyboard.TargetName="arc"
                                     Storyboard.TargetProperty="Point"
                                     Duration="0:0:5">
                <PointAnimationUsingPath.PathGeometry>
                    <PathGeometry>
                        <PathFigure StartPoint="{StaticResource SweepStart}">
                            <ArcSegment Size="{StaticResource SweepRadius}" 
                                        Point="{StaticResource SweepEnd}"
                                        SweepDirection="Clockwise"
                                        IsLargeArc="True" />
                        </PathFigure>
                    </PathGeometry>
                </PointAnimationUsingPath.PathGeometry>
            </PointAnimationUsingPath>

            <BooleanAnimationUsingKeyFrames Storyboard.TargetName="arc"
                                            Storyboard.TargetProperty="IsLargeArc" >
                <DiscreteBooleanKeyFrame KeyTime="0:0:2.5" Value="True" />
                <DiscreteBooleanKeyFrame KeyTime="0:0:5" Value="False" />
            </BooleanAnimationUsingKeyFrames>
        </Storyboard>

    </UserControl.Resources>

    <Grid Width="200" Height="200" >
        <Rectangle Fill="Black" />
        <Rectangle Fill="Gray" >
            <Rectangle.Triggers>
                <EventTrigger RoutedEvent="Loaded">
                    <BeginStoryboard Storyboard="{StaticResource Sweeper}" />
                </EventTrigger>
            </Rectangle.Triggers>
            <Rectangle.Clip>
                <PathGeometry>
                    <PathFigure StartPoint="{StaticResource SweepCenter}" 
                                IsClosed="True" >
                        <LineSegment Point="{StaticResource SweepStart}" />
                        <ArcSegment x:Name="arc"
                                    Size="{StaticResource SweepRadius}"
                                    Point="{StaticResource SweepStart}"
                                    SweepDirection="Clockwise" />
                    </PathFigure>
                </PathGeometry>
            </Rectangle.Clip>
        </Rectangle>
    </Grid>

</UserControl>

Upvotes: 3

Clemens
Clemens

Reputation: 128013

The simple derived Shape control shown below draws the countdown rectangle. You have to set its Fill (and perhaps Stroke), Width, Height and Angle properties, and you can animate Angle from 0 to 360.

public class CountdownRect : Shape
{
    static CountdownRect()
    {
        WidthProperty.OverrideMetadata(typeof(CountdownRect),
            new FrameworkPropertyMetadata((o, e) => ((CountdownRect)o).UpdateGeometry()));

        HeightProperty.OverrideMetadata(typeof(CountdownRect),
            new FrameworkPropertyMetadata((o, e) => ((CountdownRect)o).UpdateGeometry()));

        StrokeLineJoinProperty.OverrideMetadata(typeof(CountdownRect),
            new FrameworkPropertyMetadata(PenLineJoin.Round));
    }

    public static readonly DependencyProperty AngleProperty =
        DependencyProperty.Register("Angle", typeof(double), typeof(CountdownRect),
            new FrameworkPropertyMetadata((o, e) => ((CountdownRect)o).UpdateGeometry()));

    public double Angle
    {
        get { return (double)GetValue(AngleProperty); }
        set { SetValue(AngleProperty, value); }
    }

    private readonly StreamGeometry geometry = new StreamGeometry();

    protected override Geometry DefiningGeometry
    {
        get { return geometry; }
    }

    private void UpdateGeometry()
    {
        if (!double.IsNaN(Width) && !double.IsNaN(Height))
        {
            var angle = ((Angle % 360d) + 360d) % 360d;
            var margin = StrokeThickness / 2d;
            var p0 = new Point(margin, margin);
            var p1 = new Point(Width - margin, margin);
            var p2 = new Point(Width - margin, Height - margin);
            var p3 = new Point(margin, Height - margin);

            using (var context = geometry.Open())
            {
                if (angle == 0d)
                {
                    context.BeginFigure(p0, true, true);
                    context.LineTo(p1, true, false);
                    context.LineTo(p2, true, false);
                    context.LineTo(p3, true, false);
                }
                else
                {
                    var x = p2.X / 2d;
                    var y = p2.Y / 2d;
                    var a = Math.Atan2(x, y) / Math.PI * 180d;
                    var t = Math.Tan(angle * Math.PI / 180d);

                    context.BeginFigure(new Point(x, y), true, true);

                    if (angle < a)
                    {
                        context.LineTo(new Point(x + y * t, p0.Y), true, false);
                        context.LineTo(p1, true, false);
                        context.LineTo(p2, true, false);
                        context.LineTo(p3, true, false);
                        context.LineTo(p0, true, false);
                    }
                    else if (angle < 180d - a)
                    {
                        context.LineTo(new Point(p2.X, y - x / t), true, false);
                        context.LineTo(p2, true, false);
                        context.LineTo(p3, true, false);
                        context.LineTo(p0, true, false);
                    }
                    else if (angle < 180d + a)
                    {
                        context.LineTo(new Point(x - y * t, p2.Y), true, false);
                        context.LineTo(p3, true, false);
                        context.LineTo(p0, true, false);
                    }
                    else if (angle < 360d - a)
                    {
                        context.LineTo(new Point(p0.X, y + x / t), true, false);
                        context.LineTo(p0, true, false);
                    }
                    else
                    {
                        context.LineTo(new Point(x + y * t, p0.Y), true, false);
                    }

                    context.LineTo(new Point(x, p0.Y), true, false);
                }
            }
        }
    }
}

Upvotes: 3

Related Questions