1use crate::framebuffer::{pack_rgba, unpack, Framebuffer};
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub enum BlendMode {
32 Normal,
34 Multiply,
36 Screen,
38 Overlay,
40 Darken,
42 Lighten,
44}
45
46#[derive(Clone, Copy, Debug, PartialEq)]
51pub struct RgbaUnit {
52 pub r: f32,
54 pub g: f32,
56 pub b: f32,
58 pub a: f32,
60}
61
62impl RgbaUnit {
63 pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
65 Self {
66 r: r.clamp(0.0, 1.0),
67 g: g.clamp(0.0, 1.0),
68 b: b.clamp(0.0, 1.0),
69 a: a.clamp(0.0, 1.0),
70 }
71 }
72
73 pub fn from_bytes(r: u8, g: u8, b: u8, a: u8) -> Self {
75 Self {
76 r: r as f32 / 255.0,
77 g: g as f32 / 255.0,
78 b: b as f32 / 255.0,
79 a: a as f32 / 255.0,
80 }
81 }
82
83 pub fn to_bytes(self) -> (u8, u8, u8, u8) {
85 (
86 (self.r.clamp(0.0, 1.0) * 255.0).round() as u8,
87 (self.g.clamp(0.0, 1.0) * 255.0).round() as u8,
88 (self.b.clamp(0.0, 1.0) * 255.0).round() as u8,
89 (self.a.clamp(0.0, 1.0) * 255.0).round() as u8,
90 )
91 }
92
93 pub fn premultiply(self) -> Self {
95 Self {
96 r: self.r * self.a,
97 g: self.g * self.a,
98 b: self.b * self.a,
99 a: self.a,
100 }
101 }
102
103 pub fn unpremultiply(self) -> Self {
106 if self.a <= f32::EPSILON {
107 Self {
108 r: 0.0,
109 g: 0.0,
110 b: 0.0,
111 a: 0.0,
112 }
113 } else {
114 Self {
115 r: self.r / self.a,
116 g: self.g / self.a,
117 b: self.b / self.a,
118 a: self.a,
119 }
120 }
121 }
122}
123
124pub fn to_unit(px: u32) -> RgbaUnit {
126 let (r, g, b, a) = unpack(px);
127 RgbaUnit::from_bytes(r, g, b, a)
128}
129
130pub fn from_unit(p: RgbaUnit) -> u32 {
132 let (r, g, b, a) = p.to_bytes();
133 pack_rgba(r, g, b, a)
134}
135
136#[inline]
145fn to_premult(r: u8, g: u8, b: u8, a: u8) -> (u16, u16, u16, u16) {
146 let a16 = a as u16;
147 (
148 (r as u16 * a16 + 127) / 255,
149 (g as u16 * a16 + 127) / 255,
150 (b as u16 * a16 + 127) / 255,
151 a16,
152 )
153}
154
155#[inline]
158fn from_premult(pr: u16, pg: u16, pb: u16, a: u16) -> (u8, u8, u8, u8) {
159 if a == 0 {
160 return (0, 0, 0, 0);
161 }
162 let half = a / 2;
163 (
164 ((pr * 255 + half) / a).min(255) as u8,
165 ((pg * 255 + half) / a).min(255) as u8,
166 ((pb * 255 + half) / a).min(255) as u8,
167 a as u8,
168 )
169}
170
171#[inline]
182#[allow(clippy::too_many_arguments)]
183pub(crate) fn blend_over_premult_u8(
184 src_r: u8,
185 src_g: u8,
186 src_b: u8,
187 src_a: u8,
188 dst_r: u8,
189 dst_g: u8,
190 dst_b: u8,
191 dst_a: u8,
192) -> (u8, u8, u8, u8) {
193 let (spr, spg, spb, sa) = to_premult(src_r, src_g, src_b, src_a);
194 let (dpr, dpg, dpb, da) = to_premult(dst_r, dst_g, dst_b, dst_a);
195 let inv_sa = 255u16 - sa;
196 let out_r = spr + (dpr * inv_sa + 127) / 255;
197 let out_g = spg + (dpg * inv_sa + 127) / 255;
198 let out_b = spb + (dpb * inv_sa + 127) / 255;
199 let out_a = sa + (da * inv_sa + 127) / 255;
200 from_premult(out_r, out_g, out_b, out_a)
201}
202
203pub fn blend_mode(mode: BlendMode, src: RgbaUnit, dst: RgbaUnit) -> RgbaUnit {
218 match mode {
219 BlendMode::Normal => over(src, dst),
220 BlendMode::Multiply => mode_combine(src, dst, |a, b| a * b),
221 BlendMode::Screen => mode_combine(src, dst, |a, b| a + b - a * b),
222 BlendMode::Overlay => mode_combine(src, dst, |cs, cb| {
223 if cb <= 0.5 {
225 2.0 * cs * cb
226 } else {
227 1.0 - 2.0 * (1.0 - cs) * (1.0 - cb)
228 }
229 }),
230 BlendMode::Darken => mode_combine(src, dst, f32::min),
231 BlendMode::Lighten => mode_combine(src, dst, f32::max),
232 }
233}
234
235fn over(src: RgbaUnit, dst: RgbaUnit) -> RgbaUnit {
237 let out_a = src.a + dst.a * (1.0 - src.a);
238 if out_a <= f32::EPSILON {
239 return RgbaUnit {
240 r: 0.0,
241 g: 0.0,
242 b: 0.0,
243 a: 0.0,
244 };
245 }
246 let blend = |s: f32, d: f32| (s * src.a + d * dst.a * (1.0 - src.a)) / out_a;
247 RgbaUnit {
248 r: blend(src.r, dst.r),
249 g: blend(src.g, dst.g),
250 b: blend(src.b, dst.b),
251 a: out_a,
252 }
253}
254
255fn mode_combine<F>(src: RgbaUnit, dst: RgbaUnit, blend_fn: F) -> RgbaUnit
258where
259 F: Fn(f32, f32) -> f32,
260{
261 let out_a = src.a + dst.a * (1.0 - src.a);
262 if out_a <= f32::EPSILON {
263 return RgbaUnit {
264 r: 0.0,
265 g: 0.0,
266 b: 0.0,
267 a: 0.0,
268 };
269 }
270 let channel = |cs: f32, cb: f32| -> f32 {
272 let b = blend_fn(cs.clamp(0.0, 1.0), cb.clamp(0.0, 1.0));
273 let v = (1.0 - dst.a) * cs * src.a + (1.0 - src.a) * cb * dst.a + src.a * dst.a * b;
274 (v / out_a).clamp(0.0, 1.0)
276 };
277 RgbaUnit {
278 r: channel(src.r, dst.r),
279 g: channel(src.g, dst.g),
280 b: channel(src.b, dst.b),
281 a: out_a,
282 }
283}
284
285pub fn blend_pixel(fb: &mut Framebuffer, x: u32, y: u32, src: RgbaUnit, mode: BlendMode) {
289 if x >= fb.width() || y >= fb.height() {
290 return;
291 }
292 let Some(dst_px) = fb.get(x, y) else { return };
293 let dst = to_unit(dst_px);
294 let out = blend_mode(mode, src, dst);
295 fb.set(x, y, from_unit(out));
296}
297
298pub fn composite_into(
306 fb: &mut Framebuffer,
307 src: &[u8],
308 w: u32,
309 h: u32,
310 dst_x: i64,
311 dst_y: i64,
312 mode: BlendMode,
313) -> usize {
314 if w == 0 || h == 0 || src.len() < (w * h * 4) as usize {
315 return 0;
316 }
317 let mut written = 0;
318 for j in 0..h {
319 for i in 0..w {
320 let si = ((j * w + i) * 4) as usize;
321 let r = src[si];
322 let g = src[si + 1];
323 let b = src[si + 2];
324 let a = src[si + 3];
325 if a == 0 {
326 continue;
327 }
328 let px = dst_x + i as i64;
329 let py = dst_y + j as i64;
330 if px < 0 || py < 0 {
331 continue;
332 }
333 let pu = px as u32;
334 let pv = py as u32;
335 if pu >= fb.width() || pv >= fb.height() {
336 continue;
337 }
338 if mode == BlendMode::Normal {
339 let dst_px = fb.get(pu, pv).unwrap_or(0);
342 let (dr, dg, db, da) = unpack(dst_px);
343 let (or_, og, ob, oa) = blend_over_premult_u8(r, g, b, a, dr, dg, db, da);
344 fb.set(pu, pv, crate::framebuffer::pack_rgba(or_, og, ob, oa));
345 } else {
346 blend_pixel(fb, pu, pv, RgbaUnit::from_bytes(r, g, b, a), mode);
347 }
348 written += 1;
349 }
350 }
351 written
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use oxiui_core::Color;
358
359 fn approx_eq(a: f32, b: f32, eps: f32) -> bool {
360 (a - b).abs() <= eps
361 }
362
363 #[test]
364 fn round_trip_unit_bytes() {
365 let p = RgbaUnit::from_bytes(123, 45, 200, 99);
366 let (r, g, b, a) = p.to_bytes();
367 assert_eq!((r, g, b, a), (123, 45, 200, 99));
368 }
369
370 #[test]
371 fn premultiply_inverse() {
372 let p = RgbaUnit::new(0.8, 0.4, 0.2, 0.5);
373 let q = p.premultiply().unpremultiply();
374 assert!(approx_eq(q.r, p.r, 1e-5));
375 assert!(approx_eq(q.g, p.g, 1e-5));
376 assert!(approx_eq(q.b, p.b, 1e-5));
377 assert!(approx_eq(q.a, p.a, 1e-5));
378 }
379
380 #[test]
381 fn premultiply_zero_alpha_safe() {
382 let p = RgbaUnit::new(0.9, 0.9, 0.9, 0.0);
383 let q = p.premultiply().unpremultiply();
384 assert!(approx_eq(q.a, 0.0, 1e-5));
385 assert!(approx_eq(q.r, 0.0, 1e-5));
386 }
387
388 #[test]
389 fn multiply_white_is_identity() {
390 let src = RgbaUnit::from_bytes(255, 255, 255, 255);
392 let dst = RgbaUnit::from_bytes(255, 0, 0, 255);
393 let out = blend_mode(BlendMode::Multiply, src, dst);
394 let (r, g, b, a) = out.to_bytes();
395 assert_eq!((r, g, b, a), (255, 0, 0, 255));
396 }
397
398 #[test]
399 fn screen_black_is_identity() {
400 let src = RgbaUnit::from_bytes(0, 0, 0, 255);
401 let dst = RgbaUnit::from_bytes(80, 120, 200, 255);
402 let out = blend_mode(BlendMode::Screen, src, dst);
403 let (r, g, b, _) = out.to_bytes();
404 assert!((r as i32 - 80).abs() <= 1);
406 assert!((g as i32 - 120).abs() <= 1);
407 assert!((b as i32 - 200).abs() <= 1);
408 }
409
410 #[test]
411 fn darken_picks_min_per_channel() {
412 let src = RgbaUnit::from_bytes(100, 200, 50, 255);
413 let dst = RgbaUnit::from_bytes(150, 50, 50, 255);
414 let out = blend_mode(BlendMode::Darken, src, dst);
415 let (r, g, b, _) = out.to_bytes();
416 assert!((r as i32 - 100).abs() <= 1);
417 assert!((g as i32 - 50).abs() <= 1);
418 assert!((b as i32 - 50).abs() <= 1);
419 }
420
421 #[test]
422 fn lighten_picks_max_per_channel() {
423 let src = RgbaUnit::from_bytes(100, 200, 50, 255);
424 let dst = RgbaUnit::from_bytes(150, 50, 50, 255);
425 let out = blend_mode(BlendMode::Lighten, src, dst);
426 let (r, g, b, _) = out.to_bytes();
427 assert!((r as i32 - 150).abs() <= 1);
428 assert!((g as i32 - 200).abs() <= 1);
429 assert!((b as i32 - 50).abs() <= 1);
430 }
431
432 #[test]
433 fn overlay_changes_with_dst_mid() {
434 let src = RgbaUnit::from_bytes(128, 128, 128, 255);
437 let dst = RgbaUnit::from_bytes(128, 128, 128, 255);
438 let out = blend_mode(BlendMode::Overlay, src, dst);
439 let (r, _, _, a) = out.to_bytes();
440 assert_eq!(a, 255);
441 let _ = r;
444 }
445
446 #[test]
447 fn over_zero_alpha_safe() {
448 let src = RgbaUnit::new(0.0, 0.0, 0.0, 0.0);
449 let dst = RgbaUnit::new(0.0, 0.0, 0.0, 0.0);
450 let out = blend_mode(BlendMode::Normal, src, dst);
451 let (_, _, _, a) = out.to_bytes();
452 assert_eq!(a, 0);
453 }
454
455 #[test]
456 fn composite_into_writes_pixels() {
457 let mut fb = Framebuffer::with_fill(4, 4, Color(0, 0, 0, 255));
458 let src = vec![
460 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
461 ];
462 let written = composite_into(&mut fb, &src, 2, 2, 1, 1, BlendMode::Normal);
463 assert_eq!(written, 4);
464 let result = fb.get_rgba(1, 1).unwrap_or_default();
465 assert_eq!(result, (255, 255, 255, 255));
466 }
467
468 #[test]
469 fn blend_pixel_out_of_bounds_noop() {
470 let mut fb = Framebuffer::with_fill(2, 2, Color(10, 20, 30, 255));
471 blend_pixel(
472 &mut fb,
473 99,
474 99,
475 RgbaUnit::from_bytes(255, 255, 255, 255),
476 BlendMode::Normal,
477 );
478 let v = fb.get_rgba(0, 0).unwrap_or((0, 0, 0, 0));
479 assert_eq!(v, (10, 20, 30, 255));
480 }
481
482 #[test]
483 fn blend_multiply_black_is_black() {
484 let black = RgbaUnit::from_bytes(0, 0, 0, 255);
486 let src = RgbaUnit::from_bytes(200, 100, 50, 255);
487 let out = blend_mode(BlendMode::Multiply, src, black);
488 let (r, g, b, _) = out.to_bytes();
489 assert_eq!((r, g, b), (0, 0, 0), "multiply with black must give black");
490 }
491
492 #[test]
493 fn blend_screen_white_is_white() {
494 let white = RgbaUnit::from_bytes(255, 255, 255, 255);
496 let src = RgbaUnit::from_bytes(100, 150, 200, 255);
497 let out = blend_mode(BlendMode::Screen, src, white);
498 let (r, g, b, _) = out.to_bytes();
499 assert_eq!(
500 (r, g, b),
501 (255, 255, 255),
502 "screen with white must give white"
503 );
504 }
505
506 #[test]
507 fn premultiply_roundtrip() {
508 let p = RgbaUnit::new(0.7, 0.3, 0.9, 0.6);
510 let q = p.premultiply().unpremultiply();
511 assert!(approx_eq(q.r, p.r, 1e-4), "r: {:.5} vs {:.5}", q.r, p.r);
512 assert!(approx_eq(q.g, p.g, 1e-4), "g: {:.5} vs {:.5}", q.g, p.g);
513 assert!(approx_eq(q.b, p.b, 1e-4), "b: {:.5} vs {:.5}", q.b, p.b);
514 assert!(approx_eq(q.a, p.a, 1e-5), "a: {:.5} vs {:.5}", q.a, p.a);
515 }
516
517 #[test]
522 fn test_premult_blend_golden() {
523 let (r, g, b, a) = blend_over_premult_u8(255, 0, 0, 128, 255, 255, 255, 255);
535 assert!((r as i32 - 255).abs() <= 2, "r={r} expected ~255");
536 assert!((g as i32 - 127).abs() <= 2, "g={g} expected ~127");
537 assert!((b as i32 - 127).abs() <= 2, "b={b} expected ~127");
538 assert!((a as i32 - 255).abs() <= 2, "a={a} expected ~255");
539 }
540
541 #[test]
542 fn test_blend_signatures_stable() {
543 let src = RgbaUnit::from_bytes(128, 64, 32, 200);
548 let dst = RgbaUnit::from_bytes(10, 20, 30, 255);
549 let _ = blend_mode(BlendMode::Normal, src, dst);
550 let _ = blend_mode(BlendMode::Multiply, src, dst);
551 let _ = blend_mode(BlendMode::Screen, src, dst);
552 let _ = blend_mode(BlendMode::Overlay, src, dst);
553 let _ = blend_mode(BlendMode::Darken, src, dst);
554 let _ = blend_mode(BlendMode::Lighten, src, dst);
555
556 let px = crate::framebuffer::pack_rgba(100, 150, 200, 180);
558 let unit = to_unit(px);
559 let _ = from_unit(unit);
560
561 let mut fb = Framebuffer::with_fill(4, 4, Color(0, 0, 0, 255));
563 blend_pixel(&mut fb, 0, 0, src, BlendMode::Normal);
564 blend_pixel(&mut fb, 99, 99, src, BlendMode::Screen); let src_bytes = [255u8, 0, 0, 128];
568 let written = composite_into(&mut fb, &src_bytes, 1, 1, 0, 0, BlendMode::Normal);
569 assert!(written <= 1);
570 }
571}