Fun with Silverlight: Building a photo mosaic

Most of the things I talk about on this weblog are serious posts on serious problems and solutions. However that is not the only thing I like to do with Software. From time to time I come across whaky ideas that aren’t necessarily useful.
The photo mosaic is one of those things. In this article I’m going to show you what it is and how you can build one yourself. Ooh and if you don’t want to build one, you can also download the code right at the bottom of the article.
Control demo
Unfortunately this website doesn’t really work great for showing demo code, so you will have to look at some screenshots.
The idea is that the application will take any kind of picture and divide it into a preconfigured number of horizontal and vertical tiles. The control will animate the tiles when you change the picture that is displayed into another picture.
Basic structure of the mosaic control
The control is made out of a couple of components. First there’s a grid that provides a basic layout for the tiles. Inside this grid the tiles are placed that display the photo. Each tile in the grid contains its own piece of the puzzle. The outer grid is there to prevent the mosaic layout from getting deformed by bad layout of the app that the control is hosted on.
The following picture shows the schematics of the control.
Building the grid
Because you can change the number of tiles and even the size of the tiles being displayed there’s really no point in making a static control template for the photo mosaic. So instead of doing that, I just created a template with an empty grid on it. When the control is initialized I will fire up a method that will generate a grid for me.
The following snippet demonstrates this:
1: /// <summary>
2: /// Rebuilds the control layout
3: /// </summary>
4: private void RebuildControlLayout()
5: {
6: if (_tileCanvas == null)
7: return;
8:
9: // Clear the grid contents
10: _tileCanvas.Children.Clear();
11:
12: BuildTileLayout();
13: BuildGridLines();
14: BuildImageTiles();
15: }
16:
17: /// <summary>
18: /// Applies a new tile layout to the control.
19: /// </summary>
20: private void BuildTileLayout()
21: {
22: _tileCanvas.ColumnDefinitions.Clear();
23: _tileCanvas.RowDefinitions.Clear();
24:
25: // Generate the columns for the grid
26: // Add one extra to the left and right for the gradient line effect.
27: for (int i = 0; i < ((HorizontalTiles + 2) * 2) - 1; i++)
28: {
29: ColumnDefinition column = null;
30:
31: if (i % 2 != 0)
32: {
33: column = new ColumnDefinition
34: {
35: Width = GridLength.Auto
36: };
37: }
38: else
39: {
40: column = new ColumnDefinition
41: {
42: Width = new GridLength(TileSize.Width)
43: };
44: }
45:
46: _tileCanvas.ColumnDefinitions.Add(column);
47: }
48:
49: // Generate the rows for the grid
50: // Add one extra to the top and bottom for the gradient line effect.
51: for (int i = 0; i < ((VerticalTiles + 2) * 2) - 1; i++)
52: {
53: RowDefinition row = null;
54:
55: if (i % 2 != 0)
56: {
57: row = new RowDefinition
58: {
59: Height = GridLength.Auto
60: };
61: }
62: else
63: {
64: row = new RowDefinition
65: {
66: Height = new GridLength(TileSize.Height)
67: };
68: }
69:
70: _tileCanvas.RowDefinitions.Add(row);
71: }
72: }
The first method shown in the snippet is the OnApplyTemplate method. This method is called by the runtime when the controltemplate is applied to the control. By invoking this method, the runtime allows you to link the layout of the control to the behavior of the control.
The second method shown generates a grid that contains (Rows + 2) * 2 – 1 RowDefinition objects to accomodate for the fact that there are lines between the cells. The same is done for the columns in the grid.
Each time you select a different number of tiles or change the size of the tiles, the grid will get updated with the new layout.
Reading between the tiles
No grid is complete without the gridlines. The photo mosaic sample has a pretty neat set of lines with the lines fading towards the ends. I created those by adding one tile to the end of each row and column. The brush applied to each line is made using the following settings in Blend:
For those less inclined to just poke around in blend, but instead like to work with hard figures here’s the settings:
- Gradientstop 1: Transparent (Offset = 0%)
- Gradientstop 2: White (Offset = 6%)
- Gradientstop 3: White (Offset = 94%)
- Gradientstop 4: White (Offset = 100%)
I used the same settings for horizontal and vertical lines, but with a different start and endpoint for the brush.
This makes the line fade inside the extra tile at the end of each row and column. Not a very difficult trick, but it takes a bit of fiddling in Blend to get it right.
The lines themselves are created using the following snippet of code:
1: /// <summary>
2: /// Creates the gridlines
3: /// </summary>
4: private void BuildGridLines()
5: {
6: int rowSpan = ((VerticalTiles + 2) * 2) - 1;
7: int columnSpan = ((HorizontalTiles + 2) * 2) - 1;
8:
9: for (int x = 1; x < ((HorizontalTiles + 2) * 2) - 1; x += 2)
10: {
11: Path path = new Path
12: {
13: Data = new LineGeometry
14: {
15: StartPoint = new Point(0, 0),
16: EndPoint = new Point(0, 1),
17: },
18: Stretch = Stretch.Fill,
19: Stroke = HorizontalLineBrush
20: };
21:
22: // Provide the correct placement on the grid
23: Grid.SetColumn(path, x);
24: Grid.SetRowSpan(path, rowSpan);
25: Grid.SetRow(path, 0);
26:
27: // Add the path to the grid
28: _tileCanvas.Children.Add(path);
29: }
30:
31: for (int x = 1; x < ((VerticalTiles + 2) * 2) - 1; x += 2)
32: {
33: Path path = new Path
34: {
35: Data = new LineGeometry
36: {
37: StartPoint = new Point(0, 0),
38: EndPoint = new Point(1, 0)
39: },
40: Stretch = Stretch.Fill,
41: Stroke = VerticalLineBrush
42: };
43:
44: // Provide the correct placement on the grid
45: Grid.SetRow(path, x);
46: Grid.SetColumnSpan(path, columnSpan);
47: Grid.SetColumn(path, 0);
48:
49: // Add the path to the grid
50: _tileCanvas.Children.Add(path);
51: }
52: }
There’s a ColumnSpan (to create a horizontal line) or RowSpan (to create a vertical line) value applied to each linetTo make the line go all the way horizontally or vertically.
Note: Because the lines are generated dynamically there isn’t any templatebinding applied. This can be somewhat annoying, because the line brushes aren’t updated when you change them in Visual Studio or in Expression Blend. I’ve added a bit of extra code to the dependency properties for the brushes to fix this issue.
1: /// <summary>
2: /// Dependency property registration for VerticalLineBrush
3: /// </summary>
4: /// <param name="oldValue">The old value for the property</param>
5: /// <param name="newValue">The new value for the property</param>
6: protected virtual void OnVerticalLineBrushChanged(Brush oldValue, Brush newValue)
7: {
8: if (_tileCanvas == null)
9: return;
10:
11: foreach (var line in _tileCanvas.Children.OfType<Line>())
12: {
13: // Skip the lines that have a column span larger than one.
14: // These are horizontal lines
15: if (Grid.GetColumnSpan(line) == 1)
16: {
17: line.Stroke = newValue;
18: }
19: }
20: }
21:
22: /// <summary>
23: /// Dependency property registration for HorizontalLineBrush
24: /// </summary>
25: /// <param name="oldValue">The old value for the property</param>
26: /// <param name="newValue">The new value for the property</param>
27: protected virtual void OnHorizontalLineBrushChanged(Brush oldValue, Brush newValue)
28: {
29: if (_tileCanvas == null)
30: return;
31:
32: foreach (var line in _tileCanvas.Children.OfType<Line>())
33: {
34: // Skip the lines that have a column span of one
35: // These are vertical lines
36: if (Grid.GetColumnSpan(line) > 1)
37: {
38: line.Stroke = newValue;
39: }
40: }
41: }
Adding the tiles to the grid
The next step in creating the mosaic, is to create the contents of the mosaic itself. This is done by generating tiles for each column and row.
1: /// <summary>
2: /// Builds the image tiles
3: /// </summary>
4: private void BuildImageTiles()
5: {
6: int row = 2;
7: int column = 2;
8:
9: // Leave one tile empty at the outside of the grid around the image.
10: for (int y = 0; y < VerticalTiles; y++)
11: {
12: column = 2;
13:
14: for (int x = 0; x < HorizontalTiles; x++)
15: {
16: Rectangle tile = new Rectangle
17: {
18: Width = TileSize.Width,
19: Height = TileSize.Height,
20: Fill = CreateTileBrush(y, x),
21: RenderTransformOrigin = new Point(0.5, 0.5)
22: };
23:
24: // Add a dummy transform that will be used by the animation later on
25: TransformGroup tileTransformGroup = new TransformGroup();
26: TranslateTransform tileTranslateTransform = new TranslateTransform();
27:
28: tileTransformGroup.Children.Add(tileTranslateTransform);
29: tile.RenderTransform = tileTransformGroup;
30:
31: Grid.SetRow(tile, row);
32: Grid.SetColumn(tile, column);
33:
34: _tileCanvas.Children.Add(tile);
35: _tiles.Add(tile);
36:
37: column += 2;
38: }
39:
40: row += 2;
41: }
42: }
For the image to be tiled correctly I’m creating a brush with a relative transform applied to it. This causes the image to be moved over the tile by the specified translation coordinates. The translation coordinates range from 0.0 in the top-left corner to 1.1 in the bottom-right corner of each tile. By specifying a number bigger than one, I’m basically saying the image should be moved a specified number of tiles.
The following snippet demonstrates the creation of the tile brush:
1: /// <summary>
2: /// Creates a new tile brush based on the location of the tile
3: /// </summary>
4: /// <param name="row"></param>
5: /// <param name="column"></param>
6: /// <returns></returns>
7: private Brush CreateTileBrush(int row, int column)
8: {
9: // Return a transparent brush if the image source is empty
10: if (_actualImageSource == null)
11: {
12: return new SolidColorBrush(Colors.Transparent);
13: }
14:
15: // Apply a translate transform to get the right piece of the
16: // image onto the tile
17: TranslateTransform tileTransform = new TranslateTransform
18: {
19: X = -column,
20: Y = -row
21: };
22:
23: // Create the new tile brush
24: ImageBrush tileBrush = new ImageBrush
25: {
26: AlignmentX = AlignmentX.Left,
27: AlignmentY = AlignmentY.Top,
28: ImageSource = _actualImageSource,
29: RelativeTransform = tileTransform,
30: Stretch = Stretch.None
31: };
32:
33: return tileBrush;
34: }
Animating the tiles
The tiles in the mosaic are animated using two storyboards. One to move the tiles away from the grid and one to move them back in with the new image. Again because the grid and tiles are generated dynamically I had to create the animations in code to.
To create the animation for moving tiles away from the grid is created using the following two methods:
1: /// <summary>
2: /// Creates a storyboard that moves the tiles away from the grid
3: /// </summary>
4: /// <returns></returns>
5: private Storyboard CreateHideTilesAnimation()
6: {
7: Storyboard board = new Storyboard();
8: IEasingFunction easingFunction = new CubicEase
9: {
10: EasingMode = EasingMode.EaseOut
11: };
12:
13: // Animate all the tiles at once
14: foreach (var tile in _tileCanvas.Children.OfType<Rectangle>())
15: {
16: double targetDirection = Math.Round(_randomizer.NextDouble());
17: double targetOffsetX = 0.0;
18: double targetOffsetY = 0.0;
19:
20: // Choose a random direction
21: if (targetDirection == 0)
22: {
23: targetOffsetX = (_randomizer.Next(3) - 1) * (TileSize.Width * 4);
24: }
25: else
26: {
27: targetOffsetY = (_randomizer.Next(3) - 1) * (TileSize.Height * 4);
28: }
29:
30: // Add all the generated items to the story board
31: foreach (var animation in BuildTileAnimations(tile, targetOffsetX, targetOffsetY, 0.0, easingFunction))
32: {
33: board.Children.Add(animation);
34: }
35: }
36:
37: return board;
38: }
39:
40: /// <summary>
41: /// Builds a set of tile animations for the specified tile
42: /// </summary>
43: /// <param name="tile">Tile to generate animations for</param>
44: /// <param name="horizontalTranslation">Horizontal translation</param>
45: /// <param name="verticalTranslation">Vertical translation</param>
46: /// <param name="targetOpacity">Target opacity</param>
47: /// <returns>Returns the generated animations</returns>
48: private IEnumerable<Timeline> BuildTileAnimations(Rectangle tile,
49: double horizontalTranslation, double verticalTranslation, double targetOpacity, IEasingFunction easingFunction)
50: {
51: Storyboard board = new Storyboard();
52: TimeSpan animationDuration = TimeSpan.FromMilliseconds(400);
53:
54: DoubleAnimation horizontalAnimation = new DoubleAnimation
55: {
56: To = horizontalTranslation,
57: Duration = animationDuration,
58: EasingFunction = easingFunction
59: };
60:
61: DoubleAnimation verticalAnimation = new DoubleAnimation
62: {
63: To = verticalTranslation,
64: Duration = animationDuration,
65: EasingFunction = easingFunction
66: };
67:
68: DoubleAnimation opacityAnimation = new DoubleAnimation
69: {
70: Duration = animationDuration,
71: To = targetOpacity,
72: EasingFunction = easingFunction
73: };
74:
75: Storyboard.SetTargetProperty(horizontalAnimation, new PropertyPath(
76: "(UIElement.RenderTransform).(TransformGroup.Children)[0].(TranslateTransform.X)"));
77:
78: Storyboard.SetTargetProperty(verticalAnimation, new PropertyPath(
79: "(UIElement.RenderTransform).(TransformGroup.Children)[0].(TranslateTransform.Y)"));
80:
81: Storyboard.SetTargetProperty(opacityAnimation, new PropertyPath(Rectangle.OpacityProperty));
82:
83: Storyboard.SetTarget(horizontalAnimation, tile);
84: Storyboard.SetTarget(verticalAnimation, tile);
85: Storyboard.SetTarget(opacityAnimation, tile);
86:
87: // Return the animations to the caller
88: yield return horizontalAnimation;
89: yield return verticalAnimation;
90: yield return opacityAnimation;
91: }
The first method creates the storyboard by adding animations for each tile to it. The tile animations are created using an iterator that returns a couple of animation timeline. One to move the tile horizontally, one to move the tile vertically and one to hide the tile.
To make the animation a bit more fun I added an easing function to it. The cubic ease makes the animation appear to go faster towards the end.
Once the hide animation is completed, the new image is applied and the show animation is started. To create this animation I created a new method that move the tiles into their original position.
1: /// <summary>
2: /// Creates a storyboard that moves the tiles back into the grid
3: /// </summary>
4: /// <param name="tile"></param>
5: private Storyboard CreateShowTilesAnimation()
6: {
7: // Create a new tile animation the moves the tile back into the grid
8: Storyboard board = new Storyboard();
9: IEasingFunction easingFunction = new CubicEase
10: {
11: EasingMode = EasingMode.EaseIn
12: };
13:
14: // Animate all the tiles at once
15: foreach (var tile in _tileCanvas.Children.OfType<Rectangle>())
16: {
17: // Add all the generated items to the story board
18: foreach (var animation in BuildTileAnimations(tile, 0.0, 0.0, 1.0, easingFunction))
19: {
20: board.Children.Add(animation);
21: }
22: }
23:
24: return board;
25: }
Exactly the same animate tiles method is used here, but instead of providing a positive or negative translation value I choose a neutral translation here.
The transforms that are animated were added before hand when generating the tiles. Animating the render transforms has two advantages. The first one is that you don’t need to change the control layout for the animations to work, which means less code to write. The second advantage is that the animation can go on beyond the border of the control itself. Which makes for a prettier effect.
Next steps
The demo is pretty basic, but you can of extend it by connecting to Flickr, Bing or Google to bring in more photos. Adding new photos is easy because I used an ImageSource which can be anything from a resource to an image downloaded from a webserver.
Don’t have Silverlight or looking for an alternative? Checkout Marco’s weblog: http://www.marcofolio.net/webdesign/jfancytile_a_jquery_tile_shifting_image_viewer_plugin.html for an alternative implementation using jQuery.
Have fun!