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.