///|
/// Source-over blend into a pixel at `offset` in a RGBA8 non-premultiplied
/// buffer.
///
/// `coverage` is an integer `0..=256` (256 == fully covered).
/// `global_alpha` multiplies the source alpha further (`0.0..=1.0`).
/// Fast paths:
/// - `coverage == 0` → early return (source is invisible).
/// - `src.a * coverage/256 * global_alpha <= 0` → early return.
pub fn blend_over(
pixels : FixedArray[Byte],
offset : Int,
src : Color,
coverage : Int,
global_alpha : Double,
) -> Unit {
if coverage == 0 {
return
}
// Fast path: fully opaque source, full coverage, no alpha scaling
if src.a >= 1.0 && coverage >= 256 && global_alpha >= 1.0 {
pixels[offset] = src.r.to_byte()
pixels[offset + 1] = src.g.to_byte()
pixels[offset + 2] = src.b.to_byte()
pixels[offset + 3] = b'\xFF'
return
}
let src_a = src.a * (coverage.to_double() / 256.0) * global_alpha
if src_a <= 0.0 {
return
}
let dr = pixels[offset].to_int()
let dg = pixels[offset + 1].to_int()
let db = pixels[offset + 2].to_int()
let da = pixels[offset + 3].to_int().to_double() / 255.0
let sr = src.r.to_double()
let sg = src.g.to_double()
let sb = src.b.to_double()
let one_minus_src_a = 1.0 - src_a
let out_a = src_a + da * one_minus_src_a
if out_a <= 0.0 {
pixels[offset] = b'\x00'
pixels[offset + 1] = b'\x00'
pixels[offset + 2] = b'\x00'
pixels[offset + 3] = b'\x00'
return
}
let out_r = (sr * src_a + dr.to_double() * da * one_minus_src_a) / out_a
let out_g = (sg * src_a + dg.to_double() * da * one_minus_src_a) / out_a
let out_b = (sb * src_a + db.to_double() * da * one_minus_src_a) / out_a
pixels[offset] = clamp_u8(out_r)
pixels[offset + 1] = clamp_u8(out_g)
pixels[offset + 2] = clamp_u8(out_b)
pixels[offset + 3] = clamp_u8(out_a * 255.0)
}
///|
fn clamp_u8(v : Double) -> Byte {
if v <= 0.0 {
b'\x00'
} else if v >= 255.0 {
b'\xFF'
} else {
(v + 0.5).to_int().to_byte()
}
}