Image does not fill window correctly, when rotating

It should show the images not being stretched out, I am unsure why this happens. This is specifically only about mode 3

main.rs

use graphics::{blend_colors, set_pixel_value};

const WIDTH: usize = 900;
const HEIGHT: usize = 900;

struct Image {
    width: usize,
    height: usize,
    buffer: Vec<u32>,
}

impl Image {
    fn load(filename: &str) -> Image {
        let image =
            lodepng::decode32_file(&filename).expect(&format!("Couldn't load {}", &filename));
        Image {
            width: image.width,
            height: image.height,
            buffer: image
                .buffer
                .iter()
                .map(|color| {
                    return u32::from_le_bytes([color.b, color.g, color.r, color.a]);
                })
                .collect(),
        }
    }
}

fn main() {
    let image1 = Image::load("mainframe1.png");
    let image2 = Image::load("mainframe2.png");

    let mut window = minifb::Window::new(
        "Rotating images",
        WIDTH as usize,
        HEIGHT as usize,
        minifb::WindowOptions {
            resize: false,
            topmost: true,
            borderless: true,
            none: true,
            transparency: true,
            scale: minifb::Scale::X1,
            ..minifb::WindowOptions::default()
        },
    )
    .expect("Unable to create window");

    // The output buffer that we'll render the output image to
    let mut buffer = vec![0u32; (WIDTH * HEIGHT) as usize];

    // The current rotation in radians (0..2*Pi)
    let mut rotation: f32 = 0.0;

    //  Offsets
    let x_offset = WIDTH as f32 / 2 as f32;
    let y_offset = HEIGHT as f32 / 2 as f32;
    
    // Whether we are just drawing a single image (0), rotating a single image (1),
    // or rotating two superimposed images in different directions (2).
    let mut mode = 3;
    let mut fps_time = std::time::Instant::now();
    let mut fps_count = 0;

    while window.is_open() && !window.is_key_down(minifb::Key::Escape) {
        rotation += 0.01;

        if window.is_key_pressed(minifb::Key::Space, minifb::KeyRepeat::No) {
            mode = (mode + 1) % 3;
        }

        // Draw!
        // To prevent code duplication, you may want to move your code from
        // your `modes` answers into a function or a method of `Image`.
        if mode == 0 {
            window.update_with_buffer(&image1.buffer, image1.width, image1.height).unwrap();
        } else if mode == 1 {
            let mut buffer: Vec<u32> = image1.buffer.to_vec();
            let brightness: f32 = 0.25;

            for pixel in buffer.iter_mut() {
                let [blue, green, red, alpha] = pixel.to_ne_bytes();
                *pixel = u32::from_ne_bytes([
                    set_pixel_value(brightness, blue),
                    set_pixel_value(brightness, green),
                    set_pixel_value(brightness, red),
                    alpha,
                ]);
            }
            window.update_with_buffer(&buffer, image1.width, image1.height).unwrap();
        } else if mode == 2 {
            let brightness: f32 = 0.5;

            for y in 0..HEIGHT {
                for x in 0..WIDTH {
                    // for positive integers, the euclidean remainder is the same as the `%` operator
                    let color_1 = get_color(&image1, y, x);
                    let color_2 = get_color(&image2, y, x);
                        
                    // Blend two colors use whatever formula you wants
                    let final_color = blend_colors(color_1, color_2, brightness);
                    buffer[y * WIDTH + x] = final_color;
                }
            }
        } else if mode == 3 {
            // TODO: Offset and rotate image 1
            for x in 0..HEIGHT {
                for y in 0..WIDTH {
                    let translated_x = x as f32 - x_offset as f32;
                    let translated_y = y as f32 - y_offset as f32;
                  
                    let rotated_x = translated_x * rotation.cos() + translated_y * rotation.sin();
                    let rotated_y = translated_x * rotation.sin() - translated_y * rotation.cos();
                    
                    let color = get_color(&image1, rotated_y as usize, rotated_x as usize);
                    buffer[y * WIDTH + x] = color;
                }
            }
        } else if mode == 4 {
            let brightness = 0.5;
            for x in 0..HEIGHT {
                for y in 0..WIDTH {
                    let translated_x = x as f32 - x_offset as f32;
                    let translated_y = y as f32 - y_offset as f32;
                
                    let rotated_x = translated_x * rotation.cos() + translated_y * rotation.sin();
                    let rotated_y = translated_x * rotation.sin() - translated_y * rotation.cos();
                    
                    let color_1 = get_color(&image1, y, x);
                    let color_2 = get_color(&image2, y, x);
                    let final_color = blend_colors(color_1, color_2, brightness);
                    buffer[rotated_y as usize * WIDTH + rotated_x as usize] = final_color;
                }
            }
            
            // TODO: Everything together!
            // Offset and rotate two superimposed images in opposite directions
        } else {
            // Optimized
            // For optimization code can be combined here instead of the deduplication
            // in the name of speed
        }

        // Show the buffer in the window
        if ![0, 1].contains(&mode) {
             window.update_with_buffer(&buffer, WIDTH as usize, HEIGHT as usize).unwrap();
        }

        // Display the FPS rate 3 times per second
        let now = std::time::Instant::now();
        let duration = (now - fps_time).as_secs_f32();
        fps_count += 1;
        if duration >= 0.33 {
            println!("{} fps", (fps_count as f32 / duration).round() as u32);
            fps_time = now;
            fps_count = 0;
        }
    }
}

fn get_color(image: &Image, y: usize, x: usize) -> u32 {
    let x1 = x.rem_euclid(image.width);
    let y1 = y.rem_euclid(image.height);
    let color = image.buffer[y1 * image.width + x1];
    color
}

lib.rs

pub fn blend_colors(mut color_1: u32, mut color_2: u32, brightness: f32) -> u32 {    
    color_1 = set_all_pixels_to_value(brightness, color_1);
    let [blue_color_1, green_color_1, red_color_1, alpha_color_1] = color_1.to_ne_bytes();
    
    color_2 = set_all_pixels_to_value(brightness, color_2);
    let [blue_color_2, green_color_2, red_color_2, alpha_color_2] = color_2.to_ne_bytes();

    let final_color = u32::from_ne_bytes([
        ((blue_color_1 as u16 + blue_color_2 as u16) / 2) as u8,
        ((green_color_1 as u16 + green_color_2 as u16) / 2) as u8,
        ((red_color_1 as u16 + red_color_2 as u16) / 2) as u8,
        ((alpha_color_1 as u16 + alpha_color_2 as u16) / 2) as u8,
    ]);
    final_color
}

pub fn set_pixel_value(factor: f32, pixel_color: u8) -> u8 {
    let pixel_color = (pixel_color as f32 * factor) as u8;
    pixel_color
}

pub fn set_all_pixels_to_value(factor: f32, pixel: u32) -> u32 {
     for value in pixel.to_ne_bytes() {
         set_pixel_value(factor, value);
     }
    pixel
}

Your rotation formula is incorrect.

let rotated_x = translated_x * rotation.cos() + translated_y * rotation.sin();
let rotated_y = translated_x * rotation.sin() - translated_y * rotation.cos();

You can see that it must be incorrect because this should be the identity transformation when rotation is zero (and the sine is 0 and the cosine is 1), but it is not, because it negates translated_y.

The correct formula (which you can also find at Rotation matrix - Wikipedia ) is:

let rotated_x = translated_x * rotation.cos() - translated_y * rotation.sin();
let rotated_y = translated_x * rotation.sin() + translated_y * rotation.cos();

Your program will also be much faster if you move the sin and cos out of your per-pixel loop, which — not coincidentally — also brings it closer to the matrix form:

let cos = rotation.cos();
let sin = rotation.sin();
for x in 0..HEIGHT {
    for y in 0..WIDTH {
        let translated_x = x as f32 - x_offset as f32;
        let translated_y = y as f32 - y_offset as f32;
      
        let rotated_x = translated_x * cos - translated_y * sin;
        let rotated_y = translated_x * sin + translated_y * cos;

Thank you for the explanation, you are right. I applied the change. But that was not my main issue, sorry for explaining it badly. I have added images to demonstrate the issue:

Currently

Should be

And it should keep spinning like that infinitely, for some reason the image is stretched out on some parts. I will work on improving performance later, thank you for that suggestion too!

Thank you for the images. Graphics bugs are much easier to track down with pictures as well as code.

The portions of the image that are stretched are where the rotated coordinates you have computed are less than 0. When you use as to convert a f32 that is less than 0 to usize, the result is 0. So, the top edge and left edge of the image get effectively stretched by this.

If you want to rotate the image about its center rather than its top-left corner, then you need to add x_offset and y_offset to rotated_x and rotated_y. Note that this should result in getting the same coordinates you started with when rotation is zero.

Note: This kind of transformation can be done more efficiently and with fewer chances for error by using vector and matrix types. You should strongly consider using a vector math library, like glam or euclid, and avoiding having separate _x and _y variables. Then, define a transform/matrix value, outside your loop, by composing the translation, rotation, and translation back, and multiply by the matrix inside the loop. This expresses the operation you need in higher-level terms, and also performs fewer additions and multiplications inside the per-pixel loop.

I also attached my assignment, since I have to make it in a specific way. In a later objective I need to optimize it, so that is not a worry. The rotation seems fine, since it centers four of the images. Thank you for your advice regarding performance, I will look into what you mentioned myself later.

So I need to calculate translated_x and translated_y by doing a calculation with a integer and float. I tried this but, I had to convert those back to f21 when calculating rotated_x and rotated_y. Which did not work. I tried a lot of stuff and don't really understand what logic is needed to stop the stretching. Although I understand that I must prevent rounding of usize values wrong with as f32 now as you said. Online I could not find a function for calculating between a f32 and usize, but there was explicit conversion. But that would give the same issues I presume.

Code tried

 else if mode == 3 {
            // TODO: Offset and rotate image 1
            for x in 0..HEIGHT {
                for y in 0..WIDTH {
                    let translated_x = x - x_offset as usize;
                    let translated_y = y - y_offset as usize;
                    
                    let rotated_x = translated_x as f32 * rotation.cos() - translated_y as f32 * rotation.sin();
                    let rotated_y = translated_x as f32 * rotation.sin() + translated_y as f32 * rotation.cos();
                    
                    let color = get_color(&image1, rotated_y as usize, rotated_x as usize);
                    buffer[y * WIDTH + x] = color;
                }
            }

Assignment

In the `mode==3` case, we want `image1` to rotate and repeat to fill the screen. For each output buffer pixel, we'll want to calculate which image pixel it should copy by applying the rotation to the x and y coordinates (after casting them to `float`).

First, we want it to spin around the middle-point of the frame. This can be achieved by subtracting the x and y location of the middle-point from the coordinates of which image pixel should be selected respectively:

translated_x = x - X_OFFSET
translated_y = y - Y_OFFSET

The formula for rotating a coordinate (x: f32, y: f32) around a certain coordinate (in this example (0, 0) ) is:

rotated_x = x * rotation.cos() + y * rotation.sin()
rotated_y = x * rotation.sin() - y * rotation.cos()

Try to combine both to match the result displayed to the side. Do not forget about the wrapping

If you want the effect where four copies of the image rotate around the center of rotation, then what you have to do is make it so that negative coordinates refer to copies of the image. The calculation for doing this is the same structure that you already have here in get_color():

    let x1 = x.rem_euclid(image.width);
    let y1 = y.rem_euclid(image.height);

The problem is that usize doesn’t have negative numbers. What you need to do is perform this rem_euclid on the f32 values, without converting to usize before calling the function. (You could also use isize or i32 values, but there isn’t a good reason to do that in this case.)

fn get_color(image: &Image, y: f32, x: f32) -> u32 {
    let x1 = x.rem_euclid(image.width as f32) as usize;
    let y1 = y.rem_euclid(image.height as f32) as usize;
    let color = image.buffer[y1 * image.width + x1];
    color
}

(Incidentally, this change also brings this function closer to how GPU textures are used.)

Oh good, yet more reasons to hate the lack of explicit float to integer conversions. I didn't know I needed more!

It works, but then crashes after first frames per second are printed. The out of index may still be caused by the rounding issues I think. Since line 173 is let color = image.buffer[y1 * image.width + x1];

Output

Failed to create server-side surface decoration: Missing
57 fps

thread 'main' (9018) panicked at src/main.rs:173:29:
index out of bounds: the len is 637200 but the index is 637756
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
warning: queue 0x5df05ff0a3e0 destroyed while proxies still attached:
  wl_buffer#20 still attached
  wl_buffer#19 still attached
  wl_shm_pool#18 still attached
  wl_buffer#17 still attached
  wl_shm_pool#16 still attached
  wl_surface#15 still attached
  wl_shm_pool#14 still attached
  xdg_toplevel#13 still attached
  xdg_surface#12 still attached
  xdg_wm_base#11 still attached
  wl_buffer#10 still attached
  wl_shm_pool#9 still attached
  wl_surface#8 still attached
  wl_shm#7 still attached
  wl_compositor#6 still attached
  wl_pointer#5 still attached
  wl_keyboard#4 still attached
  wl_seat#3 still attached
  wl_registry#2 still attached

Image

Code

use graphics::{blend_colors, set_pixel_value};

const WIDTH: usize = 900;
const HEIGHT: usize = 900;

struct Image {
    width: usize,
    height: usize,
    buffer: Vec<u32>,
}

impl Image {
    fn load(filename: &str) -> Image {
        let image =
            lodepng::decode32_file(&filename).expect(&format!("Couldn't load {}", &filename));
        Image {
            width: image.width,
            height: image.height,
            buffer: image
                .buffer
                .iter()
                .map(|color| {
                    return u32::from_le_bytes([color.b, color.g, color.r, color.a]);
                })
                .collect(),
        }
    }
}

fn main() {
    let image1 = Image::load("mainframe1.png");
    let image2 = Image::load("mainframe2.png");

    let mut window = minifb::Window::new(
        "Rotating images",
        WIDTH as usize,
        HEIGHT as usize,
        minifb::WindowOptions {
            resize: false,
            topmost: true,
            borderless: true,
            none: true,
            transparency: true,
            scale: minifb::Scale::X1,
            ..minifb::WindowOptions::default()
        },
    )
    .expect("Unable to create window");

    // The output buffer that we'll render the output image to
    let mut buffer = vec![0u32; (WIDTH * HEIGHT) as usize];

    // The current rotation in radians (0..2*Pi)
    let mut rotation: f32 = 0.0;

    //  Offsets
    let x_offset = WIDTH as f32 / 2 as f32;
    let y_offset = HEIGHT as f32 / 2 as f32;
    
    // Whether we are just drawing a single image (0), rotating a single image (1),
    // or rotating two superimposed images in different directions (2).
    let mut mode = 3;
    let mut fps_time = std::time::Instant::now();
    let mut fps_count = 0;

    while window.is_open() && !window.is_key_down(minifb::Key::Escape) {
        rotation += 0.01;

        if window.is_key_pressed(minifb::Key::Space, minifb::KeyRepeat::No) {
            mode = (mode + 1) % 3;
        }

        // Draw!
        // To prevent code duplication, you may want to move your code from
        // your `modes` answers into a function or a method of `Image`.
        if mode == 0 {
            window.update_with_buffer(&image1.buffer, image1.width, image1.height).unwrap();
        } else if mode == 1 {
            let mut buffer: Vec<u32> = image1.buffer.to_vec();
            let brightness: f32 = 0.25;

            for pixel in buffer.iter_mut() {
                let [blue, green, red, alpha] = pixel.to_ne_bytes();
                *pixel = u32::from_ne_bytes([
                    set_pixel_value(brightness, blue),
                    set_pixel_value(brightness, green),
                    set_pixel_value(brightness, red),
                    alpha,
                ]);
            }
            window.update_with_buffer(&buffer, image1.width, image1.height).unwrap();
        } else if mode == 2 {
            let brightness: f32 = 0.5;

            for y in 0..HEIGHT {
                for x in 0..WIDTH {
                    // for positive integers, the euclidean remainder is the same as the `%` operator
                    let color_1 = get_color(&image1, y, x);
                    let color_2 = get_color(&image2, y, x);
                        
                    // Blend two colors use whatever formula you wants
                    let final_color = blend_colors(color_1, color_2, brightness);
                    buffer[y * WIDTH + x] = final_color;
                }
            }
        } else if mode == 3 {
            // TODO: Offset and rotate image 1
            for x in 0..HEIGHT {
                for y in 0..WIDTH {
                    let translated_x = x as f32 - x_offset as f32;
                    let translated_y = y as f32 - y_offset as f32;
                    
                    let rotated_x = translated_x * rotation.cos() - translated_y * rotation.sin();
                    let rotated_y = translated_x * rotation.sin() + translated_y * rotation.cos();
                    
                    // let color = get_color(&image1, rotated_y as usize, rotated_x as usize);
                    let color = get_color_new(&image1, rotated_y, rotated_x);
                    buffer[y * WIDTH + x] = color;
                }
            }
        } else if mode == 4 {
            let brightness = 0.5;
            for x in 0..HEIGHT {
                for y in 0..WIDTH {
                    let translated_x = x as f32 - x_offset as f32;
                    let translated_y = y as f32 - y_offset as f32;
                
                    let rotated_x = translated_x * rotation.cos() + translated_y * rotation.sin();
                    let rotated_y = translated_x * rotation.sin() - translated_y * rotation.cos();
                    
                    let color_1 = get_color(&image1, y, x);
                    let color_2 = get_color(&image2, y, x);
                    let final_color = blend_colors(color_1, color_2, brightness);
                    buffer[rotated_y as usize * WIDTH + rotated_x as usize] = final_color;
                }
            }
            
            // TODO: Everything together!
            // Offset and rotate two superimposed images in opposite directions
        } else {
            // Optimized
            // For optimization code can be combined here instead of the deduplication
            // in the name of speed
        }

        // Show the buffer in the window
        if ![0, 1].contains(&mode) {
             window.update_with_buffer(&buffer, WIDTH as usize, HEIGHT as usize).unwrap();
        }

        // Display the FPS rate 3 times per second
        let now = std::time::Instant::now();
        let duration = (now - fps_time).as_secs_f32();
        fps_count += 1;
        if duration >= 0.33 {
            println!("{} fps", (fps_count as f32 / duration).round() as u32);
            fps_time = now;
            fps_count = 0;
        }
    }
}

fn get_color(image: &Image, y: usize, x: usize) -> u32 {
    let x1 = x.rem_euclid(image.width);
    let y1 = y.rem_euclid(image.height);
    let color = image.buffer[y1 * image.width + x1];
    color
}

fn get_color_new(image: &Image, y: f32, x: f32) -> u32 {
    let x1 = x.rem_euclid(image.width as f32) as usize;
    let y1 = y.rem_euclid(image.height as f32) as usize;
    let color = image.buffer[y1 * image.width + x1];
    color
}

I don’t currently understand why the code I suggested would not round appropriately (in particular, as usize rounds towards zero), but since empirically, it doesn’t, you could go for the brute force approach of taking the remainder again in usize space:

fn get_color_new(image: &Image, y: f32, x: f32) -> u32 {
    let x1 = x.rem_euclid(image.width as f32) as usize;
    let y1 = y.rem_euclid(image.height as f32) as usize;
    let x2 = x1.rem_euclid(image.width);
    let y2 = y1.rem_euclid(image.height);
    let color = image.buffer[y2 * image.width + x2];
    color
}

This is not good code and it’s not what I’d do myself — I would play with this function to understand what case causes the arithmetic to go wrong. But, working at a distance over forum posts, this will at least get it going without overflows.

A debugging option:

fn get_color_new(image: &Image, y: f32, x: f32) -> u32 {
    const DEBUG_COLOR: u32 = 0xFF80FF80;

    let x1 = x.rem_euclid(image.width as f32) as usize;
    let y1 = y.rem_euclid(image.height as f32) as usize;
    let color = image.buffer.get(y1 * image.width + x1).copied();
    color.unwrap_or(DEBUG_COLOR)
}

It will show the overflowing pixels in a debug color, which should be purple or like if I'm not mixing up the byte order.

Thank you that did indeed work. It also worked with just setting y1 again, so the issue must be there.

fn get_color_new(image: &Image, y: f32, x: f32) -> u32 {
    let x1 = x.rem_euclid(image.width as f32) as usize;
    let y1 = y.rem_euclid(image.height as f32) as usize;
    let color = image.buffer[y1.rem_euclid(image.height) * image.width + x1];
    color
}

@ProgramCrafter thank you, it did not show any debug color and worked good. But modifying it to just unwrap the color, does cause issues. The attached image below should show purple pixels, but it did not. The code and output are of my modified version without debugging.

Code

fn get_color_new(image: &Image, y: f32, x: f32) -> u32 {
    let x1 = x.rem_euclid(image.width as f32) as usize;
    let y1 = y.rem_euclid(image.height as f32) as usize;
    let color = image.buffer.get(y1 * image.width + x1).copied();
    color.unwrap()
}

Output

Failed to create server-side surface decoration: Missing
55 fps

thread 'main' (76452) panicked at src/main.rs:191:11:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
warning: queue 0x641c47fd13e0 destroyed while proxies still attached:
  wl_buffer#19 still attached
  wl_shm_pool#18 still attached
  wl_buffer#17 still attached
  wl_shm_pool#16 still attached
  wl_surface#15 still attached
  wl_shm_pool#14 still attached
  xdg_toplevel#13 still attached
  xdg_surface#12 still attached
  xdg_wm_base#11 still attached
  wl_buffer#10 still attached
  wl_shm_pool#9 still attached
  wl_surface#8 still attached
  wl_shm#7 still attached
  wl_compositor#6 still attached
  wl_pointer#5 still attached
  wl_keyboard#4 still attached
  wl_seat#3 still attached
  wl_registry#2 still attached 

Image