At this point, we are able to print one of a the images to the terminal window using "println!()" statements. Let's convert this program to use ncurses instead of "println!()"s, so that we have better control of the text-based graphics. You'll remember tinkering with ncurses earlier, in the Curses "Graphics" for Text-mode Screens lesson. You'll recall that in order to use ncurses we must add it as a third-party crate, to our crate, by telling Cargo to add it to our "Cargo.toml" file, from "https://crates.io", with the following command:
$ cargo add ncurses
Your "Cargo.toml" file will become something like this:
First, let's get rid of our three test println's:
You'll also recall that there were several housekeeping functions that we needed to setup. We'll do that in the "main.rs" file, in the "main()" function.
And then convert the "display_image()" function to use ncurses instead of "println!"s. For now, we'll just hard-code the location of the image's "origin" (top left of the image) at, say, 3 rows down and 5 columns in from the left:
If you run this without specifying an image (or if you specify the D51 image), what you should see is the first frame of the D51 train, and then the succeeding five frames each time you press a key, only with the wheels running in reverse. We'll fix that reverse issue later. (As a reminder the "getch()" function also accomplishes the equivalent of a "refresh()", which is necessary to display the image after you've "constructed" it behind-the-scenes, and are ready to bring it forward to be visible.) However, if something goes wrong, you might wind up with a screen that is in an inconsistent state, now that we're using ncurses to put the screen in "graphics mode" (sort of). In such a situation, you should be able to fix this with a Ctrl-C, followed by blindly typing "reset" and hitting the enter key.
Move the Choice of Frame and Location to "main()"
Rather than having the "display_image()" function draw all the frames of one image, let's have it draw just one frame of an image, and let the "main()" function determine which frame gets drawn. Let's also let "main()" determine where it gets drawn. And we no longer need to print the frame number. And let's rename the function.
or, in a cleaner format:
And the requisite changes to "main.rs":
This should draw one frame of your chosen image, and then wait for a keypress before exiting. By manipulating the "row" and "col" values, we can draw the frame where we want it. If it doesn't fit in your terminal window, you'll get odd results, but we'll fix that later; for now, maybe make your window bigger, or move your (row,col) coordinates a bit to make the image fit on-screen.
Compress Two Commands Into One
This is strictly a stylistic choice, but let's use the alternative ncurses command, "mvaddstr()", to replace the two commands "mv()" and "addstr()". And let's use the "increment shortcut" to add one to the variable "row".
These two changes are really insignificant; they are just an alternative way of writing the same code.
Move the Frame Across the Screen
Let's move the frame across the screen:
Yay! With each press of a key, the image moves across the screen.
But there are several problems:
We don't want to have to press a key to advance the image.
The image starts near the top of the window; it might be better to put the bottom of the image at the bottom of the screen.
Any images (like the D51/C51) that do not have a space in the last column of each line will leave behind an unwanted trail.
We're only displaying one frame, so there's no animation of the image.
Any part of the image off the right-edge of the screen wraps around to the left edge.
When the image reaches the left edge, it doesn't continue off the screen.
We can fix these things.
Replace the Keypress with a Delay
Rather than pressing a key to advance the image across the screen, let's replace that "getch()" with a sleep/delay between frames, lasting a few milliseconds. Since we're removing the "getch()", which doubles as a "refresh()", we'll have to put a "refresh()" in between the frames. We can do this in "main()", after each frame is drawn, or we can do it in the "draw_frame()" function. Since the frame is not really drawn until it is displayed on the screen, it seems that in order to have the "draw_frame()" function do the entire job, the "refresh()" statement belongs there, rather than in "main()". So that's where we'll put it.
Now when you run the program, you no longer have to press a key to advance the image across the screen. You can adjust the speed at which the image crosses the screen by changing the value of the "delay" variable. A bigger number will create more of a delay, making the image move more slowly. Eventually, we'll give the user the ability to control the speed from the command-line.
We can also write our code in "main()" this way (which I prefer, as it makes the code a little easier to read):
This is one of those rare cases where using a "magic number" would actually make the program more readable:
let delay_between_frames = Duration::from_millis(100);
actually conveys more information than does:
let delay_between_frames = Duration::from_millis(delay);
but, if we want to later be able to vary the animation speed according to a command-line option, we need to use a variable.
Move the Image to the Bottom of the Screen
Let's draw the image so that its bottom is at the bottom of the terminal window. In order to do that, we need to know how tall the image is. The author of the C-version of "sl" solved this problem by using "#define"s in his "sl.h" file that record the number of lines in an image. For example, there's a "#define" for the D51's height, named "D51HIGHT", set to 10, since there are ten lines in that image. Rather than having the artist of an image count the images and provide that in our image definition, we can just let our program count the number of lines. Since each of the inner vectors has one element per line, if we count the number of elements in one of the vectors, we know how many lines tall the image is:
Erase the Unwanted Trail
Each time a frame is displayed on the screen, it overwrites most of the drawing of the previous frame, except for the last column of the previous frame. If the last column of a particular line is empty, no problem; we're not overwriting an empty space. But if that last column is non-empty, then it gets left behind on the screen. We could erase the whole screen between frames, but that's overkill, eating resources that we don't need to eat, and potentially having unwanted side-effects (imagine if we had other drawings on the screen; they'd get erased too). Another way to erase that last column would be to make sure each drawing has an empty last column. We could put that burden on the ASCII-artist, so that an image must be stored in "images.rs" like this:
Another way would be to add a space character to the end of each line of each frame. We'll do it that way. We'll use the format! macro, which is just like the println! macro, except that instead of printing the result, it returns the result as a String.
An alternative way to do this is:
Now you should be able to opt for the D51 and C51 trains without them leaving trails.
Animate the Image
Right now we're just moving a single frame across the screen. What we want to do instead is to "animate" the image by alternating between the available frames as we go. To do this, we need to know the number of frames in an image, and the total trip distance.
The total distance the train must travel is the width of the screen plus the length of the train. See the image below to envision that distance.
Now we need to divide the total trip distance by the number of frames, and look at the remainder of that operation. In the case of an image that has six frames, such a mathematical operation will result in a remainder of 0 to 5; with an image that has four frames, the op will produce a remainder of 0 to 3; and so on. So each time we move one column left on the screen, we'll do this math, and the remainder will tell us which frame of the image to display.
This animation almost works, except for three issues, two of which we've already seen:
Any part of the image off the right-edge of the screen wraps around to the left edge.
When the image reaches the left edge, it doesn't continue off the screen, but instead crashes the program. (A blindly-typed Ctrl-C followed by "reset" and ENTER should restore your screen to normal.)
The C51 wheels run in reverse, even though the D51 wheels now turn in the correct direction.
Fix the C51
I made some tweaks to the C51 which solves the wheels running in reverse, and, I believe, improves the animation slightly. Just replace your existing C51 constant with this one:
Trim Off the Right Edge of Image that is Beyond the Right Edge of Screen
In order to prevent the right-end of the image from wrapping around to the left edge of the screen, we need to trim off any part of the image that does not fit on the right edge of the screen. The "mvaddstr()" routine that is part of ncurses does not do that, but we can write our own version that does. So in the next lesson, we'll just Create Our Own my_mvaddstr() Function, which checks the bounds of the screen, and trims off any part of the string that doesn't fit on the screen.