Previous

Flipping cards - simple animation with images.

Next

The previous lesson described how to make a solitaire concentration game using the canvas.

When the player selected a card, we used the canvas itemconfigure command to change the image of a card back to the image of a card face.

It would look cooler to show the card flipping over, instead of just changing it from front to back.

When we create an image with the image create command, we've created an object, just like we did with the snack::sound command. The image objects have commands associated with them, just like the sound objects do.

One of the image object's commands is copy. We can use this command to copy one image to another. The nifty thing is that when we copy one image into another, we can also modify it with the option/value pairs.

Here's a couple of the things we can do. We can control where the new image gets copied into our original image, and we can control the size of the image we copy. We can even control the X and Y size separately.

We control the size by sub-sampling the image. Sub-sampling means that instead of taking every pixel from the new image and moving it into our original image, we skip a pixel, or two pixels, or 3 pixels, etc.

If we skip every other pixel and every other row, the new image will be 1/2 the size of the original. If we skip 2 pixels and 2 rows (and only take every 3'd pixel from the new image), we'll make an image that's 1/3 the size of the original.

When we subsample an image, we can tell Tcl/Tk to subsample the columns and rows at a different rate. We do this by giving the -subsample two arguments. The first argument will be the sub-sampling rate for the X dimension (the columns) and the second will be the sub-sampling rate for the Y dimension (the rows).

A value of 1 means take every pixel. A value of 2 means to skip every other pixel. A value of 3 means to skip 2 pixels, etc.

Here's an example that loads one of the card images and makes two sub-sampled copies of the image. We create a rectangle for each image, and then create the images over the rectangle to show what part of the image was copied.


# Create a card image
image create photo card -file "s_q.gif"
set ht [image height card]
set wd [image width card]

image create photo card2 -height $ht -width $wd
card2 copy -subsample 2 1 card

image create photo card3 -height $ht -width $wd
card3 copy -subsample 3 1 card

# Create and grid a canvas to work with
canvas .c -background black
grid .c

.c create rectangle 0 0 $wd $ht -fill gray
.c create image 0 0 -anchor nw -image card

.c create rectangle 100 0 [expr 100 + $wd] $ht -fill gray
.c create image 100 0 -anchor nw -image card2

.c create rectangle 200 0 [expr 200 + $wd] $ht -fill gray
.c create image 200 0 -anchor nw -image card3

This code makes images like this:

The left-most image is the original, the middle one is sub-sampled every other pixel in the X dimension. the rightmost image is sub-sampled every third pixel in the X dimension.

Try typing that code into Komodo Edit and running it. Then try changing the the sub-sampling options to see how it changes the image.

The image copy command will start copying at the upper left hand corner of the destination image. We can change this using the -to option. We can use both the -subsample and -to options in the same command.

Here's a summary of the options we've just discussed.

Option Arguments Description
-to left top right bottom Copy the image into these coordinates
-subsample xCount yCount Skip xCount pixels across and yCount pixels down when copying. This reduces the size of an image.

Lets try using just one image (card2) and copy the card image to it with different sub-sampling. The code looks like this:


# Create a card image
image create photo card -file "s_q.gif"
set ht [image height card]
set wd [image width card]

image create photo card2 -height $ht -width $wd
card2 copy -subsample 2 1 card
card2 copy -subsample 3 1 card
card2 copy -subsample 4 1 card

# Create and grid a canvas to work with
canvas .c -background black
grid .c

.c create rectangle 0 0 $wd $ht -fill gray
.c create image 0 0 -anchor nw -image card

.c create rectangle 100 0 [expr 100 + $wd] $ht -fill gray
.c create image 100 0 -anchor nw -image card2

You'll end up with a result like this:

Yuck. What happened is that Tcl/Tk copied an image sub-sampled to every other pixel, then copied one sub-sampled every third pixel over that, and then copied one sub-sampled every fourth pixel over that.

But it only changed the pixels it was copying. It didn't change the other pixels.

Sometimes, this is what we want to do, but it makes a funky looking card when we're trying to write code that looks like a card being flipped over.

We probably need to learn a new command.

The image object also has a put command. We can tell Tcl/Tk what color (or colors) to put into an area of an image.

For instance, to fill an image with a new color (like gray), we can use this command:


card2 put gray -to 0 0 $ht $wd

Try adding this before the last copy in the example above to see if it solves the problem (it does).

Here's a summary of the new commands:
Command Description
copy Copy a new image into the original image
put Put a new color at a location (or locations) in the image. You can use the -to option with this command.

With the copy and put image commands and the -subsample and -to options, we can make images that look like they're being flipped around their center.

But we don't want to show a bunch of images side by side. That's great to see what's going on, but it doesn't look like a card flipping over.

We could do a loop like this:


  delete any previous image on canvas
  create image object
  copy card with appropriate subsampling
  create new image on canvas

This would actually work, but it's kind of kludgey, and it takes a lot of computer resources to create and destroy things. It's much faster to just change them a little.

We can use the itemconfigure command that we looked at in the previous lesson to change the image the canvas is displaying to a temporary image. Then we can copy the card image into the temporary image with different sub-samplings and different locations.

Now, we're ready to flip the cards. Here's the procedure that will do the trick. Notice that we use after and update idle in the loops.

We use the after command to slow the computer down a little bit. Without the after commands making it pause between images, the cards would flip so fast we wouldn't seem them changing.

The update idle command makes the computer update the display immediately.

In this procedure we make a new image object that we can copy the original images into. We change the sub-sampling each time we go through a loop, and calculate a new location around the center of the card to copy the image into.

This makes it look as if we were turning the card over until it's edge on to us.

Once we've made the first image very narrow, we start another loop where we use the second image and subsample less each time. This makes it look like we're continuing to flip the card over and showing more and more of the second image each time.

This procedure is another generic type procedure. We can change the start and end images to flip a card from face down to face up, or from face up to face down.

We could even use images of Harry Potter and Goyle to make it look like Harry was flipping and transforming into Goyle!


################################################################
# proc flipImageX {canvas canvasID start end background}--
#    Makes it appear that an image object on a canvas is being flipped
# Arguments
#   canvas	The canvas holding the image
#   canvasID	The identifier for this canvas item
#   start	The initial image being displayed
#   end		The final  image to display
#   background  The color to show behind the image being flipped.
#               This is probably the canvas background color
# 
# Results
#   configuration for the canvas item is modified.
# 
proc flipImageX {canvas canvasID start end background} {
  global concentration
  
  # Get the height/width of the image we'll be using
  set height [image height $start]
  set width  [image width  $start]
  
  # The image will rotate around the X axis
  # Calculate and save the center, since we'll be using it a lot
  set centerX [expr $width  / 2]
  
  # Create a new temp image that we'll be modifying.
  image create photo temp -height $height -width $width
  
  # Copy the initial image into our temp image, and configure the
  # canvas to show our temp image, instead of the original image
  # in this location.
  temp copy $start
  $canvas itemconfigure $canvasID -image temp
  update idle
  after 25

  # copy the start image into the temp with greater
  #   subsampling (making it appear like more and more of an
  #   edge view of the image).  
  # Move the start of the image to the center on each pass
  #  through the loop
  for {set i 2} {$i < 8} {incr i} {
    set left [expr $centerX - $width / (2 * $i)]
    set right [expr $centerX + $width / (2 * $i)]
    temp put $background -to 0 0 $width $height
    temp copy -to $left 0 $right $height -subsample $i 1 $start
    update idle
    after 25
  }

  # copy the end image into the temp with less and less
  #   subsampling (making it appear like less and less of an
  #   edge view of the image).  
  # Move the start of the image away from thecenter on each pass
  #  through the loop
  for {set i 8} {$i > 1} {incr i -1} {
    set left [expr $centerX - $width / (2 * $i)]
    set right [expr $centerX + $width / (2 * $i)]
    temp put $background -to 0 0 $width $height
    temp copy -to $left 0 $right $height -subsample $i 1 $end
    update idle
    after 25
  }
  # configure the canvas to show the final image, and
  # delete our temporary image
  $canvas itemconfigure $canvasID -image $end
  image delete temp
}

We call the flipImageX procedure from the playerTurn procedure instead of using the itemconfigure command that we used in the previous lesson.

The new playerTurn procedure looks like this. Notice that after a player finds a match, we configure the image to be blank and use the bind command to put a binding on the images with an empty command. Assigning an empty command means to do nothing. That's the same as destroying the binding.



################################################################
# proc playerTurn {position}--
#    Selects a card for comparison, or checks the current
#    card against a previous selection.
# Arguments
# position 	The position of this card in the deck.
#
# Results
#     The selection fields of the global array "concentration"
#     are modified.
#     The GUI is modified.
# 
proc playerTurn {position} {
  global concentration
  
  set card [lindex $concentration(cards) $position]
  flipImageX .game card_$position back $card gray
  
  set rank [lindex [split $card _] 1]

  # If concentration(selected,rank) is empty, this is the first
  #   part of a turn.  Mark this card as selected and we're done.
  if {{} eq $concentration(selected,rank)} {
      # Increment the turn counter
    incr concentration(turn)

    set concentration(selected,rank) $rank
    set concentration(selected,position) $position
    set concentration(selected,card) $card
  } else {
    # If we're here, then this is the second part of a turn.
    # Compare the rank of this card to the previously saved rank.
    
    if {$position == $concentration(selected,position)} {
      return
    }

    # Update the screen *NOW* (to show the card), and pause for one second.
    update idle
    after 1000
  
    # If the ranks are identical, handle the match condition
    if {$rank eq $concentration(selected,rank)} {
      # Increase the score by one
      incr concentration(player,score)

      # Remove the two cards and their backs from the board
      .game itemconfigure card_$position -image blank 
      .game itemconfigure card_$concentration(selected,position) -image blank
      .game bind card_$position  ""
      .game bind card_$concentration(selected,position)  ""

      
      # Check to see if we've won yet.
      if {[checkForFinished]} {
        endGame
      }
    } else {
      # If we're here, the cards were not a match.
      # flip the cards to back up (turn the cards face down)

       flipImageX .game card_$position $card back gray
       flipImageX .game card_$concentration(selected,position) \
         $concentration(selected,card) back gray
    }
    
    # Whether or not we had a match, reset the concentration(selected,rank)
    # to an empty string so that the next click will be a select.
    set concentration(selected,rank) {}
  }
}


Open the previous game in Komodo Edit and change these two procedures.

Try changing the number of milliseconds that the after command pauses for to see how it changes the speed of the cards flipping.

Write a flipImageY procedure and use that to flip the cards end-over-end instead of side to side. Change the itemconfigure command in playerTurn to use the flipImageY to a blank image instead.


This lesson looked at doing some simple animations. The important parts are:


You might have noticed that index we're using to keep track of the score is player,score. I wonder if that means that we could also have a computer,score index and change this one-person game into a two-person game to play against the computer...

Find out the answer to this and other thrilling questions in the next lesson...



Previous

Next


Copyright 2007 Clif Flynt