simple_editor\engine/
blend.rs

1//! Blend modes and the per-pixel composite of a straight-alpha RGBA layer onto an opaque canvas.
2
3use crate::media::Frame;
4use crate::model::BlendMode;
5
6/// Blend one channel: `s` = layer (source), `d` = canvas (destination), both 0..1. Returns 0..1.
7#[inline]
8pub fn blend_channel(mode: BlendMode, s: f32, d: f32) -> f32 {
9    use BlendMode::*;
10    match mode {
11        Normal => s,
12        Multiply => s * d,
13        Screen => 1.0 - (1.0 - s) * (1.0 - d),
14        Overlay => {
15            if d <= 0.5 {
16                2.0 * s * d
17            } else {
18                1.0 - 2.0 * (1.0 - s) * (1.0 - d)
19            }
20        }
21        Darken => s.min(d),
22        Lighten => s.max(d),
23        Add => (s + d).min(1.0),
24        Subtract => (d - s).max(0.0),
25        Difference => (s - d).abs(),
26        SoftLight => {
27            if s <= 0.5 {
28                d - (1.0 - 2.0 * s) * d * (1.0 - d)
29            } else {
30                let g = if d <= 0.25 { ((16.0 * d - 12.0) * d + 4.0) * d } else { d.sqrt() };
31                d + (2.0 * s - 1.0) * (g - d)
32            }
33        }
34        HardLight => {
35            if s <= 0.5 {
36                2.0 * s * d
37            } else {
38                1.0 - 2.0 * (1.0 - s) * (1.0 - d)
39            }
40        }
41        ColorDodge => {
42            if d <= 0.0 {
43                0.0
44            } else if s >= 1.0 {
45                1.0
46            } else {
47                (d / (1.0 - s)).min(1.0)
48            }
49        }
50        ColorBurn => {
51            if d >= 1.0 {
52                1.0
53            } else if s <= 0.0 {
54                0.0
55            } else {
56                1.0 - ((1.0 - d) / s).min(1.0)
57            }
58        }
59    }
60}
61
62/// x / 255 rounded, for x <= 255*255.
63#[inline]
64fn div255(x: u32) -> u32 {
65    (x + 128 + ((x + 128) >> 8)) >> 8
66}
67
68/// Composite one row of straight-alpha RGBA pixels (`src`) onto one row of opaque canvas pixels (`dst`).
69/// Both slices are `4 * n` bytes; `opacity` is 0..1. Canvas alpha stays 255.
70pub fn composite_row(dst: &mut [u8], src: &[u8], mode: BlendMode, opacity: f32) {
71    let op = (opacity.clamp(0.0, 1.0) * 255.0 + 0.5) as u32;
72    if op == 0 {
73        return;
74    }
75    let px = dst.chunks_exact_mut(4).zip(src.chunks_exact(4));
76    if mode == BlendMode::Normal {
77        for (d, s) in px {
78            let a = div255(s[3] as u32 * op);
79            if a == 0 {
80                continue;
81            }
82            if a == 255 {
83                d[..3].copy_from_slice(&s[..3]);
84            } else {
85                let ia = 255 - a;
86                d[0] = div255(d[0] as u32 * ia + s[0] as u32 * a) as u8;
87                d[1] = div255(d[1] as u32 * ia + s[1] as u32 * a) as u8;
88                d[2] = div255(d[2] as u32 * ia + s[2] as u32 * a) as u8;
89            }
90            d[3] = 255;
91        }
92    } else {
93        let opf = op as f32 / 255.0;
94        for (d, s) in px {
95            if s[3] == 0 {
96                continue;
97            }
98            let a = s[3] as f32 / 255.0 * opf;
99            for i in 0..3 {
100                let sv = s[i] as f32 / 255.0;
101                let dv = d[i] as f32 / 255.0;
102                let b = blend_channel(mode, sv, dv);
103                d[i] = ((dv + (b - dv) * a) * 255.0 + 0.5) as u8;
104            }
105            d[3] = 255;
106        }
107    }
108}
109
110/// Composite `layer` (straight-alpha RGBA, same size as `dst`) onto `dst` (opaque canvas) with the
111/// given blend mode and global opacity, restricted to the pixel rect [x0, x1) × [y0, y1) (clamped to
112/// the canvas). Pixels with alpha 0 are skipped.
113pub fn composite_rect(
114    dst: &mut Frame,
115    layer: &Frame,
116    mode: BlendMode,
117    opacity: f32,
118    x0: u32,
119    y0: u32,
120    x1: u32,
121    y1: u32,
122) {
123    if dst.width != layer.width || dst.height != layer.height {
124        return;
125    }
126    let stride = dst.stride();
127    let (x0, x1) = (x0.min(dst.width) as usize * 4, x1.min(dst.width) as usize * 4);
128    if x0 >= x1 {
129        return;
130    }
131    for y in y0..y1.min(dst.height) {
132        let row = y as usize * stride;
133        composite_row(&mut dst.rgba[row + x0..row + x1], &layer.rgba[row + x0..row + x1], mode, opacity);
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn channel_formulas() {
143        assert!((blend_channel(BlendMode::Multiply, 0.5, 0.5) - 0.25).abs() < 1e-6);
144        assert!((blend_channel(BlendMode::Screen, 0.5, 0.5) - 0.75).abs() < 1e-6);
145        assert!((blend_channel(BlendMode::Add, 0.7, 0.7) - 1.0).abs() < 1e-6);
146        assert!((blend_channel(BlendMode::Difference, 0.2, 0.7) - 0.5).abs() < 1e-6);
147        assert!((blend_channel(BlendMode::Normal, 0.3, 0.9) - 0.3).abs() < 1e-6);
148    }
149
150    #[test]
151    fn composite_half_alpha_and_modes() {
152        let mut dst = Frame::new(2, 1);
153        dst.fill([100, 100, 100, 255]);
154        let mut layer = Frame::new(2, 1);
155        layer.rgba.copy_from_slice(&[200, 200, 200, 128, 50, 50, 50, 0]);
156        composite_rect(&mut dst, &layer, BlendMode::Normal, 1.0, 0, 0, 2, 1);
157        // 100 + (200-100)*128/255 = 150.2
158        assert_eq!(&dst.rgba[..4], &[150, 150, 150, 255]);
159        // alpha 0 skipped
160        assert_eq!(&dst.rgba[4..], &[100, 100, 100, 255]);
161
162        // opacity 0.5 on an opaque layer
163        dst.fill([100, 100, 100, 255]);
164        layer.rgba[3] = 255;
165        composite_rect(&mut dst, &layer, BlendMode::Normal, 0.5, 0, 0, 2, 1);
166        assert_eq!(dst.rgba[0], 150);
167
168        // multiply: 0.5*0.5 = 0.25 → 64
169        dst.fill([128, 128, 128, 255]);
170        layer.rgba[..4].copy_from_slice(&[128, 128, 128, 255]);
171        composite_rect(&mut dst, &layer, BlendMode::Multiply, 1.0, 0, 0, 2, 1);
172        assert!((dst.rgba[0] as i32 - 64).abs() <= 1, "{}", dst.rgba[0]);
173        // screen: 1-(0.5*0.5) = 0.75 → 191
174        dst.fill([128, 128, 128, 255]);
175        composite_rect(&mut dst, &layer, BlendMode::Screen, 1.0, 0, 0, 2, 1);
176        assert!((dst.rgba[0] as i32 - 191).abs() <= 1, "{}", dst.rgba[0]);
177        assert_eq!(dst.rgba[3], 255);
178    }
179
180    #[test]
181    fn rect_limited() {
182        let mut dst = Frame::new(4, 4);
183        dst.fill([0, 0, 0, 255]);
184        let mut layer = Frame::new(4, 4);
185        layer.fill([255, 0, 0, 255]);
186        composite_rect(&mut dst, &layer, BlendMode::Normal, 1.0, 1, 1, 3, 3);
187        let px = |x: usize, y: usize| dst.rgba[(y * 4 + x) * 4];
188        assert_eq!(px(0, 0), 0);
189        assert_eq!(px(1, 1), 255);
190        assert_eq!(px(2, 2), 255);
191        assert_eq!(px(3, 3), 0);
192    }
193}