Introduction
In the last couple of months since Microsoft has released WP7, I've started to build games and apps in XNA and Silverlight.
Building games is different than building applications, especially the UI... and most developers don't like dealing with UI, but UI doesn't have to be so scary and I'll start this article with a nice and simple technique I learned when I worked with Photoshop called "Masking".
The first example, I'll take from a game I built called "Raise My Dog". Raise My Dog is a Tamagotchi like game for WP7. The player gets to raise a dog inside his mobile device, feed it, and watch it grow.
Heart Control
An important part of the UI is the "heart control" that shows the dog's health. The heart has, well, a heart shape :) and thus we need to fill it without exceeding its borders; how do we do that?
As I mentioned in the article's title: using mask - to help mask out the heart fill without harming the border; this technique will save you a lot of time. The initial goal is to separate the heart border from the heart background. In my demo, I used Paths and not Images, but the concept remains the same.
Step 1: Placing the Heart Fill
The heart fill needs to be above everything.
<Path x:Name="path" Canvas.Left="246.758" Canvas.Top="199.33"
Stretch="Fill" Data="F1 M 246.759,219.964C 246.977,241.504 267.975,
259.233 284.206,265.059L 284.206,265.059C 300.302,
259.376 320.885,241.829 320.593,
220.288L 320.593,220.288C 320.441,208.875 311.149,199.525 299.872,
199.475L 299.872, 199.475C 293.324,199.446 287.546,202.799 283.884,
207.667L 283.884,207.667C 283.766,207.823 283.651,207.982 283.536,
208.142L 283.536,208.142C 279.778,202.988 273.756,199.359 266.995,
199.33L 266.995,199.33C 263.721,199.316 260.634,200.087 257.902,
201.468L 257.902,201.468C 251.226,204.844 246.679, 211.865 246.759,219.964 Z"
Margin="15,20,0,0" RenderTransformOrigin="0.5,0.5" Height="62.369"
UseLayoutRounding="False" VerticalAlignment="Top"
HorizontalAlignment="Left" Width="65.75">
<Path.RenderTransform>
<CompositeTransform/>
</Path.RenderTransform>
<Path.Fill>
<RadialGradientBrush RadiusX="0.470614" RadiusY="0.535016"
Center="0.50193,0.500556"
GradientOrigin="0.50193,0.500556">
<RadialGradientBrush.GradientStops>
<GradientStop Color="#FFED272A" Offset="0"/>
<GradientStop Color="#FF611317" Offset="1"/>
</RadialGradientBrush.GradientStops>
<RadialGradientBrush.RelativeTransform>
<TransformGroup>
<SkewTransform CenterX="0.50193"
CenterY="0.500556" AngleX="0.881065" AngleY="0"/>
<RotateTransform CenterX="0.50193"
CenterY="0.500556" Angle="0.279347"/>
</TransformGroup>
</RadialGradientBrush.RelativeTransform>
</RadialGradientBrush>
</Path.Fill>
</Path>
Step 2: Placing the Mask Between the Fill and Border
<Rectangle x:Name="Mask" Fill="{StaticResource PhoneBackgroundBrush}"
VerticalAlignment="Top" Margin="4,17,154,0"
Height="{Binding HeartValue,Converter={StaticResource HeartValueConverter}}"/>
I've added a converter to adjust the health to the mask size: if the dog's health is 50% then I need to change the mask height to the center of the heart border (half...), but the size has to be relative to the border so the mask size will be 34.126.
private const double Max = 68.253;
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
var health = (int)value;
return ((((health) * Max) / 100) - Max) * -1;
}
- Health = 0 then Mask Height= 68.253
- Health = 50 then Mask Height= 34.12
- Health = 90 then Mask Height= 6.8
Step 3: Placing the Border
<Path x:Name="border" Canvas.Left="243.55" Canvas.Top="195.863"
Stretch="Fill" Data="F1 M 243.553,218.219C 243.888,241.644 266.682,261.781 284.255,
268.184L 284.255,268.184C 301.628,262.071 323.779,242.306 323.365,218.88L 323.365,
218.88C 323.143,206.466 313.059,196.258 300.868,196.157L 300.868, 196.157C 293.56,
196.097 287.157,199.683 283.246,205.238L 283.246,205.238C 279.159,
199.616 272.639,195.925 265.329,195.864L 265.329,195.864C 261.79,
195.835 258.456,196.66 255.51,198.151L 255.51,198.151C 248.309,201.793 243.427,
209.41 243.553,218.219 Z M 284.205,265.049C 268.588,259.359 248.32,
240.575 248.022, 219.761L 248.022,219.761C 247.868,208.729 256.54,199.805 267.373,
199.894L 267.373,199.894C 273.871,199.948 279.662,203.228 283.295,208.224L 283.295,
208.224C 286.767,203.286 292.456,200.102 298.951,200.154L 298.951,
200.154C 309.784,200.245 318.746,209.315 318.942,220.346L 318.942,
220.346C 319.271,238.91 303.666,255.593 289.337,262.84L 289.337,262.84C 287.598,
263.72 285.875,264.461 284.205,265.049 Z "
Margin="12,17,0,0" RenderTransformOrigin="0.5,0.5"
Width="70.75" Height="68.253" UseLayoutRounding="False"
HorizontalAlignment="Left" VerticalAlignment="Top">
<Path.RenderTransform>
<CompositeTransform/>
</Path.RenderTransform>
<Path.Fill>
<LinearGradientBrush StartPoint="0.00275276,0.496476"
EndPoint="1.0027,0.496476">
<LinearGradientBrush.RelativeTransform>
<TransformGroup>
<SkewTransform CenterX="0.00275276"
CenterY="0.496476" AngleX="1.353" AngleY="0"/>
<RotateTransform CenterX="0.00275276"
CenterY="0.496476" Angle="0.520549"/>
</TransformGroup>
</LinearGradientBrush.RelativeTransform>
<LinearGradientBrush.GradientStops>
<GradientStop Color="#FFFAF6AF" Offset="0"/>
<GradientStop Color="#FFF1E091" Offset="0.13058"/>
<GradientStop Color="#FFE9CA74" Offset="0.159348"/>
<GradientStop Color="#FF9C6636" Offset="0.510986"/>
<GradientStop Color="#FFC29855" Offset="0.595192"/>
<GradientStop Color="#FFE9CA74" Offset="0.75824"/>
<GradientStop Color="#FFFAF6AF" Offset="0.972534"/>
<GradientStop Color="#FFFAF6AF" Offset="1"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Path.Fill>
</Path>
Rating
The Rating control is more common in applications. The same concept as heart masking applies except that here I use the same Path for Border and Fill and the difference between them is the Fill
property set to Yellow
in all Star objects. Also, instead of changing the height of the mask I'm changing the width.
Step 1: Define the Star Path as StaticResource
In App.Xaml, I create a Static Resource called StarPath
.
<Application.Resources>
<sys:String x:Key="StarPath">F1 M 0,217.042L 227.5,217.042L 297.875,
0L 367.542,217L 595.542,217L 410.208,353.667L 480.708,
569.667L 297.208,436.667L 116.208,568.167L 185.708,352.667L 0,217.042 Z</sys:String>
</Application.Resources>
Step 2: Create 5 Stars Using Path
<Path Name="star" Stretch="Uniform" Data="{StaticResource StarPath}"
Fill="Yellow" Width="73" Height="73" HorizontalAlignment="Left"
Margin="3,0.667,0,0" VerticalAlignment="Top" d:LayoutOverrides="Width, Height" />
<Path x:Name="star2" Stretch="Uniform" Data="{StaticResource StarPath}"
Fill="Yellow" Width="73" Height="73" Margin="81.922,0.333,0,0"
HorizontalAlignment="Left" VerticalAlignment="Top" />
<Path x:Name="star3" Stretch="Uniform" Data="{StaticResource StarPath}"
Fill="Yellow" Width="73" Height="73" Margin="161.569,0.333,0,0"
HorizontalAlignment="Left" VerticalAlignment="Top" />
<Path x:Name="star4" Stretch="Uniform" Data="{StaticResource StarPath}"
Fill="Yellow" Width="73" Height="73" Margin="240.895,0.333,0,0"
HorizontalAlignment="Left" VerticalAlignment="Top" />
<Path x:Name="star5" Stretch="Uniform" Data="{StaticResource StarPath}"
Fill="Yellow" Width="73" Height="73" Margin="320.89,0.333,0,0"
HorizontalAlignment="Left" VerticalAlignment="Top" />
Step 3: Placing the Mask Between the Fill and Border
<Rectangle x:Name="Mask" Fill="{StaticResource PhoneBackgroundBrush}"
VerticalAlignment="Top" Height="74" HorizontalAlignment="Left"
Width="{Binding Value, Converter={StaticResource RaitingValueToWidthConverter}}"
RenderTransformOrigin="0.5,0.5"></Rectangle>
And I've added a converter to adjust the width to the mask size (same as Heart control).
private const double Max = 395;
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
var rate = (double)value;
return ((((rate) * Max) / 5) - Max) * -1;
}
Step 4: Create 5 Stars Using Path But Without the Fill Property
<Path Name="border" Stretch="Uniform" Data="{StaticResource StarPath}"
StrokeThickness="2" Stroke="White" Width="75" Height="75"
HorizontalAlignment="Left" Margin="2,-0.333,0,0"
VerticalAlignment="Top" d:LayoutOverrides="Width, Height"/>
<Path x:Name="border2" Stretch="Uniform" Data="{StaticResource StarPath}"
StrokeThickness="2" Stroke="White" Width="75" Height="75"
Margin="81,-0.333,0,0" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<Path x:Name="border3" Stretch="Uniform" Data="{StaticResource StarPath}"
StrokeThickness="2" Stroke="White" Width="75" Height="75"
Margin="160.659,-0.334,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" />
<Path x:Name="border4" Stretch="Uniform" Data="{StaticResource StarPath}"
StrokeThickness="2" Stroke="White" Width="75" Height="75"
Margin="240.318,-0.334,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" />
<Path x:Name="border5" Stretch="Uniform" Data="{StaticResource StarPath}"
StrokeThickness="2" Stroke="White" Width="75" Height="75"
Margin="319.98,-0.334,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" />