Monthly Archives: January 2019

Fun with Bindable Squares in UWP

In the previous article we introduced some bindable Composition-drawn Path controls for UWP. In this article we will extend the list of controls with Rectangle and Square shapes, and introduce some optimizations in the framework. But above all: we’re going to have fun recreating some famous optical illusions with squares.

Bindable Rectangle

For starters, we created a Rectangle control, inspired by its SVG definition. Our base class already exposed a start point, and the necessary stroke and fill properties, so our new Rectangle subclass only required a height and a width. Semantically this new Rectangle control refers to ‘a panel that contains a rectangle figure somewhere in it’, so it did not make sense to reuse the inherited Height and Width properties. Instead we created SideX and SideY dependency properties.

Rendering a rectangle with a CanvasPathBuilder is quite obvious:

  • move to the start point with BeginFigure,
  • draw three lines with AddLine, and
  • close the figure with EndFigure (it will add the missing fourth line).

Here’s the full Render code:

protected override void Render()
{
    var canvasPathBuilder = GetCanvasPathBuilder();

    // Figure
    canvasPathBuilder.BeginFigure(new Vector2((float)StartPointX, (float)StartPointY));
    canvasPathBuilder.AddLine(
        new Vector2((float)(SideX + StartPointX), (float)(StartPointY)));
    canvasPathBuilder.AddLine(
        new Vector2((float)(SideX + StartPointX), (float)(SideY + StartPointY)));
    canvasPathBuilder.AddLine(
        new Vector2((float)(StartPointX), (float)(SideY + StartPointY)));
    canvasPathBuilder.EndFigure(CanvasFigureLoop.Closed);

    Render(canvasPathBuilder);
}

Observe that most of the rendering (creating the CanvasPathBuilder, creating and manipulating the CanvasGeometry, the SpriteShape, and the ShapeVisual) is common to all shapes, so we moved that to the base class.

Of course we created a small page to test the new control and its binding capabilities. Here how it looks like:

Rectangle

Bindable Square

Next in this series of bindable path controls comes a Square with a center point and a side – all implemented as dependency properties. Here’s the obvious rendering code:

protected override void Render()
{
    var canvasPathBuilder = GetCanvasPathBuilder();

    // Move starting point.
    StartPointX = CenterPointX - (Side / 2);
    StartPointY = CenterPointY - (Side / 2);

    // Figure
    canvasPathBuilder.BeginFigure(new Vector2((float)StartPointX, (float)StartPointY));
    canvasPathBuilder.AddLine(
        new Vector2((float)(Side + StartPointX), (float)(StartPointY)));
    canvasPathBuilder.AddLine(
        new Vector2((float)(Side + StartPointX), (float)(Side + StartPointY)));
    canvasPathBuilder.AddLine(
        new Vector2((float)(StartPointX), (float)(Side + StartPointY)));
    canvasPathBuilder.EndFigure(CanvasFigureLoop.Closed);

    Render(canvasPathBuilder);
}

Here’s the sample page:

Square

It already contains the elements to test some properties that we’ll define later in this article.

Here’s the XAML for the test square:

<controls:Square x:Name="Square"
                    Stroke="LightSlateGray"
                    CenterPointX="150"
                    CenterPointY="250"
                    Side="100"
                    Height="400"
                    Width="400" />

Let’s start the fun

To discover new requirements and optimizations the framework, we decided to reenact some famous optical illusions involving only squares.

Most of the pages start with just an empty canvas like this:

<Viewbox>
    <Canvas x:Name="Canvas"
            Height="1000"
            Width="1500"
            Background="Transparent" />
</Viewbox>

We’ll draw most of the squares programmatically.

The first one is the Café Wall Illusion (a.k.a. the shifted chessboard), a 2D geometrical-optical illusion where parallel lines appear to be sloped. Here’s how our bindable shape implementation looks like:

CafeWall

Believe it or not, all these horizontal lines are parallel.

Here’s how a square is added to the canvas:

var blackSquare = new Square
{
    Side = side,
    Fill = Colors.LightSlateGray,
    Height = Canvas.Height,
    Width = Canvas.Width,
    CenterPointX = (240 * i),
    CenterPointY = 120 + (240 * j)
};
Canvas.Children.Add(blackSquare);

This first test case did not reveal new requirements, but allowed us to demonstrate the programmatic instantiation of our new controls – and we had a lot of fun.

Optimizing for quantity

Our second test case is the Bulging Checkerboard Illusion, a 3D geometrical-optical illusion that makes a checkerboard to look like bulging out of the screen, just by adding small squares (or circles) in it:

Checkerboard

When we rendered the image, we noticed that it took an unacceptably long time. Our first attempt to solve this, was to drastically reducing the number of calls to the core rendering code (a technique that we borrowed from some of the WinRT XAML Toolkit Controls). Every change of every dependency property triggers rendering, so that’s a lot of calls when the control is loading. So we added a DelayRendering property to the base class an made sure not to call the core logic as long as the property is set to false:

public CompositionPathHost(bool delayRendering) : this()
{
    _delayRendering = delayRendering;
}
protected static void Render(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var path = (CompositionPathHost)d;
    if (path._delayRendering || path.Container.ActualWidth == 0)
    {
        return;
    }

    path.Render();
}
public bool DelayRendering
{
    get
    {
        return _delayRendering;
    }

    set
    {
        _delayRendering = value;
        if (!_delayRendering)
        {
            Render();
        }
    }
}

Of course this has an impact on the drawing code on the client side. We have to make sure that the property is assigned before any other (in a constructor), and have to reset is after all the other properties have their value:

var blackSquare = new Square(true)
{
    Side = side,
    Fill = Colors.LightSlateGray,
    Height = Canvas.Height,
    Width = Canvas.Width,
    CenterPointX = i * 100 + 50,
    CenterPointY = j * 100 + 50
};
Canvas.Children.Add(blackSquare);
blackSquare.DelayRendering = false;

Unfortunately we were underwhelmed by the impact of this code on the rendering time of all squares. So we started looking into other ways to speed up the process, by looking at opportunities to share ‘expensive’ objects – probably in the Win2D space. From the home page of CompositionProToolkit we learned that a CanvasDevice can be shared between CanvasPathBuilder instances, so we created a static device and reused it:

protected static CanvasDevice CanvasDevice = new CanvasDevice();

protected CanvasPathBuilder GetCanvasPathBuilder()
{
    var canvasPathBuilder = new CanvasPathBuilder(CanvasDevice);
    // ...

    return canvasPathBuilder;
}

The difference in rendering time is quite significant, and there is no impact on the client code. So it’s another successful test case!

Adding bindable Rotation properties

Our third test case (and personal favorite) is the hypnotizing Square Circe Spiral Illusion, an intertwining illusion where a set of concentric circles made of squares appear to spiral and intersect:

Intertwining

The effect is caused by selecting appropriate colors and by applying a rotation scheme to the squares. The latter is missing in our current API. Since it’s quite common to apply a rotation to shapes, we added the necessary dependency properties to the base class: RotationCenterX, RotationCenterY, and RotationAngle. We then only had to update the shared part of the rendering code to use these new properties:

shapeVisual.CenterPoint = new Vector3((float)RotationCenterX, (float)RotationCenterY, 0f);
shapeVisual.RotationAxis = new Vector3(0f, 0f, 1f);
shapeVisual.RotationAngle = (float)RotationAngle;

Et voilà: free rotation capabilities for all bindable shapes. Here’s a code snippet from the sample page:

var square = new Square
{
    Side = side,
    Stroke = i % 2 == 0 ? Colors.WhiteSmoke : Colors.LightSlateGray,
    StrokeThickness = 6,
    Height = Canvas.Height,
    Width = Canvas.Width,
    CenterPointX = (size / 2) + Math.Sin(Math.PI * 2 * i / squares) * radius,
    CenterPointY = (size / 2) + Math.Cos(Math.PI * 2 * i / squares) * radius,
    RotationCenterX = (size / 2) + Math.Sin(Math.PI * 2 * i / squares) * radius,
    RotationCenterY = (size / 2) + Math.Cos(Math.PI * 2 * i / squares) * radius,
    RotationAngle = (90d - i * (360d / squares) + rot * 15d) * Math.PI / 180
};
Canvas.Children.Add(square);

Adding animation

The fourth test case required animation: in the Breathing Square Illusion a spinning rectangle seems to grow and shrink, but it’s actually just rotating without any size change.

BreathingSquare

There is no way to expose animation behavior by just adding dependency properties. Instead we decided to expose the rendered ShapeVisual to the client of the control – not as a dependency property however, since the visual is recreated at each Render call. The shape visual is exposed via an Event on the control, so we first declared an argument:

public class RenderedEventArgs : EventArgs
{
    public ShapeVisual ShapeVisual { get; set; }
}

And then used it in a Rendered event:

public event EventHandler<RenderedEventArgs> Rendered;

protected virtual void OnRendered(RenderedEventArgs e)
{
    Rendered?.Invoke(this, e);
}

protected void Render(CanvasPathBuilder canvasPathBuilder)
{
    // ...
    root.Children.InsertAtTop(shapeVisual);
    OnRendered(new RenderedEventArgs { ShapeVisual = shapeVisual });
}

Here’s the definition of the square in the back:

var backSquare = new Square
{
    Side = 600,
    Fill = Colors.LightSteelBlue,
    Height = Canvas.Height,
    Width = Canvas.Width,
    CenterPointX = 500,
    CenterPointY = 500,
    RotationCenterX = 500,
    RotationCenterY = 500
};
backSquare.Rendered += BackSquare_Rendered;
Canvas.Children.Add(backSquare);

And here’s the handler for the Rendered event. It starts a ScalarKeyFrameAnimation with a LinearEasingFunction on the RotationAngleInDegrees:

private void BackSquare_Rendered(object sender, RenderedEventArgs e)
{
    // Start animation
    var shape = e.ShapeVisual;
    var compositor = Window.Current.Compositor;
    var animation = compositor.CreateScalarKeyFrameAnimation();
    var linear = compositor.CreateLinearEasingFunction();
    animation.InsertKeyFrame(1f, 360f, linear);
    animation.Duration = TimeSpan.FromSeconds(8);
    animation.Direction = AnimationDirection.Normal;
    animation.IterationBehavior = AnimationIterationBehavior.Forever;
    shape.StartAnimation(nameof(shape.RotationAngleInDegrees), animation);
}

Each time that the Square control is Rendered (when a dependency property changed), the shape visual is exposed via the event, and the rotation is started.

With these few –but fun- test cases, we were able to extend the base class of the Bindable Path API with nice features and improvements. This is how it now looks like:

ClassDiagram1

Note: there are more subclasses than just Square …

Look Mom, all XAML

Most of the test cases created the ‘bindable’ path controls programmatically, so we decided to add one entirely in XAML, to finish it off. Here’s how the Motion Binding Illusion looks like:

MotionBinding

At first sight there is nothing common in the movement of the four bars, except that they seem to move in pairs. Again this illusion is entirely made up of squares. The four bars are the sides of a 45° rotated square of which the corners are hidden by four smaller (and also rotated) square. The center square is making a circular motion.

Here’s how these five squares are defined in XAML – we now surely wish we had added a RotationInDegrees property:

<Canvas x:Name="Canvas"
        Height="1000"
        Width="1000"
        Background="LightSteelBlue">
    <controls:Square x:Name="Square"
                        Stroke="LightSlateGray"
                        StrokeThickness="16"
                        CenterPointX="500"
                        CenterPointY="500"
                        Side="540"
                        Height="1000"
                        Width="1000"
                        RotationCenterX="500"
                        RotationCenterY="500"
                        RotationAngle=".785"
                        Canvas.Left="-50"
                        Canvas.Top="-50">

    </controls:Square>
    <controls:Square Fill="LightSteelBlue"
                        StrokeThickness="0"
                        CenterPointX="500"
                        CenterPointY="142"
                        Side="200"
                        Height="1000"
                        Width="1000"
                        RotationCenterX="500"
                        RotationCenterY="142"
                        RotationAngle=".785" />
    <controls:Square Fill="LightSteelBlue"
                        StrokeThickness="0"
                        CenterPointX="500"
                        CenterPointY="858"
                        Side="200"
                        Height="1000"
                        Width="1000"
                        RotationCenterX="500"
                        RotationCenterY="858"
                        RotationAngle=".785" />
    <controls:Square Fill="LightSteelBlue"
                        StrokeThickness="0"
                        CenterPointX="142"
                        CenterPointY="500"
                        Side="200"
                        Height="1000"
                        Width="1000"
                        RotationCenterX="142"
                        RotationCenterY="500"
                        RotationAngle=".785" />
    <controls:Square Fill="LightSteelBlue"
                        StrokeThickness="0"
                        CenterPointX="858"
                        CenterPointY="500"
                        Side="200"
                        Height="1000"
                        Width="1000"
                        RotationCenterX="858"
                        RotationCenterY="500"
                        RotationAngle=".785" />
</Canvas>

The circular motion of the is done through a storyboarded animation on the Canvas.Top and Canvas.Left attached properties – both DoubleAnimations with a SineEase easing function in ‘EaseInOut’ mode (slow-fast-slow). To end up with a circular motion, one animation should follow a cosine function while the follows a sine function. This is solved by delaying the start of one animation with one second (half the full duration of the 180° animation).

Here’s the whole XAML: 

<Storyboard x:Name="myStoryboard"
            RepeatBehavior="Forever">
    <DoubleAnimation Storyboard.TargetName="Square"
                        Storyboard.TargetProperty="(Canvas.Left)"
                        From="-50"
                        To="50"
                        Duration="0:0:2"
                        AutoReverse="True"
                        RepeatBehavior="Forever">
        <DoubleAnimation.EasingFunction>
            <SineEase EasingMode="EaseInOut" />
        </DoubleAnimation.EasingFunction>
    </DoubleAnimation>
    <DoubleAnimation Storyboard.TargetName="Square"
                        Storyboard.TargetProperty="(Canvas.Top)"
                        From="-50"
                        To="50"
                        BeginTime="0:0:1"
                        Duration="0:0:2"
                        AutoReverse="True"
                        RepeatBehavior="Forever">
        <DoubleAnimation.EasingFunction>
            <SineEase EasingMode="EaseInOut" />
        </DoubleAnimation.EasingFunction>
    </DoubleAnimation>
</Storyboard>

Want some more?

There are a lot more optical illusions right here. Our sample project lives on GitHub.

Enjoy!

Advertisements