From 0f7ecba1ab5f6e4959085db7211fb507d2888ab2 Mon Sep 17 00:00:00 2001 From: reidlab Date: Fri, 29 Sep 2023 15:07:51 -0700 Subject: [PATCH] some small refactoring --- .gitignore | 4 + Cargo.lock | 532 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 9 + readme.md | 14 ++ src/assets.rs | 165 +++++++++++++++ src/constants.rs | 65 ++++++ src/lib.rs | 36 ++++ src/renderer.rs | 147 +++++++++++++ 8 files changed, 972 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 readme.md create mode 100644 src/assets.rs create mode 100644 src/constants.rs create mode 100644 src/lib.rs create mode 100644 src/renderer.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8559859 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target + +/assets +rendered_icon.png \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bf832da --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,532 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "exr" +version = "1.71.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832a761f35ab3e6664babfbdc6cef35a4860e816ec3916dcfd0882954e98a8a8" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "spin", +] + +[[package]] +name = "gd-icon-renderer" +version = "0.1.0" +dependencies = [ + "image", + "maplit", + "plist", +] + +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "image" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", + "qoi", + "tiff", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "memchr" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "plist" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdc0001cfea3db57a2e24bc0d818e9e20e554b5f97fabb9bc231dc240269ae06" +dependencies = [ + "base64", + "indexmap", + "line-wrap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "proc-macro2" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-xml" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "smallvec" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "syn" +version = "2.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tiff" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" +dependencies = [ + "deranged", + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c8f7147 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "gd-icon-renderer" +version = "0.1.0" +edition = "2021" + +[dependencies] +image = "0.24.7" +maplit = "1.0.2" +plist = "1.5.0" diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..4ddf515 --- /dev/null +++ b/readme.md @@ -0,0 +1,14 @@ +# gd-icon-renderer + +rust geometryd ash icon redner!! + +## usage + +Provide your `GJ_GameSheet02-uhd`, `GJ_GameSheetGlow-uhd`, `Robot_AnimDesc2`, and `Spider_AnimDesc2` files along with their corresponding `*.plist` files. + +## todo + +- maybe use custom plist parser +- spider + robot support +- make `get_sprite_from_loaded` and `get_sprite` merged into `get_sprite`. i think this needs traits or something +- trim empty alpha space (robtop didnt make the bounds correctly :sob:) \ No newline at end of file diff --git a/src/assets.rs b/src/assets.rs new file mode 100644 index 0000000..6d18a2f --- /dev/null +++ b/src/assets.rs @@ -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 = parts + .iter() + .map(|s| s.trim().parse::().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 = parts + .iter() + .map(|s| s.trim().parse::().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 = parts + .iter() + .map(|s| s.trim().parse::().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 = 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, + + 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; +} \ No newline at end of file diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..945897a --- /dev/null +++ b/src/constants.rs @@ -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> = 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 }, +}}); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4b36c24 --- /dev/null +++ b/src/lib.rs @@ -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)); + } +} diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..4bf45df --- /dev/null +++ b/src/renderer.rs @@ -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) -> 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, positions: Vec>, colors: Vec<[f32; 3]>, scales: Vec>, rotations: Vec>) -> DynamicImage { + let transformed: Vec = 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> = 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] + ); +} \ No newline at end of file