Share via


Inspired By Flash Math Creativity #2: WPF Planets

My next inspiration from the algorithms introduced in Flash Math Creativity was based on the work of Jamie MacDonald. In looking at his work,  I quickly realized that I was going to need to brush up on my trigonometry skills. This led me to another great Friend of Ed book, Foundation ActionScript Animation: Making Things Move, which has some great trigonometry for animation explanations. 

You can see the results of my latest experiment here and the code is here. Just to make sure I was up an running with basic trig, I created a sine wave in 01 as follows:

double x = 0;
double y = 0;
double angle = 0;
for (int i = 0; i < 500; i++)
{
    x += 1;
    y = 200 + Math.Sin(angle) * 50;
    Ellipse el = new Ellipse();
    el.Width = 2;
    el.Height = 2;
    el.Fill = Brushes.Red;
    Canvas.SetLeft(el, x);
    Canvas.SetTop(el, y);
    stage.Children.Add(el);
    angle += .05;  }

Nothing fancy here.  To make a circle, I changed the x to be x = Math.Cos(angle) * 50.  This is the basis for doing circle and ellipse animations.  Now, instead of trying to do animations using CompositionTarget.Rendering, I remembered that the SDK has a great sample called CustomAnimations, in which a CircleAnimation exists already using this formula.  Here's the key code where it overrides the GetCurrentValueCore method to create the circle effect using basic trig:

protected override double GetCurrentValueCore(double defaultOriginValue, double defaultDestinationValue, AnimationClock clock)
{
    double returnValue;
    double time = clock.CurrentProgress.Value;

    // math magic: calculate new coordinates using polar coordinate equations. This requires two
    // animations to be wired up in order to move in a circle, since we don't make any assumptions
    // about what we're animating (e.g. a TranslateTransform).
    if (Direction == DirectionEnum.XDirection)
    {
        returnValue = Math.Cos(2 * Math.PI * time);
    }
    else
    {
        returnValue = Math.Sin(2 * Math.PI * time);
    }

    // Need to add the defaultOriginValue so that composition works.
    return returnValue * Radius + defaultOriginValue;
}

At first, I used this in code to create the effect of these circles circling toward you using BeginAnimation, based on the TowardUs sample in the Flash Math Creativity book (the very first sample):

CircleAnimation cx = new CircleAnimation();
cx.Duration = new Duration(TimeSpan.FromSeconds(1.5));
cx.Direction = CircleAnimation.DirectionEnum.XDirection;
cx.Radius = 150;
cx.RepeatBehavior = RepeatBehavior.Forever;

CircleAnimation cy = new CircleAnimation();
cy.Duration = new Duration(TimeSpan.FromSeconds(1.5));
cy.Direction = CircleAnimation.DirectionEnum.YDirection;
cy.Radius = 150;
cy.RepeatBehavior = RepeatBehavior.Forever;

stage.Children.Add(el);
el.BeginAnimation(Ellipse.OpacityProperty, opacityAnimation);
st.BeginAnimation(ScaleTransform.ScaleXProperty, d);
st.BeginAnimation(ScaleTransform.ScaleYProperty, d);
tt.BeginAnimation(TranslateTransform.XProperty, cx);
tt.BeginAnimation(TranslateTransform.YProperty, cy);

This resulted in 03.  It worked, but the code started getting ugly as far as getting each circle to fire when I wanted it to.  I found myself using a timer and wiring up an anonymous delegate. I realized Storyboards and ParallelTimelines would serve me much better, which are much easier to wire up in XAML than in code.  So,  04 is the same thing, but a XAML solution instead:

<ParallelTimeline BeginTime="0:0:0">
  <DoubleAnimation  Duration="0:0:1.5" To="25" Storyboard.TargetName="ellipse0" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)"/>
  <DoubleAnimation Duration="0:0:1.5"  To="25" Storyboard.TargetName="ellipse0" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)"/>
  <DoubleAnimation Duration="0:0:1.5" From="1"  To="0" Storyboard.TargetName="ellipse0" Storyboard.TargetProperty="(UIElement.Opacity)"/>
  <CustomAnimations:CircleAnimation Duration="0:0:1.5" Radius="150" Direction="YDirection" Storyboard.TargetName="ellipse0" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.Y)"/>
  <CustomAnimations:CircleAnimation Duration="0:0:1.5" Radius="150" Direction="XDirection" Storyboard.TargetName="ellipse0" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.X)"/>
</ParallelTimeline>

By using parallel timelines I could structure my animations and when they fired.

A nice thing about this CustomAnimation is that it can be applied to 3D animations as well, applied to the TranslateTransform3D which I did 05, changing the Radius property to be much smaller to fit the coordinates I was using for my ViewPort. 

void towardus3d_Loaded(object sender, RoutedEventArgs e)
{
    sphere = new GeometryModel3D(sphereFactory.Mesh, materialGreen);
    ModelVisual3D mv3d = myViewPort3D.Children[1] as ModelVisual3D;
    mv3d.Content = sphere;

    CircleAnimation ca3d_y = new CircleAnimation();
    ca3d_y.Duration = TimeSpan.FromSeconds(5);
    ca3d_y.RepeatBehavior = RepeatBehavior.Forever;
    ca3d_y.Radius = .5;
    ca3d_y.Direction = CircleAnimation.DirectionEnum.YDirection;

    CircleAnimation ca3d_x = new CircleAnimation();
    ca3d_x.Duration = TimeSpan.FromSeconds(5);
    ca3d_x.RepeatBehavior = RepeatBehavior.Forever;
    ca3d_x.Radius = .5;
    ca3d_x.Direction = CircleAnimation.DirectionEnum.XDirection;

    ModelTranslate.BeginAnimation(TranslateTransform3D.OffsetYProperty, ca3d_y);
    ModelTranslate.BeginAnimation(TranslateTransform3D.OffsetXProperty, ca3d_x);
}

 

 

It then dawned on me, as I looked at 05, an orbiting sphere, that it would make for great planets.  I went out to NASA and found the texture maps for Jupiter and its moons.    I also wanted to do all of this 3D work in XAML, so I made a class called SphereModelVisual3D that I could instantiate Jupiter, for example, like this:

<mv3d:SphereModelVisual3D x:Name="Jupiter" >
  <mv3d:SphereModelVisual3D.Material>
    <DiffuseMaterial>
      <DiffuseMaterial.Brush >
        <ImageBrush ImageSource="https://maps.jpl.nasa.gov/pix/jup0vss1.jpg%22/>
      </DiffuseMaterial.Brush>
    </DiffuseMaterial>
  </mv3d:SphereModelVisual3D.Material>

      <mv3d:SphereModelVisual3D.Transform>
    <Transform3DGroup >
      <ScaleTransform3D ScaleX="2" ScaleY="2"  ScaleZ="2" />
      <RotateTransform3D>
        <RotateTransform3D.Rotation>
          <AxisAngleRotation3D Angle="0" Axis="0 1 0" x:Name="Jupiter_rotation">
          </AxisAngleRotation3D>
        </RotateTransform3D.Rotation>
      </RotateTransform3D>
      <TranslateTransform3D OffsetX="0" OffsetY="0" OffsetZ="0" />
    </Transform3DGroup>
  </mv3d:SphereModelVisual3D.Transform>
</mv3d:SphereModelVisual3D> 

The animation that handles the elliptical orbits needs to transform the Z property the TranslateTransform3D.  It then looks like this:

<ParallelTimeline RepeatBehavior="Forever">
  <DoubleAnimation Duration="0:0:10"  To="360"  Storyboard.TargetName="Jupiter_rotation" Storyboard.TargetProperty="Angle"/>
</ParallelTimeline>
<ParallelTimeline RepeatBehavior="Forever">
  <CustomAnimations:CircleAnimation Duration="0:0:2"  Radius="9" Direction="YDirection" Storyboard.TargetName="Io" Storyboard.TargetProperty="(ModelVisual3D.Transform).(Transform3DGroup.Children)[2].(TranslateTransform3D.OffsetZ)"/>
  <CustomAnimations:CircleAnimation Duration="0:0:2"  Radius="2" Direction="XDirection" Storyboard.TargetName="Io" Storyboard.TargetProperty="(ModelVisual3D.Transform).(Transform3DGroup.Children)[2].(TranslateTransform3D.OffsetX)"/>
  <DoubleAnimation Duration="0:0:2"  To="360"  Storyboard.TargetName="Io_rotation" Storyboard.TargetProperty="Angle"/>
</ParallelTimeline> 

The result is in 06. It is not astronomically correct as far as scale -- I'll leave that for someone with some more time on their hands, but I liked the end result.  I also threw in the TrackBall control from the 3d tools project so you can trackball the whole model.

The code is here.

Comments

  • Anonymous
    February 25, 2007
    The comment has been removed

  • Anonymous
    February 26, 2007
    The comment has been removed

  • Anonymous
    March 07, 2007
    I think the reference to the 3D dll is also incorrect. it was for me when I opened the solution.. also.. 1-5 work fine for me.. but i dont see anything in 6.. not sure what's wrong... maybe my video card can't handle it?

  • Anonymous
    March 07, 2007
    or maybe the internet hiccuped and the program was unable to retreive some image.. i'll continue to try to get it to work, but thanks for the demos.. 1-5 looks great... 6 looks cool from the screenshot... btw, do you have any cool links on this type of 3d animation with .net 3.0?  

  • Anonymous
    March 08, 2007
    Yeah, I don't embed the planet textures from NASA to keep the initial download size small, so when 6 loads, it has to go fetch them.  This would be a great time for me to use incremental downloads w/ clickonce -- maybe I'll update the app to do that.

  • Anonymous
    March 16, 2007
    The comment has been removed

  • Anonymous
    March 19, 2007
    You could easily do that circle positioning in WPF using the #2 example in the planets application I posted.  Or, use the RadialPanel that is in the SDK. (http://msdn2.microsoft.com/en-us/library/ms771363.aspx) The math to do a rectangle or triangle you get for free with the WPF layout engine using Grid or UniformGrid, depending on the effect you are trying to get.

  • Anonymous
    April 03, 2007
    One of the cool tools we had a chance to use while up at the WPF Bootcamp in Redmond was Expression Blend