1use std::fmt;
8use std::str::FromStr;
9
10use crate::Error;
11
12macro_rules! modes {
13 ($($(#[$m:meta])* $v:ident = $s:literal),+ $(,)?) => {
14 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
16 pub enum BlendMode {
17 #[default]
18 $($(#[$m])* $v),+
19 }
20
21 impl BlendMode {
22 pub const ALL: &[Self] = &[$(Self::$v),+];
24
25 pub fn as_str(self) -> &'static str {
27 match self { $(Self::$v => $s),+ }
28 }
29 }
30
31 impl FromStr for BlendMode {
32 type Err = Error;
33 fn from_str(s: &str) -> Result<Self, Error> {
34 match s {
35 $($s => Ok(Self::$v),)+
36 _ => Err(Error::Invalid { what: "blend mode", value: s.to_owned() }),
37 }
38 }
39 }
40 };
41}
42
43modes! {
44 Normal = "normal",
45 Add = "add",
46 Subtract = "subtract",
47 Difference = "difference",
48 Multiply = "multiply",
49 Screen = "screen",
50 Overlay = "overlay",
51 HardLight = "hardlight",
52 SoftLight = "softlight",
53 Darken = "darken",
54 Lighten = "lighten",
55 ColorDodge = "color_dodge",
56 ColorBurn = "color_burn",
57 Exclusion = "exclusion",
58 Hue = "hue",
59 Saturation = "saturation",
60 Color = "color",
61 Luminosity = "luminosity",
62 Divide = "divide",
63 LinearDodge = "linear_dodge",
65 LinearBurn = "linear_burn",
66 LinearLight = "linear_light",
67 VividLight = "vivid_light",
68 PinLight = "pin_light",
69 HardMix = "hard_mix",
70 LighterColor = "lighter_color",
71 DarkerColor = "darker_color",
72}
73
74impl fmt::Display for BlendMode {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 f.write_str(self.as_str())
77 }
78}
79
80impl BlendMode {
81 pub fn index(self) -> i32 {
83 Self::ALL
84 .iter()
85 .position(|m| *m == self)
86 .and_then(|i| i32::try_from(i).ok())
87 .unwrap_or(0)
88 }
89
90 pub fn apply(self, b: [f32; 3], s: [f32; 3]) -> [f32; 3] {
92 let (b, s) = (b.map(|x| x.clamp(0.0, 1.0)), s.map(|x| x.clamp(0.0, 1.0)));
93 let sep = |f: fn(f32, f32) -> f32| [f(b[0], s[0]), f(b[1], s[1]), f(b[2], s[2])];
94 match self {
95 Self::Normal => s,
96 Self::Add | Self::LinearDodge => sep(|b, s| (b + s).min(1.0)),
97 Self::Subtract => sep(|b, s| (b - s).max(0.0)),
98 Self::Difference => sep(|b, s| (b - s).abs()),
99 Self::Multiply => sep(|b, s| b * s),
100 Self::Screen => sep(screen),
101 Self::Overlay => sep(|b, s| hard_light(s, b)),
102 Self::HardLight => sep(hard_light),
103 Self::SoftLight => sep(soft_light),
104 Self::Darken => sep(f32::min),
105 Self::Lighten => sep(f32::max),
106 Self::ColorDodge => sep(dodge),
107 Self::ColorBurn => sep(burn),
108 Self::Exclusion => sep(|b, s| b + s - 2.0 * b * s),
109 Self::Hue => set_lum(set_sat(s, sat(b)), lum(b)),
110 Self::Saturation => set_lum(set_sat(b, sat(s)), lum(b)),
111 Self::Color => set_lum(s, lum(b)),
112 Self::Luminosity => set_lum(b, lum(s)),
113 Self::Divide => sep(|b, s| {
114 if s <= 0.0 {
115 if b > 0.0 { 1.0 } else { 0.0 }
116 } else {
117 (b / s).min(1.0)
118 }
119 }),
120 Self::LinearBurn => sep(|b, s| (b + s - 1.0).max(0.0)),
121 Self::LinearLight => sep(|b, s| (b + 2.0 * s - 1.0).clamp(0.0, 1.0)),
122 Self::VividLight => sep(|b, s| {
123 if s <= 0.5 {
124 burn(b, 2.0 * s)
125 } else {
126 dodge(b, 2.0 * s - 1.0)
127 }
128 }),
129 Self::PinLight => sep(|b, s| {
130 if s <= 0.5 {
131 b.min(2.0 * s)
132 } else {
133 b.max(2.0 * s - 1.0)
134 }
135 }),
136 Self::HardMix => sep(|b, s| if b + s >= 1.0 { 1.0 } else { 0.0 }),
137 Self::LighterColor => {
138 if lum(s) > lum(b) {
139 s
140 } else {
141 b
142 }
143 }
144 Self::DarkerColor => {
145 if lum(s) < lum(b) {
146 s
147 } else {
148 b
149 }
150 }
151 }
152 }
153
154 pub fn composite(self, b: [f32; 3], s: [f32; 3], a: f32) -> [f32; 3] {
156 let a = a.clamp(0.0, 1.0);
157 let m = self.apply(b, s);
158 [0, 1, 2].map(|i| b[i] + a * (m[i] - b[i]))
159 }
160}
161
162fn screen(b: f32, s: f32) -> f32 {
163 b + s - b * s
164}
165
166fn hard_light(b: f32, s: f32) -> f32 {
167 if s <= 0.5 {
168 b * 2.0 * s
169 } else {
170 screen(b, 2.0 * s - 1.0)
171 }
172}
173
174fn soft_light(b: f32, s: f32) -> f32 {
175 if s <= 0.5 {
176 b - (1.0 - 2.0 * s) * b * (1.0 - b)
177 } else {
178 let d = if b <= 0.25 {
179 ((16.0 * b - 12.0) * b + 4.0) * b
180 } else {
181 b.sqrt()
182 };
183 b + (2.0 * s - 1.0) * (d - b)
184 }
185}
186
187fn dodge(b: f32, s: f32) -> f32 {
188 if b <= 0.0 {
189 0.0
190 } else if s >= 1.0 {
191 1.0
192 } else {
193 (b / (1.0 - s)).min(1.0)
194 }
195}
196
197fn burn(b: f32, s: f32) -> f32 {
198 if b >= 1.0 {
199 1.0
200 } else if s <= 0.0 {
201 0.0
202 } else {
203 1.0 - ((1.0 - b) / s).min(1.0)
204 }
205}
206
207fn lum(c: [f32; 3]) -> f32 {
208 0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
209}
210
211fn clip_color(c: [f32; 3]) -> [f32; 3] {
212 let l = lum(c);
213 let n = c[0].min(c[1]).min(c[2]);
214 let x = c[0].max(c[1]).max(c[2]);
215 let mut c = c;
216 if n < 0.0 {
217 c = c.map(|v| l + (v - l) * l / (l - n));
218 }
219 if x > 1.0 {
220 c = c.map(|v| l + (v - l) * (1.0 - l) / (x - l));
221 }
222 c
223}
224
225fn set_lum(c: [f32; 3], l: f32) -> [f32; 3] {
226 let d = l - lum(c);
227 clip_color(c.map(|v| v + d))
228}
229
230fn sat(c: [f32; 3]) -> f32 {
231 c[0].max(c[1]).max(c[2]) - c[0].min(c[1]).min(c[2])
232}
233
234fn set_sat(c: [f32; 3], s: f32) -> [f32; 3] {
235 let n = c[0].min(c[1]).min(c[2]);
236 let x = c[0].max(c[1]).max(c[2]);
237 if x > n {
238 c.map(|v| (v - n) * s / (x - n))
239 } else {
240 [0.0; 3]
241 }
242}
243
244pub fn glsl() -> &'static str {
247 r"
248float bl_screen(float b, float s) { return b + s - b * s; }
249float bl_hard(float b, float s) { return s <= 0.5 ? b * 2.0 * s : bl_screen(b, 2.0 * s - 1.0); }
250float bl_soft(float b, float s) {
251 if (s <= 0.5)
252 return b - (1.0 - 2.0 * s) * b * (1.0 - b);
253 float d = b <= 0.25 ? ((16.0 * b - 12.0) * b + 4.0) * b : sqrt(b);
254 return b + (2.0 * s - 1.0) * (d - b);
255}
256float bl_dodge(float b, float s) {
257 return b <= 0.0 ? 0.0 : (s >= 1.0 ? 1.0 : min(b / (1.0 - s), 1.0));
258}
259float bl_burn(float b, float s) {
260 return b >= 1.0 ? 1.0 : (s <= 0.0 ? 0.0 : 1.0 - min((1.0 - b) / s, 1.0));
261}
262float bl_divide(float b, float s) {
263 return s <= 0.0 ? (b > 0.0 ? 1.0 : 0.0) : min(b / s, 1.0);
264}
265float bl_vivid(float b, float s) { return s <= 0.5 ? bl_burn(b, 2.0 * s) : bl_dodge(b, 2.0 * s - 1.0); }
266float bl_pin(float b, float s) { return s <= 0.5 ? min(b, 2.0 * s) : max(b, 2.0 * s - 1.0); }
267float bl_lum(vec3 c) { return dot(c, vec3(0.3, 0.59, 0.11)); }
268vec3 bl_clip(vec3 c) {
269 float l = bl_lum(c);
270 float n = min(c.r, min(c.g, c.b));
271 float x = max(c.r, max(c.g, c.b));
272 if (n < 0.0)
273 c = l + (c - l) * l / (l - n);
274 if (x > 1.0)
275 c = l + (c - l) * (1.0 - l) / (x - l);
276 return c;
277}
278vec3 bl_set_lum(vec3 c, float l) { return bl_clip(c + (l - bl_lum(c))); }
279float bl_sat(vec3 c) { return max(c.r, max(c.g, c.b)) - min(c.r, min(c.g, c.b)); }
280vec3 bl_set_sat(vec3 c, float s) {
281 float n = min(c.r, min(c.g, c.b));
282 float x = max(c.r, max(c.g, c.b));
283 return x > n ? (c - n) * s / (x - n) : vec3(0.0);
284}
285#define BL_SEP(f) vec3(f(b.r, s.r), f(b.g, s.g), f(b.b, s.b))
286vec3 blend_mode(int mode, vec3 b, vec3 s) {
287 b = clamp(b, 0.0, 1.0);
288 s = clamp(s, 0.0, 1.0);
289 switch (mode) {
290 case 1: case 19: return min(b + s, 1.0);
291 case 2: return max(b - s, 0.0);
292 case 3: return abs(b - s);
293 case 4: return b * s;
294 case 5: return b + s - b * s;
295 case 6: return vec3(bl_hard(s.r, b.r), bl_hard(s.g, b.g), bl_hard(s.b, b.b));
296 case 7: return BL_SEP(bl_hard);
297 case 8: return BL_SEP(bl_soft);
298 case 9: return min(b, s);
299 case 10: return max(b, s);
300 case 11: return BL_SEP(bl_dodge);
301 case 12: return BL_SEP(bl_burn);
302 case 13: return b + s - 2.0 * b * s;
303 case 14: return bl_set_lum(bl_set_sat(s, bl_sat(b)), bl_lum(b));
304 case 15: return bl_set_lum(bl_set_sat(b, bl_sat(s)), bl_lum(b));
305 case 16: return bl_set_lum(s, bl_lum(b));
306 case 17: return bl_set_lum(b, bl_lum(s));
307 case 18: return BL_SEP(bl_divide);
308 case 20: return max(b + s - 1.0, 0.0);
309 case 21: return clamp(b + 2.0 * s - 1.0, 0.0, 1.0);
310 case 22: return BL_SEP(bl_vivid);
311 case 23: return BL_SEP(bl_pin);
312 case 24: return step(1.0, b + s);
313 case 25: return bl_lum(s) > bl_lum(b) ? s : b;
314 case 26: return bl_lum(s) < bl_lum(b) ? s : b;
315 default: return s;
316 }
317}
318"
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 fn near(a: [f32; 3], b: [f32; 3]) -> bool {
326 a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-5)
327 }
328
329 #[test]
330 fn names_round_trip_and_index_matches_glsl() {
331 assert_eq!(BlendMode::ALL.len(), 27);
332 for (i, m) in BlendMode::ALL.iter().enumerate() {
333 assert_eq!(m.as_str().parse::<BlendMode>().unwrap(), *m);
334 assert_eq!(m.index(), i32::try_from(i).unwrap());
335 if !matches!(
336 m,
337 BlendMode::Normal | BlendMode::Add | BlendMode::LinearDodge
338 ) {
339 assert!(glsl().contains(&format!("case {i}:")), "{m}");
340 }
341 }
342 assert!(glsl().contains("case 1: case 19:"));
343 assert!("bogus".parse::<BlendMode>().is_err());
344 assert_eq!(BlendMode::default(), BlendMode::Normal);
345 }
346
347 #[test]
348 fn separable_modes() {
349 let (b, s) = ([0.2, 0.5, 0.8], [0.6, 0.5, 0.1]);
350 let t = |m: BlendMode| m.apply(b, s);
351 assert!(near(t(BlendMode::Normal), s));
352 assert!(near(t(BlendMode::Add), [0.8, 1.0, 0.9]));
353 assert!(near(t(BlendMode::LinearDodge), t(BlendMode::Add)));
354 assert!(near(t(BlendMode::Subtract), [0.0, 0.0, 0.7]));
355 assert!(near(t(BlendMode::Difference), [0.4, 0.0, 0.7]));
356 assert!(near(t(BlendMode::Multiply), [0.12, 0.25, 0.08]));
357 assert!(near(t(BlendMode::Screen), [0.68, 0.75, 0.82]));
358 assert!(near(t(BlendMode::Darken), [0.2, 0.5, 0.1]));
359 assert!(near(t(BlendMode::Lighten), [0.6, 0.5, 0.8]));
360 assert!(near(t(BlendMode::Exclusion), [0.56, 0.5, 0.74]));
361 assert!(near(t(BlendMode::LinearBurn), [0.0, 0.0, 0.0]));
362 assert!(near(t(BlendMode::LinearLight), [0.4, 0.5, 0.0]));
363 assert!(near(t(BlendMode::Overlay), [0.24, 0.5, 0.64]));
365 assert!(near(t(BlendMode::HardLight), [0.36, 0.5, 0.16]));
366 assert!(near(t(BlendMode::ColorDodge), [0.5, 1.0, 0.888_889]));
367 assert!(near(t(BlendMode::ColorBurn), [0.0, 0.0, 0.0]));
368 assert!(near(t(BlendMode::Divide), [1.0 / 3.0, 1.0, 1.0]));
369 assert!(near(t(BlendMode::PinLight), [0.2, 0.5, 0.2]));
370 assert!(near(t(BlendMode::HardMix), [0.0, 1.0, 0.0]));
371 assert!(near(
373 BlendMode::SoftLight.apply([0.3; 3], [0.5; 3]),
374 [0.3; 3]
375 ));
376 assert!(near(
377 BlendMode::SoftLight.apply([0.81; 3], [1.0; 3]),
378 [0.9; 3]
379 ));
380 assert!(near(
382 BlendMode::VividLight.apply([0.6; 3], [0.25; 3]),
383 [0.2; 3]
384 ));
385 assert!(near(
386 BlendMode::VividLight.apply([0.3; 3], [0.75; 3]),
387 [0.6; 3]
388 ));
389 }
390
391 #[test]
392 #[allow(clippy::many_single_char_names)]
393 fn non_separable_modes() {
394 let (b, s) = ([0.2, 0.4, 0.6], [0.9, 0.1, 0.1]);
395 let l = BlendMode::Luminosity.apply(b, s);
397 assert!((lum(l) - lum(s)).abs() < 1e-5);
398 let c = BlendMode::Color.apply(b, s);
399 assert!((lum(c) - lum(b)).abs() < 1e-5);
400 assert!(c[0] > c[1] && (c[1] - c[2]).abs() < 1e-6);
401 let g = BlendMode::Saturation.apply(b, [0.5; 3]);
403 assert!(near(g, [lum(b); 3]));
404 let h = BlendMode::Hue.apply(b, s);
406 assert!((lum(h) - lum(b)).abs() < 1e-5);
407 assert!((sat(h) - sat(b)).abs() < 1e-5);
408 assert!(h[0] > h[1]);
409 assert!(near(BlendMode::LighterColor.apply(b, s), b));
410 assert!(near(BlendMode::DarkerColor.apply(b, s), s));
411 }
412
413 #[test]
414 fn composite_mixes_by_alpha() {
415 let b = [0.2, 0.4, 0.6];
416 let s = [1.0, 0.0, 0.5];
417 assert!(near(BlendMode::Multiply.composite(b, s, 0.0), b));
418 assert!(near(
419 BlendMode::Multiply.composite(b, s, 1.0),
420 [0.2, 0.0, 0.3]
421 ));
422 assert!(near(
423 BlendMode::Multiply.composite(b, s, 0.5),
424 [0.2, 0.2, 0.45]
425 ));
426 }
427}