Plan C - Add Tweaks

Return to Plan C - Animate the Image Across the Screen

Plan C - Add Tweaks

An Easy Tweak - Increase Jack's "Resolution"

This one is easy. Just copy the second frame of the Jack image and paste it in as a fourth frame. This will increase the "resolution" of Jumping Jack.

images.rs
pub const JACK: &str = r"
' \ 0 /   '
'  \|/    '
'   |     '
'  / \    '
'_/   \_  '

'         '
' __0__   '
'/  |  \  '
'  / \    '
' _\ /_   '

'         '
'   o     '
' /\ /\   '
' |/ \|   '
' _\ /_   '

'         '
' __0__   '
'/  |  \  '
'  / \    '
' _\ /_   '
"; // end of JACK

If Image Flies off Top Edge of Screen, Bring It Back in from the Bottom

drawing.rs
...
pub fn draw_image(delay: u64, fly: bool, kind: String, oops: bool, image_vecvec: Vec<Vec<String>>) {
...
           if fly {
                // And check to see if we're flying. If so...
                row -= 1; //... raise the next drawing one row up, and ...
                          // ... erase last (bottom) line drawn, using enough repeated spaces for train's length.
                line = row + height;
                my_mvaddstr(
                    line,
                    col,
                    &" ".repeat(length as usize),
                    screen_width,
                    screen_height,
                );
                // If the entire height of the image is off the top edge of screen (top row of screen is 0)...
                if (row + height) < 0 {
                    // ... start drawing image at bottom of screen.
                    row = screen_height;
                }
                // No need to refresh(); next frame will do it for us.
            }
...

Technically, the "0" in the "if" statement is a "magic number", something we'd normally want to avoid, but in this case, the comment explains what it means, and to make a variable or constant just for the top of the screen, just to avoid the use of a magic number, seems a little ... overboard. I guess we could say something like if (row + height) < (screen_height - screen_height), but that seems a little nutzoid also. Sometimes it just makes sense to "break the rules".

Add Coalcar

The original "sl" program has the coalcar attached to both the C51 and the D51 trains, without any option to not have it attached. Later we'll give the user the option to not include the coalcar, but for now, we'll just add it. The easiest way to add the coalcar (if we don't give the user to not add it), is to add it to the ASCII-art, like this:

images.rs
...
pub const D51: &str = r"
'      ====        ________                ___________                              '
'  _D _|  |_______/        \__I_I_____===__|_________|                              '
'   |(_)---  |   H\________/ |   |        =|___ ___|      _________________         '
'   /     |  |   H  |  |     |   |         ||_| |_||     _|                \_____A  '
'  |      |  |   H  |__--------------------| [___] |   =|                        |  '
'  | ________|___H__/__|_____/[][]~\_______|       |   -|                        |  '
'  |/ |   |-----------I_____I [][] []  D   |=======|____|________________________|_ '
'__/ =| o |=-~~\  /~~\  /~~\  /~~\ ____Y___________|__|__________________________|_ '
' |/-=|___|=    ||    ||    ||    |_____/~\___/          |_D__D__D_|  |_D__D__D_|   '
'  \_/      \O=====O=====O=====O_/      \_/               \_/   \_/    \_/   \_/    '
...

But like the original "sl" program in the C-language, we'll keep the coalcar a completely separate ASCII-art drawing, which we can stitch onto the back-end of an image.

So let's put the coalcar in our "images.rs" file as a separate art-work, like so:

images.rs
...
 for your constant in both the documentation string of the "What kind of object" arg
 in the "struct Args" section of the "get_options.rs" file, and in the "Set 'kind'."
 section of the "parse_opts()" function in that same file.
*/

pub const COALCAR: &str = r"
'                              '
'                              '
'    _________________         '
'   _|                \_____A  '
' =|                        |  '
' -|                        |  '
'__|________________________|_ '
'|__________________________|_ '
'   |_D__D__D_|  |_D__D__D_|   '
'    \_/   \_/    \_/   \_/    '
"; // end of COALCAR

pub const D51: &str = r"
'      ====        ________                ___________'
'  _D _|  |_______/        \__I_I_____===__|_________|'
'   |(_)---  |   H\________/ |   |        =|___ ___|  '
'   /     |  |   H  |  |     |   |         ||_| |_||  '
'  |      |  |   H  |__--------------------| [___] |  '
'  | ________|___H__/__|_____/[][]~\_______|       |  '
'  |/ |   |-----------I_____I [][] []  D   |=======|__'
'__/ =| o |=-~~\  /~~\  /~~\  /~~\ ____Y___________|__'
' |/-=|___|=    ||    ||    ||    |_____/~\___/       '
'  \_/      \O=====O=====O=====O_/      \_/           '

'      ====        ________                ___________'
'  _D _|  |_______/        \__I_I_____===__|_________|'
'   |(_)---  |   H\________/ |   |        =|___ ___|  '
'   /     |  |   H  |  |     |   |         ||_| |_||  '
...

If a main image does not have a tail image attached, we can specify that in the "Set 'kind'" match statement with an empty string.

get_options.rs
...
pub fn parse_opts() -> (u64, bool, String, bool, String, String) {
...
    // Set "kind".
    let (kind, image_string, tail) = match (&switches.kind.to_uppercase()).as_str() {
        "D" | "D51" => ("D51".to_string(), D51.to_string(), COALCAR.to_string()),
        "C" | "C51" => (String::from("C51"), String::from(C51), COALCAR.to_string()),
        "L" | "LITTLE" => ("LITTLE".to_owned(), LITTLE.to_owned(), "".to_string()),
        "J" | "JACK" => (String::from("JACK"), JACK.to_string(), "".to_string()),
        "B" | "BOAT" => (String::from("BOAT"), D51.to_string(), "".to_string()),
        "P" | "PLANE" => (String::from("PLANE"), D51.to_string(), "".to_string()),
        _ => {
            // If none of them match, then ...
            println!("Invalid input; run program with '--help' option for more information. You entered '{}', so the default of 'd51' was used.", switches.kind);
            (String::from("D51"), D51.to_string(), COALCAR.to_string())
        }
    };
    return (speed, switches.fly, kind, switches.oops, image_string, tail);
} // end of parse_opts()

The following temporary configuration of "main()" should print out the coalcar image in single-quotes, if you provide either the D51 or the C51 image as the option ($ cargo run or $ cargo run -- kind=c51, etc).

main.rs
fn main() {
    // Get the program options from the command-line, if any.
    let (speed, fly, kind, oops, image_string, tail_string) = parse_opts(); // Get the program options from the command-line, if any.
    let image_vecvec = string_to_stringvecvec(&image_string);
// test what we got
println!("{}", tail_string);
//    draw_image(speed, fly, kind, oops, image_vecvec);
} // end of main()

If that worked, we're ready to move on.

Just like we did with the "image_string" variable, containing the main body of the image we've been animating across the screen, we can send the coalcar image to the "string_to_stringvecvec()" function to convert it from a String to a vector of String vectors. Unlike the main image, our outer vector won't contain several inner vectors; it'll only contain one inner vector. But the two images, the main body, and the tailing coalcar, will be configured similarly, as a vector of String vector[s]. Then we'll be able to massage the two images into one image, before animating that one image across the screen.

So let's convert the coalcar image, and then animate it by itself across the screen to make sure it seems to be working as we expect.

main.rs
fn main() {
    // Get the program options from the command-line, if any.
    let (speed, fly, kind, oops, image_string, tail_string) = parse_opts();
    let image_vecvec = string_to_stringvecvec(&image_string);
    let tail_vecvec = string_to_stringvecvec(&tail_string);
// test what we got
println!("{}", tail);
//    draw_image(speed, fly, kind, oops, image_vecvec);
    draw_image(speed, fly, kind, oops, tail_vecvec);
} // end of main()

And ... it does!

Now let's handle the situation where there's not a tailstring:

main.rs
fn main() {
    // Get the program options from the command-line, if any.
    let (speed, fly, kind, oops, image_string, tail_string) = parse_opts();
    
    // Convert image_string to a vector of String vectors.
    let image_vecvec = string_to_stringvecvec(&image_string);
    

    // If we have a tail for the main image, such as a coalcar, do likewise to it, and then stitch the two images together.
    if tail_string != "".to_string() {
        let tail_vecvec = string_to_stringvecvec(&tail_string);
        draw_image(speed, fly, kind, oops, tail_vecvec); // This will soon be replaced by a function to add the tail to the main body....
    }    
    
//    draw_image(speed, fly, kind, oops, image_vecvec);
    draw_image(speed, fly, kind, oops, tail_vecvec);

} // end of main()

In other words, if the tailstring is not an empty string (remember, we set "tail" to "" for the JACK option in "parse_opts()"), that means there is a tail, and so the two commands in the "if" clause are executed. But if that tailstring is an empty string, then that means there is not a tail, so we won't do those two commands in the "if clause", and since there's nothing left for the program to do, the program ends without doing anything. (We'll soon have it draw the main image without the tail in the latter case, but this is just for testing/development purposes.) You can see the different behavior by first running:

$ cargo run -- --kind=d51

and then running:

$ cargo run -- --kind=jack

The first one will animate the coalcar across the screen; the second one will do nothing.

Now we're ready to stitch the tail to the main body of the image.

Since we've already co-opted the "get_options.rs" file to hold some of our pre-processing routines, we might as well put our new function in this same file.

get_options.rs
...
    return outer_vec;
} // end of string_to_stringvecvec()
  
  
pub fn stitch_tail_to_body(body: Vec<Vec<String>>, tail: Vec<Vec<String>>) -> Vec<Vec<String>> {
} // end of stitch_tail_to_body() 

Just for testing the basic strucure, we can have the function return either of the vectors.

get_options.rs
...
pub fn stitch_tail_to_body(body: Vec<Vec<String>>, tail: Vec<Vec<String>>) -> Vec<Vec<String>> {
    tail  
} // end of stitch_tail_to_body()

And in "main()":

main.rs
fn main() {
    // Get the program options from the command-line, if any.
    let (speed, fly, kind, oops, image_string, tail_string) = parse_opts();
    
    // Convert image_string to a vector of String vectors.
    let mut image_vecvec = string_to_stringvecvec(&image_string);
    
    // If we have a tail for the main image, such as a coalcar, do likewise to it, and then stitch the two images together.
    if tail_string != "".to_string() {
        let tail_vecvec = string_to_stringvecvec(&tail_string);
        draw_image(speed, fly, kind, oops, tail_vecvec); // This will soon be replaced by a function to add the tail to the main body....
        image_vecvec = stitch_tail_to_body(image_vecvec, tail_vecvec);
    }
    // Animate the resulting image vector.
//    draw_image(speed, fly, kind, oops, image_vecvec);

} // end of main()

When we feed the "image_vecvec" vector to our function, we lose ownership of that value, and when the function returns a similar value, we are binding that value to the same "image_vecvec" variable name we were using earlier, but we can only do that if we first declare that binding as mutable. That's why we had to add the "mut" to our initial declaration of "image_vecvec".

Ideally, the main body of the image, stored initially in "image_vecvec", is sent, along with the tail image, stored in "tail_vecvec", to the function "stitch_tail_to_body()", which returns a new value consisting of the main body image stitched together with the tail image, which new value is placed into the variable named "image_vecvec". So whereas "image_vecvec" starts out as just the body image, it comes back as the body + tail image.

If you give the option of "D51" or "C51" as the image to draw (or give no image, which will default to the D51), you should see the coalcar travel across the screen if this test succeeds. But if you specify an image that does not have a tail ("jack" is the only such image we currently have working), you should see that image animate across the screen. In a few minutes, once we have the function working, we'll have it working so that if you specify the D51 or C51, not only will the tailing coalcar animate across the screen, but it will be tailing the main body of the D51 or C51.

On to the next step. We need to make sure both the main image and the tail image are the same height. We could write program code to do this, but it would get pretty complicated, and it's easier just to inform the ASCII-artist to draw his art with the same number of lines for his "tail" as for his main "body".

images.rs
*========================================
*    sl.h: SL version 5.02
*      Copyright 1993,2002,2014
*                Toyoda Masashi
*                (mtoyoda@acm.org)
*      Last Modified: 2014/06/03
*========================================

 This conversion is based on a tutorial for this project, written by Kent West, 2023.

 To add an art image, just follow the pattern of the images below, making sure to give
 a unique name as the constant value, and making sure to put a blank line between each
 animation frame, and making sure to enclose each side of each line of each frame in
 single-quotes (which will be converted by the program into spaces). Then add an entry
 for your constant in both the documentation string of the "What kind of object" arg
 in the "struct Args" section of the "get_options.rs" file, and in the "Set 'kind'."
 section of the "parse_opts()" function in that same file.

 To add an optional "tail", such as the COALCAR, it must have the same number of rows
 as the body to which it will be attached, and then its entry must be added to the main
 body's entry in the "Set 'kind'" match statement of the "parse_opts()" function in the
 "get_otions.rs" file.
*/

Now back to the function to stitch the tail to the main body.

get_options.rs
...
pub fn stitch_tail_to_body(body: Vec<Vec<String>>, tail: Vec<Vec<String>>) -> Vec<Vec<String>> {
    // Clone the main body vector.
    let mut image = body.clone();

    // How many frames in the main body?
    let num_of_frames = image.len();

    // Cycle through each frame, and stitch each row of tail to each row of each frame of main body.
    for frame in 0..num_of_frames {
        for (row, each_line) in body[frame].iter().enumerate() {
            image[frame][row].push_str(&tail[0][row]);
        }
    }
    // Return the modified cloned image.
    image
    tail
} // end of stitch_tail_to_body()

When I ran the program with cargo run -- --kind=d51, I discovered several problems:

One problem is that the wheels of the coalcar "shimmied". When I went back and looked at the "images.rs" file for the "D51" train, I realized that there was an extra space after the closing single-quote on the bottom line of about half the D51 frames. The easiest way to see this is to highlight all the frames in your text editor, which will let you visually see the extra space, as in the image below. We should remove those extra spaces, because inconsistent data could lead to unforeseen behavior. I originally left the extra space in, because I kind of liked the shimmy, but a few steps farther on, it introduces a small "blip" in the animation of the D51+trailer set.

Extra space at end of line of a D51 frame.

Another problem is that selecting the C51 image causes the program to crash. Again, looking more closely at the "images.rs" file, I see that whereas the D51 and the COALCAR images both have 10 lines, the C51 image has 11. Oops. To fix this, we can either not include the COALCAR tail on the C51 image, or we can add a blank line at the tops of both the D51 and COALCAR images. Your choice. I added the blank lines.

images.rs
...
pub const COALCAR: &str = r"
'                              '
'                              '
'                              '
'    _________________         '
'   _|                \_____A  '
' =|                        |  '
' -|                        |  '
'__|________________________|_ '
'|__________________________|_ '
'   |_D__D__D_|  |_D__D__D_|   '
'    \_/   \_/    \_/   \_/    '
"; // end of COALCAR

pub const D51: &str = r"
'                                                     '
'      ====        ________                ___________'
'  _D _|  |_______/        \__I_I_____===__|_________|'
'   |(_)---  |   H\________/ |   |        =|___ ___|  '
'   /     |  |   H  |  |     |   |         ||_| |_||  '
'  |      |  |   H  |__--------------------| [___] |  '
'  | ________|___H__/__|_____/[][]~\_______|       |  '
'  |/ |   |-----------I_____I [][] []  D   |=======|__'
'__/ =| o |=-~~\  /~~\  /~~\  /~~\ ____Y___________|__'
' |/-=|___|=    ||    ||    ||    |_____/~\___/       '
'  \_/      \O=====O=====O=====O_/      \_/           '

'                                                     '
'      ====        ________                ___________'
'  _D _|  |_______/        \__I_I_____===__|_________|'
'   |(_)---  |   H\________/ |   |        =|___ ___|  '
'   /     |  |   H  |  |     |   |         ||_| |_||  '
...

A third problem is that there is an unwanted space between the D51/C51 and the COALCAR in the animated image. There are several ways this could be fixed; or we can just ignore it. I fixed mine by changing one line in the "string_to_stringvecvec()" function from:

let mut image_string: String = image_str.replace("\'", " ");

to

let mut image_string: String = image_str.replace("\'", " ");

so that instead of replacing all the single-quotes with spaces, we're simply replacing them with nothing, effectively removing all the single-quotes. (This has unforeseen repercussions, which we'll discuss shortly.)

However, this leaves a trail behind when in "Fly" mode, because we no longer have that trailing space that deletes any leftovers from the previous frame (which is visible with a command like $ cargo run -- --kind=c51 -f). So we'd still like to have a trailing space at the end of the image. Sometimes that image will have a tail, such as with the D51 having the COALCAR tail, and sometimes it won't. So instead of adding a space to either the main body or to the tail, let's add it to the finished stitched-together product. And instead of adding it to the whole image prior to displaying the whole image, we can just add it to each line as each line is displayed, over in the "mymvaddstring()" function, like so:

drawing.rs
...
fn my_mvaddstr(row: i32, mut col: i32, frame_line: &str, screen_width: i32, screen_height: i32) {
    /* This function recieves one line of an ASCII-art image, and
       trims away any of that line that would otherwise be displayed
       off-screen; then it displays the remaining portion of the
       line.
    */
    let mut line = frame_line.to_string(); // An &str is immutable by nature; we need a mutable string.

    // Add a space to end of line as eraser of leftovers from previous frame.
    line.push(' ');
    
    // Trim from left side as train moves off left edge
    if col < 0 {
...

That took care of the leftovers, but when drawing the C51, the program now crashes near the end of the animation, and the top of the coalcar looks broken. Something's obviously wrong.

These problems bring to light a problem that has existed all along, but which I'm only now noticing. The ASCII-artwork for the C51 train implements single-quotes as part of its art. When we removed all the single-quotes above, this also removed parts of the actual ASCII-artwork, which caused some of the lines of the C51 to be shorter than the others, which has led to the crashing at the end of the animation, and which causes the coalcar, and the C51 itself, now that I look at it more closely, to be misaligned.

The fix for this is to not remove all the single-quotes, but only the single-quotes at the beginning and the end of each line.

get_options.rs
      
pub fn string_to_stringvecvec(image_str: &str) -> Vec<Vec<String>> {
    // First, replace, with spaces, the quote-marks that delimit the side boundaries of the ASCII-art image.
    let mut image_string: String = image_str.replace("\'", "");
    let image_string: String = image_str.replace("\n\'", "\n");
    let mut image_string: String = image_string.replace("\'\n", "\n");
      
...

The first line above removes all single-quotes that follow immediately after a newline, and the second line removes all single-quotes immediately before a newline.

btw, it's at this point that the aforementioned "blip in the animation of the D51" shows up if we had left the shimmy in the D51 art.

A final issue, very minor, is that the compiler complains that we never use the "each_line" variable in our "for" loop. (It also complains that we're not using "kind" or "oops", but we plan to use those, so we'll just ignore those warnings for now.) This is easy enough to fix; we just do what the compiler suggests:

get_options.rs
   
...
    // Cycle through each frame, and stitch each row of tail to each row of each frame of main body.
    for frame in 0..num_of_frames {
        for (row, _each_line) in body[frame].iter().enumerate() {
...

This should have the coalcar working with the D51 and C51 trains. In just a bit, we'll make the coalcar an option that can be turned off. But first, let's Add Some More Drawings.