Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF Jigsaw Puzzle

0.00/5 (No votes)
18 Oct 2010 13  
How to create a puzzle exploring some great features of Windows Presentation Foundation
Silverlight Menus

Table of Contents

ntroduction

Silverlight Menus

Almost every person in the world has played with jigsaw puzzles in some moment in life. They improve cognitive skills, stimulate memory, logical thinking, develop social skills, and so on. In other words, they make you smarter.

Since long ago, I have been thinking of creating such a puzzle game with WPF (and possibly Silverlight), and I'm glad that the time has come now, and finally WPF Jigsaw Puzzle is available for download.

ystem Requirements

To use WPF Jigsaw Puzzle provided with this article, if you already have Visual Studio 2010, that's enough to run the application. If you don't, you can download the following 100% free development tool directly from Microsoft:

reating The Pieces

There are infinite ways to create puzzle pieces, including square shaped, hexagon pieces, and carefully hand-made carved pieces. All of them would work perfectly. It could be much more easy to present here a puzzle made of simple square shaped pieces. But for me, the beauty of jigsaw puzzle pieces is the fact that not all pieces fit in a particular place, which doesn't occur with square pieces. That is, for a given gap in the puzzle, not only the image portion, but also the piece shape must match in that gap.

Although this jigsaw puzzle game could have been done with traditional Windows Forms, it would lack many of the fine features provided by WPF technology, which I present here, such as:

  • Ability to render smooth, vector-based shapes
  • Ability to zoom in and zoom out the puzzle without the undesired pixelated effect
  • Ability to work easily with animations, more precisely rotation animations
  • Ability to use drop shadow bitmap effect, which gives convincingly realistic 3D effect

The heart of the shape construction is the BezierCurve element. As all visual elements in WPF, it's a vectorized element, so you can magnify it as much as you want, and it will never give the ugly "pixelated" effect.

Each shape is made of 4 bezier curves, one for each side, and depending on a random piece generation process, each side can have 2 possible shapes:

  • A tab shape
  • A blank shape

All bezier segments are made of the following coordinates:

var curvyCoords = new double[]
{
      0, 0, 35, 15, 37, 5,
      37, 5, 40, 0, 38, -5,
      38, -5, 20, -20, 50, -20,
      50, -20, 80, -20, 62, -5,
      62, -5, 60, 0, 63, 5,
      63, 5, 65, 15, 100, 0
};

Although in this jigsaw puzzle the sides have curvy shapes, technically each shape is a square piece. But you can't see this because the square's background is transparent. The inner oddly-shaped piece is that which holds the image for the particular piece. And once the pieces are in place, you can't see the table's background through the transparent background because the sides of any particular piece "invade" the blank spaces of the neighboring pieces. This technique is called tesselation, and is employed in many areas, from arts (such as in mosaics) to structural engineering (such as wall buildings) and even in nature (such as honeycombs).

In WPF, this is possible thanks to Path element, which is made of a PathGeometry element, which in turn is made of 4 PolyBezierSegment elements (one for each side), which in turn are made of a collection of points for which the BezierCurves will be generated.

private static PathGeometry GetPathGeometry(int upperConnection,
	int rightConnection, int bottomConnection, int leftConnection,
	double[] blankCoords, double[] tabCoords)
{
    var upperPoints = new List<point>();
    var rightPoints = new List<point>();
    var bottomPoints = new List<point>();
    var leftPoints = new List<point>();

    for (var i = 0; i < (tabCoords.Length / 2); i++)
    {
        double[] upperCoords = (upperConnection ==
        	(int)ConnectionType.Blank) ? blankCoords : tabCoords;
        double[] rightCoords = (rightConnection ==
        	(int)ConnectionType.Blank) ? blankCoords : tabCoords;
        double[] bottomCoords = (bottomConnection ==
        	(int)ConnectionType.Blank) ? blankCoords : tabCoords;
        double[] leftCoords = (leftConnection ==
        	(int)ConnectionType.Blank) ? blankCoords : tabCoords;

        upperPoints.Add(new Point(upperCoords[i * 2], 0 +
        	upperCoords[i * 2 + 1] * upperConnection));
        rightPoints.Add(new Point(100 - rightCoords[i * 2 + 1] *
        	rightConnection, rightCoords[i * 2]));
        bottomPoints.Add(new Point(100 - bottomCoords[i * 2],
        	100 - bottomCoords[i * 2 + 1] * bottomConnection));
        leftPoints.Add(new Point(0 + leftCoords[i * 2 + 1] *
        	leftConnection, 100 - leftCoords[i * 2]));
    }

    var upperSegment = new PolyBezierSegment(upperPoints, true);
    var rightSegment = new PolyBezierSegment(rightPoints, true);
    var bottomSegment = new PolyBezierSegment(bottomPoints, true);
    var leftSegment = new PolyBezierSegment(leftPoints, true);

    var pathFigure = new PathFigure()
    {
        IsClosed = false,
        StartPoint = new Point(0, 0)
    };
    pathFigure.Segments.Add(upperSegment);
    pathFigure.Segments.Add(rightSegment);
    pathFigure.Segments.Add(bottomSegment);
    pathFigure.Segments.Add(leftSegment);

    var pathGeometry = new PathGeometry();
    pathGeometry.Figures.Add(pathFigure);
    return pathGeometry;
}

The shape construction starts from the first row, first column position, then passes to the first row, second column position, that is, from left to the right and top to down direction.

For a correct shape construction, some rules must be taken into consideration:

  • Each piece in the top row must have a flat top side
  • Each piece in the leftmost column must have a flat left side
  • Each piece in the rightmost column must have a flat right side
  • Each piece in the bottom row must have a flat bottom side
  • Each left side shape in any particular piece must match the right side shape for the previous piece in the same row
  • Each top side shape in any particular piece must match the bottom side shape for the previous piece in the same column

This is all there is for shape construction. The next step is how to cut out that pieces from the original image.

utting Out the Image

With such oddly shaped pieces, we must be careful while cutting out the image, otherwise the pieces wouldn't give us the original image when all pieces are in place.

But the technique here is not that complex. First, we must take into consideration that each piece will "invade" part of the areas assigned to the neighboring pieces. This is so because we have tabs and blanks connecting each other. Obviously we wouldn't need to worry about this if we used simple square pieces. But since it's a jigsaw puzzle, we must pay attention to these details.

All pieces measure 140 x 140 pixels, but each shape has a inner square (not visible, though) measuring 100 x 100 pixels. This inner area is not "invaded" by neighboring pieces. This leaves 20 pixels at each of the 4 sides, just to be filled by the connection tabs and blanks.

The following snippet, found in the constructor of the Piece class, defines which portion of the original image will be placed as a filling for that particular piece.

path.Fill = new ImageBrush()
{
    ImageSource = imageSource,
    Stretch = Stretch.None,
    Viewport = new Rect(-20, -20, 140, 140),
    ViewportUnits = BrushMappingMode.Absolute,
    Viewbox = new Rect(
        x * 100 - 10,
        y * 100 - 10,
        120,
        120
        ),
    ViewboxUnits = BrushMappingMode.Absolute,
    Transform = imageScaleTransform
};

crambling The Pieces

Here we have a horizontal stack panel just to hold the pieces that will picked up. We have to add the pieces to that stack panel in a random manner, and that is an easy task: all we have to do is to choose a random index among the valid indexes of the piece list.

Next, we rotate each piece in 4 different random angles: 0, 90, 180 and 270 degrees.

You can see how we randomly populate the pick up panel in the snippet below:

foreach (var p in pieces)
{
    Random random = new Random();
    int i = random.Next(0, pnlPickUp.Children.Count);

    p.ScaleTransform.ScaleX = 1.0;
    p.ScaleTransform.ScaleY = 1.0;
    p.RenderTransform = tt;
    p.X = -1;
    p.Y = -1;

    pnlPickUp.Children.Insert(i, p);

    int angle = angles[rnd.Next(0, 4)];
    p.Rotate(angle);
    shadowPieces[p.Index].Rotate(angle);
}

Once we have done that, the scrambling is done, as shown in the figure below:

ositioning The Pieces

The game user has 20 x 20 cells to place the pieces. The user clicks the mouser over the pick-up panel, then goes to the game board and places that piece in one of the cells.

The game board measures 2000 x 2000 pixels. If you want to play with images greater than that, please copy and resize them to lower sizes. Or you can do it yourself by changing the code to allow that.

Once the mouse is moving over the game board, it appears zoomed in 10% of the original size, and this is made just to make the piece appear as if it is floating over the board.

Another helpful effect is the DropShadowBitmapEffect, which creates a realistic shadow cast by the piece's shape over the underlying board.

Here is how we create the shadow effect:

shadowEffect = new DropShadowBitmapEffect()
{
    Color = Colors.Black,
    Direction = 320,
    ShadowDepth = 25,
    Softness = 1,
    Opacity = 0.5
};
.
.
.
chosenPiece.BitmapEffect = shadowEffect;

One thing you should know is that the DropShadowBitmapEffect has a heavy performance cost, so we use DropShadowBitmapEffect carefully so to avoid performance issues (only one piece at a time, use it in this game).

If you are moving a selected piece around the board and click with left mouse button, the game will try to place the piece in that position. If the piece fits in there, the shadow effect is destroyed and the piece is correctly placed. The next section explains how to match the connectors.

electing a Group Of Pieces: The Lasso Tool

This is a feature asked by some readers, and fortunately it's been implemented in the current revision. The ability to move a group of pieces makes a lot of sense in a jigsaw puzzle. It saves a lot of time and effort. This has been achieved thanks to the new Lasso tool for rectangular selection.

private void MouseUp()
{
    if (currentSelection.Count == 0)
    {
        double x1 = (double)rectSelection.GetValue(Canvas.LeftProperty) - 20;
        double y1 = (double)rectSelection.GetValue(Canvas.TopProperty) - 20;
        double x2 = x1 + rectSelection.Width;
        double y2 = y1 + rectSelection.Height;

        int cellX1 = (int)(x1 / 100);
        int cellY1 = (int)(y1 / 100);
        int cellX2 = (int)(x2 / 100);
        int cellY2 = (int)(y2 / 100);

        var query = from p in pieces
                    where
                    (p.X >= cellX1) && (p.X <= cellX2) &&
                    (p.Y >= cellY1) && (p.Y <= cellY2)
                    select p;

        //all pieces within that area will be selected
        foreach (var currentPiece in query)
        {
            currentSelection.Add(currentPiece);

            currentPiece.SetValue(Canvas.ZIndexProperty, 5000);
            shadowPieces[currentPiece.Index].SetValue(Canvas.ZIndexProperty, 4999);
            currentPiece.BitmapEffect = shadowEffect;

            currentPiece.RenderTransform = stZoomed;
            currentPiece.IsSelected = true;
            shadowPieces[currentPiece.Index].RenderTransform = stZoomed;
        }
        SetSelectionRectangle(-1, -1, -1, -1);
    }
    ...

Instead of moving pieces one at a time, now we can press the mouse button, select a rectangular area with the button pressed, and release the button. Automatically all pieces within that area will be selected.

After a group of pieces has been selected, you can move them around freely, trying to put them down in a place where they match.

private void MouseMoving()
{
    var newX = Mouse.GetPosition((IInputElement)cnvPuzzle).X - 20;
    var newY = Mouse.GetPosition((IInputElement)cnvPuzzle).Y - 20;

    int cellX = (int)((newX) / 100);
    int cellY = (int)((newY) / 100);

    if (Mouse.LeftButton == MouseButtonState.Pressed)
    {
        SetSelectionRectangle(initialRectangleX, initialRectangleY, newX, newY);
    }
    else
    {
        if (currentSelection.Count > 0)
        {
            var firstPiece = currentSelection[0];

            //This can move around more than one piece at the same time
            foreach (var currentPiece in currentSelection)
            {
                var relativeCellX = currentPiece.X - firstPiece.X;
                var relativeCellY = currentPiece.Y - firstPiece.Y;

                double rotatedCellX = relativeCellX;
                double rotatedCellY = relativeCellY;

                currentPiece.SetValue
		(Canvas.LeftProperty, newX - 50 + rotatedCellX * 100);
                currentPiece.SetValue(Canvas.TopProperty, newY - 50 + rotatedCellY * 100);

                shadowPieces[currentPiece.Index].SetValue
                	(Canvas.LeftProperty, newX - 50 + rotatedCellX * 100);
                shadowPieces[currentPiece.Index].SetValue
                	(Canvas.TopProperty, newY - 50 + rotatedCellY * 100);
            }
        }
    }
}

atching Connectors

Each of the 4 piece sides may have different shapes: tab, blank or flat. A flat side means that the side fits in one of the edges of the original picture. Two neighboring flat edges means that the piece is one of the corners of the puzzle (but obviously you already knew this...). The tab means that the side connects to a blank side in another piece. Simple as that.

Before placing the selected piece in a desired cell, the game must test if the piece fits in there. Some rules are used to do so:

  • The desired cell must be empty, that is, not taken yet by any other piece
  • If the cell at the top of the desired cell is taken by some piece, the edges of both pieces must match. That is, they must have either tab/blank or blank/tab edges. If they are tab/tab, blank/blank, or flat/(any type), the piece does not fit in there and the piece is not placed.
  • The above rule is applied also to the right, bottom and left sides in a similar manner.

The following function uses Linq to find out if the above rules are respected. Otherwise, the piece cannot be placed in the desired cell.

Notice that we have to test each of the currently selected pieces, considering the position in relation to the main piece in the selection group. If any of them is not compatible to the target place, the group will not be put down.

private bool TrySetCurrentPiecePosition(double newX, double newY)
{
    bool ret = true;

    double cellX = (int)((newX) / 100);
    double cellY = (int)((newY) / 100);

    var firstPiece = currentSelection[0];

    foreach (var currentPiece in currentSelection)
    {
        var relativeCellX = currentPiece.X - firstPiece.X;
        var relativeCellY = currentPiece.Y - firstPiece.Y;

        var q = from p in pieces
                where (
                        (p.Index != currentPiece.Index) &&
                        (!p.IsSelected) &&
                        (cellX + relativeCellX > 0) &&
                        (cellY + relativeCellY > 0) &&
                        (
                        ((p.X == cellX + relativeCellX) &&
                        (p.Y == cellY + relativeCellY))
                        || ((p.X == cellX + relativeCellX - 1) &
                        & (p.Y == cellY + relativeCellY) &&
                        (p.RightConnection + currentPiece.LeftConnection != 0))
                        || ((p.X == cellX + relativeCellX + 1) &
                        & (p.Y == cellY + relativeCellY) &&
                        (p.LeftConnection + currentPiece.RightConnection != 0))
                        || ((p.X == cellX + relativeCellX) &&
                        (p.Y == cellY - 1 + relativeCellY) &&
                        (p.BottomConnection + currentPiece.UpperConnection != 0))
                        || ((p.X == cellX + relativeCellX) &&
                        (p.Y == cellY + 1 + relativeCellY) &&
                        (p.UpperConnection + currentPiece.BottomConnection != 0))
                        )
                      )
                select p;

        if (q.Any())
        {
            ret = false;
            break;
        }
    }

    return ret;
}

otating Pieces

As said before, the pieces are initially rotated randomly in one of four possible angles (0, 90, 180, 270 degrees) in order to increase the game difficulty.

Rotating the pieces is simple and straightforward: just click the right mouse button and the selected piece will rotate 90 degrees clockwise. Click it again and the piece will rotate another 90 degrees. Keep rotating until you find the correct rotation.

As a side effect, this rotation feature requires the game to recalculate the side connectors for the new side. That is, once you rotate a piece 90 degrees clockwise, the connectors are rotated too: the top connector becomes the right connector. The right connector becomes the bottom connector. And so on...

As an example, this is how we recalculate the upper connector based on the angle of the piece:

public int UpperConnection
{
    get
    {
        var connection = 0;
        switch (angle)
        {
            case 0:
                connection = initialUpperConnection;
                break;
            case 90:
                connection = initialLeftConnection;
                break;
            case 180:
                connection = initialBottomConnection;
                break;
            case 270:
                connection = initialRightConnection;
                break;
        }
        return connection;
    }
}

otating a Group of Pieces

Rotating a group of pieces is not as trivial as rotating a single piece. We have not only to rotate the piece, but also discover where is the central point for the rotation. This allows us to rotate the entire group, as a single block.

public void Rotate(Piece axisPiece, double rotationAngle)
{
    var deltaCellX = this.X - axisPiece.X;
    var deltaCellY = this.Y - axisPiece.Y;

    double rotatedCellX = 0;
    double rotatedCellY = 0;

    int a = (int)rotationAngle;
    switch (a)
    {
        case 0:
            rotatedCellX = deltaCellX;
            rotatedCellY = deltaCellY;
            break;
        case 90:
            rotatedCellX = -deltaCellY;
            rotatedCellY = deltaCellX;
            break;
        case 180:
            rotatedCellX = -deltaCellX;
            rotatedCellY = -deltaCellY;
            break;
        case 270:
            rotatedCellX = deltaCellY;
            rotatedCellY = -deltaCellX;
            break;
    }

    this.X = axisPiece.X + rotatedCellX;
    this.Y = axisPiece.Y + rotatedCellY;

    var rt1 = (RotateTransform)tg1.Children[1];
    var rt2 = (RotateTransform)tg2.Children[1];

    angle += rotationAngle;

    if (angle == -90)
        angle = 270;

    if (angle == 360)
        angle = 0;

    rt1.Angle =
    rt2.Angle = angle;

    this.SetValue(Canvas.LeftProperty, this.X * 100);
    this.SetValue(Canvas.TopProperty, this.Y * 100);
}

esting for Puzzle Completion

Any time a piece is placed, the game application tests if the puzzle is completed. There are some rules that must be observed for this:

  • All pieces must have rotation of 0 degrees (compared to the original image)
  • All pieces must be connected horizontally (with no gaps between them)
  • All pieces must be connected vertically (with no gaps between them)

Again, we resort to Linq to find out if the puzzle is completed:

private bool IsPuzzleCompleted()
{
    //All pieces must have rotation of 0 degrees
    var query = from p in pieces
                where p.Angle != 0
                select p;

    if (query.Any())
        return false;

    //All pieces must be connected horizontally
    query = from p1 in pieces
            join p2 in pieces on p1.Index equals p2.Index - 1
            where (p1.Index % columns < columns - 1) && (p1.X + 1 != p2.X)
            select p1;

    if (query.Any())
        return false;

    //All pieces must be connected vertically
    query = from p1 in pieces
            join p2 in pieces on p1.Index equals p2.Index - columns
            where (p1.Y + 1 != p2.Y)
            select p1;

    if (query.Any())
        return false;

    return true;
}

Once the puzzle is completed, the user is invited to save a file containing the image of the puzzle.

aving the Complete Puzzle Image

This is not really a requirement for a puzzle game, but more of a consideration for our poor user who spent a lot of time playing with our game. So, once the puzzle is completed, the user can save an image for that puzzle.

Saving images from visual elements in WPF is easy and straightforward, as you can see below. In this case, the source element is the Canvas holding the puzzle pieces.

private void SavePuzzle()
{
    var sfd = new SaveFileDialog()
        {
            Filter = "All Image Files ( JPEG,GIF,BMP,PNG)|" +
            "*.jpg;*.jpeg;*.gif;*.bmp;*.png|JPEG Files ( *.jpg;*.jpeg )|"+
            "*.jpg;*.jpeg|GIF Files ( *.gif )|*.gif|BMP Files ( *.bmp )|"+
            "*.bmp|PNG Files ( *.png )|*.png",
            Title = "Save the image of your completed puzzle",
            FileName = srcFileName.Split('.')[0] + "_puzzle." +
            srcFileName.Split('.')[1]
        };

    sfd.DefaultExt = "png";
    sfd.ShowDialog();

    var query = from p in pieces
                    select p;

    var minX = query.Min<piece />(x => x.X);
    var maxX = query.Max<piece />(x => x.X);
    var minY = query.Min<piece />(x => x.Y);
    var maxY = query.Max<piece />(x => x.Y);

    var rtb = new RenderTargetBitmap((int)(maxX - minX + 1) * 100 + 40,
        (int)(maxY - minY + 1) * 100 + 40, 100, 100, PixelFormats.Pbgra32);
    cnvPuzzle.Arrange(new Rect(-minX * 100, -minY * 100,
        (int)(maxX - minX + 1) * 100 + 40, (int)(maxY - minY + 1) * 100 + 40));
    rtb.Render(cnvPuzzle);

    png = new PngBitmapEncoder();
    png.Frames.Add(BitmapFrame.Create(rtb));

    using (StreamWriter sw = new StreamWriter(sfd.FileName))
    {
        png.Save(sw.BaseStream);
    }
}

inal Considerations

Since you reached this line, I'd like to thank you very much for your patience and interest. Please leave a comment below, telling me what you liked or disliked in the game.

I hope this article will be useful for you in some way. Or, at least, that you have lots of fun.

istory

  • 2010-10-10: Initial version
  • 2010-10-13: Code block formatting corrected
  • 2010-10-16: Enhancements:
    • Ability to move a group of pieces
    • Rotation of a group of pieces at the same time
    • Rectangular lasso tool for selection of multiple pieces
    • Wrap panel instead of stack panel for holding pieces
  • 2010-10-19: Corrections on the rotation algorithm

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here