some small refactoring
This commit is contained in:
commit
0f7ecba1ab
8 changed files with 972 additions and 0 deletions
165
src/assets.rs
Normal file
165
src/assets.rs
Normal file
|
@ -0,0 +1,165 @@
|
|||
use plist;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use image::*;
|
||||
use image::{DynamicImage, ImageBuffer, imageops};
|
||||
|
||||
// "{1,2}" -> `(1, 2)`
|
||||
fn parse_vec(str: &str) -> (i32, i32) {
|
||||
let parts: Vec<&str> = str[1..str.len()-1].split(",").collect();
|
||||
let a: Vec<i32> = parts
|
||||
.iter()
|
||||
.map(|s| s.trim().parse::<i32>().unwrap())
|
||||
.collect();
|
||||
|
||||
return (a[0], a[1])
|
||||
}
|
||||
// parse_vec, but for float64
|
||||
fn parse_vec_f32(str: &str) -> (f32, f32) {
|
||||
let parts: Vec<&str> = str[1..str.len()-1].split(",").collect();
|
||||
let a: Vec<f32> = parts
|
||||
.iter()
|
||||
.map(|s| s.trim().parse::<f32>().unwrap())
|
||||
.collect();
|
||||
|
||||
return (a[0], a[1])
|
||||
}
|
||||
// `"{{1,2},{3,4}}"` -> `{{1, 2}, {3, 4}}`
|
||||
fn parse_rect_vecs(str: &str) -> ((i32, i32), (i32, i32)) {
|
||||
let cleaned_str = str.replace("{", "").replace("}", "");
|
||||
let parts: Vec<&str> = cleaned_str.split(",").collect();
|
||||
let a: Vec<i32> = parts
|
||||
.iter()
|
||||
.map(|s| s.trim().parse::<i32>().unwrap())
|
||||
.collect();
|
||||
|
||||
return ((a[0], a[1]), (a[2], a[3]))
|
||||
}
|
||||
|
||||
// Represents a sprite along with its texture data in a spritesheet.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Sprite {
|
||||
// Whenever rendering the sprite, offset it by this much
|
||||
pub offset: (f32, f32),
|
||||
// {left, top}, {width, height}. Controls the cropping
|
||||
rect: ((i32, i32), (i32, i32)),
|
||||
// Whether the texture needs to be counter-rotated 90 degrees counter-clockwise
|
||||
rotated: bool,
|
||||
size: (i32, i32),
|
||||
// Difference between this and `size` is unknown to me
|
||||
source_size: (i32, i32)
|
||||
}
|
||||
|
||||
impl Sprite {
|
||||
// Shorthand for initializing a sprite with its .plist representation.
|
||||
fn initialize(obj: plist::Value) -> Sprite {
|
||||
let hash = obj.as_dictionary().expect("object must be a dict");
|
||||
|
||||
let hash_keys = vec!["spriteOffset", "spriteSize", "spriteSourceSize", "textureRect", "textureRotated"];
|
||||
|
||||
let isolated: Vec<(&&str, Option<&plist::Value>)> = hash_keys
|
||||
.iter()
|
||||
.map(|s| (s, hash.get(s)))
|
||||
.collect();
|
||||
|
||||
let missing: Vec<&(&&str, Option<&plist::Value>)> = isolated
|
||||
.iter()
|
||||
.filter(|&&(_, value)| value.is_none())
|
||||
.collect();
|
||||
|
||||
if !missing.is_empty() {
|
||||
let missing_entries: Vec<&str> = missing.iter().map(|(&key, _)| key).collect();
|
||||
panic!("missing entries: {:?}", missing_entries);
|
||||
}
|
||||
|
||||
let isolated_hash: HashMap<String, plist::Value> = isolated
|
||||
.iter()
|
||||
.map(|&(key, value)| (key.to_string(), value.expect("value is none after checking").clone()))
|
||||
.collect();
|
||||
|
||||
return Sprite {
|
||||
offset: parse_vec_f32(isolated_hash.get("spriteOffset").expect("missing spriteOffset").as_string().expect("spriteOffset is not a string")),
|
||||
rect: parse_rect_vecs(isolated_hash.get("textureRect").expect("missing textureRect").as_string().expect("textureRect is not a string")),
|
||||
rotated: isolated_hash.get("textureRotated").unwrap_or(&plist::Value::from(false)).as_boolean().expect("textureRotated is not a boolean").clone(),
|
||||
size: parse_vec(isolated_hash.get("spriteSize").expect("missing spriteSize").as_string().expect("spriteSize is not a string")),
|
||||
source_size: parse_vec(isolated_hash.get("spriteSourceSize").expect("missing spriteSourceSize").as_string().expect("spriteSourceSize is not a string"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Represents a spritesheet along with its sprites.
|
||||
#[derive(Clone)]
|
||||
pub struct Spritesheet {
|
||||
sprites: HashMap<String, Sprite>,
|
||||
|
||||
texture_file_name: String,
|
||||
size: (i32, i32)
|
||||
}
|
||||
|
||||
impl Spritesheet {
|
||||
// Shorthand for initializing a spritesheet with its .plist representation.
|
||||
fn initialize(obj: plist::Value) -> Spritesheet {
|
||||
let hash = obj.as_dictionary().expect("object must be a dict");
|
||||
|
||||
let sprites = hash.get("frames").expect("object must have a `frames` object").as_dictionary().expect("`frames` must be a dict");
|
||||
let metadata = hash.get("metadata").expect("object must have a `metadata` object").as_dictionary().expect("`metadata` must be a dict");
|
||||
|
||||
return Spritesheet {
|
||||
sprites: sprites.iter().map(|(key, value)| (key.clone(), Sprite::initialize(value.clone()))).collect(),
|
||||
texture_file_name: metadata.get("textureFileName").expect("metadata must have a `textureFileName` object").as_string().expect("`textureFileName` must be a string").to_string(),
|
||||
size: parse_vec(metadata.get("size").expect("metadata must have a `size` object").as_string().expect("`size` must be a string"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stores both a spritesheet and its associated `DynamicImage` for easy access.
|
||||
#[derive(Clone)]
|
||||
pub struct LoadedSpritesheet {
|
||||
spritesheet: Spritesheet,
|
||||
texture: DynamicImage
|
||||
}
|
||||
|
||||
// Loads the spritesheet and readies the associated image.
|
||||
pub fn load_spritesheet(path: &str) -> LoadedSpritesheet {
|
||||
return LoadedSpritesheet {
|
||||
spritesheet: Spritesheet::initialize(plist::from_file(path).expect("could not load plist")),
|
||||
texture: image::open(path.replace(".plist", ".png")).expect("could not load texture")
|
||||
}
|
||||
}
|
||||
|
||||
// Trims out a sprite from an image according to a .plist spritesheet.
|
||||
pub fn get_sprite(spritesheet: Spritesheet, img: DynamicImage, key: String) -> Option<(DynamicImage, Sprite)> {
|
||||
let sprite = spritesheet.sprites.get(&key);
|
||||
|
||||
let mut canvas = img.clone();
|
||||
|
||||
if sprite.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(sprite) = sprite {
|
||||
let rect = sprite.rect;
|
||||
|
||||
let (mut left, mut top, mut width, mut height) = (rect.0.0, rect.0.1, rect.1.0, rect.1.1);
|
||||
if sprite.rotated {
|
||||
(left, top, width, height) = (left, top, height, width);
|
||||
}
|
||||
|
||||
canvas = canvas.crop(left as u32, top as u32, width as u32, height as u32);
|
||||
|
||||
if sprite.rotated {
|
||||
canvas = canvas.rotate270();
|
||||
}
|
||||
|
||||
return Some((canvas, sprite.clone()));
|
||||
}
|
||||
|
||||
panic!("The sprite should have been found in the spritesheet or not found at all")
|
||||
}
|
||||
|
||||
pub fn get_sprite_from_loaded(spritesheet: LoadedSpritesheet, key: String) -> Option<(DynamicImage, Sprite)> {
|
||||
let texture = spritesheet.texture.clone();
|
||||
let sprite = get_sprite(spritesheet.spritesheet.clone(), texture, key);
|
||||
return sprite;
|
||||
}
|
65
src/constants.rs
Normal file
65
src/constants.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use std::{collections::HashMap, sync::LazyLock};
|
||||
|
||||
use maplit::hashmap;
|
||||
|
||||
pub const COLORS: &'static [[f32; 3]] = &[
|
||||
[125.0 / 255.0, 255.0 / 255.0, 0.0 / 255.0],
|
||||
[0.0 / 255.0, 255.0 / 255.0, 0.0 / 255.0],
|
||||
[0.0 / 255.0, 255.0 / 255.0, 125.0 / 255.0],
|
||||
[0.0 / 255.0, 255.0 / 255.0, 255.0 / 255.0],
|
||||
[0.0 / 255.0, 125.0 / 255.0, 255.0 / 255.0],
|
||||
[0.0 / 255.0, 0.0 / 255.0, 255.0 / 255.0],
|
||||
[125.0 / 255.0, 0.0 / 255.0, 255.0 / 255.0],
|
||||
[255.0 / 255.0, 0.0 / 255.0, 255.0 / 255.0],
|
||||
[255.0 / 255.0, 0.0 / 255.0, 125.0 / 255.0],
|
||||
[255.0 / 255.0, 0.0 / 255.0, 0.0 / 255.0],
|
||||
[255.0 / 255.0, 125.0 / 255.0, 0.0 / 255.0],
|
||||
[255.0 / 255.0, 255.0 / 255.0, 0.0 / 255.0],
|
||||
[255.0 / 255.0, 255.0 / 255.0, 255.0 / 255.0],
|
||||
[185.0 / 255.0, 0.0 / 255.0, 255.0 / 255.0],
|
||||
[255.0 / 255.0, 185.0 / 255.0, 0.0 / 255.0],
|
||||
[0.0 / 255.0, 0.0 / 255.0, 0.0 / 255.0],
|
||||
[0.0 / 255.0, 200.0 / 255.0, 255.0 / 255.0],
|
||||
[175.0 / 255.0, 175.0 / 255.0, 175.0 / 255.0],
|
||||
[90.0 / 255.0, 90.0 / 255.0, 90.0 / 255.0],
|
||||
[255.0 / 255.0, 125.0 / 255.0, 125.0 / 255.0],
|
||||
[0.0 / 255.0, 175.0 / 255.0, 75.0 / 255.0],
|
||||
[0.0 / 255.0, 125.0 / 255.0, 125.0 / 255.0],
|
||||
[0.0 / 255.0, 75.0 / 255.0, 175.0 / 255.0],
|
||||
[75.0 / 255.0, 0.0 / 255.0, 175.0 / 255.0],
|
||||
[125.0 / 255.0, 0.0 / 255.0, 125.0 / 255.0],
|
||||
[175.0 / 255.0, 0.0 / 255.0, 75.0 / 255.0],
|
||||
[175.0 / 255.0, 75.0 / 255.0, 0.0 / 255.0],
|
||||
[125.0 / 255.0, 125.0 / 255.0, 0.0 / 255.0],
|
||||
[75.0 / 255.0, 175.0 / 255.0, 0.0 / 255.0],
|
||||
[255.0 / 255.0, 75.0 / 255.0, 0.0 / 255.0],
|
||||
[150.0 / 255.0, 50.0 / 255.0, 0.0 / 255.0],
|
||||
[150.0 / 255.0, 100.0 / 255.0, 0.0 / 255.0],
|
||||
[100.0 / 255.0, 150.0 / 255.0, 0.0 / 255.0],
|
||||
[0.0 / 255.0, 150.0 / 255.0, 100.0 / 255.0],
|
||||
[0.0 / 255.0, 100.0 / 255.0, 150.0 / 255.0],
|
||||
[100.0 / 255.0, 0.0 / 255.0, 150.0 / 255.0],
|
||||
[150.0 / 255.0, 0.0 / 255.0, 100.0 / 255.0],
|
||||
[150.0 / 255.0, 0.0 / 255.0, 0.0 / 255.0],
|
||||
[0.0 / 255.0, 150.0 / 255.0, 0.0 / 255.0],
|
||||
[0.0 / 255.0, 0.0 / 255.0, 150.0 / 255.0],
|
||||
[125.0 / 255.0, 255.0 / 255.0, 175.0 / 255.0],
|
||||
[125.0 / 255.0, 125.0 / 255.0, 255.0 / 255.0]
|
||||
];
|
||||
|
||||
// `zany` = uses 2.0 gamemode render system w/ multiple moving parts
|
||||
pub struct Gamemode {
|
||||
prefix: String,
|
||||
zany: bool
|
||||
}
|
||||
|
||||
pub static GAMEMODES: LazyLock<HashMap<&str, Gamemode>> = LazyLock::new(|| { hashmap! {
|
||||
"cube" => Gamemode { prefix: "player_".to_string(), zany: false },
|
||||
"ship" => Gamemode { prefix: "ship_".to_string(), zany: false },
|
||||
"ball" => Gamemode { prefix: "player_ball_".to_string(), zany: false },
|
||||
"ufo" => Gamemode { prefix: "bird_".to_string(), zany: false },
|
||||
"wave" => Gamemode { prefix: "dart_".to_string(), zany: false },
|
||||
// unimplemented
|
||||
// "robot" => Gamemode { prefix: "robot_".to_string(), zany: true },
|
||||
// "spider" => Gamemode { prefix: "spider_".to_string(), zany: true },
|
||||
}});
|
36
src/lib.rs
Normal file
36
src/lib.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
#![feature(lazy_cell)]
|
||||
|
||||
pub mod assets;
|
||||
pub mod constants;
|
||||
pub mod renderer;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use renderer::*;
|
||||
use assets::*;
|
||||
|
||||
// not actually used, just for benchmarking
|
||||
use std::time::Instant;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let game_sheet_02 = load_spritesheet("assets/GJ_GameSheet02-uhd.plist");
|
||||
let game_sheet_glow = load_spritesheet("assets/GJ_GameSheetGlow-uhd.plist");
|
||||
|
||||
let start = Instant::now();
|
||||
let rendered_img = render_normal(
|
||||
"ship_18".to_string(),
|
||||
[0.0/255.0, 0.0/255.0, 0.0/255.0],
|
||||
[0.0/255.0, 0.0/255.0, 0.0/255.0],
|
||||
true,
|
||||
game_sheet_02,
|
||||
game_sheet_glow,
|
||||
);
|
||||
|
||||
rendered_img.save("rendered_icon.png").expect("saving image failed");
|
||||
let end = Instant::now();
|
||||
println!("Time elapsed: {:?}", end.duration_since(start));
|
||||
}
|
||||
}
|
147
src/renderer.rs
Normal file
147
src/renderer.rs
Normal file
|
@ -0,0 +1,147 @@
|
|||
use image::*;
|
||||
use image::{DynamicImage, imageops};
|
||||
|
||||
use std::cmp;
|
||||
|
||||
use crate::assets;
|
||||
use crate::assets::LoadedSpritesheet;
|
||||
|
||||
// Internal function to easily transform an image
|
||||
fn transform(image: &DynamicImage, color: Option<[f32; 3]>, scale: Option<(f32, f32)>, rotation: Option<i32>) -> DynamicImage {
|
||||
let mut transformed_image = image.clone();
|
||||
|
||||
if let Some(color) = color {
|
||||
let mut img_buffer = image.to_rgba8();
|
||||
|
||||
for (_x, _y, pixel) in img_buffer.enumerate_pixels_mut() {
|
||||
for channel in 0..3 {
|
||||
pixel.0[channel] = (pixel.0[channel] as f32 * color[channel]) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
transformed_image = DynamicImage::ImageRgba8(img_buffer);
|
||||
}
|
||||
|
||||
if let Some((scale_x, scale_y)) = scale {
|
||||
let width = transformed_image.width();
|
||||
let height = transformed_image.height();
|
||||
|
||||
let abs_scale_x = scale_x.abs();
|
||||
let abs_scale_y = scale_y.abs();
|
||||
|
||||
transformed_image = transformed_image.resize_exact(
|
||||
(width as f32 * abs_scale_x) as u32,
|
||||
(height as f32 * abs_scale_y) as u32,
|
||||
image::imageops::FilterType::Lanczos3
|
||||
);
|
||||
|
||||
if scale_x < 0.0 {
|
||||
transformed_image = transformed_image.fliph();
|
||||
}
|
||||
if scale_y < 0.0 {
|
||||
transformed_image = transformed_image.flipv();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(rotation) = rotation {
|
||||
match rotation {
|
||||
0 => (),
|
||||
90 => transformed_image = transformed_image.rotate90(),
|
||||
180 => transformed_image = transformed_image.rotate180(),
|
||||
270 => transformed_image = transformed_image.rotate270(),
|
||||
_ => panic!("rotation must be 0, 90, 180, or 270"),
|
||||
}
|
||||
}
|
||||
|
||||
return transformed_image;
|
||||
}
|
||||
|
||||
// Mainly for internal use; given an array of images, their sizes and colors, tints and composits them into a single image
|
||||
pub fn render_layered(images: Vec<DynamicImage>, positions: Vec<Option<(f32, f32)>>, colors: Vec<[f32; 3]>, scales: Vec<Option<(f32, f32)>>, rotations: Vec<Option<i32>>) -> DynamicImage {
|
||||
let transformed: Vec<DynamicImage> = images.iter().enumerate().map(|(i, img)| {
|
||||
transform(img, Some(colors[i]), scales[i], rotations[i])
|
||||
}).collect();
|
||||
let sizes: Vec<(i64, i64)> = transformed.iter().map(|img| {
|
||||
(img.width() as i64, img.height() as i64)
|
||||
}).collect();
|
||||
|
||||
let positions: Vec<(f32, f32)> = images.iter().enumerate().map(|(i, _v)| {
|
||||
positions[i].unwrap_or((0.0, 0.0))
|
||||
}).collect();
|
||||
|
||||
let bounding_box = sizes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold((0, 0), |acc, (i, &size)| {
|
||||
let (width, height) = size;
|
||||
let (x, y) = positions.get(i).cloned().unwrap_or((0.0, 0.0));
|
||||
|
||||
(
|
||||
cmp::max(acc.0, (width as f32 + x.abs() * 2.0) as i32),
|
||||
cmp::max(acc.1, (height as f32 + y.abs() * 2.0) as i32)
|
||||
)
|
||||
});
|
||||
|
||||
let mut canvas = ImageBuffer::new(bounding_box.0 as u32, bounding_box.1 as u32);
|
||||
|
||||
// base
|
||||
canvas.copy_from(
|
||||
transformed.get(0).expect("no images provided"),
|
||||
(bounding_box.0 as f32 / 2.0 + positions[0].0 as f32 - sizes[0].0 as f32 / 2.0) as u32,
|
||||
(bounding_box.1 as f32 / 2.0 + positions[0].1 as f32 - sizes[0].1 as f32 / 2.0) as u32
|
||||
).expect("couldnt copy from img");
|
||||
|
||||
// stacking
|
||||
for (i, image) in transformed.iter().enumerate().skip(1) {
|
||||
let x = (bounding_box.0 as f32 / 2.0 + positions[i].0 as f32 - image.width() as f32 / 2.0) as i64;
|
||||
let y = (bounding_box.1 as f32 / 2.0 + positions[i].1 as f32 - image.height() as f32 / 2.0) as i64;
|
||||
|
||||
imageops::overlay(&mut canvas, image, x, y)
|
||||
}
|
||||
|
||||
return DynamicImage::ImageRgba8(canvas);
|
||||
}
|
||||
|
||||
fn is_black(c: [f32; 3]) -> bool {
|
||||
c == [0.0, 0.0, 0.0]
|
||||
}
|
||||
|
||||
// Renders out a non-robot/spider icon. You may be looking for `render_icon`.
|
||||
pub fn render_normal(basename: String, col1: [f32; 3], col2: [f32; 3], glow: bool, game_sheet_02: LoadedSpritesheet, game_sheet_glow: LoadedSpritesheet) -> DynamicImage {
|
||||
let glow_col = if is_black(col2) { if is_black(col1) { [1.0, 1.0, 1.0] } else { col1 } } else { col2 };
|
||||
|
||||
let layers = vec![
|
||||
(if glow || (is_black(col1) && is_black(col2)) {
|
||||
assets::get_sprite_from_loaded(game_sheet_glow, format!("{}_glow_001.png", basename))
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
assets::get_sprite_from_loaded(game_sheet_02.clone(), format!("{}_2_001.png", basename)),
|
||||
assets::get_sprite_from_loaded(game_sheet_02.clone(), format!("{}_3_001.png", basename)),
|
||||
assets::get_sprite_from_loaded(game_sheet_02.clone(), format!("{}_001.png", basename)),
|
||||
assets::get_sprite_from_loaded(game_sheet_02, format!("{}_extra_001.png", basename))
|
||||
];
|
||||
|
||||
let colors: Vec<Option<[f32; 3]>> = vec![
|
||||
Some(glow_col),
|
||||
Some(col2),
|
||||
None,
|
||||
Some(col1),
|
||||
None
|
||||
];
|
||||
|
||||
return render_layered(
|
||||
layers.iter()
|
||||
.filter_map(|s| s.as_ref().map(|(img, _spr)| img.to_owned()))
|
||||
.collect(),
|
||||
layers.iter()
|
||||
.filter_map(|s| s.as_ref().map(|(_img, spr)| Some((spr.offset.0, spr.offset.1 * -1.0))))
|
||||
.collect(),
|
||||
colors.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, color)| layers[i].clone().map(|_| color.unwrap()))
|
||||
.collect(),
|
||||
vec![None, None, None, None, None],
|
||||
vec![None, None, None, None, None]
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue