Skip to content

Commit 4a73c5b

Browse files
authored
IconForge: BYOND Parity + Tests, Optimizations, New Transforms (#230)
1 parent b12c13d commit 4a73c5b

16 files changed

+1760
-299
lines changed

.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@ insert_final_newline = true
1010
[*.dm]
1111
indent_style = tab
1212

13+
[*.dme]
14+
indent_style = tab
15+
1316
[*.yml]
1417
indent_size = 2

Cargo.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ noise = { version = "0.9", optional = true }
4646
redis = { version = "0.32", optional = true, features = ["ahash"] }
4747
ureq = { version = "2.12", optional = true }
4848
serde = { version = "1.0", optional = true, features = ["derive"] }
49-
serde_json = { version = "1.0", optional = true }
49+
serde_json = { version = "1.0", optional = true, features = ["preserve_order"] }
5050
once_cell = { version = "1.21", optional = true }
5151
mysql = { git = "https://github.com/ZeWaka/rust-mysql-simple.git", tag = "v26.0.0", default-features = false, optional = true }
5252
dashmap = { version = "6.1", optional = true, features = ["rayon", "serde"] }
@@ -68,6 +68,8 @@ symphonia = { version = "0.5.4", optional = true, features = ["all-codecs"] }
6868
caith = { version = "4.2.4", optional = true }
6969
uuid = { version = "1.17", optional = true, features = ["v4", "v7", "fast-rng"] }
7070
cuid2 = { version = "0.1.4", optional = true }
71+
indexmap = { version = "2.9.0", optional = true, features = ["serde", "rayon"] }
72+
ordered-float = { version = "5.0.0", optional = true, features = ["serde"] }
7173

7274
[features]
7375
default = [
@@ -147,8 +149,10 @@ iconforge = [
147149
"dep:dmi",
148150
"hash",
149151
"image",
152+
"indexmap",
150153
"jobs",
151154
"once_cell",
155+
"ordered-float",
152156
"png",
153157
"rayon",
154158
"serde",

dmsrc/iconforge.dm

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,15 @@
2424
/// )
2525
/// TRANSFORM_OBJECT format:
2626
/// list("type" = RUSTG_ICONFORGE_BLEND_COLOR, "color" = "#ff0000", "blend_mode" = ICON_MULTIPLY)
27-
/// list("type" = RUSTG_ICONFORGE_BLEND_ICON, "icon" = [SPRITE_OBJECT], "blend_mode" = ICON_OVERLAY)
27+
/// list("type" = RUSTG_ICONFORGE_BLEND_ICON, "icon" = [SPRITE_OBJECT], "blend_mode" = ICON_OVERLAY, "x" = 1, "y" = 1) // offsets optional
2828
/// list("type" = RUSTG_ICONFORGE_SCALE, "width" = 32, "height" = 32)
2929
/// list("type" = RUSTG_ICONFORGE_CROP, "x1" = 1, "y1" = 1, "x2" = 32, "y2" = 32) // (BYOND icons index from 1,1 to the upper bound, inclusive)
30+
/// list("type" = RUSTG_ICONFORGE_MAP_COLORS, "rr" = 0.5, "rg" = 0.5, "rb" = 0.5, "ra" = 1, "gr" = 1, "gg" = 1, "gb" = 1, "ga" = 1, ...) // alpha arguments and rgba0 optional
31+
/// list("type" = RUSTG_ICONFORGE_FLIP, "dir" = SOUTH)
32+
/// list("type" = RUSTG_ICONFORGE_TURN, "angle" = 90.0)
33+
/// list("type" = RUSTG_ICONFORGE_SHIFT, "dir" = EAST, "offset" = 10, "wrap" = FALSE)
34+
/// list("type" = RUSTG_ICONFORGE_SWAP_COLOR, "src_color" = "#ff0000", "dst_color" = "#00ff00") // alpha bits supported
35+
/// list("type" = RUSTG_ICONFORGE_DRAW_BOX, "color" = "#ff0000", "x1" = 1, "y1" = 1, "x2" = 32, "y2" = 32) // alpha bits supported. color can be null/omitted for transparency. x2 and y2 will default to x1 and y1 if omitted
3036
///
3137
/// Returns a SpritesheetResult as JSON, containing fields:
3238
/// list(
@@ -60,7 +66,7 @@
6066
/// Provided a /datum/greyscale_config typepath, JSON string containing the greyscale config, and path to a DMI file containing the base icons,
6167
/// Loads that config into memory for later use by rustg_iconforge_gags(). The config_path is the unique identifier used later.
6268
/// JSON Config schema: https://hackmd.io/@tgstation/GAGS-Layer-Types
63-
/// Unsupported features: color_matrix layer type, 'or' blend_mode. May not have BYOND parity with animated icons or varying dirs between layers.
69+
/// Adding dirs or frames (via blending larger icons) to icons with more than 1 dir or 1 frame is not supported.
6470
/// Returns "OK" if successful, otherwise, returns a string containing the error.
6571
#define rustg_iconforge_load_gags_config(config_path, config_json, config_icon_path) RUSTG_CALL(RUST_G, "iconforge_load_gags_config")("[config_path]", config_json, config_icon_path)
6672
/// Given a config_path (previously loaded by rustg_iconforge_load_gags_config), and a string of hex colors formatted as "#ff00ff#ffaa00"
@@ -76,3 +82,9 @@
7682
#define RUSTG_ICONFORGE_BLEND_ICON "BlendIcon"
7783
#define RUSTG_ICONFORGE_CROP "Crop"
7884
#define RUSTG_ICONFORGE_SCALE "Scale"
85+
#define RUSTG_ICONFORGE_MAP_COLORS "MapColors"
86+
#define RUSTG_ICONFORGE_FLIP "Flip"
87+
#define RUSTG_ICONFORGE_TURN "Turn"
88+
#define RUSTG_ICONFORGE_SHIFT "Shift"
89+
#define RUSTG_ICONFORGE_SWAP_COLOR "SwapColor"
90+
#define RUSTG_ICONFORGE_DRAW_BOX "DrawBox"

src/iconforge/blending.rs

Lines changed: 102 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,24 @@
1+
use once_cell::sync::Lazy;
12
use serde::Serialize;
23
use std::str::FromStr;
34

4-
#[derive(Clone)]
5-
pub struct Rgba {
6-
r: f32,
7-
g: f32,
8-
b: f32,
9-
a: f32,
10-
}
11-
12-
impl Rgba {
13-
pub fn into_array(self) -> [u8; 4] {
14-
[
15-
self.r.round() as u8,
16-
self.g.round() as u8,
17-
self.b.round() as u8,
18-
self.a.round() as u8,
19-
]
20-
}
5+
pub static ALPHA_TABLE: Lazy<[u8; 256 * 256]> = Lazy::new(|| {
6+
let mut table = [0u8; 256 * 256];
217

22-
pub fn from_array(rgba: &[u8]) -> Rgba {
23-
Self {
24-
r: rgba[0] as f32,
25-
g: rgba[1] as f32,
26-
b: rgba[2] as f32,
27-
a: rgba[3] as f32,
8+
for dst in 0..256 {
9+
for src in 0..256 {
10+
let index = dst * 256 + src;
11+
let value = ((src as f32) * (dst as f32 / 255.0) + 0.5).floor() as i32;
12+
table[index] = if (0..256).contains(&value) {
13+
value as u8
14+
} else {
15+
0xFF
16+
};
2817
}
2918
}
3019

31-
fn map_each<F, T>(color: &Rgba, color2: &Rgba, rgb_fn: F, a_fn: T) -> Rgba
32-
where
33-
F: Fn(f32, f32) -> f32,
34-
T: Fn(f32, f32) -> f32,
35-
{
36-
Rgba {
37-
r: rgb_fn(color.r, color2.r),
38-
g: rgb_fn(color.g, color2.g),
39-
b: rgb_fn(color.b, color2.b),
40-
a: a_fn(color.a, color2.a),
41-
}
42-
}
43-
44-
fn map_each_a<F, T>(color: &Rgba, color2: &Rgba, rgb_fn: F, a_fn: T) -> Rgba
45-
where
46-
F: Fn(f32, f32, f32, f32) -> f32,
47-
T: Fn(f32, f32) -> f32,
48-
{
49-
Rgba {
50-
r: rgb_fn(color.r, color2.r, color.a, color2.a),
51-
g: rgb_fn(color.g, color2.g, color.a, color2.a),
52-
b: rgb_fn(color.b, color2.b, color.a, color2.a),
53-
a: a_fn(color.a, color2.a),
54-
}
55-
}
56-
57-
/// Takes two [u8; 4]s, converts them to Rgba structs, then blends them according to blend_mode by calling blend().
58-
pub fn blend_u8(color: &[u8], other_color: &[u8], blend_mode: &BlendMode) -> [u8; 4] {
59-
Rgba::from_array(color)
60-
.blend(&Rgba::from_array(other_color), blend_mode)
61-
.into_array()
62-
}
63-
64-
/// Blends two colors according to blend_mode.
65-
pub fn blend(&self, other_color: &Rgba, blend_mode: &BlendMode) -> Rgba {
66-
match blend_mode {
67-
BlendMode::Add => Rgba::map_each(self, other_color, |c1, c2| c1 + c2, f32::min),
68-
BlendMode::Subtract => Rgba::map_each(self, other_color, |c1, c2| c1 - c2, f32::min),
69-
BlendMode::Multiply => Rgba::map_each(
70-
self,
71-
other_color,
72-
|c1, c2| c1 * c2 / 255.0,
73-
|a1: f32, a2: f32| a1 * a2 / 255.0,
74-
),
75-
BlendMode::Overlay => Rgba::map_each_a(
76-
self,
77-
other_color,
78-
|c1, c2, c1_a, c2_a| {
79-
if c1_a == 0.0 {
80-
return c2;
81-
}
82-
c1 + (c2 - c1) * c2_a / 255.0
83-
},
84-
|a1, a2| {
85-
let high = f32::max(a1, a2);
86-
let low = f32::min(a1, a2);
87-
high + (high * low / 255.0)
88-
},
89-
),
90-
BlendMode::Underlay => Rgba::map_each_a(
91-
other_color,
92-
self,
93-
|c1, c2, c1_a, c2_a| {
94-
if c1_a == 0.0 {
95-
return c2;
96-
}
97-
c1 + (c2 - c1) * c2_a / 255.0
98-
},
99-
|a1, a2| {
100-
let high = f32::max(a1, a2);
101-
let low = f32::min(a1, a2);
102-
high + (high * low / 255.0)
103-
},
104-
),
105-
}
106-
}
107-
}
20+
table
21+
});
10822

10923
// The numbers correspond to BYOND ICON_X blend modes. https://www.byond.com/docs/ref/#/icon/proc/Blend
11024
#[derive(Clone, Hash, Eq, PartialEq, Serialize)]
@@ -114,6 +28,8 @@ pub enum BlendMode {
11428
Subtract = 1,
11529
Multiply = 2,
11630
Overlay = 3,
31+
And = 4,
32+
Or = 5,
11733
Underlay = 6,
11834
}
11935

@@ -124,10 +40,94 @@ impl BlendMode {
12440
1 => Ok(BlendMode::Subtract),
12541
2 => Ok(BlendMode::Multiply),
12642
3 => Ok(BlendMode::Overlay),
43+
4 => Ok(BlendMode::And),
44+
5 => Ok(BlendMode::Or),
12745
6 => Ok(BlendMode::Underlay),
12846
_ => Err(format!("blend_mode '{blend_mode}' is not supported!")),
12947
}
13048
}
49+
50+
pub fn blend_u8(&self, color: &[u8], other_color: &[u8]) -> [u8; 4] {
51+
let (r1, g1, b1, a1) = (color[0], color[1], color[2], color[3]);
52+
let (r2, g2, b2, a2) = (
53+
other_color[0],
54+
other_color[1],
55+
other_color[2],
56+
other_color[3],
57+
);
58+
59+
let add_channel = |c_src: u8, c_dst: u8| c_src.saturating_add(c_dst);
60+
let subtract_channel = |c_src: u8, c_dst: u8| c_src.saturating_sub(c_dst);
61+
let multiply_channel = |c_src: u8, c_dst: u8| ((c_src as u16 * c_dst as u16) / 255) as u8;
62+
let overlay_channel = |c_src: u8, c_dst: u8, a_src: u8, a_dst: u8| {
63+
if a_src == 0 {
64+
c_dst
65+
} else {
66+
let delta = (c_dst as i32 - c_src as i32) * a_dst as i32 / 255;
67+
(c_src as i32 + delta).clamp(0, 255) as u8
68+
}
69+
};
70+
let overlay_alpha = |a_src: u8, a_dst: u8| {
71+
let a_src = a_src as f32 / 255.0;
72+
let a_dst = a_dst as f32 / 255.0;
73+
((a_src + a_dst * (1.0 - a_src)) * 255.0)
74+
.round()
75+
.clamp(0.0, 255.0) as u8
76+
};
77+
78+
let alpha_lookup =
79+
|a_src: u8, a_dst: u8| ALPHA_TABLE[a_dst as usize + (a_src as usize) * 256];
80+
81+
match self {
82+
BlendMode::Add | BlendMode::And => [
83+
add_channel(r1, r2),
84+
add_channel(g1, g2),
85+
add_channel(b1, b2),
86+
alpha_lookup(a1, a2),
87+
],
88+
BlendMode::Subtract => [
89+
subtract_channel(r1, r2),
90+
subtract_channel(g1, g2),
91+
subtract_channel(b1, b2),
92+
alpha_lookup(a1, a2),
93+
],
94+
BlendMode::Multiply => [
95+
multiply_channel(r1, r2),
96+
multiply_channel(g1, g2),
97+
multiply_channel(b1, b2),
98+
alpha_lookup(a1, a2),
99+
],
100+
BlendMode::Overlay => [
101+
overlay_channel(r1, r2, a1, a2),
102+
overlay_channel(g1, g2, a1, a2),
103+
overlay_channel(b1, b2, a1, a2),
104+
overlay_alpha(a1, a2),
105+
],
106+
BlendMode::Or => {
107+
if a1 == 0 {
108+
return [r2, g2, b2, a2];
109+
}
110+
if a2 == 0 {
111+
return [r1, g1, b1, a1];
112+
}
113+
[
114+
add_channel(r1, r2),
115+
add_channel(g1, g2),
116+
add_channel(b1, b2),
117+
!ALPHA_TABLE[0x10000usize
118+
.wrapping_sub(a1 as usize)
119+
.wrapping_sub(a2 as usize * 256)
120+
.min(65535)],
121+
]
122+
}
123+
BlendMode::Underlay => [
124+
overlay_channel(r2, r1, a2, a1),
125+
overlay_channel(g2, g1, a2, a1),
126+
overlay_channel(b2, b1, a2, a1),
127+
overlay_alpha(a2, a1),
128+
],
129+
}
130+
}
131131
}
132132

133133
impl FromStr for BlendMode {
@@ -139,6 +139,8 @@ impl FromStr for BlendMode {
139139
"subtract" => Ok(BlendMode::Subtract),
140140
"multiply" => Ok(BlendMode::Multiply),
141141
"overlay" => Ok(BlendMode::Overlay),
142+
"and" => Ok(BlendMode::And),
143+
"or" => Ok(BlendMode::Or),
142144
"underlay" => Ok(BlendMode::Underlay),
143145
_ => Err(format!("blend_mode '{blend_mode}' is not supported!")),
144146
}

0 commit comments

Comments
 (0)