Silverlight 3D Animated Topic Selector With Titled Menu Items

The infamous David Silverlight was contracted by us awhile back to write our 3D Silverlight Topic Menu that features tilted animated eggs cracking open and closing. We've decided to make the source code available to the public.

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();
        }

   }
}


By Robbe Morris   Popularity  (3080 Views)
Picture
Biography - Robbe Morris
Robbe has been a Microsoft MVP in C# since 2004. He is also the co-founder of NullSkull.com which provides .NET articles, book reviews, software reviews, and software download and purchase advice.  Robbe also loves to scuba dive and go deep sea fishing in the Florida Keys or off the coast of Daytona Beach. Microsoft MVP