Drag and Twist Photo Album
I took up Silverlight almost a year ago and I can still remember one of the first projects someone else had posted on the Microsoft’s Silverlight Show that really sparked my interest in the technology - A Twist and Drag photo album. In searching the net for specific examples I found it very difficult to find any in depth descriptions of methodologies used to emulate what I had seen. So with a bit of elbow grease and determination I went at my notebook and came up with the following.
I think it's important to note that I originally wrote this article for www.meanbyte.com. You should check it out =D !
The BoxView Control
The image (media) that we will be manipulating will be contained within a BoxView Control. This container element exposes a number of properties that store important values used to calculate the particular object’s move-ability (twist, drag, and scale).
public int zIndex;
public double aScale, aTransX, aTransY, aAngle;
public Point centerPoint, lastPoint, lastCenterPoint;
We store the last set ZIndex value, ScaleTransform, TranslateTransform (Canvas.Left/TopProperty), and RotateTransform values. As well, we store the last calculated center point, the current center point, and the last point (mouse capture point used in the control’s Scale and Rotate calculation). We’ll dive into these a bit later.
Other than that, the BoxView control contains a Canvas and a few other controls used to spruce up the display of an Image or MediaElement etc.

The Application (App.xaml)
For each Image (MediaElement) we add a BoxView control to our Application’s root UIElement (LayoutRoot).
A couple of properties are exposed off of the Main App:
private bool isDragging, isRotating;
private Point offset, currentPoint;
These allows us to determine if a BoxView is already being dragged/rotated and is interactable as well as storing the last MouseCapture points for the current BoxView that is being dragged (offset, currentPoint).
The actual control manipulation takes place in a single function…
private void renderTransformation(BoxView senderBoxView, double translateX, double translateY, double rotateAngle, double scale)
… that we call whenever we wish to update the X/Y Axis position of or RotateTransform or ScaleTransform properties of a BoxView. We’ll see this in action a little later.
Drag (Translate)
The first obstacle to tackle was the easiest, dragging objects. Silverlight has some built in functionality that allows us to accomplish this with relative ease.
There are two methods that I found in my initial thinking that would serve us well to translate the BoxView control over our application. We could employ a TranslateTranform or we could set the control’s Canvas.LeftProperty and Canvas.TopProperty. In testing both methods I found that since our dragging motion is procured through a continually updated lastpoint/currentpoint calculation over a mouse drag movement every few milliseconds using the TranslateTransform method proved a bit sluggish. Updating the Canvas.Left/TopProperty values to move the BoxViews was much smoother.
Do do this we must capture a few events – MouseDown on our BoxView, MouseUp on our BoxView, and MouseDrag while MouseDown on our BoxView.
On our Application’s LayoutRoot I’ve added three Event Handlers (which we attach to each child BoxView).
protected void BoxView_Drag_MouseDown(object sender, MouseButtonEventArgs e)
{
isDragging = true;
isRotating = false;
Canvas senderLayoutRoot = (Canvas)parentCanvas.Parent as Canvas;
BoxView senderBoxView = (BoxView)sender as BoxView;
senderBoxView.CaptureMouse();
offset = e.GetPosition(senderBoxView);
selectBoxView(senderBoxView);
}
protected void BoxView_Drag_MouseUp(object sender, MouseButtonEventArgs e)
{
isDragging = false;
BoxView senderBoxView = (BoxView)sender as BoxView;
senderBoxView.ReleaseMouseCapture();
}
protected void BoxView_Drag_MouseMove(object sender, MouseEventArgs e)
{
if (isDragging)
{
BoxView senderBoxView = (BoxView)sender as BoxView;
Point newPosition = e.GetPosition(senderBoxView);
renderTransformation(senderBoxView, (newPosition.X - offset.X),
(newPosition.Y - offset.Y), 0, senderBoxView.aScale, false);
double centerX = (senderBoxView.aTransX + (senderBoxView.Width / 2));
double centerY = (senderBoxView.aTransY + (senderBoxView.Height / 2));
senderBoxView.centerPoint = new Point(centerX, centerY);
}
}
When we click on a BoxView we fire the BoxView_Drag_MouseDown handler which sets the main app’s isDragging to true and isRotating to false (above). This dissallows the application from calling our renderTransformation when unwanted. We also call the senderBoxView.CaptureMouse();. A side note, we could clean up our code and utilize the CaptureMouse bool value for each BoxView instead of the isDragging property to determine interactability, however we need to distinguish between mouse capture for dragging purposes and mouse capture for rotating purposes. We also set the offset Point property for our App to the current mouse position (more about the MouseEventArgs.GetPosition() function below).
When we release the mouse on a BoxView we fire the BoxView_Drag_MouseUp handler which releases the mouse capture and sets our isDragging property back to false.
When we Move the mouse while isDragging is set to true (click and move the mouse before releasing it), we fire the BoxView_Drag_MouseMove handler which does each of three things:
1. We employ the MouseEventArgs(e).GetPosition(UIElement); function that gets the Point data for the mouse location relative to a Parent UIElement.
Point newPosition = e.GetPosition(ParentElement);
2. We employ the renderTransform function passing the selected BoxView so we know what to update, the difference between the offset (original mousecapture point) and the newPosition (current mousecapture point) or total X/Y distances to effectively translate:
(newPosition.X - offset.X), (newPosition.Y - offset.Y)
Note that our offset position is set with each new MouseDown event fire.
3. We update the current centerpoint for the BoxView (used to calculate the scale/rotate properties of the BoxView for Twist/Rotate actions). We’ll look into this shortly.
It’s pertinant to note that we also call a selectBox() function that iterates through each BoxView control within our LayoutRoot and resets their ZIndex properties so that the selected BoxView remains on top.
private void selectBoxView(BoxView selectedBoxView)
{
for (int i = 0; i < LayoutRoot.Children.Count; i++)
{
if (LayoutRoot.Children[i].GetType == typeOf(BoxView))
{
BoxView boxview = (BoxView)LayoutRoot.Children[i];
if (boxview.zIndex >= selectedBoxView.zIndex)
{
if (boxview.zIndex <= 1)
{
boxview.zIndex = 2;
}
else
{
boxview.zIndex--;
}
}
boxview.SetValue(Canvas.ZIndexProperty, boxview.zIndex);
}
//numboxview set when controls added originally (total num boxviews)
selectedBoxView.zIndex = numBoxView + 1;
selectedBoxView.SetValue(Canvas.ZIndexProperty, numBoxView + 1);
}
The RenderTransformation Function
The renderTransform sets the appropriate values for the BoxView’s TransformGroup children (Rotate/Scale) as well as sets the Canvas.Top/LeftProperty values. Note the BoxView properties we’re updating as well (aScale, aTransX, and aTransY). These properties store the last value used to update the (actual)Scale.X/Yand (actual)Translate.X/Y values which maintains greater consistency in historical movement data rather than relying on methods like DependancyObject.GetValue(); which can return unexpected data as manipulation to these properties can occur autonomously.
private void renderTransformation(BoxView senderBoxView, double translateX, double translateY, double rotateAngle, double scale)
{
TransformGroup tg_senderBoxView = (TransformGroup)senderBoxView.LayoutRoot.RenderTransform;
TranslateTransform tt_senderBoxView = (TranslateTransform)tg_senderBoxView.Children[0];
RotateTransform rt_senderBoxView = (RotateTransform)tg_senderBoxView.Children[1];
ScaleTransform st_senderBoxView = (ScaleTransform)tg_senderBoxView.Children[2];
rt_senderBoxView.Angle += rotateAngle;
senderBoxView.SetValue(Canvas.LeftProperty, (senderBoxView.aTransX + translateX));
senderBoxView.SetValue(Canvas.TopProperty, (senderBoxView.aTransY + translateY));
st_senderBoxView.ScaleX = scale;
st_senderBoxView.ScaleY = scale;
senderBoxView.aScale = scale;
senderBoxView.aTransX = (double)senderBoxView.GetValue(Canvas.LeftProperty);
senderBoxView.aTransY = (double)senderBoxView.GetValue(Canvas.TopProperty);
senderBoxView.UpdateLayout();
}
Twist (Rotate)
Now we get to the good stuff.
Much like the BoxView_Drag_MouseMove function discussed before, a BoxView_Rotate_MouseMove function allows for similair calculation and manipulation of our BoxView controls in a circular motion.
Observe the following diagram:

In the above, we use the Inverse Tangent function to calculate the two angles (la and ca). We calculate the each angle (lastAngle and currentAngle) and subtract the last from current to get the new total RotateAngle value. To get the angle we use the following equation:
Tan α = Opposite/Adjacent
To get our Opposite and Adjacent side lengths we use values we know, Point coordinates. We know our BoxView centerPoint as well as the current mouse point and last (drag motion start) mouse point.
Note that the Tan α will result in a Radian value. RotateTransforms take a double angle input value and so we must convert our Radian value to degrees using the following:
double degrees = radians * (180 / Math.PI);
Scale
Our final calculation is our Scale. As our mouse drag motion moves outward or inward our BoxView control scales itself to keep our MouseCapture point aligned with our Cursor position.
Observe the following diagram:

We’ll use Geometry to calculate the appropriate ScaleTransform value we should apply to our BoxView based on mouse movement. Using the Pythagoras theorem (a^2 + b^2) = c^2 we will calculate our C1 and C2 values (hypotenuses) and derive a percentage C2 is of C1 (newScale above).
In order to calculate our A1 and B1 values we will use what data we posses, Point data (p1 – p4), control width, current and last updated scale values.
Calculating A1 and B1 can be done using our controls base dimensions and last known scale transform value (aScale).
A1 = control.height * control.aScale;
B1 = control.width * control.aScale;
Calculating A2 and B2 can be done by measuring the distance from p4.X or p4.Y to the BoxView centerPoint.X or centerPoint.Y. The only catch to using point data here relies on our knowing where our currentPoint (p4 above) is in relation to our BoxView’s centerPoint. For this we use quadrants (q1 – q4) above to determine if we need to use the difference between p4.X and centerPoint.X or p4.Y and centerPoint.Y.
if (currentPoint.X >= senderBoxView.centerPoint.X && currentPoint.Y <senderBoxView.centerPoint.Y)
{
//-- We know that our cursor must be in quadrant 1
A2 = currentPoint.X - senderBoxView.centerPoint.X;
B2 = senderBoxView.centerPoint.Y - currentPoint.Y;
}
Once we have our A2 and B2 values we use the Pythagoras theorem to calculate the value of C2, however we must multiply this value by 2 as we’ve used a triangle twice as small to derive our C2 value (see purple line above.
Finally we use our newScale value and pass this to our renderTransformation() function to update our BoxView’s scale.
Conclusion and Notes
Creating a Twist and Drag Photo album isn’t all to difficult once it’s conceptully broken down. As with any project tweaking will be necessary. Personally I believe minor alterations to algorithms used within artistic applications can provide a more organic user experience which in my optinion is more impressionable.
To see a live example visit http://www.meanbyte.com/?slcid=portfolio.