This Silverlight 3D tilted egg topic selector component is pretty tightly integrated into our main Silverlight application for authoring articles and faqs. So, I opted to throw in several classes
used in our Silverlight application versus trying to tear apart all of David's code. These support classes
if you will, should not be viewed as critical to this code sample nor the proper
way to organize classes, namespaces, etc. in a well architected application.
They were extracted from 13 different properly organized assemblies in our live
application. I've put all of the EggHeadCafe specific classes I wrote myself
in the "EggHeadCafe" folder.
The primary purpose of making this code available was for Silverlight developers to look at the work David did in XAML as well as the code behind to interact with storyboards and animation. If you would
like to see a live demo, launch our main Silverlight application and click on the "egg based" topic selector. Notice how the egg tilts
and moves during the mouse over event. If you expand out several levels, you
can see the entire set of selected eggs tilt during the mouse over event. Pretty
cool stuff. The egg cracking animation (and reversals of that animation) is
all done in XAML. David converted the images we had created for him into XAML in order to make the animation look cool.
Feel free to take this source code, adapt it as needed, and use in your own private
or commericial applications. However, as with all EggHeadCafe articles, you
are not permitted to republish the source code on another web site or blog.
Here's a snapshot:

Other items of interest include the use of custom EventArgs in the EggHeadCafe DataEventArg<T> class. It demonstrates how to use .NET generics to pass around or bubble up data
classes via event handlers. It also includes built capabilities to pass around
rule exceptions and trapped errors from asynchronous method calls to say maybe
a WCF operation method. As part of the event bubbling, it takes care of turning on and
off hour glass and progress bar indicators in a generic way. This drastically reduces the amount of code you have to write
in the UI to deal with these niceties.
Here are a few excerpts of the source code included in the download link below:
Download Source Code
// Vector.cs
namespace Portal.Xaml
{
public struct Vector
{
#region Constructors
public Vector(double x, double y)
: this()
{
X = x;
Y = y;
}
#endregion Constructors
#region Properties
public double Length
{
get
{
return Math.Sqrt(LengthSquared);
}
}
public double LengthSquared
{
get
{
return (this.X * this.X) + (this.Y * this.Y);
}
}
public double X
{
get;
set;
}
public double Y
{
get;
set;
}
#endregion Properties
#region Methods
public static explicit operator Size(Vector vector)
{
return new Size(Math.Abs(vector.X), Math.Abs(vector.Y));
}
public static explicit operator Point(Vector vector)
{
return new Point(vector.X, vector.Y);
}
public static bool operator !=(Vector vector1, Vector vector2)
{
return !(vector1 == vector2);
}
public static Vector operator *(Vector vector, double scalar)
{
return new Vector(vector.X * scalar, vector.Y * scalar);
}
public static Vector operator *(double scalar, Vector vector)
{
return new Vector(vector.X * scalar, vector.Y * scalar);
}
public static double operator *(Vector vector1, Vector vector2)
{
return ((vector1.X * vector2.X) + (vector1.Y * vector2.Y));
}
public static Vector operator +(Vector vector1, Vector vector2)
{
return new Vector(vector1.X + vector2.X, vector1.Y + vector2.Y);
}
public static Point operator +(Vector vector, Point point)
{
return new Point(point.X + vector.X, point.Y + vector.Y);
}
public static Vector operator -(Vector vector)
{
return new Vector(-vector.X, -vector.Y);
}
public static Vector operator -(Vector vector1, Vector vector2)
{
return new Vector(vector1.X - vector2.X, vector1.Y - vector2.Y);
}
public static Vector operator /(Vector vector, double scalar)
{
return (Vector)(vector * (1.0 / scalar));
}
public static bool operator ==(Vector vector1, Vector vector2)
{
return ((vector1.X == vector2.X) && (vector1.Y == vector2.Y));
}
public static Vector Add(Vector vector1, Vector vector2)
{
return new Vector(vector1.X + vector2.X, vector1.Y + vector2.Y);
}
public static Point Add(Vector vector, Point point)
{
return new Point(point.X + vector.X, point.Y + vector.Y);
}
public static double AngleBetween(Vector vector1, Vector vector2)
{
double y = (vector1.X * vector2.Y) - (vector2.X * vector1.Y);
double x = (vector1.X * vector2.X) + (vector1.Y * vector2.Y);
return (Math.Atan2(y, x) * 57.295779513082323);
}
public static double CrossProduct(Vector vector1, Vector vector2)
{
return ((vector1.X * vector2.Y) - (vector1.Y * vector2.X));
}
public static double Determinant(Vector vector1, Vector vector2)
{
return ((vector1.X * vector2.Y) - (vector1.Y * vector2.X));
}
public static Vector Divide(Vector vector, double scalar)
{
return (Vector)(vector * (1.0 / scalar));
}
public static bool Equals(Vector vector1, Vector vector2)
{
return (vector1.X.Equals(vector2.X) && vector1.Y.Equals(vector2.Y));
}
public static Vector Multiply(Vector vector, double scalar)
{
return new Vector(vector.X * scalar, vector.Y * scalar);
}
public static Vector Multiply(double scalar, Vector vector)
{
return new Vector(vector.X * scalar, vector.Y * scalar);
}
public static double Multiply(Vector vector1, Vector vector2)
{
return ((vector1.X * vector2.X) + (vector1.Y * vector2.Y));
}
public static Vector Subtract(Vector vector1, Vector vector2)
{
return new Vector(vector1.X - vector2.X, vector1.Y - vector2.Y);
}
public override bool Equals(object o)
{
if ((o == null) || !(o is Vector))
{
return false;
}
Vector vector = (Vector)o;
return Equals(this, vector);
}
public bool Equals(Vector value)
{
return Equals(this, value);
}
public override int GetHashCode()
{
return (this.X.GetHashCode() ^ this.Y.GetHashCode());
}
public void Negate()
{
this.X = -this.X;
this.Y = -this.Y;
}
public void Normalize()
{
this = (Vector)(this / Math.Max(Math.Abs(this.X), Math.Abs(this.Y)));
this = (Vector)(this / this.Length);
}
#endregion Methods
}
}
// SilverlightHelper.cs
namespace EggHeadCafe.Silverlight.helpers
{
public static class SilverlightHelper
{
public static string GetAppSetting(string SettingName)
{
string SettingValue = "";
try
{
SettingValue = IsolatedStorageSettings.ApplicationSettings["LastState"].ToString();
}
catch
{
SettingValue = "";
}
return SettingValue;
}
public static void SetAppSetting(string SettingName, string SettingValue)
{
IsolatedStorageSettings.ApplicationSettings[SettingName] = SettingValue;
}
}
}
// _3DMouseInteractionV3.cs
namespace Portal.Xaml
{
public class _3DMouseInteractionV3
{
public FrameworkElement FE { get; set; }
public FrameworkElement FE4MouseMove { get; set; }
public Boolean HasMouse { get; set; }
public PlaneProjection PP { get; set; }
public UserControl UC { get; set; }
public _3DMouseInteractionV3(FrameworkElement feMouseBinding, FrameworkElement Element2Move)
{
FE = Element2Move;
FE4MouseMove = feMouseBinding;
PP = FE.Projection as PlaneProjection; //new PlaneProjection();
FE.Projection = PP;
bindFwElement();
}
private void bindFwElement()
{
FE4MouseMove.MouseMove += new MouseEventHandler(MouseMove);
FE4MouseMove.MouseLeave += new MouseEventHandler(MouseLeave);
}
private void MouseMove(object sender, MouseEventArgs e)
{
FrameworkElement feAnimated3D = (FrameworkElement)sender;
HasMouse = true; //If it moves over, it is over it :)
Point posMouse = e.GetPosition(feAnimated3D);
double RotationDegrees = 25;
double distX = 0;
if (posMouse.X > (feAnimated3D.ActualWidth / 2))
{
distX = posMouse.X - (feAnimated3D.ActualWidth / 2);
}
else
{
distX = (posMouse.X - (feAnimated3D.ActualWidth / 2)) * 1;
}
double ratioX = 1 / (feAnimated3D.ActualWidth / 2);
double rotationX = distX * ratioX;
rotationX = rotationX * RotationDegrees;
double distY = 0;
if (posMouse.Y > (feAnimated3D.ActualHeight / 2))
{
distY = posMouse.Y - (feAnimated3D.ActualHeight / 2);
}
else
{
distY = (posMouse.Y - (feAnimated3D.ActualHeight / 2)) * 1;
}
double ratioY = 1 / (feAnimated3D.ActualHeight / 2);
double rotationY = distY * ratioY;
rotationY = rotationY * RotationDegrees;
RotateLayerXY(FE, -rotationY, rotationX);
//PP.RotationX = -rotationY;
//PP.RotationY = rotationX;
}
private void RotateLayerXY(FrameworkElement c, double X, double Y)
{
Storyboard sbX = new Storyboard();
Storyboard sbY = new Storyboard();
if (FE.Resources.Contains("SbAnimX"))
FE.Resources.Remove("SbAnimX");
FE.Resources.Add("SbAnimX", sbX);
if (FE.Resources.Contains("SbAnimY"))
FE.Resources.Remove("SbAnimY");
FE.Resources.Add("SbAnimY", sbY);
Duration duration = new Duration(TimeSpan.FromSeconds(0.6));
DoubleAnimation myDoubleAnimationX = new DoubleAnimation();
sbX.Children.Add(myDoubleAnimationX);
DoubleAnimation myDoubleAnimationY = new DoubleAnimation();
sbY.Children.Add(myDoubleAnimationY);
myDoubleAnimationX.Duration = duration;
sbX.Duration = duration;
myDoubleAnimationY.Duration = duration;
sbY.Duration = duration;
Storyboard.SetTarget(myDoubleAnimationX, c);
Storyboard.SetTarget(myDoubleAnimationY, c);
// Set the attached properties of Canvas.Left and Canvas.Top
// to be the target properties of the two respective DoubleAnimations
PropertyPath pX = new PropertyPath("(UIElement.Projection).(PlaneProjection.RotationX)");
PropertyPath pY = new PropertyPath("(UIElement.Projection).(PlaneProjection.RotationY)");
Storyboard.SetTargetProperty(myDoubleAnimationX, pX);
Storyboard.SetTargetProperty(myDoubleAnimationY, pY);
myDoubleAnimationX.To = X;
myDoubleAnimationY.To = Y;
// Begin the animation.
sbX.Begin();
sbY.Begin();
}
private void MouseLeave(object sender, MouseEventArgs e)
{
HasMouse = false;
FrameworkElement fe3D = sender as FrameworkElement;
Storyboard sbX = new Storyboard();
Storyboard sbY = new Storyboard();
// Make the Storyboard a resource.
if (FE.Resources.Contains("SbAnimX"))
FE.Resources.Remove("SbAnimX");
FE.Resources.Add("SbAnimX", sbX);
if (FE.Resources.Contains("SbAnimY"))
FE.Resources.Remove("SbAnimY");
FE.Resources.Add("SbAnimY", sbY);
// Create a duration of 2 seconds.
Duration duration = new Duration(TimeSpan.FromSeconds(0.6));
DoubleAnimation myDoubleAnimationX = new DoubleAnimation();
sbX.Children.Add(myDoubleAnimationX);
DoubleAnimation myDoubleAnimationY = new DoubleAnimation();
sbY.Children.Add(myDoubleAnimationY);
myDoubleAnimationX.Duration = duration;
sbX.Duration = duration;
myDoubleAnimationY.Duration = duration;
sbY.Duration = duration;
Storyboard.SetTarget(myDoubleAnimationX, fe3D);
Storyboard.SetTarget(myDoubleAnimationY, fe3D);
// Set the attached properties of Canvas.Left and Canvas.Top
// to be the target properties of the two respective DoubleAnimations
PropertyPath pX = new PropertyPath("(UIElement.Projection).(PlaneProjection.RotationX)");
PropertyPath pY = new PropertyPath("(UIElement.Projection).(PlaneProjection.RotationY)");
Storyboard.SetTargetProperty(myDoubleAnimationX, pX);
Storyboard.SetTargetProperty(myDoubleAnimationY, pY);
myDoubleAnimationX.To = 0;
myDoubleAnimationY.To = 0;
// Begin the animation.
sbX.Begin();
sbY.Begin();
}
}
}
// AnimationHelper.cs
namespace Portal.Xaml
{
public class AnimationHelper
{
public DoubleAnimationUsingKeyFrames keyFrameDoubleAnimation(int keyTimeBeginMilliseconds,
double keyTimeEndMilliseconds, double keyFrameValue, System.Windows.DependencyObject
target,
string PropertyName)
{
DoubleAnimationUsingKeyFrames myDoubleAnimationKeyFrames = new DoubleAnimationUsingKeyFrames();
//Create the Keyframe Object, setting the time and value
EasingDoubleKeyFrame myEasingDoubleKeyFrame = new EasingDoubleKeyFrame();
myEasingDoubleKeyFrame.KeyTime = TimeSpan.FromMilliseconds(keyTimeEndMilliseconds);
myEasingDoubleKeyFrame.Value = keyFrameValue;
//Add the Easing Keyframe to the KeyframeCollection
myDoubleAnimationKeyFrames.KeyFrames.Add(myEasingDoubleKeyFrame);
myDoubleAnimationKeyFrames.BeginTime = new TimeSpan(0, 0, 0, 0, keyTimeBeginMilliseconds);
//Set the Target Element of the DoubleKeyFrame animation
Storyboard.SetTarget(myDoubleAnimationKeyFrames, target);
//Set the TargetProperty of the DoubleKeyFrame animation
Storyboard.SetTargetProperty(myDoubleAnimationKeyFrames, new System.Windows.PropertyPath(PropertyName));
return myDoubleAnimationKeyFrames;
}
}
}
// Egg.cs
namespace EggHeadCafe.Silverlight.controls
{
public partial class Egg : UserControl
{
public event EventHandler<DataEventArgs<Topic>> TopicSelected;
public Boolean IsOpen = false;
public Boolean IsLeaf = false;
public string ItemType = "TopicArea";
public string Description = string.Empty;
public int EggID = 0;
string EggHeader = "";
public Egg()
{
InitializeComponent();
SetEvents();
}
public Egg(string TopicName, string TopicID, string itemType, Boolean isLeaf)
{
InitializeComponent();
string TopicIconSource = "http://www.eggheadcafe.com/graphics/TopicIcons/" + itemType ;
string ImageSource = TopicIconSource + TopicID + ".png";
imageEggLogo.Source = new BitmapImage(new Uri(ImageSource, UriKind.Absolute));
imageEggLogo.Width = 48;
imageEggLogo.Height = 48;
imageEggLogo.Cursor = Cursors.Hand;
imageEggLogo.Visibility = Visibility.Visible;
EggID = System.Convert.ToInt32(TopicID);
Description = TopicName;
IsLeaf = isLeaf;
SetEvents();
EggHeader = TopicName;
ItemType = itemType;
((TextBlock)borderView.Resources["textBlockTopicName"]).Text = TopicName;
((TextBlock)borderView.Resources["textBlockTopicID"]).Text = TopicID;
if ((ItemType == "TopicArea") || (IsLeaf == false))
{
this.Cursor = Cursors.Hand;
}
else
{
this.Cursor = Cursors.Hand;
}
}
public void SetEvents()
{
this.MouseEnter += new MouseEventHandler(Egg_MouseEnter);
this.MouseLeave += new MouseEventHandler(Egg_MouseLeave);
this.MouseLeftButtonUp += new MouseButtonEventHandler(Egg_MouseLeftButtonUp);
this.imageEggLogo.MouseEnter += new MouseEventHandler(Egg_MouseEnter);
}
void Egg_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
string CurrentState = SilverlightHelper.GetAppSetting("LastState");
if (IsOpen == true)
{
IsOpen = false;
AnimateEggClose.Begin();
SilverlightHelper.SetAppSetting("LastState", "Close");
}
else
{
IsOpen = true;
AnimateEggOpen.Begin();
SilverlightHelper.SetAppSetting("LastState", "Open");
}
if (ItemType != "Topic") return;
if (TopicSelected == null) return;
var topic = new Topic();
topic.TopicID = EggID;
topic.Description = Description;
TopicSelected(null, new DataEventArgs<Topic>(topic));
}
void Egg_MouseLeave(object sender, MouseEventArgs e)
{
animationViewHide.Begin();
}
void Egg_MouseEnter(object sender, MouseEventArgs e)
{
animationViewShow.Begin();
}
}
}