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
10fn 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 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
77pub 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 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 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
151pub 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
197pub 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 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
258pub 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}