Rotate super-imposed images over each other

A better explanation is here: Rotate super-imposed images over each other - #4 by canina

I have to use async to rotate two images over each other translucently. When trying myself I assumed I needed a big function like.

async fn rotate_translucently(translated_x: f32, translated_y: f32, cos: f32, sin: f32, image: Image, brightness: f32,y: usize, buffer: Vec<u32>, x: usize){
    let rotated_x: f32 = translated_x * cos + translated_y * sin;
    let rotated_y: f32 = translated_x * sin - translated_y * cos;
    let mut color: u32 = get_color_new(&image, rotated_y, rotated_x);
    
    let [blue, green, red, alpha] = color.to_ne_bytes();
    color = u32::from_ne_bytes([
        set_pixel_value(brightness, blue),
        set_pixel_value(brightness, green),
        set_pixel_value(brightness, red),
        alpha,
    ]);
    
    buffer[y * WIDTH + x] = color;
}

Then I could call it for both image1 and image2. But it seems to big. And I can't call my buffer right since it is a global. What would be the best way to try this? My current solution only rotates image2 because the buffer gets overwritten immeditatly after updating the buffer for image 1.

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 = 4;
    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 cos: f32 = rotation.cos();
            let sin: f32 = rotation.sin();
            let brightness: f32 = 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;
                    
                    for i in 0..2 {    
                        if i == 0 { 
                            let rotated_x = translated_x * cos + translated_y * sin;
                            let rotated_y = translated_x * sin - translated_y * cos;
                            let mut color = get_color_new(&image1, rotated_y, rotated_x);
                            
                            let [blue, green, red, alpha] = color.to_ne_bytes();
                            color = u32::from_ne_bytes([
                                set_pixel_value(brightness, blue),
                                set_pixel_value(brightness, green),
                                set_pixel_value(brightness, red),
                                alpha,
                            ]);
                            
                            
                            buffer[y * WIDTH + x] = color;
                        }
                        else if i == 1 {
                            let rotated_x = translated_x * cos - translated_y * sin;
                            let rotated_y = translated_x * sin + translated_y * cos;
                            let mut color = get_color_new(&image2, rotated_y, rotated_x);
                            
                            let [blue, green, red, alpha] = color.to_ne_bytes();
                            color = u32::from_ne_bytes([
                                set_pixel_value(brightness, blue),
                                set_pixel_value(brightness, green),
                                set_pixel_value(brightness, red),
                                alpha,
                            ]);
                            
                            
                            buffer[y * WIDTH + x] = 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.rem_euclid(image.height) * image.width + x1];
    color
}

async fn rotate_translucently(translated_x: f32, translated_y: f32, cos: f32, sin: f32, image: Image, brightness: f32,y: usize, buffer: Vec<u32>, x: usize){
    let rotated_x: f32 = translated_x * cos + translated_y * sin;
    let rotated_y: f32 = translated_x * sin - translated_y * cos;
    let mut color: u32 = get_color_new(&image, rotated_y, rotated_x);
    
    let [blue, green, red, alpha] = color.to_ne_bytes();
    color = u32::from_ne_bytes([
        set_pixel_value(brightness, blue),
        set_pixel_value(brightness, green),
        set_pixel_value(brightness, red),
        alpha,
    ]);
    
    buffer[y * WIDTH + x] = color; // cannot borrow `buffer` as mutable, as it is not declared as mutable
cannot borrow as mutable
}

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
}

I don’t really get why you need async here? All your code seems to be synchronous, am I missing something?

first of all, do you understand what the async keyword in rust is used for? I don't know why you want async, but it's completely irrelevant for an image operation.

now, on the image operation: you are conflating two different operations: rotation and blending. you can rotate an image clockwise, or counter-clockwise, or you can rotate by some angle, but there's no such thing as "rotate translucently".

I think you should understand the problem clearly and clarify the correct concepts and terminologies before writing the actual code.

I want to rotate to images at the same time in a different direction over each other. As shown in the image below. I don't think this can be done synchronously, because I need to modfiy the buffer per pixel.

@nerditation I could have made my isssue a bit clearer, sorry for the confusion. With rotating translucently I meant that I decreased the brightness of all the pixels, so that you can see both images at the same time. The issue is specifically about mode 4 by the way. You can ignore all the other modes. Hopefully this clarifies it a bit.

async is (mainly) for IO. IO means: Waiting for data receiving from a network connection, or waiting until the operating system has written data to the disk, or waiting for the user to move the mouse and so on.

But you have to compute your final image, you don't have to wait for something to happen.

Ignoring multiple CPU cores, absolute nothing happens in a computer at the same time (conceptually). All steps are purely sequential. It might be, that these steps are so fast, that it looks like they happen parallel. But this is an illusion.

For your image rotation,this is something like this:

let orig_image_a = ...;
let orig_image_b = ...;

let temporary_buffer_a = rotate(orig_image_a, angle_a);
let temporary_buffer_b = rotate(orig_image_b, angle_b);

let result_buffer = blend_together(temporary_buffer_a, temporary_buffer_b);

do you mean you want to return a new image instead of modifying the pixels of the old image? what you need is a heap allocation for the buffer, that's nothing to do with async.

you can either clone the buffer of an old image, or allocate a new buffer initialized with zeros. which is more efficient depending on the algorithm you perform.

please learn and understand the concept before trying to use a feature, use terminologies properly. the term "synchronously" has a specific definition in programming languages, don't just guess what a keyword (such as async) means.

again, that's two different things, the rotation only cares about the angle (and maybe the pivot if your origin has been translated), and the translucency is a property of the composition.

you can perform a rotation followed by an alpha blending, in two separate steps, that's fine, just don't call it rotate_translucently, that name is gibberish. use proper names in code, if you want it to be understandable by human.

Honestly, I don't really understand what's your question about then. I mentioned in your code, that you're trying to set value in buffer from a function and it doesn't compile. To solve this one specifically, you could just return a color you calculated from this function and set it in your main function or elsewhere the buffer is in scope.

But also I'd like to say, that it looks like you're trying to solve quite complex task from the start. I'd like to recommend you to read rustbook thoroughly, it's an excellent starting point where you can find really good explanations how to write your code without compilation errors, what is asynchronous code and where it makes sense to use it, it also has some example projects. I believe it can improve your skill and the code you write significantly.

Performance can be improved obviously, but it works fine.

let brightness: f32 = 0.5;
            let cos = rotation.cos();
            let sin = rotation.sin();
            
            for x in 0..HEIGHT {
                for y in 0..WIDTH {
                    let translated_x_image = x as f32 - x_offset as f32;
                    let translated_y_image = y as f32 - y_offset as f32;
                    
                    // Position image1
                    let rotated_x_image1 = translated_x_image * cos + translated_y_image * sin;
                    let rotated_y_image1 = translated_x_image * sin - translated_y_image * cos;
                    
                    // Position image2
                    let rotated_x_image2 = translated_x_image * cos - translated_y_image * sin;
                    let rotated_y_image2 = translated_x_image * sin + translated_y_image * cos;
                    
                    // Set brightness for colors
                    let color1 = get_color_new(&image1, rotated_y_image1, rotated_x_image1);
                    let color2 = get_color_new(&image2, rotated_y_image2, rotated_x_image2);
                    let final_color = blend_colors(color1, color2, brightness);
                    buffer[y * WIDTH + x] = final_color;
                }
            }

Edit: nevermind it crashes after a while:

thread 'main' (121476) panicked at src/main.rs:256:29:
index out of bounds: the len is 225920 but the index is 225920
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
warning: queue 0x5721ccca73e0 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

What is the source code in line 256 at your main.rs?

Maybe you have a classic Off-by-one error - Wikipedia ?

Full code 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;

    //  Custom added constants
    let x_offset: f32 = WIDTH as f32 / 2 as f32;
    let y_offset: f32 = HEIGHT as f32 / 2 as f32;
    let brightness: f32 = 0.5;
    
    // 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 = 4;
    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 {
            let cos: f32 = rotation.cos();
            let sin: f32 = rotation.sin();
            // TODO: Offset and rotate image 1
            for x in 0..HEIGHT {
                for y in 0..WIDTH {
                    let translated_x = x as f32 - x_offset;
                    let translated_y = y as f32 - y_offset;

                    let rotated_x = translated_x * cos + translated_y * sin;
                    let rotated_y = translated_x * sin - translated_y * cos;

                    let color = get_color_with_float_coordinates(&image1, rotated_y, rotated_x);
                    buffer[y * WIDTH + x] = color;
                }
            }
        } else if mode == 4 {
            // Offset and rotate two superimposed images in opposite directions
            let cos: f32 = rotation.cos();
            let sin: f32 = rotation.sin();

            for x in 0..HEIGHT {
                let translated_x_image = x as f32 - x_offset;

                for y in 0..WIDTH {
                    let translated_y_image = y as f32 - y_offset;

                    // Position image1
                    let rotated_x_image1 = translated_x_image * cos + translated_y_image * sin;
                    let rotated_y_image1 = translated_x_image * sin - translated_y_image * cos;

                    // Position image2
                    let rotated_x_image2 = translated_x_image * cos - translated_y_image * sin;
                    let rotated_y_image2 = translated_x_image * sin + translated_y_image * cos;

                    // Set brightness for colors
                    let color1 = get_color_with_float_coordinates(&image1, rotated_y_image1, rotated_x_image1);
                    let color2 = get_color_with_float_coordinates(&image2, rotated_y_image2, rotated_x_image2);
                    let final_color = blend_colors(color1, color2, brightness);
                    buffer[y * WIDTH + x] = final_color;
                }
            }
        } else if mode == 5 {
            // Optimized
            // For optimization code can be combined here instead of the deduplication
            // in the name of speed
            let cos: f32 = rotation.cos();
            let sin: f32 = rotation.sin();
            
            for x in 0..900 {
                let translated_x_image = x as f32 - x_offset;
                let translated_x_image_cos = translated_x_image * cos;
                let translated_x_image_sin = translated_x_image * sin;

                for y in 0..900 {
                    let translated_y_image = y as f32 - y_offset;

                    let translated_y_image_cos = translated_y_image * cos;
                    let translated_y_image_sin = translated_y_image * sin;

                    let final_color = {
                        let mut color_1 = image1.buffer[
                            ((translated_x_image_sin - translated_y_image_cos).rem_euclid(image1.height as f32) as usize).rem_euclid(image1.height)
                            * image1.width
                            + (translated_x_image_cos + translated_y_image_sin).rem_euclid(image1.width as f32) as usize
                        ];
                        let mut color_2 = image2.buffer[
                            ((translated_x_image_sin + translated_y_image_cos).rem_euclid(image2.height as f32) as usize).rem_euclid(image2.height)
                            * image2.width
                            + (translated_x_image_cos - translated_y_image_sin).rem_euclid(image2.width as f32) as usize
                        ];

                        color_1 = {
                            for value in color_1.to_ne_bytes() {
                                {
                                    (value as f32 * 0.5) as u8;
                                };
                            }
                        color_1
                        };
                        let [blue_color_1, green_color_1, red_color_1, alpha_color_1] = color_1.to_ne_bytes();

                        color_2 = {
                            for value in color_2.to_ne_bytes() {
                                {
                                (value as f32 * 0.5) as u8;
                            };
                            }
                            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
                    };
                    buffer[y * 900 + x] = final_color;
                }
            }
        }

        // 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_with_float_coordinates(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
}

Output

hread 'main' (19885) panicked at src/main.rs:241:29:
index out of bounds: the len is 225920 but the index is 225920
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
warning: queue 0x5be7cb5e23e0 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

The lib.rs is unchanged. Thank you for offering help btw!

You have floating point rounding issues:

let rotated_x_image1 = translated_x_image * cos + translated_y_image * sin;
let rotated_y_image1 = translated_x_image * sin - translated_y_image * cos;

let rotated_x_image2 = translated_x_image * cos - translated_y_image * sin;
let rotated_y_image2 = translated_x_image * sin + translated_y_image * cos;

let color1 = get_color_with_float_coordinates(&image1, rotated_y_image1, rotated_x_image1);

// ...

fn get_color_with_float_coordinates(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
}

Quote from: f32.rem_euclid

In particular, the return value r satisfies 0.0 <= r < rhs.abs() in most cases. However, due to a floating point round-off error it can result in r == rhs.abs(), violating the mathematical definition, if self is much smaller than rhs.abs() in magnitude and self < 0.0.

Floating point numbers are always only an approximation. You have to anticipate errors when working with floats. Adding and especially subtracting floats introduce often relatively huge errors.

A quick fix for your use case might be:

let x1 = (x.rem_euclid(image.width as f32) as usize).clamp(0, image.width - 1);
let y1 = (y.rem_euclid(image.height as f32) as usize).clamp(0, image.height - 1);

Please note the important -1...

Thank you that makes sense, and it worked for me!