///|
/// Applies a per-pixel transform to the R, G, B channels while leaving alpha
/// untouched, clamping every result back into `[0, 255]`. This is the shared
/// engine behind the point filters below, so each concrete filter only has to
/// express its channel math.
pub fn Image::map_rgb(
self : Image,
transform : (Int, Int, Int) -> (Int, Int, Int),
) -> Image {
let out = self.copy()
let n = self.pixel_count()
for p in 0.. Image {
self.map_rgb(fn(r, g, b) {
let y = (299 * r + 587 * g + 114 * b) / 1000
(y, y, y)
})
}
///|
/// Produces the photographic negative by flipping each channel around 255.
pub fn Image::invert(self : Image) -> Image {
self.map_rgb(fn(r, g, b) { (255 - r, 255 - g, 255 - b) })
}
///|
/// Shifts every channel by `delta` (negative darkens, positive brightens).
/// Out-of-range results saturate at 0 and 255.
pub fn Image::brightness(self : Image, delta : Int) -> Image {
self.map_rgb(fn(r, g, b) { (r + delta, g + delta, b + delta) })
}
///|
/// Scales contrast around the 128 mid-gray point. `factor > 1.0` boosts
/// contrast, `0.0 < factor < 1.0` flattens it, and `1.0` is a no-op.
pub fn Image::contrast(self : Image, factor : Double) -> Image {
self.map_rgb(fn(r, g, b) {
let apply = fn(c : Int) -> Int {
((c.to_double() - 128.0) * factor + 128.0).to_int()
}
(apply(r), apply(g), apply(b))
})
}