gd_icon_renderer/
renderer.rs

1use image::*;
2use image::{DynamicImage, imageops};
3use imageproc::geometric_transformations::{rotate_about_center, Interpolation};
4
5use std::cmp;
6
7use crate::assets;
8use crate::assets::{LoadedSpritesheet, Animations, Sprite};
9
10/// Internal function to easily transform an image
11fn transform(image: &DynamicImage, color: Option<[f32; 3]>, scale: Option<(f32, f32)>, rotation: Option<f32>) -> DynamicImage {
12    let mut transformed_image = image.clone();
13
14    if let Some(color) = color {
15        let mut img_buffer = image.to_rgba8();
16
17        for (_x, _y, pixel) in img_buffer.enumerate_pixels_mut() {
18            for channel in 0..3 {
19                pixel.0[channel] = (pixel.0[channel] as f32 * color[channel]) as u8;
20            }
21        }
22
23        transformed_image = DynamicImage::ImageRgba8(img_buffer);
24    }
25
26    if let Some((scale_x, scale_y)) = scale {
27        let width = transformed_image.width();
28        let height = transformed_image.height();
29
30        let abs_scale_x = scale_x.abs();
31        let abs_scale_y = scale_y.abs();
32
33        transformed_image = transformed_image.resize_exact(
34            (width as f32 * abs_scale_x) as u32,
35            (height as f32 * abs_scale_y) as u32,
36            image::imageops::FilterType::Lanczos3
37        );
38
39        if scale_x < 0.0 {
40            transformed_image = transformed_image.fliph();
41        }
42        if scale_y < 0.0 {
43            transformed_image = transformed_image.flipv();
44        }
45    }
46
47    if let Some(rotation) = rotation {
48        // lets not rotate if we dont need to
49        if rotation == 0.0 {
50            return transformed_image;
51        }
52
53        let radians = rotation.to_radians();
54
55        let (width, height) = transformed_image.dimensions();
56        
57        let trig_width = (width as f32 * radians.cos() + height as f32 * radians.sin())
58            .abs()
59            .ceil() as u32;
60        let trig_height = (width as f32 * radians.sin() + height as f32 * radians.cos())
61            .abs()
62            .ceil() as u32;
63
64        let transform_x = ((trig_width as f32 / 2.0) - (width as f32 / 2.0)).ceil() as u32;
65        let transform_y = ((trig_height as f32 / 2.0) - (height as f32 / 2.0)).ceil() as u32;
66
67        let mut canvas = ImageBuffer::new(cmp::max(trig_width, width), cmp::max(trig_height, height));
68        canvas.copy_from(&transformed_image, transform_x, transform_y).expect("couldnt copy from img");
69        canvas = rotate_about_center(&canvas, radians, Interpolation::Bilinear, Rgba([0, 0, 0, 0]));
70    
71        transformed_image = DynamicImage::ImageRgba8(canvas);
72    }
73
74    return transformed_image;
75}
76
77/// Mainly for internal use; given an array of images, their sizes and colors, tints and composits them into a single image
78pub fn render_layered(images: Vec<DynamicImage>, positions: Vec<Option<(f32, f32)>>, colors: Vec<Option<[f32; 3]>>, scales: Vec<Option<(f32, f32)>>, rotations: Vec<Option<f32>>) -> Result<DynamicImage, &'static str> {
79    let transformed: Vec<DynamicImage> = images.iter().enumerate().map(|(i, img)| {
80        transform(img, colors[i], scales[i], rotations[i])
81    }).collect();
82    let sizes: Vec<(i64, i64)> = transformed.iter().map(|img| {
83        (img.width() as i64, img.height() as i64)
84    }).collect();
85
86    let positions: Vec<(f32, f32)> = images.iter().enumerate().map(|(i, _v)| {
87        positions[i].unwrap_or((0.0, 0.0))
88    }).collect();
89
90    let bounding_box = sizes
91        .iter()
92        .enumerate()
93        .map(|(i, &size)| {
94            let (width, height) = size;
95            let (x, y) = positions.get(i).cloned().unwrap_or((0.0, 0.0));
96            ((width as f32 + x.abs() * 2.0) as i32, (height as f32 + y.abs() * 2.0) as i32)
97        })
98        .fold((0, 0), |acc, size| {
99            (cmp::max(acc.0, size.0), cmp::max(acc.1, size.1))
100        });
101
102    let mut canvas = ImageBuffer::new(bounding_box.0 as u32, bounding_box.1 as u32);
103
104    // base
105    canvas.copy_from(
106        transformed.get(0).ok_or("Could not get image of inputted icon ID")?,
107        (bounding_box.0 as f32 / 2.0 + positions[0].0 as f32 - sizes[0].0 as f32 / 2.0) as u32,
108        (bounding_box.1 as f32 / 2.0 + positions[0].1 as f32 - sizes[0].1 as f32 / 2.0) as u32
109    ).expect("couldnt copy from img");
110    
111    // stacking
112    for (i, image) in transformed.iter().enumerate().skip(1) {
113        let x = (bounding_box.0 as f32 / 2.0 + positions[i].0 as f32 - image.width() as f32 / 2.0) as i64;
114        let y = (bounding_box.1 as f32 / 2.0 + positions[i].1 as f32 - image.height() as f32 / 2.0) as i64;
115    
116        imageops::overlay(&mut canvas, image, x, y)
117    }
118
119    return Ok(DynamicImage::ImageRgba8(canvas));
120}
121
122fn is_black(c: [f32; 3]) -> bool {
123    c == [0.0, 0.0, 0.0]
124}
125
126fn crop_whitespace(img: DynamicImage) -> DynamicImage {
127    let (width, height) = img.dimensions();
128
129    let mut left = width;
130    let mut right = 0;
131    let mut top = height;
132    let mut bottom = 0;
133    
134    for x in 0..width {
135        for y in 0..height {
136            let pixel = img.get_pixel(x, y);
137            if pixel[3] != 0 {
138                left = left.min(x);
139                right = right.max(x);
140                top = top.min(y);
141                bottom = bottom.max(y);
142            }
143        }
144    }
145
146    let cropped_image = img.clone().crop(left, top, right - left, bottom - top);
147
148    return cropped_image
149}
150
151/// Renders out a non-robot/spider icon. You may be looking for `render_icon`.
152pub fn render_normal(basename: String, col1: [f32; 3], col2: [f32; 3], glow: bool, game_sheet_02: LoadedSpritesheet, game_sheet_glow: LoadedSpritesheet) -> Result<DynamicImage, &'static str> {
153    let glow_col = if is_black(col2) { if is_black(col1) { [1.0, 1.0, 1.0] } else { col1 } } else { col2 };
154
155    let layers = vec![
156        (if glow || (is_black(col1) && is_black(col2)) {
157            assets::get_sprite_from_loaded(&game_sheet_glow, format!("{}_glow_001.png", basename))
158        } else {
159            None
160        }),
161        assets::get_sprite_from_loaded(&game_sheet_02, format!("{}_2_001.png", basename)),
162        assets::get_sprite_from_loaded(&game_sheet_02, format!("{}_3_001.png", basename)),
163        assets::get_sprite_from_loaded(&game_sheet_02, format!("{}_001.png", basename)),
164        assets::get_sprite_from_loaded(&game_sheet_02, format!("{}_extra_001.png", basename))
165    ];
166
167    let colors: Vec<Option<[f32; 3]>> = vec![
168        Some(glow_col),
169        Some(col2),
170        None,
171        Some(col1),
172        None
173    ];
174
175    let layered_images = render_layered(
176        layers.iter()
177            .filter_map(|s| s.as_ref().map(|(img, _spr)| img.to_owned()))
178            .collect(),
179        layers.iter()
180            .filter_map(|s| s.as_ref().map(|(_img, spr)| Some((spr.offset.0, spr.offset.1 * -1.0))))
181            .collect(), 
182        colors.iter()
183            .enumerate()
184            .filter_map(|(i, color)| layers[i].clone().map(|_| color.to_owned()))
185            .collect(),
186        vec![None, None, None, None, None],
187        vec![None, None, None, None, None]
188    )?;
189
190    return Ok(crop_whitespace(layered_images));
191}
192
193fn flip(scale: (f32, f32), flipped: (bool, bool)) -> (f32, f32) {
194    (scale.0 * (if flipped.0 { -1 } else { 1 }) as f32, scale.1 * (if flipped.1 { -1 } else { 1 }) as f32)
195}
196
197/// Renders out a robot/spider icon. You may be looking for `render_icon`.
198pub fn render_zany(basename: String, col1: [f32; 3], col2: [f32; 3], glow: bool, game_sheet_02: LoadedSpritesheet, _game_sheet_glow: LoadedSpritesheet, animations: Animations) -> Result<DynamicImage, &'static str> {
199    let glow_col = if is_black(col2) { if is_black(col1) { [1.0, 1.0, 1.0] } else { col1 } } else { col2 };
200    let glow = glow || (is_black(col1) && is_black(col2));
201
202    let mut anim = animations.get("Robot_idle_001.png").unwrap_or_else(|| animations.get("Spider_idle_001.png").expect("no animations found")).clone();
203    anim.sort_by_key(|spr| spr.z);
204
205    let mut layers: Vec<(Option<(DynamicImage, Sprite)>, (f32, f32), (f32, f32), f64, bool, Option<[f32; 3]>)> = Vec::new();
206
207    for a in anim {
208        let texture_name = a.texture.replace("spider_01", &basename).replace("robot_01", &basename);
209        let mut names = vec![
210            texture_name.replace("_001.png", "_2_001.png"),
211            texture_name.replace("_001.png", "_3_001.png"),
212            texture_name.clone(),
213            texture_name.replace("_001.png", "_extra_001.png")
214        ];
215        let mut colors = vec![
216            Some(col2),
217            None,
218            Some(col1),
219            None
220        ];
221
222        if glow {
223            names.push(texture_name.replace("_001.png", "_glow_001.png"));
224            colors.push(Some(glow_col));
225        }
226
227        layers.extend(names.iter().enumerate().map(|(i, v)| {
228            (
229                assets::get_sprite_from_loaded(&game_sheet_02, v.clone()),
230                a.position,
231                flip(a.scale, a.flipped),
232                a.rotation,
233                glow && i == names.len() - 1,
234                colors[i]
235            )
236        }))
237    }
238
239    // put glow b4 everything else
240    layers.sort_by_key(|t| if t.4 { 0 } else { 1 });
241
242    let layers_r = layers.iter()
243        .filter(|v| v.0.is_some())
244        .filter_map(|(opt_sprite, pos, scale, rot, glow, color)| opt_sprite.clone().map(|sprite| ((sprite.0, sprite.1), *pos, *scale, *rot, *glow, *color)))
245        .collect::<Vec<((DynamicImage, Sprite), (f32, f32), (f32, f32), f64, bool, Option<[f32; 3]>)>>();
246
247    let layered_images = render_layered(
248        layers_r.iter().map(|t| t.0.0.clone()).collect(),
249        layers_r.iter().map(|t| Some((t.0.1.offset.0 + t.1.0 * 4.0, t.0.1.offset.1 * -1.0  + t.1.1 * -4.0))).collect(),
250        layers_r.iter().map(|t| t.5).collect(),
251        layers_r.iter().map(|t| Some(t.2)).collect(),
252        layers_r.iter().map(|t| Some(t.3 as f32)).collect()
253    )?;
254
255    return Ok(crop_whitespace(layered_images));
256}
257
258/// The main entrypoint for icon rendering; this should be all you need to render out an icon.
259///
260/// `gamemode` must be one of `cube`, `ship`, `ball`, `ufo`, `wave`, `robot`, or `spider`
261pub fn render_icon(gamemode_str: &str, icon: i32, col1: [f32; 3], col2: [f32; 3], glow: bool, game_sheet_02: LoadedSpritesheet, game_sheet_glow: LoadedSpritesheet, robot_animations: Animations, spider_animations: Animations) -> Result<DynamicImage, &'static str> {
262    let gamemode = crate::constants::GAMEMODES.get(gamemode_str).ok_or("Invalid gamemode")?;
263
264    if gamemode.zany {
265        return Ok(render_zany(format!("{}{:02}", gamemode.prefix, icon), col1, col2, glow, game_sheet_02, game_sheet_glow, if gamemode_str == "robot" { robot_animations } else { spider_animations }))?
266    } else {
267        return Ok(render_normal(format!("{}{:02}", gamemode.prefix, icon), col1, col2, glow, game_sheet_02, game_sheet_glow))?
268    }
269}