Thursday, December 07, 2006

WPF Composite Transformations - Order Matters

Today, I got it in my head that I wanted to create some code that could break apart an image and then reconstruct it using animation. I wanted the image to simply “tear” apart, piece by piece, one piece at a time. Using WPF I was able to make this happen.

 

The code to do this is actually very straight forward. In XAML, I simply created a Grid object and gave it a few column and row definitions. The columns and rows will determine how my image gets “sliced”. In the code-behind, I simply grab a source image (defined as a resource in XAML), iterate the rows and columns of the Grid, and place a canvas in each cell using an ImageBrush to paint the canvas background. Using the ImageBrush Viewbox property, you can set the correct area of the image to place into each canvas, essentially recreating the entire image piece by piece.

 

With an image sliced, the final step was simply to create a TranslateTransform and a RotateTransform, and then animate the individual slices. I wanted the image slices to “drop” one at a time, so, in order to stagger the animation for each image slice, I simply set the BeginTime property for the animation to a value which takes into account which slice we’re animating. 

 

Of course, nearing completion, I did stumble on one "gotcha". My transformations just weren't working properly. What I’m doing is translating (moving) each image slice down along the y-axis. At the same time I’m applying a rotation to simply give a nice "falling down" effect. Each transformation is added to a TransformGroup object, and applied to the image slice (a.k.a. the canvas.RenderTransform property). Long story short, it just wasn’t working correctly, and after much frustration, I stumbled on the fact that if I added my RotateTransform to the TransformGroup prior to adding the TranslateTransform, all worked well. Back to the MSDN I went, and found my answer - order matters when creating composite transformations.

 

You can read about it here:

 

http://msdn2.microsoft.com/en-us/library/system.windows.media.transformgroup.aspx

 

The results (click image for larger view):

 

The code to recreate the solution is below. If you swap the order of the RotateTransform and TranslateTransform, you'll see the behavior changes quite a bit.

 

Window1.xaml

 

<Window x:Class="PictureTearDown.Window1"

  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

  Title="Image Effect"

  Width="725" Height="440"

  >

 

  <Window.Resources>

    <Image x:Key="TheImage" Source="Images\Crystal.jpg"/>

  </Window.Resources>

 

  <StackPanel ClipToBounds="True"

    Margin="10" Orientation="Vertical">

 

    <Border CornerRadius="5" Width="{Binding

      ElementName=TheImage,   

      Path=Width}" Height="{Binding ElementName=TheImage,

      Path=Height}"

      BorderBrush="Black" BorderThickness="0.5"  

      Background="Transparent" ClipToBounds="True">

 

      <Grid Name="ImageGrid"

        Width="{Binding ElementName=TheImage, Path=Width}"

        Height="{Binding ElementName=TheImage, Path=Height}"

        Margin="10" HorizontalAlignment="Center"

        VerticalAlignment="Center">

 

        <Grid.ColumnDefinitions>

          <ColumnDefinition Width="*"/>

          <ColumnDefinition Width="*"/>

          <ColumnDefinition Width="*"/>

          <ColumnDefinition Width="*"/>

          <ColumnDefinition Width="*"/>

          <ColumnDefinition Width="*"/>

        </Grid.ColumnDefinitions>

 

        <Grid.RowDefinitions>

          <RowDefinition Height="*"/>

          <RowDefinition Height="*"/>

          <RowDefinition Height="*"/>

          <RowDefinition Height="*"/>

          <RowDefinition Height="*"/>

          <RowDefinition Height="*"/>

          <RowDefinition Height="*"/>

          <RowDefinition Height="*"/>

        </Grid.RowDefinitions>

 

      </Grid>

 

    </Border>

 

    <Button Width="100" Click="Animate">Animate!</Button>

 

  </StackPanel>

 

</Window>

 

Window1.xaml.cs

 

using System;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Media;

using System.Windows.Media.Animation;

 

namespace PictureTearDown

{

 

  public partial class Window1 : System.Windows.Window

  {

 

    public Window1()

    {

      InitializeComponent();

      SliceAndDice();

    }

 

    private void SliceAndDice()

    {

      Image image = (Image)this.Resources["TheImage"];

 

      int rows = this.ImageGrid.RowDefinitions.Count;

      int cols = this.ImageGrid.ColumnDefinitions.Count;

 

      // --- slice image

      double cellH = image.Source.Height / rows;

      double cellW = image.Source.Width / cols;

 

      for (int row = 0; row < rows; row++)

      {

        for (int column = 0; column < cols; column++)

        {

          Canvas canvas = new Canvas();

 

          canvas.Height = cellH;

          canvas.Width = cellW;

 

          canvas.SetValue(Grid.ColumnProperty, column);

          canvas.SetValue(Grid.RowProperty, row);

 

          ImageBrush ib = new ImageBrush(image.Source);

 

          float sizeCols = 1.0f / cols;

          float sizeRows = 1.0f / rows;

          ib.Viewbox = new Rect(column * sizeCols, row * 

            sizeRows,

            sizeCols, sizeRows);

          ib.ViewboxUnits =

            BrushMappingMode.RelativeToBoundingBox;

 

          canvas.Background = ib;

          this.ImageGrid.Children.Add(canvas);

        }

      }

    }

 

    private void Animate(object sender, RoutedEventArgs e)

    {

      Image image = (Image)this.Resources["TheImage"];

 

      int item = 0;

      int zindex = 100;

      double animationSpeed = 1000;

 

      int rows = this.ImageGrid.RowDefinitions.Count;

      int cols = this.ImageGrid.ColumnDefinitions.Count;

 

      double cellH = image.Source.Height / rows;

      double cellW = image.Source.Width / cols;

 

      for (int row = 0; row < rows; row++)

      {

        for (int column = 0; column < cols; column++)

        {

          Canvas canvas = (Canvas)this.ImageGrid.Children

            [item];

          canvas.SetValue(Canvas.ZIndexProperty, zindex);

          canvas.ClipToBounds = false;

 

          // --- the order which transforms are added to a

          // --- transformgroup is important!

          TransformGroup group = new TransformGroup();

          RotateTransform rotate = new RotateTransform();

          TranslateTransform translate = new TranslateTransform

            ();

          group.Children.Add(rotate);

          group.Children.Add(translate);

          canvas.RenderTransform = group;

 

          int delay = 100 * item;

 

          // --- translate cell canvas

          DoubleAnimation translateAnimation = new

            DoubleAnimation(image.Source.Height*2,

              TimeSpan.FromMilliseconds(animationSpeed));

          translateAnimation.BeginTime = new TimeSpan(0, 0, 0,

            0, delay);

          translateAnimation.AutoReverse = true;

          translate.BeginAnimation

            (TranslateTransform.YProperty,

            translateAnimation);

 

          // --- rotate cell.canvas.background.imagebrush

          rotate.CenterX = cellH / 2;

          rotate.CenterY = cellW / 2;

          DoubleAnimation rotAnimation = new DoubleAnimation

            (720,

            TimeSpan.FromMilliseconds(1000));

          rotAnimation.BeginTime = new TimeSpan(0, 0, 0, 0,

            delay);

          rotAnimation.AutoReverse = true;

          rotate.BeginAnimation(RotateTransform.AngleProperty,

            rotAnimation);

 

          item++;

          zindex--;

        }

      }

    }

 

  }

}

 

 

 





Technorati Tags:
, , , ,

No comments: