262 lines
8.2 KiB
Rust
262 lines
8.2 KiB
Rust
#![allow(dead_code)]
|
||
/// Shared helper functions for layer effect rendering.
|
||
///
|
||
/// Provides alpha extraction, compositing, spread/choke mapping,
|
||
/// smoothstep math, and box blur algorithms used across all effect modules.
|
||
|
||
/// Extract the alpha channel from an RGBA pixel buffer.
|
||
///
|
||
/// **Arguments:**
|
||
/// * `pixels` — Flat RGBA buffer (4 bytes per pixel)
|
||
/// * `px_count` — Total number of pixels
|
||
///
|
||
/// **Returns:**
|
||
/// A `Vec<u8>` containing one alpha byte per pixel.
|
||
pub(crate) fn extract_alpha(pixels: &[u8], px_count: usize) -> Vec<u8> {
|
||
let mut alpha = vec![0u8; px_count];
|
||
for i in 0..px_count {
|
||
alpha[i] = pixels[i * 4 + 3];
|
||
}
|
||
alpha
|
||
}
|
||
|
||
/// Composite an effect buffer onto a destination buffer using standard blend math.
|
||
///
|
||
/// Skips fully transparent source pixels. Used for "behind" effects (drop shadow,
|
||
/// outer glow) and standard inside effects (bevel, satin, inner glow, inner shadow).
|
||
///
|
||
/// **Arguments:**
|
||
/// * `dst` — Destination RGBA buffer (modified in-place)
|
||
/// * `effect_buf` — Effect RGBA buffer to composite
|
||
/// * `blend_mode` — Blend mode for compositing
|
||
/// * `opacity` — Effect opacity (0.0–1.0)
|
||
/// * `px_count` — Total number of pixels
|
||
pub(crate) fn composite_effect(
|
||
dst: &mut [u8],
|
||
effect_buf: &[u8],
|
||
blend_mode: hcie_blend::BlendMode,
|
||
opacity: f32,
|
||
px_count: usize,
|
||
) {
|
||
for i in 0..px_count {
|
||
let dst_px = [dst[i * 4], dst[i * 4 + 1], dst[i * 4 + 2], dst[i * 4 + 3]];
|
||
let src_px = [effect_buf[i * 4], effect_buf[i * 4 + 1], effect_buf[i * 4 + 2], effect_buf[i * 4 + 3]];
|
||
if src_px[3] == 0 { continue; }
|
||
let out = hcie_blend::blend_pixels(dst_px, src_px, blend_mode, opacity);
|
||
dst[i * 4..i * 4 + 4].copy_from_slice(&out);
|
||
}
|
||
}
|
||
|
||
/// Composite an inside effect buffer, preserving the original layer alpha.
|
||
///
|
||
/// For Normal blend: lerps between dst and src color based on effect alpha.
|
||
/// For other blend modes: uses standard blend_pixels.
|
||
/// Preserves the original alpha channel from the destination.
|
||
///
|
||
/// **Arguments:**
|
||
/// * `dst` — Destination RGBA buffer (modified in-place)
|
||
/// * `effect_buf` — Effect RGBA buffer to composite
|
||
/// * `blend_mode` — Blend mode for compositing
|
||
/// * `opacity` — Effect opacity (0.0–1.0)
|
||
/// * `px_count` — Total number of pixels
|
||
pub(crate) fn composite_inside_effect(
|
||
dst: &mut [u8],
|
||
effect_buf: &[u8],
|
||
blend_mode: hcie_blend::BlendMode,
|
||
opacity: f32,
|
||
px_count: usize,
|
||
) {
|
||
for i in 0..px_count {
|
||
let dst_px = [dst[i * 4], dst[i * 4 + 1], dst[i * 4 + 2], dst[i * 4 + 3]];
|
||
let src_px = [effect_buf[i * 4], effect_buf[i * 4 + 1], effect_buf[i * 4 + 2], effect_buf[i * 4 + 3]];
|
||
if src_px[3] == 0 || dst_px[3] == 0 { continue; }
|
||
|
||
let eff_sa = (src_px[3] as f32 / 255.0) * opacity;
|
||
|
||
if blend_mode == hcie_blend::BlendMode::Normal {
|
||
if eff_sa >= 1.0 {
|
||
dst[i * 4] = src_px[0];
|
||
dst[i * 4 + 1] = src_px[1];
|
||
dst[i * 4 + 2] = src_px[2];
|
||
} else {
|
||
let dst_r = dst_px[0] as f32;
|
||
let dst_g = dst_px[1] as f32;
|
||
let dst_b = dst_px[2] as f32;
|
||
let src_r = src_px[0] as f32;
|
||
let src_g = src_px[1] as f32;
|
||
let src_b = src_px[2] as f32;
|
||
dst[i * 4] = (dst_r * (1.0 - eff_sa) + src_r * eff_sa).round() as u8;
|
||
dst[i * 4 + 1] = (dst_g * (1.0 - eff_sa) + src_g * eff_sa).round() as u8;
|
||
dst[i * 4 + 2] = (dst_b * (1.0 - eff_sa) + src_b * eff_sa).round() as u8;
|
||
}
|
||
} else {
|
||
let out = hcie_blend::blend_pixels(dst_px, src_px, blend_mode, opacity);
|
||
dst[i * 4..i * 4 + 4].copy_from_slice(&out);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Apply smooth contrast scaling for spread/choke mapping.
|
||
///
|
||
/// Boosts alpha values based on spread percentage. Used by drop shadow
|
||
/// before blur to create harder or softer shadow edges.
|
||
///
|
||
/// **Arguments:**
|
||
/// * `alpha` — Alpha buffer to modify in-place
|
||
/// * `spread` — Spread percentage (0.0–100.0)
|
||
pub(crate) fn apply_spread(alpha: &mut [u8], spread: f32) {
|
||
if spread <= 0.0 {
|
||
return;
|
||
}
|
||
if spread >= 100.0 {
|
||
for a in alpha.iter_mut() {
|
||
if *a > 0 { *a = 255; }
|
||
}
|
||
return;
|
||
}
|
||
let factor = 1.0 / (1.0 - spread / 100.0);
|
||
for a in alpha.iter_mut() {
|
||
*a = ((*a as f32 * factor).round().min(255.0)) as u8;
|
||
}
|
||
}
|
||
|
||
/// Hermite S-curve smoothstep function for soft slope shading.
|
||
pub(crate) fn smoothstep(t: f32) -> f32 {
|
||
let t = t.clamp(0.0, 1.0);
|
||
t * t * (3.0 - 2.0 * t)
|
||
}
|
||
|
||
/// O(W*H) sliding-window box blur for f32 height fields.
|
||
///
|
||
/// Used as a building block for Gaussian blur approximation (multiple passes).
|
||
/// This is the public version used by `tuned.rs` and all effect modules.
|
||
///
|
||
/// **Arguments:**
|
||
/// * `data` — Input f32 buffer
|
||
/// * `w` — Image width
|
||
/// * `h` — Image height
|
||
/// * `radius` — Blur radius
|
||
///
|
||
/// **Returns:**
|
||
/// Blurred f32 buffer of the same size.
|
||
pub fn box_blur_f32(data: &[f32], w: u32, h: u32, radius: i32) -> Vec<f32> {
|
||
let n = (w * h) as usize;
|
||
let iw = w as usize;
|
||
let ih = h as usize;
|
||
let r = radius as usize;
|
||
let mut tmp = vec![0.0f32; n];
|
||
let mut out = vec![0.0f32; n];
|
||
|
||
for y in 0..ih {
|
||
let mut sum: f32 = 0.0;
|
||
let mut count: usize = 0;
|
||
let row = y * iw;
|
||
for nx in 0..=(r.min(iw - 1)) {
|
||
sum += data[row + nx];
|
||
count += 1;
|
||
}
|
||
tmp[row] = sum / count as f32;
|
||
|
||
for x in 1..iw {
|
||
let add_x = x + r;
|
||
if add_x < iw {
|
||
sum += data[row + add_x];
|
||
count += 1;
|
||
}
|
||
let sub_x = x as isize - r as isize - 1;
|
||
if sub_x >= 0 {
|
||
sum -= data[row + sub_x as usize];
|
||
count -= 1;
|
||
}
|
||
tmp[row + x] = sum / count as f32;
|
||
}
|
||
}
|
||
|
||
for x in 0..iw {
|
||
let mut sum: f32 = 0.0;
|
||
let mut count: usize = 0;
|
||
for ny in 0..=(r.min(ih - 1)) {
|
||
sum += tmp[ny * iw + x];
|
||
count += 1;
|
||
}
|
||
out[x] = sum / count as f32;
|
||
|
||
for y in 1..ih {
|
||
let add_y = y + r;
|
||
if add_y < ih {
|
||
sum += tmp[add_y * iw + x];
|
||
count += 1;
|
||
}
|
||
let sub_y = y as isize - r as isize - 1;
|
||
if sub_y >= 0 {
|
||
sum -= tmp[sub_y as usize * iw + x];
|
||
count -= 1;
|
||
}
|
||
out[y * iw + x] = sum / count as f32;
|
||
}
|
||
}
|
||
|
||
out
|
||
}
|
||
|
||
/// O(W*H) sliding-window box blur for u8 alpha masks.
|
||
pub(crate) fn box_blur(data: &[u8], w: u32, h: u32, radius: i32) -> Vec<u8> {
|
||
let n = (w * h) as usize;
|
||
let iw = w as usize;
|
||
let ih = h as usize;
|
||
let r = radius as usize;
|
||
let mut tmp = vec![0u8; n];
|
||
let mut out = vec![0u8; n];
|
||
|
||
for y in 0..ih {
|
||
let mut sum: u32 = 0;
|
||
let mut count: u32 = 0;
|
||
let row = y * iw;
|
||
for nx in 0..=(r.min(iw - 1)) {
|
||
sum += data[row + nx] as u32;
|
||
count += 1;
|
||
}
|
||
tmp[row] = (sum / count) as u8;
|
||
|
||
for x in 1..iw {
|
||
let add_x = x + r;
|
||
if add_x < iw {
|
||
sum += data[row + add_x] as u32;
|
||
count += 1;
|
||
}
|
||
let sub_x = x as isize - r as isize - 1;
|
||
if sub_x >= 0 {
|
||
sum -= data[row + sub_x as usize] as u32;
|
||
count -= 1;
|
||
}
|
||
tmp[row + x] = (sum / count) as u8;
|
||
}
|
||
}
|
||
|
||
for x in 0..iw {
|
||
let mut sum: u32 = 0;
|
||
let mut count: u32 = 0;
|
||
for ny in 0..=(r.min(ih - 1)) {
|
||
sum += tmp[ny * iw + x] as u32;
|
||
count += 1;
|
||
}
|
||
out[x] = (sum / count) as u8;
|
||
|
||
for y in 1..ih {
|
||
let add_y = y + r;
|
||
if add_y < ih {
|
||
sum += tmp[add_y * iw + x] as u32;
|
||
count += 1;
|
||
}
|
||
let sub_y = y as isize - r as isize - 1;
|
||
if sub_y >= 0 {
|
||
sum -= tmp[sub_y as usize * iw + x] as u32;
|
||
count -= 1;
|
||
}
|
||
out[y * iw + x] = (sum / count) as u8;
|
||
}
|
||
}
|
||
|
||
out
|
||
}
|