This commit is contained in:
2026-07-09 02:59:53 +03:00
commit 7ace048d94
817 changed files with 234289 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "hcie-fx"
version = "0.1.0"
edition = "2021"
[dependencies]
hcie-blend = { path = "../hcie-blend" }
hcie-protocol = { path = "../hcie-protocol" }
rayon = "1.10"
[lib]
crate-type = ["staticlib", "rlib"]
+175
View File
@@ -0,0 +1,175 @@
/// Effect application orchestrator.
///
/// Applies all enabled layer effects to a layer's pixel buffer in standard
/// Photoshop order. Delegates to individual effect modules for rendering.
use crate::types::LayerEffect;
use crate::emboss::generate_bevel_emboss;
use crate::outer_glow::generate_glow;
use crate::color_overlay::generate_color_overlay;
use crate::satin::generate_satin;
use crate::gradient_overlay::generate_gradient_overlay;
use crate::stroke::generate_stroke;
use crate::drop_shadow::generate_shadow;
use crate::inner_shadow::generate_inner_shadow;
use crate::helpers::{extract_alpha, composite_effect, composite_inside_effect};
use hcie_blend::blend_pixels;
/// Apply all enabled layer effects to a layer's pixel buffer in standard Photoshop order.
///
/// Returns a new RGBA buffer (canvas-sized) containing the composited result
/// of the original pixels with all effects applied.
pub fn apply_layer_effects(
pixels: &[u8],
canvas_w: u32,
canvas_h: u32,
effects: &[LayerEffect],
fill_opacity: f32,
) -> Vec<u8> {
let px_count = (canvas_w * canvas_h) as usize;
if effects.is_empty() {
if fill_opacity >= 1.0 {
return pixels.to_vec();
} else {
let mut out = pixels.to_vec();
for i in 0..px_count {
out[i * 4 + 3] = (out[i * 4 + 3] as f32 * fill_opacity).round().clamp(0.0, 255.0) as u8;
}
return out;
}
}
let alpha = extract_alpha(pixels, px_count);
// 1. Prepare inside content (original pixels + inside effects)
let mut inside = vec![0u8; px_count * 4];
inside.copy_from_slice(pixels);
if fill_opacity < 1.0 {
for i in 0..px_count {
inside[i * 4 + 3] = (inside[i * 4 + 3] as f32 * fill_opacity).round().clamp(0.0, 255.0) as u8;
}
}
// Inside effects — Photoshop spec order:
// a. Bevel & Emboss
if let Some(LayerEffect::BevelEmboss {
enabled: true, style, technique, depth, direction, size, soften,
angle, altitude, highlight_blend, highlight_color, highlight_opacity,
shadow_blend, shadow_color, shadow_opacity, ..
}) = effects.iter().find(|e| matches!(e, LayerEffect::BevelEmboss { .. })) {
// Emboss is rendered on top of the original layer fill; do not replace
// it with a hard-coded grey. Photoshop keeps the original pixel color
// and applies highlight/shadow blends over it.
let (highlight, shadow) = generate_bevel_emboss(
&alpha, canvas_w, canvas_h,
*depth, *size, *soften, *angle, *altitude,
*direction, *technique, *style,
*highlight_color, *shadow_color,
);
composite_effect(&mut inside, &highlight, *highlight_blend, *highlight_opacity, px_count);
composite_effect(&mut inside, &shadow, *shadow_blend, *shadow_opacity, px_count);
}
// b. Satin
if let Some(LayerEffect::Satin {
enabled: true, blend_mode, color, opacity, angle, distance, size, invert, ..
}) = effects.iter().find(|e| matches!(e, LayerEffect::Satin { .. })) {
let satin = generate_satin(&alpha, canvas_w, canvas_h, *angle, *distance, *size, *color, *invert);
composite_effect(&mut inside, &satin, *blend_mode, *opacity, px_count);
}
// c. Color Overlay
if let Some(LayerEffect::ColorOverlay {
enabled: true, blend_mode, color, opacity, ..
}) = effects.iter().find(|e| matches!(e, LayerEffect::ColorOverlay { .. })) {
let overlay = generate_color_overlay(&alpha, canvas_w, canvas_h, *color);
composite_inside_effect(&mut inside, &overlay, *blend_mode, *opacity, px_count);
}
// d. Gradient Overlay
if let Some(LayerEffect::GradientOverlay {
enabled: true, blend_mode, opacity, angle, scale, gradient, ..
}) = effects.iter().find(|e| matches!(e, LayerEffect::GradientOverlay { .. })) {
let overlay = generate_gradient_overlay(&alpha, canvas_w, canvas_h, *angle, *scale, gradient);
composite_inside_effect(&mut inside, &overlay, *blend_mode, *opacity, px_count);
}
// e. Pattern Overlay
if let Some(LayerEffect::PatternOverlay {
enabled: true, blend_mode, opacity, ..
}) = effects.iter().find(|e| matches!(e, LayerEffect::PatternOverlay { .. })) {
let overlay = generate_color_overlay(&alpha, canvas_w, canvas_h, [128, 128, 128, 255]);
composite_inside_effect(&mut inside, &overlay, *blend_mode, *opacity, px_count);
}
// f. Inner Glow
if let Some(LayerEffect::InnerGlow {
enabled: true, blend_mode, color, opacity, choke, size, ..
}) = effects.iter().find(|e| matches!(e, LayerEffect::InnerGlow { .. })) {
let glow = generate_glow(&alpha, canvas_w, canvas_h, *choke, *size, *color, true);
composite_effect(&mut inside, &glow, *blend_mode, *opacity, px_count);
}
// g. Inner Shadow
if let Some(LayerEffect::InnerShadow {
enabled: true, blend_mode, color, opacity, angle, distance, choke, size, ..
}) = effects.iter().find(|e| matches!(e, LayerEffect::InnerShadow { .. })) {
let shadow = generate_inner_shadow(&alpha, canvas_w, canvas_h, *angle, *distance, *choke, *size, *color);
composite_effect(&mut inside, &shadow, *blend_mode, *opacity, px_count);
}
// h. Stroke (Inside and Center — Outside handled in behind phase)
if let Some(LayerEffect::Stroke {
enabled: true, position, blend_mode, opacity, size, color, ..
}) = effects.iter().find(|e| matches!(e, LayerEffect::Stroke { .. })) {
if !matches!(position, crate::types::StrokePosition::Outside) {
let stroke = generate_stroke(&alpha, canvas_w, canvas_h, *size, *position, *color, *opacity);
composite_inside_effect(&mut inside, &stroke, *blend_mode, 1.0, px_count);
}
}
// 2. Behind content (behind effects rendered on empty canvas)
let mut behind = vec![0u8; px_count * 4];
// Behind effects — Photoshop spec order:
// a. Drop Shadow
if let Some(LayerEffect::DropShadow {
enabled: true, blend_mode, color, opacity, angle, distance, spread, size, ..
}) = effects.iter().find(|e| matches!(e, LayerEffect::DropShadow { .. })) {
let shadow = generate_shadow(&alpha, canvas_w, canvas_h, *angle, *distance, *spread, *size, *color, false);
composite_effect(&mut behind, &shadow, *blend_mode, *opacity, px_count);
}
// b. Outer Glow
if let Some(LayerEffect::OuterGlow {
enabled: true, blend_mode, color, opacity, spread, size, ..
}) = effects.iter().find(|e| matches!(e, LayerEffect::OuterGlow { .. })) {
let glow = generate_glow(&alpha, canvas_w, canvas_h, *spread, *size, *color, false);
composite_effect(&mut behind, &glow, *blend_mode, *opacity, px_count);
}
// c. Outside Stroke
if let Some(LayerEffect::Stroke {
enabled: true, position, blend_mode, opacity, size, color, ..
}) = effects.iter().find(|e| matches!(e, LayerEffect::Stroke { .. })) {
if matches!(position, crate::types::StrokePosition::Outside) {
let stroke = generate_stroke(&alpha, canvas_w, canvas_h, *size, *position, *color, *opacity);
composite_effect(&mut behind, &stroke, *blend_mode, 1.0, px_count);
}
}
// 3. Blend inside ON TOP of behind using Normal blend
for i in 0..px_count {
let b = [behind[i * 4], behind[i * 4 + 1], behind[i * 4 + 2], behind[i * 4 + 3]];
let ins = [inside[i * 4], inside[i * 4 + 1], inside[i * 4 + 2], inside[i * 4 + 3]];
if ins[3] == 0 {
behind[i * 4..i * 4 + 4].copy_from_slice(&b);
} else {
let out = blend_pixels(b, ins, hcie_blend::BlendMode::Normal, 1.0);
behind[i * 4..i * 4 + 4].copy_from_slice(&out);
}
}
behind
}
+21
View File
@@ -0,0 +1,21 @@
pub fn generate_color_overlay(
alpha: &[u8],
w: u32,
h: u32,
color: [u8; 4],
) -> Vec<u8> {
let px = (w * h) as usize;
let mut buf = vec![0u8; px * 4];
for i in 0..px {
let a = alpha[i];
if a > 0 {
buf[i * 4] = color[0];
buf[i * 4 + 1] = color[1];
buf[i * 4 + 2] = color[2];
buf[i * 4 + 3] = 255;
}
}
buf
}
+103
View File
@@ -0,0 +1,103 @@
/// Drop Shadow effect generator.
///
/// Renders a drop shadow by offsetting the layer alpha, applying spread,
/// blurring, and filling with the shadow color. Supports knock-out mode.
use crate::helpers::{apply_spread, box_blur_f32};
/// Generate a drop shadow buffer.
///
/// **Purpose:**
/// Renders a drop shadow effect on the transparent background by shifting the layer alpha,
/// applying spread scaling, blurring, and coloring.
///
/// **Logic & Workflow:**
/// 1. Offset layer alpha by the specified angle and distance.
/// 2. Apply spread scaling to the offset alpha (multiplied by `spread_scale = 0.00` based on Krita tuning).
/// 3. Apply box blur for Gaussian approximation using the tuned parameters (`blur_factor = 1.5`, `passes = 1`).
/// 4. Populate the final RGBA buffer with the shadow color and the computed alpha values.
/// 5. Optionally knock out the shadow under the original opaque layer pixels.
///
/// **Arguments:**
/// * `alpha` — `&[u8]`: Layer alpha channel.
/// * `w` — `u32`: Canvas width.
/// * `h` — `u32`: Canvas height.
/// * `angle` — `f32`: Shadow angle in degrees (PSD convention: 0=down, 90=left).
/// * `distance` — `f32`: Shadow offset distance in pixels.
/// * `spread` — `f32`: Spread percentage (0.0100.0).
/// * `size` — `f32`: Blur radius.
/// * `color` — `[u8; 4]`: Shadow RGBA color.
/// * `knock_out` — `bool`: If true, shadow is hidden behind the layer.
///
/// **Returns:**
/// `Vec<u8>`: RGBA buffer with the rendered drop shadow.
///
/// **Side Effects / Dependencies:**
/// Relies on `box_blur_f32` and `apply_spread` helpers from the `helpers` module.
pub(crate) fn generate_shadow(
alpha: &[u8],
w: u32,
h: u32,
angle: f32,
distance: f32,
spread: f32,
size: f32,
color: [u8; 4],
knock_out: bool,
) -> Vec<u8> {
let px = (w * h) as usize;
let mut buf = vec![0u8; px * 4];
let rad = angle.to_radians();
let dx_raw = (-rad.cos() * distance).round() as i32;
let dy_raw = (rad.sin() * distance).round() as i32;
let mut offset_alpha = vec![0u8; px];
for y in 0..h as i32 {
for x in 0..w as i32 {
let a = alpha[(y as u32 * w + x as u32) as usize];
if a == 0 { continue; }
let dx = x + dx_raw;
let dy = y + dy_raw;
if dx >= 0 && dx < w as i32 && dy >= 0 && dy < h as i32 {
let di = (dy as u32 * w + dx as u32) as usize;
offset_alpha[di] = offset_alpha[di].max(a);
}
}
}
// MAE-optimized against Krita ground truth (768 samples, 350 combos, 800x800)
// Best: blur_factor=5.0, passes=5, spread_scale=0.00
// Since spread_scale is 0.00, we apply 0.0 spread to offset_alpha.
apply_spread(&mut offset_alpha, spread * 0.00);
let radius = size.max(0.0).round() as i32;
if radius > 0 {
let gaussian_r = (radius as f32 / 5.0).round().max(1.0) as i32;
let mut f32_buf: Vec<f32> = offset_alpha.iter().map(|&a| a as f32).collect();
for _ in 0..5 {
f32_buf = box_blur_f32(&f32_buf, w, h, gaussian_r);
}
offset_alpha = f32_buf.iter().map(|&a| (a as u8).min(255)).collect();
}
for i in 0..px {
let a = offset_alpha[i];
if a > 0 {
buf[i * 4] = color[0];
buf[i * 4 + 1] = color[1];
buf[i * 4 + 2] = color[2];
buf[i * 4 + 3] = a;
}
}
if knock_out {
for i in 0..px {
if alpha[i] > 0 {
buf[i * 4 + 3] = 0;
}
}
}
buf
}
+339
View File
@@ -0,0 +1,339 @@
use crate::types;
use crate::helpers::box_blur_f32;
pub fn generate_bevel_emboss(
alpha: &[u8],
w: u32,
h: u32,
depth: f32,
size: f32,
soften: f32,
angle: f32,
altitude: f32,
direction: types::Direction,
technique: types::Technique,
style: types::BevelStyle,
highlight_color: [u8; 4],
shadow_color: [u8; 4],
) -> (Vec<u8>, Vec<u8>) {
let px = (w * h) as usize;
let mut highlight_buf = vec![0u8; px * 4];
let mut shadow_buf = vec![0u8; px * 4];
let iw = w as usize;
let ih = h as usize;
let radius = size.max(1.0);
// Compute signed distance field
let dist_inside_sq = edt_inside(alpha, iw, ih);
let dist_outside_sq = edt_outside(alpha, iw, ih);
// Compute shape center for Emboss tilt
let (cx, cy) = {
let mut min_x = iw;
let mut max_x = 0usize;
let mut min_y = ih;
let mut max_y = 0usize;
for i in 0..px {
if alpha[i] > 0 {
let x = i % iw;
let y = i / iw;
if x < min_x { min_x = x; }
if x > max_x { max_x = x; }
if y < min_y { min_y = y; }
if y > max_y { max_y = y; }
}
}
((min_x + max_x) as f32 / 2.0, (min_y + max_y) as f32 / 2.0)
};
// Light direction calculations (standard 2D screen coordinates with Y inverted)
let rad = angle.to_radians();
let alt_rad = altitude.to_radians();
let cos_alt = alt_rad.cos();
let light_x = rad.cos() * cos_alt;
let light_y = -rad.sin() * cos_alt;
let light_z = alt_rad.sin().max(0.1);
// Build continuous height field (unscaled — depth affects intensity, not profile shape)
let mut height = vec![0.0f32; px];
for i in 0..px {
let d_in = dist_inside_sq[i].sqrt();
let d_out = dist_outside_sq[i].sqrt();
let val = match style {
types::BevelStyle::InnerBevel => {
if alpha[i] > 0 {
(d_in / radius).min(1.0) * radius
} else {
0.0
}
}
types::BevelStyle::OuterBevel => {
if alpha[i] > 0 {
radius
} else {
((radius - d_out).max(0.0) / radius) * radius
}
}
types::BevelStyle::Emboss => {
let px_x = (i % iw) as f32;
let px_y = (i / iw) as f32;
let tilt = (px_x - cx) * light_x + (px_y - cy) * light_y;
if alpha[i] > 0 {
let edge_factor = (d_in / radius).min(1.0);
radius * edge_factor + tilt
} else {
let t = (d_out / radius).min(1.0);
(radius * (1.0 - t) * 0.5 + tilt).max(0.0)
}
}
_ => {
if alpha[i] > 0 {
(d_in / radius).min(1.0) * radius
} else {
0.0
}
}
};
height[i] = val;
}
// Apply soften box-blur on the height field.
// Even at soften=0, apply minimal 1px blur for smoother gradient estimation at edges.
let soften_radius = (soften.max(0.0).round() as i32).max(1);
height = box_blur_f32(&height, w, h, soften_radius);
// Depth controls how prominent the bevel appears.
// Higher depth = steeper slopes = more contrast between highlight and shadow.
let depth_scale = depth / 100.0;
// Compute lighting factors in a first pass
let mut lighting = vec![0.0f32; px];
for y in 0..ih {
for x in 0..iw {
let i = y * iw + x;
let show = match style {
types::BevelStyle::InnerBevel => alpha[i] > 0,
types::BevelStyle::OuterBevel => alpha[i] == 0,
types::BevelStyle::Emboss => true,
_ => alpha[i] > 0,
};
if !show { continue; }
let is_inner = alpha[i] > 0;
let dist_abs = if is_inner { dist_inside_sq[i].sqrt() } else { dist_outside_sq[i].sqrt() };
if !is_inner && dist_abs > radius { continue; }
let technique_show = match technique {
types::Technique::Smooth => true,
_ => dist_abs <= radius,
};
if !technique_show { continue; }
if is_inner && dist_abs > radius { continue; }
let x0 = if x > 0 { x - 1 } else { x };
let x1 = if x < iw - 1 { x + 1 } else { x };
let y0 = if y > 0 { y - 1 } else { y };
let y1 = if y < ih - 1 { y + 1 } else { y };
let dx_val = (height[y * iw + x1] - height[y * iw + x0]) * depth_scale;
let dy_val = (height[y1 * iw + x] - height[y0 * iw + x]) * depth_scale;
let nx = -dx_val / 2.0;
let ny = -dy_val / 2.0;
let nz = 1.0;
let len = (nx * nx + ny * ny + nz * nz).sqrt();
let (nx, ny, nz) = if len > 1e-6 {
(nx / len, ny / len, nz / len)
} else {
(0.0, 0.0, 1.0)
};
let ndl = nx * light_x + ny * light_y + nz * light_z;
let factor = if matches!(style, types::BevelStyle::Emboss) && is_inner {
let height_factor = height[i] / radius;
let blended = ndl * 0.4 + height_factor * 0.6;
blended * depth_scale
} else {
ndl
};
lighting[i] = match direction {
types::Direction::Up => factor,
types::Direction::Down => -factor,
};
}
}
// Smooth the lighting to reduce wrinkles at sharp corners.
// MAE-optimized against Krita ground truth (320 samples, 256×256).
// Best: lighting_blur=5, hl_scale=0.30, sh_scale=2.00, avg_mae=17.18
// BEGIN_TUNING_ZONE
let lighting_blur = (soften_radius + 5).max(1);
lighting = box_blur_f32(&lighting, w, h, lighting_blur);
for i in 0..px {
let is_inner = alpha[i] > 0;
let show = match style {
types::BevelStyle::InnerBevel => is_inner,
types::BevelStyle::OuterBevel => !is_inner,
types::BevelStyle::Emboss => true,
_ => is_inner,
};
if !show { continue; }
let dist_abs = if is_inner { dist_inside_sq[i].sqrt() } else { dist_outside_sq[i].sqrt() };
if !is_inner && dist_abs > radius { continue; }
let technique_show = match technique {
types::Technique::Smooth => true,
_ => dist_abs <= radius,
};
if !technique_show { continue; }
let ndl = lighting[i];
let is_flat = is_inner && dist_abs > radius;
if is_flat {
if style == types::BevelStyle::Emboss && depth > 100.0 {
shadow_buf[i * 4 + 3] = alpha[i];
}
continue;
}
// MAE-optimized against Krita ground truth (320 samples, 256×256).
// Best: lighting_blur=5, hl_scale=0.30, sh_scale=2.00, avg_mae=17.18
let hl_pred = (ndl * 0.30).clamp(0.0, 1.0);
let sh_pred = (-ndl * 2.00).clamp(0.0, 1.0);
let hl_a = (alpha[i] as f32 * hl_pred).round() as u8;
let sh_a = (alpha[i] as f32 * sh_pred).round() as u8;
if hl_a > 0 {
highlight_buf[i * 4] = highlight_color[0];
highlight_buf[i * 4 + 1] = highlight_color[1];
highlight_buf[i * 4 + 2] = highlight_color[2];
highlight_buf[i * 4 + 3] = hl_a;
}
if sh_a > 0 {
shadow_buf[i * 4] = shadow_color[0];
shadow_buf[i * 4 + 1] = shadow_color[1];
shadow_buf[i * 4 + 2] = shadow_color[2];
shadow_buf[i * 4 + 3] = sh_a;
}
}
// END_TUNING_ZONE
(highlight_buf, shadow_buf)
}
/// Euclidean Distance Transform for "inside" pixels (distance to nearest transparent pixel).
/// Returns squared distances.
fn edt_inside(alpha: &[u8], w: usize, h: usize) -> Vec<f32> {
let n = w * h;
let mut dt = vec![1e20f32; n];
for y in 0..h {
for x in 0..w {
let i = y * w + x;
if alpha[i] == 0 {
dt[i] = 0.0;
}
}
}
edt_2d(&mut dt, w, h);
dt
}
/// Euclidean Distance Transform for "outside" pixels (distance to nearest opaque pixel).
fn edt_outside(alpha: &[u8], w: usize, h: usize) -> Vec<f32> {
let n = w * h;
let mut dt = vec![1e20f32; n];
for y in 0..h {
for x in 0..w {
let i = y * w + x;
if alpha[i] > 0 {
dt[i] = 0.0;
}
}
}
edt_2d(&mut dt, w, h);
dt
}
/// 2D Euclidean Distance Transform using the Felzenszwalb & Huttenlocher algorithm.
fn edt_2d(dt: &mut [f32], w: usize, h: usize) {
let mut col = vec![0.0f32; h];
for x in 0..w {
for y in 0..h {
col[y] = dt[y * w + x];
}
edt_1d(&mut col);
for y in 0..h {
dt[y * w + x] = col[y];
}
}
let mut row = vec![0.0f32; w];
for y in 0..h {
for x in 0..w {
row[x] = dt[y * w + x];
}
edt_1d(&mut row);
for x in 0..w {
dt[y * w + x] = row[x];
}
}
}
/// 1D EDT (Felzenszwalb & Huttenlocher).
fn edt_1d(f: &mut [f32]) {
let n = f.len();
if n == 0 { return; }
let mut d = vec![0.0f32; n];
let mut v = vec![0usize; n];
let mut z = vec![0.0f32; n + 1];
let mut k = 0usize;
v[0] = 0;
z[0] = f32::NEG_INFINITY;
z[1] = f32::INFINITY;
for q in 1..n {
let qf = q as f32;
let fq = f[q] + qf * qf;
let vk = v[k];
let fv_vk = f[vk] + vk as f32 * vk as f32;
let mut s = (fq - fv_vk) / (2.0 * qf - 2.0 * vk as f32);
while s <= z[k] {
k = k.wrapping_sub(1);
if k == usize::MAX { k = 0; break; }
let vk2 = v[k];
let fv_vk2 = f[vk2] + vk2 as f32 * vk2 as f32;
s = (fq - fv_vk2) / (2.0 * qf - 2.0 * vk2 as f32);
}
k += 1;
v[k] = q;
z[k] = s;
z[k + 1] = f32::INFINITY;
}
k = 0;
for q in 0..n {
let qf = q as f32;
while z[k + 1] < qf { k += 1; }
let vk = v[k];
let diff = qf - vk as f32;
d[q] = diff * diff + f[vk];
}
f.copy_from_slice(&d);
}
+92
View File
@@ -0,0 +1,92 @@
pub fn generate_gradient_overlay(
alpha: &[u8],
w: u32,
h: u32,
angle: f32,
scale: f32,
gradient: &crate::types::GradientDef,
) -> Vec<u8> {
let px = (w * h) as usize;
let mut buf = vec![0u8; px * 4];
let rad = angle.to_radians();
let cos_a = rad.cos();
let sin_a = rad.sin();
let scale_factor = if scale > 0.0 { scale / 100.0 } else { 1.0 };
let cx = w as f32 / 2.0;
let cy = h as f32 / 2.0;
let max_dist = ((w * w + h * h) as f32).sqrt() / 2.0 * scale_factor;
for y in 0..h as usize {
for x in 0..w as usize {
let i = y * w as usize + x;
let a = alpha[i];
if a == 0 { continue; }
let fx = x as f32 - cx;
let fy = y as f32 - cy;
let proj = fx * cos_a + fy * sin_a;
let t = ((proj / max_dist) + 0.5).clamp(0.0, 1.0);
let color = sample_gradient(t, gradient);
buf[i * 4] = color[0];
buf[i * 4 + 1] = color[1];
buf[i * 4 + 2] = color[2];
buf[i * 4 + 3] = a;
}
}
buf
}
fn sample_gradient(t: f32, gradient: &crate::types::GradientDef) -> [u8; 4] {
let stops = &gradient.color_stops;
if stops.is_empty() {
let v = (t * 255.0).round().clamp(0.0, 255.0) as u8;
return [v, v, v, 255];
}
if stops.len() == 1 {
return stops[0].1;
}
let max_loc = stops.iter().map(|s| s.0).max().unwrap_or(4096) as f32;
let norm = if max_loc > 0.0 { max_loc } else { 4096.0 };
let t_abs = t * norm;
if t_abs <= stops[0].0 as f32 {
return stops[0].1;
}
if t_abs >= stops[stops.len() - 1].0 as f32 {
return stops[stops.len() - 1].1;
}
for j in 0..stops.len() - 1 {
let loc0 = stops[j].0 as f32;
let loc1 = stops[j + 1].0 as f32;
if t_abs >= loc0 && t_abs <= loc1 {
let seg_len = loc1 - loc0;
if seg_len < 1e-4 {
return stops[j].1;
}
let local_t = (t_abs - loc0) / seg_len;
let c0 = stops[j].1;
let c1 = stops[j + 1].1;
let r = c0[0] as f32 + local_t * (c1[0] as f32 - c0[0] as f32);
let g = c0[1] as f32 + local_t * (c1[1] as f32 - c0[1] as f32);
let b = c0[2] as f32 + local_t * (c1[2] as f32 - c0[2] as f32);
let a = c0[3] as f32 + local_t * (c1[3] as f32 - c0[3] as f32);
return [
r.round().clamp(0.0, 255.0) as u8,
g.round().clamp(0.0, 255.0) as u8,
b.round().clamp(0.0, 255.0) as u8,
a.round().clamp(0.0, 255.0) as u8,
];
}
}
stops[stops.len() - 1].1
}
+261
View File
@@ -0,0 +1,261 @@
#![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.01.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.01.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.0100.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
}
+87
View File
@@ -0,0 +1,87 @@
/// Inner Shadow effect generator.
///
/// Renders an inner shadow by offsetting the inverted layer alpha,
/// blurring, and masking with the original alpha. MAE-optimized
/// parameters for Photoshop-compatible output.
use crate::helpers::box_blur_f32;
/// Generate an inner shadow buffer.
///
/// **Algorithm:**
/// 1. Invert layer alpha (255 - alpha)
/// 2. Offset inverted alpha by (distance, angle)
/// 3. Box blur 2-pass (blur_factor=10.0)
/// 4. Mask with original alpha (mask_strength=0.50)
/// 5. Fill with shadow color
///
/// **MAE-optimized parameters** (from grid search on 1536 PSD samples):
/// - blur_factor = 10.0
/// - passes = 2
/// - choke_scale = 0.0 (choke disabled)
/// - mask_strength = 0.50
///
/// **Arguments:**
/// * `alpha` — Layer alpha channel
/// * `w`, `h` — Canvas dimensions
/// * `angle` — Shadow angle in degrees
/// * `distance` — Shadow offset distance in pixels
/// * `choke` — Choke amount (currently unused, kept for API compatibility)
/// * `size` — Blur radius
/// * `color` — Shadow RGBA color
///
/// **Returns:**
/// RGBA buffer with the rendered inner shadow.
pub(crate) fn generate_inner_shadow(
alpha: &[u8],
w: u32,
h: u32,
angle: f32,
distance: f32,
_choke: f32,
size: f32,
color: [u8; 4],
) -> Vec<u8> {
let px = (w * h) as usize;
let mut buf = vec![0u8; px * 4];
let rad = angle.to_radians();
let dx_raw = (-rad.cos() * distance).round() as i32;
let dy_raw = (rad.sin() * distance).round() as i32;
let mut offset_alpha = vec![255u8; px];
for y in 0..h as i32 {
for x in 0..w as i32 {
let orig_i = (y as u32 * w + x as u32) as usize;
let val = 255 - alpha[orig_i];
let dx = x + dx_raw;
let dy = y + dy_raw;
if dx >= 0 && dx < w as i32 && dy >= 0 && dy < h as i32 {
let di = (dy as u32 * w + dx as u32) as usize;
offset_alpha[di] = val;
}
}
}
let radius = size.max(0.0).round() as i32;
if radius > 0 {
let gaussian_r = (radius as f32 / 10.0).round().max(1.0) as i32;
let mut f32_buf: Vec<f32> = offset_alpha.iter().map(|&a| a as f32).collect();
f32_buf = box_blur_f32(&f32_buf, w, h, gaussian_r);
f32_buf = box_blur_f32(&f32_buf, w, h, gaussian_r);
offset_alpha = f32_buf.iter().map(|&a| (a as u8).min(255)).collect();
}
for i in 0..px {
let a = ((offset_alpha[i] as f32 * 0.50) as u32 * alpha[i] as u32 / 255) as u8;
if a > 0 {
buf[i * 4] = color[0];
buf[i * 4 + 1] = color[1];
buf[i * 4 + 2] = color[2];
buf[i * 4 + 3] = a;
}
}
buf
}
+46
View File
@@ -0,0 +1,46 @@
#![allow(dead_code)]
//! # hcie-fx
//!
//! Layer effects engine for the HCIE image editor.
//!
//! Handles parsing and rendering of PSD layer effects:
//! Drop Shadow, Inner Shadow, Outer Glow, Inner Glow, Bevel & Emboss,
//! Satin, Color Overlay, Gradient Overlay, Pattern Overlay, Stroke.
//!
//! ## Architecture
//! - `types.rs` — Data structures for layer effects
//! - `parser.rs` — Binary parsing of PSD `lfx2` and `lrFX` tagged blocks
//! - `apply_effects.rs` — Effect application orchestrator
//! - `helpers.rs` — Shared utilities (blur, compositing, alpha extraction)
//! - `drop_shadow.rs` — Drop shadow renderer
//! - `inner_shadow.rs` — Inner shadow renderer
//! - `emboss.rs` — Bevel & emboss renderer
//! - `outer_glow.rs` — Outer/inner glow renderer
//! - `color_overlay.rs` — Color overlay renderer
//! - `satin.rs` — Satin effect renderer
//! - `gradient_overlay.rs` — Gradient overlay renderer
//! - `stroke.rs` — Stroke renderer
//! - `tuned.rs` — MAE-optimized tuned versions for grid search
//!
//! ## Dependencies
//! - `hcie-blend` — Blend mode math for compositing effects with layers
pub mod types;
pub mod parser;
pub mod helpers;
pub mod apply_effects;
pub mod drop_shadow;
pub mod inner_shadow;
pub mod emboss;
pub mod outer_glow;
pub mod color_overlay;
pub mod satin;
pub mod gradient_overlay;
pub mod stroke;
pub mod tuned;
pub use types::LayerEffect;
pub use types::{blend_mode_to_string, blend_mode_from_string, protocol_to_hcie_fx_effect, hcie_fx_effect_to_protocol, layer_style_to_effect};
pub use parser::{parse_lfx2, parse_lrFX, parse_asl, parse_asl_styles};
pub use apply_effects::apply_layer_effects;
pub use helpers::box_blur_f32;
+76
View File
@@ -0,0 +1,76 @@
use crate::helpers::box_blur_f32;
pub fn generate_glow(
alpha: &[u8],
w: u32,
h: u32,
spread: f32,
size: f32,
color: [u8; 4],
inner: bool,
) -> Vec<u8> {
let px = (w * h) as usize;
let mut buf = vec![0u8; px * 4];
let radius = size.max(0.0).round() as i32;
let glow_alpha = if inner {
// MAE-optimized against Krita ground truth (48 samples, 3000 combos, 800x800)
// Best: blur_factor=4.0, passes=4, alpha_scale=0.00, glow_boost=2.00, spread_before_blur=false
let mut ga = alpha.iter().map(|&a| a as f32).collect::<Vec<f32>>();
if radius > 0 {
let gaussian_r = (radius as f32 / 4.0).round().max(1.0) as i32;
for _ in 0..4 {
ga = box_blur_f32(&ga, w, h, gaussian_r);
}
}
for i in 0..px {
ga[i] = 255.0 - ga[i];
}
if spread > 0.0 {
let factor = 1.0 / (1.0 - spread / 100.0);
for a in ga.iter_mut() {
*a = (*a * factor).round().min(255.0);
}
}
for i in 0..px {
ga[i] = (ga[i] - alpha[i] as f32 * 0.00).max(0.0) * 2.00 * alpha[i] as f32 / 255.0;
}
ga.iter().map(|&a| a as u8).collect::<Vec<u8>>()
} else {
let mut ga = alpha.iter().map(|&a| a as f32).collect::<Vec<f32>>();
// BEGIN_GLOW_TUNING_ZONE
// MAE-optimized against Krita ground truth (48 samples, 3000 combos, 800x800)
// Best: blur_factor=4.0, passes=4, alpha_scale=0.00, glow_boost=2.00, spread_before_blur=false
if radius > 0 {
let gaussian_r = (radius as f32 / 4.0).round().max(1.0) as i32;
for _ in 0..4 {
ga = box_blur_f32(&ga, w, h, gaussian_r);
}
}
if spread > 0.0 {
let factor = 1.0 / (1.0 - spread / 100.0);
for a in ga.iter_mut() {
*a = (*a * factor).round().min(255.0);
}
}
for i in 0..px {
let mask = 255.0 - alpha[i] as f32;
ga[i] = (ga[i] - alpha[i] as f32 * 0.00).max(0.0) * 2.00 * mask / 255.0;
}
// END_GLOW_TUNING_ZONE
ga.iter().map(|&a| a as u8).collect::<Vec<u8>>()
};
for i in 0..px {
let a = glow_alpha[i];
if a > 0 {
buf[i * 4] = color[0];
buf[i * 4 + 1] = color[1];
buf[i * 4 + 2] = color[2];
buf[i * 4 + 3] = a;
}
}
buf
}
File diff suppressed because it is too large Load Diff
+77
View File
@@ -0,0 +1,77 @@
use crate::helpers::box_blur_f32;
pub fn generate_satin(
alpha: &[u8],
w: u32,
h: u32,
angle: f32,
distance: f32,
size: f32,
color: [u8; 4],
invert: bool,
) -> Vec<u8> {
let px = (w * h) as usize;
let mut buf = vec![0u8; px * 4];
let rad = angle.to_radians();
let dx = (rad.cos() * distance).round() as i32;
let dy = -(rad.sin() * distance).round() as i32;
let mut offset1 = vec![0u8; px];
let mut offset2 = vec![0u8; px];
for y in 0..h as i32 {
for x in 0..w as i32 {
let di = (y as u32 * w + x as u32) as usize;
let sx1 = x - dx;
let sy1 = y - dy;
if sx1 >= 0 && sx1 < w as i32 && sy1 >= 0 && sy1 < h as i32 {
offset1[di] = alpha[(sy1 as u32 * w + sx1 as u32) as usize];
}
let sx2 = x + dx;
let sy2 = y + dy;
if sx2 >= 0 && sx2 < w as i32 && sy2 >= 0 && sy2 < h as i32 {
offset2[di] = alpha[(sy2 as u32 * w + sx2 as u32) as usize];
}
}
}
let mut satin_alpha = vec![0u8; px];
for i in 0..px {
let a = (offset1[i] as u32 * offset2[i] as u32 / 255) as u8;
satin_alpha[i] = a;
}
let radius = size.max(0.0).round() as i32;
if radius > 0 {
let gaussian_r = (radius as f32 / 2.5).round().max(1.0) as i32;
let mut f32_buf: Vec<f32> = satin_alpha.iter().map(|&a| a as f32).collect();
for _ in 0..3 {
f32_buf = box_blur_f32(&f32_buf, w, h, gaussian_r);
}
satin_alpha = f32_buf.iter().map(|&a| (a as u8).min(255)).collect();
}
for i in 0..px {
satin_alpha[i] = (satin_alpha[i] as u32 * alpha[i] as u32 / 255) as u8;
}
if invert {
for v in satin_alpha.iter_mut() {
*v = 255 - *v;
}
}
for i in 0..px {
let a = satin_alpha[i];
if a > 0 {
buf[i * 4] = color[0];
buf[i * 4 + 1] = color[1];
buf[i * 4 + 2] = color[2];
buf[i * 4 + 3] = a;
}
}
buf
}
+126
View File
@@ -0,0 +1,126 @@
pub fn generate_stroke(
alpha: &[u8],
w: u32,
h: u32,
size: f32,
position: crate::types::StrokePosition,
color: [u8; 4],
opacity: f32,
) -> Vec<u8> {
let px = (w * h) as usize;
let mut buf = vec![0u8; px * 4];
let mut stroke_alpha = vec![0.0f32; px];
let radius = size.max(1.0);
let iw = w as usize;
let ih = h as usize;
// BEGIN_TUNING_ZONE
// MAE-optimized against Photoshop ground truth (384 asymmetric-shape samples, 49 combos)
// Best: aa=0.25, feather=0.25, avg MAE=8.2781
// Per-shape bests ranged 1.16-2.69; 0.25/0.25 chosen as the robust global optimum.
let aa = 0.25f32;
let feather = 0.25f32;
let normal_step = 0.00f32;
// END_TUNING_ZONE
let aa = aa.max(0.25).min(4.0);
match position {
crate::types::StrokePosition::Inside => {
let dist_sq = crate::tuned::edt_inside(alpha, iw, ih);
for i in 0..px {
if alpha[i] == 0 { continue; }
let d = dist_sq[i].sqrt();
if d >= radius { continue; }
let outer_dist = radius - d;
let coverage = (outer_dist / aa).clamp(0.0, 1.0);
stroke_alpha[i] = coverage * alpha[i] as f32;
}
}
crate::types::StrokePosition::Outside => {
let dist_sq = crate::tuned::edt_outside(alpha, iw, ih);
let nearest = crate::tuned::edt_outside_src(alpha, iw, ih);
for i in 0..px {
let d = dist_sq[i].sqrt();
if d == 0.0 || d >= radius { continue; }
let x_i = (i % iw) as f32;
let y_i = (i / iw) as f32;
let x_n = (nearest[i] % iw) as f32;
let y_n = (nearest[i] / iw) as f32;
let dx = x_i - x_n;
let dy = y_i - y_n;
let src_a = if normal_step > 0.0 {
let sx = (x_n - dx / d * normal_step).round() as i32;
let sy = (y_n - dy / d * normal_step).round() as i32;
if sx >= 0 && sx < iw as i32 && sy >= 0 && sy < ih as i32 {
alpha[sy as usize * iw + sx as usize] as f32
} else {
alpha[nearest[i]] as f32
}
} else {
alpha[nearest[i]] as f32
};
let outer_dist = radius - d;
let coverage = (outer_dist / aa).clamp(0.0, 1.0);
stroke_alpha[i] = coverage * src_a;
}
}
crate::types::StrokePosition::Center => {
let dist_in_sq = crate::tuned::edt_inside(alpha, iw, ih);
let dist_out_sq = crate::tuned::edt_outside(alpha, iw, ih);
let nearest = crate::tuned::edt_outside_src(alpha, iw, ih);
let half_r = radius / 2.0;
for i in 0..px {
let (d, is_inside) = if alpha[i] > 0 {
(dist_in_sq[i].sqrt(), true)
} else {
(dist_out_sq[i].sqrt(), false)
};
if d == 0.0 || d >= half_r { continue; }
let src_a = if is_inside {
alpha[i] as f32
} else {
let x_i = (i % iw) as f32;
let y_i = (i / iw) as f32;
let x_n = (nearest[i] % iw) as f32;
let y_n = (nearest[i] / iw) as f32;
let dx = x_i - x_n;
let dy = y_i - y_n;
if normal_step > 0.0 {
let sx = (x_n - dx / d * normal_step).round() as i32;
let sy = (y_n - dy / d * normal_step).round() as i32;
if sx >= 0 && sx < iw as i32 && sy >= 0 && sy < ih as i32 {
alpha[sy as usize * iw + sx as usize] as f32
} else {
alpha[nearest[i]] as f32
}
} else {
alpha[nearest[i]] as f32
}
};
let outer_dist = half_r - d;
let coverage = (outer_dist / aa).clamp(0.0, 1.0);
stroke_alpha[i] = coverage * src_a;
}
}
}
if feather > 0.0 {
let feather_radius = (feather.round() as i32).max(1);
stroke_alpha = crate::helpers::box_blur_f32(&stroke_alpha, w, h, feather_radius);
}
for i in 0..px {
let a = (stroke_alpha[i] * opacity).round() as u8;
if a > 0 {
buf[i * 4] = color[0];
buf[i * 4 + 1] = color[1];
buf[i * 4 + 2] = color[2];
buf[i * 4 + 3] = a;
}
}
buf
}
+792
View File
@@ -0,0 +1,792 @@
use crate::helpers::box_blur_f32;
use crate::types;
// ── Bevel & Emboss Tuned ──
pub fn generate_bevel_tuned(
alpha: &[u8],
w: u32,
h: u32,
depth: f32,
size: f32,
soften: f32,
angle: f32,
altitude: f32,
direction: &types::Direction,
_technique: &types::Technique,
style: &types::BevelStyle,
lighting_blur: i32,
hl_scale: f32,
sh_scale: f32,
profile_exp: f32,
) -> (Vec<u8>, Vec<u8>) {
let hl_color = [255u8; 4];
let sh_color = [0u8; 4];
let px = (w * h) as usize;
let mut highlight_buf = vec![0u8; px * 4];
let mut shadow_buf = vec![0u8; px * 4];
let iw = w as usize;
let ih = h as usize;
let radius = size.max(1.0);
let dist_inside_sq = edt_inside(alpha, iw, ih);
let dist_outside_sq = edt_outside(alpha, iw, ih);
// Compute shape center for Emboss tilt
let (cx, cy) = {
let mut min_x = iw;
let mut max_x = 0usize;
let mut min_y = ih;
let mut max_y = 0usize;
for i in 0..px {
if alpha[i] > 0 {
let x = i % iw;
let y = i / iw;
if x < min_x { min_x = x; }
if x > max_x { max_x = x; }
if y < min_y { min_y = y; }
if y > max_y { max_y = y; }
}
}
((min_x + max_x) as f32 / 2.0, (min_y + max_y) as f32 / 2.0)
};
let rad = angle.to_radians();
let alt_rad = altitude.to_radians();
let cos_alt = alt_rad.cos();
let light_x = rad.cos() * cos_alt;
let light_y = -rad.sin() * cos_alt;
let light_z = alt_rad.sin().max(0.1);
let mut height = vec![0.0f32; px];
for i in 0..px {
let d_in = dist_inside_sq[i].sqrt();
let d_out = dist_outside_sq[i].sqrt();
let val = match style {
types::BevelStyle::InnerBevel => {
if alpha[i] > 0 {
let t = (d_in / radius).min(1.0);
t.powf(profile_exp) * radius
} else {
0.0
}
}
types::BevelStyle::OuterBevel => {
if alpha[i] > 0 {
radius
} else {
let t = ((radius - d_out).max(0.0) / radius).min(1.0);
(1.0 - t).powf(profile_exp) * radius
}
}
types::BevelStyle::Emboss => {
let px_x = (i % iw) as f32;
let px_y = (i / iw) as f32;
let tilt = (px_x - cx) * light_x + (px_y - cy) * light_y;
if alpha[i] > 0 {
let t = (d_in / radius).min(1.0);
radius * t.powf(profile_exp) + tilt
} else {
let t = (d_out / radius).min(1.0);
(radius * (1.0 - t).powf(profile_exp) * 0.5 + tilt).max(0.0)
}
}
_ => {
if alpha[i] > 0 {
let t = (d_in / radius).min(1.0);
t.powf(profile_exp) * radius
} else {
0.0
}
}
};
height[i] = val;
}
let soften_radius = (soften.max(0.0).round() as i32).max(1);
height = box_blur_f32(&height, w, h, soften_radius);
let depth_scale = depth / 100.0;
let mut lighting = vec![0.0f32; px];
for y in 0..ih {
for x in 0..iw {
let i = y * iw + x;
let show = match style {
types::BevelStyle::InnerBevel => alpha[i] > 0,
types::BevelStyle::OuterBevel => alpha[i] == 0,
types::BevelStyle::Emboss => true,
_ => alpha[i] > 0,
};
if !show { continue; }
let is_inner = alpha[i] > 0;
let dist_abs = if is_inner { dist_inside_sq[i].sqrt() } else { dist_outside_sq[i].sqrt() };
if !is_inner && dist_abs > radius { continue; }
let x0 = if x > 0 { x - 1 } else { x };
let x1 = if x < iw - 1 { x + 1 } else { x };
let y0 = if y > 0 { y - 1 } else { y };
let y1 = if y < ih - 1 { y + 1 } else { y };
let dx_val = (height[y * iw + x1] - height[y * iw + x0]) * depth_scale;
let dy_val = (height[y1 * iw + x] - height[y0 * iw + x]) * depth_scale;
let nx = -dx_val / 2.0;
let ny = -dy_val / 2.0;
let nz = 1.0;
let len = (nx * nx + ny * ny + nz * nz).sqrt();
let (nx, ny, nz) = if len > 1e-6 {
(nx / len, ny / len, nz / len)
} else {
(0.0, 0.0, 1.0)
};
let ndl = nx * light_x + ny * light_y + nz * light_z;
let factor = if matches!(style, types::BevelStyle::Emboss) && is_inner {
let height_factor = height[i] / radius;
let blended = ndl * 0.4 + height_factor * 0.6;
blended * depth_scale
} else {
ndl
};
lighting[i] = match direction {
types::Direction::Up => factor,
types::Direction::Down => -factor,
};
}
}
lighting = box_blur_f32(&lighting, w, h, lighting_blur);
for i in 0..px {
let is_inner = alpha[i] > 0;
let show = match style {
types::BevelStyle::InnerBevel => is_inner,
types::BevelStyle::OuterBevel => !is_inner,
types::BevelStyle::Emboss => true,
_ => is_inner,
};
if !show { continue; }
let dist_abs = if is_inner { dist_inside_sq[i].sqrt() } else { dist_outside_sq[i].sqrt() };
if !is_inner && dist_abs > radius { continue; }
let ndl = lighting[i];
let is_flat = is_inner && dist_abs > radius;
if is_flat {
if *style == types::BevelStyle::Emboss && depth > 100.0 {
shadow_buf[i * 4 + 3] = alpha[i];
}
continue;
}
let hl_pred = (ndl * hl_scale).clamp(0.0, 1.0);
let sh_pred = (-ndl * sh_scale).clamp(0.0, 1.0);
let hl_a = (alpha[i] as f32 * hl_pred).round() as u8;
let sh_a = (alpha[i] as f32 * sh_pred).round() as u8;
if hl_a > 0 {
highlight_buf[i * 4] = hl_color[0];
highlight_buf[i * 4 + 1] = hl_color[1];
highlight_buf[i * 4 + 2] = hl_color[2];
highlight_buf[i * 4 + 3] = hl_a;
}
if sh_a > 0 {
shadow_buf[i * 4] = sh_color[0];
shadow_buf[i * 4 + 1] = sh_color[1];
shadow_buf[i * 4 + 2] = sh_color[2];
shadow_buf[i * 4 + 3] = sh_a;
}
}
(highlight_buf, shadow_buf)
}
// ── Outer Glow Tuned ──
pub fn generate_glow_tuned(
alpha: &[u8],
w: u32,
h: u32,
spread: f32,
size: f32,
color: [u8; 4],
inner: bool,
_blur_factor: f32,
_passes: i32,
_alpha_scale: f32,
_glow_boost: f32,
_spread_before_blur: bool,
) -> Vec<u8> {
let px = (w * h) as usize;
let mut buf = vec![0u8; px * 4];
let radius = size.max(0.0).round() as i32;
let glow_alpha = if inner {
// MAE-optimized against Krita ground truth (48 samples, 3000 combos, 800x800)
// Best: blur_factor=4.0, passes=4, alpha_scale=0.00, glow_boost=2.00, spread_before_blur=false
let mut ga = alpha.iter().map(|&a| a as f32).collect::<Vec<f32>>();
if radius > 0 {
let gaussian_r = (radius as f32 / 4.0).round().max(1.0) as i32;
for _ in 0..4 {
ga = box_blur_f32(&ga, w, h, gaussian_r);
}
}
for i in 0..px {
ga[i] = 255.0 - ga[i];
}
if spread > 0.0 {
let factor = 1.0 / (1.0 - spread / 100.0);
for a in ga.iter_mut() {
*a = (*a * factor).round().min(255.0);
}
}
for i in 0..px {
ga[i] = (ga[i] - alpha[i] as f32 * 0.00).max(0.0) * 2.00 * alpha[i] as f32 / 255.0;
}
ga.iter().map(|&a| a as u8).collect::<Vec<u8>>()
} else {
let mut ga = alpha.iter().map(|&a| a as f32).collect::<Vec<f32>>();
// MAE-optimized against Krita ground truth (48 samples, 3000 combos, 800x800)
// Best: blur_factor=4.0, passes=4, alpha_scale=0.00, glow_boost=2.00, spread_before_blur=false
if radius > 0 {
let gaussian_r = (radius as f32 / 4.0).round().max(1.0) as i32;
for _ in 0..4 {
ga = box_blur_f32(&ga, w, h, gaussian_r);
}
}
if spread > 0.0 {
let factor = 1.0 / (1.0 - spread / 100.0);
for a in ga.iter_mut() {
*a = (*a * factor).round().min(255.0);
}
}
for i in 0..px {
let mask = 255.0 - alpha[i] as f32;
ga[i] = (ga[i] - alpha[i] as f32 * 0.00).max(0.0) * 2.00 * mask / 255.0;
}
ga.iter().map(|&a| a as u8).collect::<Vec<u8>>()
};
for i in 0..px {
let a = glow_alpha[i];
if a > 0 {
buf[i * 4] = color[0];
buf[i * 4 + 1] = color[1];
buf[i * 4 + 2] = color[2];
buf[i * 4 + 3] = a;
}
}
buf
}
// ── Drop Shadow Tuned ──
pub fn generate_shadow_tuned(
alpha: &[u8],
w: u32,
h: u32,
angle: f32,
distance: f32,
spread: f32,
size: f32,
color: [u8; 4],
knock_out: bool,
_blur_factor: f32,
_passes: i32,
_spread_scale: f32,
) -> Vec<u8> {
let px = (w * h) as usize;
let mut buf = vec![0u8; px * 4];
let rad = angle.to_radians();
let dx_raw = (-rad.cos() * distance).round() as i32;
let dy_raw = (rad.sin() * distance).round() as i32;
let mut offset_alpha = vec![0u8; px];
for y in 0..h as i32 {
for x in 0..w as i32 {
let a = alpha[(y as u32 * w + x as u32) as usize];
if a == 0 { continue; }
let dx = x + dx_raw;
let dy = y + dy_raw;
if dx >= 0 && dx < w as i32 && dy >= 0 && dy < h as i32 {
let di = (dy as u32 * w + dx as u32) as usize;
offset_alpha[di] = offset_alpha[di].max(a);
}
}
}
// MAE-optimized against Krita ground truth (768 samples, 350 combos, 800x800)
// Best: blur_factor=5.0, passes=5, spread_scale=0.00
if spread > 0.0 {
let factor = 1.0 / (1.0 - spread * 0.00 / 100.0);
for a in offset_alpha.iter_mut() {
*a = ((*a as f32 * factor).round().min(255.0)) as u8;
}
}
let radius = size.max(0.0).round() as i32;
if radius > 0 {
let gaussian_r = (radius as f32 / 5.0).round().max(1.0) as i32;
let mut f32_buf: Vec<f32> = offset_alpha.iter().map(|&a| a as f32).collect();
for _ in 0..5 {
f32_buf = box_blur_f32(&f32_buf, w, h, gaussian_r);
}
offset_alpha = f32_buf.iter().map(|&a| (a as u8).min(255)).collect();
}
for i in 0..px {
let a = offset_alpha[i];
if a > 0 {
buf[i * 4] = color[0];
buf[i * 4 + 1] = color[1];
buf[i * 4 + 2] = color[2];
buf[i * 4 + 3] = a;
}
}
if knock_out {
for i in 0..px {
if alpha[i] > 0 {
buf[i * 4 + 3] = 0;
}
}
}
buf
}
// ── Inner Shadow Tuned ──
pub fn generate_inner_shadow_tuned(
alpha: &[u8],
w: u32,
h: u32,
angle: f32,
distance: f32,
choke: f32,
size: f32,
color: [u8; 4],
blur_factor: f32,
passes: i32,
choke_scale: f32,
mask_strength: f32,
) -> Vec<u8> {
let px = (w * h) as usize;
let mut buf = vec![0u8; px * 4];
let rad = angle.to_radians();
let dx_raw = (-rad.cos() * distance).round() as i32;
let dy_raw = (rad.sin() * distance).round() as i32;
let mut offset_alpha = vec![255u8; px];
for y in 0..h as i32 {
for x in 0..w as i32 {
let orig_i = (y as u32 * w + x as u32) as usize;
let val = 255 - alpha[orig_i];
let dx = x + dx_raw;
let dy = y + dy_raw;
if dx >= 0 && dx < w as i32 && dy >= 0 && dy < h as i32 {
let di = (dy as u32 * w + dx as u32) as usize;
offset_alpha[di] = val;
}
}
}
let radius = size.max(0.0).round() as i32;
if radius > 0 {
let gaussian_r = (radius as f32 / blur_factor).round().max(1.0) as i32;
let mut f32_buf: Vec<f32> = offset_alpha.iter().map(|&a| a as f32).collect();
for _ in 0..passes {
f32_buf = box_blur_f32(&f32_buf, w, h, gaussian_r);
}
offset_alpha = f32_buf.iter().map(|&a| (a as u8).min(255)).collect();
}
if choke > 0.0 {
let factor = 1.0 / (1.0 - choke * choke_scale / 100.0);
for a in offset_alpha.iter_mut() {
*a = ((*a as f32 * factor).round().min(255.0)) as u8;
}
}
for i in 0..px {
let a = ((offset_alpha[i] as u32 * alpha[i] as u32 / 255) as f32 * mask_strength) as u8;
if a > 0 {
buf[i * 4] = color[0];
buf[i * 4 + 1] = color[1];
buf[i * 4 + 2] = color[2];
buf[i * 4 + 3] = a;
}
}
buf
}
// ── Satin Tuned ──
pub fn generate_satin_tuned(
alpha: &[u8],
w: u32,
h: u32,
angle: f32,
distance: f32,
size: f32,
color: [u8; 4],
invert: bool,
blur_factor: f32,
passes: i32,
intersect_strength: f32,
) -> Vec<u8> {
let px = (w * h) as usize;
let mut buf = vec![0u8; px * 4];
let rad = angle.to_radians();
let dx = (rad.cos() * distance).round() as i32;
let dy = -(rad.sin() * distance).round() as i32;
let mut offset1 = vec![0u8; px];
let mut offset2 = vec![0u8; px];
for y in 0..h as i32 {
for x in 0..w as i32 {
let di = (y as u32 * w + x as u32) as usize;
let sx1 = x - dx;
let sy1 = y - dy;
if sx1 >= 0 && sx1 < w as i32 && sy1 >= 0 && sy1 < h as i32 {
offset1[di] = alpha[(sy1 as u32 * w + sx1 as u32) as usize];
}
let sx2 = x + dx;
let sy2 = y + dy;
if sx2 >= 0 && sx2 < w as i32 && sy2 >= 0 && sy2 < h as i32 {
offset2[di] = alpha[(sy2 as u32 * w + sx2 as u32) as usize];
}
}
}
let mut satin_alpha = vec![0u8; px];
for i in 0..px {
let a = (offset1[i] as u32 * offset2[i] as u32 / 255) as u8;
satin_alpha[i] = ((a as f32) * intersect_strength) as u8;
}
let radius = size.max(0.0).round() as i32;
if radius > 0 {
let gaussian_r = (radius as f32 / blur_factor).round().max(1.0) as i32;
let mut f32_buf: Vec<f32> = satin_alpha.iter().map(|&a| a as f32).collect();
for _ in 0..passes {
f32_buf = box_blur_f32(&f32_buf, w, h, gaussian_r);
}
satin_alpha = f32_buf.iter().map(|&a| (a as u8).min(255)).collect();
}
for i in 0..px {
satin_alpha[i] = (satin_alpha[i] as u32 * alpha[i] as u32 / 255) as u8;
}
if invert {
for v in satin_alpha.iter_mut() {
*v = 255 - *v;
}
}
for i in 0..px {
let a = satin_alpha[i];
if a > 0 {
buf[i * 4] = color[0];
buf[i * 4 + 1] = color[1];
buf[i * 4 + 2] = color[2];
buf[i * 4 + 3] = a;
}
}
buf
}
// ── Color Overlay Tuned ──
pub fn generate_color_overlay_tuned(
alpha: &[u8],
w: u32,
h: u32,
color: [u8; 4],
alpha_mode: u8,
) -> Vec<u8> {
let px = (w * h) as usize;
let mut buf = vec![0u8; px * 4];
for i in 0..px {
let a = alpha[i];
if a > 0 {
buf[i * 4] = color[0];
buf[i * 4 + 1] = color[1];
buf[i * 4 + 2] = color[2];
buf[i * 4 + 3] = if alpha_mode == 0 { 255 } else { a };
}
}
buf
}
// ── Gradient Overlay Tuned ──
pub fn generate_gradient_overlay_tuned(
alpha: &[u8],
w: u32,
h: u32,
angle: f32,
scale: f32,
gradient: &types::GradientDef,
) -> Vec<u8> {
crate::gradient_overlay::generate_gradient_overlay(alpha, w, h, angle, scale, gradient)
}
// ── Stroke Tuned ──
pub fn generate_stroke_tuned(
alpha: &[u8],
w: u32,
h: u32,
size: f32,
position: types::StrokePosition,
color: [u8; 4],
opacity: f32,
anti_alias_width: f32,
feather: f32,
) -> Vec<u8> {
let px = (w * h) as usize;
let mut buf = vec![0u8; px * 4];
let mut stroke_alpha = vec![0.0f32; px];
let radius = size.max(1.0);
let iw = w as usize;
let ih = h as usize;
let aa = anti_alias_width.max(0.25).min(4.0);
match position {
types::StrokePosition::Inside => {
let dist_sq = edt_inside(alpha, iw, ih);
for i in 0..px {
if alpha[i] == 0 { continue; }
let d = dist_sq[i].sqrt();
if d >= radius { continue; }
let outer_dist = radius - d; // distance to outer stroke edge
let coverage = (outer_dist / aa).clamp(0.0, 1.0);
stroke_alpha[i] = coverage * alpha[i] as f32;
}
}
types::StrokePosition::Outside => {
let dist_sq = edt_outside(alpha, iw, ih);
let nearest = edt_outside_src(alpha, iw, ih);
for i in 0..px {
let d = dist_sq[i].sqrt();
if d == 0.0 || d >= radius { continue; }
let src_a = alpha[nearest[i]] as f32;
let outer_dist = radius - d; // distance to outer stroke edge
let coverage = (outer_dist / aa).clamp(0.0, 1.0);
stroke_alpha[i] = coverage * src_a;
}
}
types::StrokePosition::Center => {
let dist_in_sq = edt_inside(alpha, iw, ih);
let dist_out_sq = edt_outside(alpha, iw, ih);
let nearest = edt_outside_src(alpha, iw, ih);
let half_r = radius / 2.0;
for i in 0..px {
let (d, base_a) = if alpha[i] > 0 {
(dist_in_sq[i].sqrt(), alpha[i] as f32)
} else {
(dist_out_sq[i].sqrt(), alpha[nearest[i]] as f32)
};
if d == 0.0 || d >= half_r { continue; }
let outer_dist = half_r - d; // distance to outer stroke edge
let coverage = (outer_dist / aa).clamp(0.0, 1.0);
stroke_alpha[i] = coverage * base_a;
}
}
}
if feather > 0.0 {
let feather_radius = (feather.round() as i32).max(1);
let blurred = crate::helpers::box_blur_f32(&stroke_alpha, w, h, feather_radius);
stroke_alpha = blurred;
}
for i in 0..px {
let a = (stroke_alpha[i] * opacity).round() as u8;
if a > 0 {
buf[i * 4] = color[0];
buf[i * 4 + 1] = color[1];
buf[i * 4 + 2] = color[2];
buf[i * 4 + 3] = a;
}
}
buf
}
// ── Private EDT helpers (duplicated from emboss.rs for independence) ──
pub fn edt_inside(alpha: &[u8], w: usize, h: usize) -> Vec<f32> {
let n = w * h;
let mut dt = vec![1e20f32; n];
for y in 0..h {
for x in 0..w {
let i = y * w + x;
if alpha[i] == 0 { dt[i] = 0.0; }
}
}
edt_2d(&mut dt, w, h);
dt
}
pub fn edt_outside_helper(alpha: &[u8], w: usize, h: usize) -> Vec<f32> {
edt_outside(alpha, w, h)
}
pub fn edt_outside(alpha: &[u8], w: usize, h: usize) -> Vec<f32> {
let n = w * h;
let mut dt = vec![1e20f32; n];
for y in 0..h {
for x in 0..w {
let i = y * w + x;
if alpha[i] > 0 { dt[i] = 0.0; }
}
}
edt_2d(&mut dt, w, h);
dt
}
pub fn edt_outside_src(alpha: &[u8], w: usize, h: usize) -> Vec<usize> {
let n = w * h;
let mut dt = vec![1e20f32; n];
let mut src = vec![0usize; n];
for y in 0..h {
for x in 0..w {
let i = y * w + x;
if alpha[i] > 0 {
dt[i] = 0.0;
src[i] = i;
}
}
}
edt_2d_src(&mut dt, &mut src, w, h);
src
}
fn edt_1d_src(f: &mut [f32], src_idx: &mut [usize]) {
let n = f.len();
if n == 0 { return; }
let mut d = vec![0.0f32; n];
let mut d_src = vec![0usize; n];
let mut v = vec![0usize; n];
let mut z = vec![0.0f32; n + 1];
let mut k = 0usize;
v[0] = 0;
z[0] = f32::NEG_INFINITY;
z[1] = f32::INFINITY;
for q in 1..n {
let qf = q as f32;
let fq = f[q] + qf * qf;
let vk = v[k];
let fv_vk = f[vk] + vk as f32 * vk as f32;
let mut s = (fq - fv_vk) / (2.0 * qf - 2.0 * vk as f32);
while s <= z[k] {
k = k.wrapping_sub(1);
if k == usize::MAX { k = 0; break; }
let vk2 = v[k];
let fv_vk2 = f[vk2] + vk2 as f32 * vk2 as f32;
s = (fq - fv_vk2) / (2.0 * qf - 2.0 * vk2 as f32);
}
k += 1;
v[k] = q;
z[k] = s;
z[k + 1] = f32::INFINITY;
}
k = 0;
for q in 0..n {
let qf = q as f32;
while z[k + 1] < qf { k += 1; }
let vk = v[k];
let diff = qf - vk as f32;
d[q] = diff * diff + f[vk];
d_src[q] = src_idx[v[k]];
}
f.copy_from_slice(&d);
src_idx.copy_from_slice(&d_src);
}
fn edt_2d_src(dt: &mut [f32], src_idx: &mut [usize], w: usize, h: usize) {
let mut col = vec![0.0f32; h];
let mut col_src = vec![0usize; h];
for x in 0..w {
for y in 0..h { col[y] = dt[y * w + x]; col_src[y] = src_idx[y * w + x]; }
edt_1d_src(&mut col, &mut col_src);
for y in 0..h { dt[y * w + x] = col[y]; src_idx[y * w + x] = col_src[y]; }
}
let mut row = vec![0.0f32; w];
let mut row_src = vec![0usize; w];
for y in 0..h {
for x in 0..w { row[x] = dt[y * w + x]; row_src[x] = src_idx[y * w + x]; }
edt_1d_src(&mut row, &mut row_src);
for x in 0..w { dt[y * w + x] = row[x]; src_idx[y * w + x] = row_src[x]; }
}
}
fn edt_2d(dt: &mut [f32], w: usize, h: usize) {
let mut col = vec![0.0f32; h];
for x in 0..w {
for y in 0..h { col[y] = dt[y * w + x]; }
edt_1d(&mut col);
for y in 0..h { dt[y * w + x] = col[y]; }
}
let mut row = vec![0.0f32; w];
for y in 0..h {
for x in 0..w { row[x] = dt[y * w + x]; }
edt_1d(&mut row);
for x in 0..w { dt[y * w + x] = row[x]; }
}
}
fn edt_1d(f: &mut [f32]) {
let n = f.len();
if n == 0 { return; }
let mut d = vec![0.0f32; n];
let mut v = vec![0usize; n];
let mut z = vec![0.0f32; n + 1];
let mut k = 0usize;
v[0] = 0;
z[0] = f32::NEG_INFINITY;
z[1] = f32::INFINITY;
for q in 1..n {
let qf = q as f32;
let fq = f[q] + qf * qf;
let vk = v[k];
let fv_vk = f[vk] + vk as f32 * vk as f32;
let mut s = (fq - fv_vk) / (2.0 * qf - 2.0 * vk as f32);
while s <= z[k] {
k = k.wrapping_sub(1);
if k == usize::MAX { k = 0; break; }
let vk2 = v[k];
let fv_vk2 = f[vk2] + vk2 as f32 * vk2 as f32;
s = (fq - fv_vk2) / (2.0 * qf - 2.0 * vk2 as f32);
}
k += 1;
v[k] = q;
z[k] = s;
z[k + 1] = f32::INFINITY;
}
k = 0;
for q in 0..n {
let qf = q as f32;
while z[k + 1] < qf { k += 1; }
let vk = v[k];
let diff = qf - vk as f32;
d[q] = diff * diff + f[vk];
}
f.copy_from_slice(&d);
}
+559
View File
@@ -0,0 +1,559 @@
use hcie_blend::BlendMode;
use hcie_protocol::effects;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BevelStyle { InnerBevel, OuterBevel, Emboss, PillowEmboss, StrokeEmboss }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Technique { Smooth, ChiselHard, ChiselSoft }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction { Up, Down }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StrokePosition { Inside, Center, Outside }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StrokeFillType { Color, Gradient, Pattern }
#[derive(Debug, Clone)]
pub struct ContourCurve {
pub points: Vec<(f32, f32)>,
}
#[derive(Debug, Clone)]
pub struct GradientDef {
pub color_stops: Vec<(u32, [u8; 4])>,
pub transparency_stops: Vec<(u32, u8)>,
pub midpoint: f32,
pub angle: f32,
pub scale: f32,
pub gradient_type: u32,
}
#[derive(Debug, Clone)]
pub enum LayerEffect {
DropShadow {
enabled: bool,
blend_mode: BlendMode,
color: [u8; 4],
opacity: f32,
angle: f32,
distance: f32,
spread: f32,
size: f32,
noise: f32,
contour: Option<ContourCurve>,
},
InnerShadow {
enabled: bool,
blend_mode: BlendMode,
color: [u8; 4],
opacity: f32,
angle: f32,
distance: f32,
choke: f32,
size: f32,
noise: f32,
contour: Option<ContourCurve>,
},
OuterGlow {
enabled: bool,
blend_mode: BlendMode,
color: [u8; 4],
opacity: f32,
spread: f32,
size: f32,
noise: f32,
contour: Option<ContourCurve>,
},
InnerGlow {
enabled: bool,
blend_mode: BlendMode,
color: [u8; 4],
opacity: f32,
choke: f32,
size: f32,
noise: f32,
contour: Option<ContourCurve>,
source: u32,
},
BevelEmboss {
enabled: bool,
style: BevelStyle,
technique: Technique,
depth: f32,
direction: Direction,
size: f32,
soften: f32,
angle: f32,
altitude: f32,
highlight_blend: BlendMode,
highlight_color: [u8; 4],
highlight_opacity: f32,
shadow_blend: BlendMode,
shadow_color: [u8; 4],
shadow_opacity: f32,
contour: Option<ContourCurve>,
},
Satin {
enabled: bool,
blend_mode: BlendMode,
color: [u8; 4],
opacity: f32,
angle: f32,
distance: f32,
size: f32,
invert: bool,
contour: Option<ContourCurve>,
},
ColorOverlay {
enabled: bool,
blend_mode: BlendMode,
color: [u8; 4],
opacity: f32,
},
GradientOverlay {
enabled: bool,
blend_mode: BlendMode,
opacity: f32,
angle: f32,
scale: f32,
gradient: GradientDef,
},
PatternOverlay {
enabled: bool,
blend_mode: BlendMode,
opacity: f32,
scale: f32,
pattern_id: String,
},
Stroke {
enabled: bool,
position: StrokePosition,
fill_type: StrokeFillType,
blend_mode: BlendMode,
opacity: f32,
size: f32,
color: [u8; 4],
},
}
impl LayerEffect {
pub fn is_enabled(&self) -> bool {
match self {
Self::DropShadow { enabled, .. }
| Self::InnerShadow { enabled, .. }
| Self::OuterGlow { enabled, .. }
| Self::InnerGlow { enabled, .. }
| Self::BevelEmboss { enabled, .. }
| Self::Satin { enabled, .. }
| Self::ColorOverlay { enabled, .. }
| Self::GradientOverlay { enabled, .. }
| Self::PatternOverlay { enabled, .. }
| Self::Stroke { enabled, .. } => *enabled,
}
}
pub fn effect_name(&self) -> &'static str {
match self {
Self::DropShadow { .. } => "Drop Shadow",
Self::InnerShadow { .. } => "Inner Shadow",
Self::OuterGlow { .. } => "Outer Glow",
Self::InnerGlow { .. } => "Inner Glow",
Self::BevelEmboss { .. } => "Bevel & Emboss",
Self::Satin { .. } => "Satin",
Self::ColorOverlay { .. } => "Color Overlay",
Self::GradientOverlay { .. } => "Gradient Overlay",
Self::PatternOverlay { .. } => "Pattern Overlay",
Self::Stroke { .. } => "Stroke",
}
}
}
pub fn blend_mode_to_string(mode: &BlendMode) -> String {
match mode {
BlendMode::Normal => "normal",
BlendMode::Dissolve => "dissolve",
BlendMode::Darken => "darken",
BlendMode::Multiply => "multiply",
BlendMode::ColorBurn => "colorburn",
BlendMode::LinearBurn => "linearburn",
BlendMode::DarkerColor => "darkercolor",
BlendMode::Lighten => "lighten",
BlendMode::Screen => "screen",
BlendMode::ColorDodge => "colordodge",
BlendMode::LinearDodge => "lineardodge",
BlendMode::LighterColor => "lightercolor",
BlendMode::Overlay => "overlay",
BlendMode::SoftLight => "softlight",
BlendMode::HardLight => "hardlight",
BlendMode::VividLight => "vividlight",
BlendMode::LinearLight => "linear light",
BlendMode::PinLight => "pinlight",
BlendMode::HardMix => "hardmix",
BlendMode::Difference => "difference",
BlendMode::Exclusion => "exclusion",
BlendMode::Subtract => "subtract",
BlendMode::Divide => "divide",
BlendMode::Hue => "hue",
BlendMode::Saturation => "saturation",
BlendMode::Color => "color",
BlendMode::PassThrough => "passthrough",
BlendMode::Luminosity => "luminosity",
}.to_string()
}
pub fn blend_mode_from_string(s: &str) -> BlendMode {
let cleaned = s.to_lowercase().replace(' ', "");
match cleaned.as_str() {
"normal" => BlendMode::Normal,
"dissolve" => BlendMode::Dissolve,
"darken" => BlendMode::Darken,
"multiply" => BlendMode::Multiply,
"colorburn" => BlendMode::ColorBurn,
"linearburn" => BlendMode::LinearBurn,
"darkercolor" => BlendMode::DarkerColor,
"lighten" => BlendMode::Lighten,
"screen" => BlendMode::Screen,
"colordodge" => BlendMode::ColorDodge,
"lineardodge" => BlendMode::LinearDodge,
"lightercolor" => BlendMode::LighterColor,
"overlay" => BlendMode::Overlay,
"softlight" => BlendMode::SoftLight,
"hardlight" => BlendMode::HardLight,
"vividlight" => BlendMode::VividLight,
"linear light" | "linearlight" => BlendMode::LinearLight,
"pinlight" => BlendMode::PinLight,
"hardmix" => BlendMode::HardMix,
"difference" => BlendMode::Difference,
"exclusion" => BlendMode::Exclusion,
"subtract" => BlendMode::Subtract,
"divide" => BlendMode::Divide,
"hue" => BlendMode::Hue,
"saturation" => BlendMode::Saturation,
"color" => BlendMode::Color,
"luminosity" => BlendMode::Luminosity,
_ => BlendMode::Normal,
}
}
/// Converts protocol-layer effect (serialization format with String blend_mode)
/// to hcie-fx effect (rendering format with BlendMode enum).
/// Used by engine-api before calling apply_layer_effects().
pub fn protocol_to_hcie_fx_effect(fx: &effects::LayerEffect) -> LayerEffect {
match fx {
effects::LayerEffect::DropShadow { enabled, blend_mode, color, opacity, angle, distance, spread, size, noise, contour } => {
LayerEffect::DropShadow {
enabled: *enabled, blend_mode: blend_mode_from_string(blend_mode), color: *color,
opacity: *opacity, angle: *angle, distance: *distance, spread: *spread, size: *size,
noise: *noise, contour: contour.as_ref().map(|c| ContourCurve { points: c.points.clone() }),
}
}
effects::LayerEffect::InnerShadow { enabled, blend_mode, color, opacity, angle, distance, choke, size, noise, contour } => {
LayerEffect::InnerShadow {
enabled: *enabled, blend_mode: blend_mode_from_string(blend_mode), color: *color,
opacity: *opacity, angle: *angle, distance: *distance, choke: *choke, size: *size,
noise: *noise, contour: contour.as_ref().map(|c| ContourCurve { points: c.points.clone() }),
}
}
effects::LayerEffect::OuterGlow { enabled, blend_mode, color, opacity, spread, size, noise, contour } => {
LayerEffect::OuterGlow {
enabled: *enabled, blend_mode: blend_mode_from_string(blend_mode), color: *color,
opacity: *opacity, spread: *spread, size: *size,
noise: *noise, contour: contour.as_ref().map(|c| ContourCurve { points: c.points.clone() }),
}
}
effects::LayerEffect::InnerGlow { enabled, blend_mode, color, opacity, choke, size, noise, contour, source } => {
LayerEffect::InnerGlow {
enabled: *enabled, blend_mode: blend_mode_from_string(blend_mode), color: *color,
opacity: *opacity, choke: *choke, size: *size,
noise: *noise, contour: contour.as_ref().map(|c| ContourCurve { points: c.points.clone() }),
source: *source,
}
}
effects::LayerEffect::BevelEmboss { enabled, style, technique, depth, direction, size, soften, angle, altitude, highlight_blend, highlight_color, highlight_opacity, shadow_blend, shadow_color, shadow_opacity, contour } => {
LayerEffect::BevelEmboss {
enabled: *enabled,
style: match style { effects::BevelStyle::InnerBevel => BevelStyle::InnerBevel, effects::BevelStyle::OuterBevel => BevelStyle::OuterBevel, effects::BevelStyle::Emboss => BevelStyle::Emboss, effects::BevelStyle::PillowEmboss => BevelStyle::PillowEmboss, effects::BevelStyle::StrokeEmboss => BevelStyle::StrokeEmboss },
technique: match technique { effects::Technique::Smooth => Technique::Smooth, effects::Technique::ChiselHard => Technique::ChiselHard, effects::Technique::ChiselSoft => Technique::ChiselSoft },
depth: *depth,
direction: match direction { effects::Direction::Up => Direction::Up, effects::Direction::Down => Direction::Down },
size: *size, soften: *soften, angle: *angle, altitude: *altitude,
highlight_blend: blend_mode_from_string(highlight_blend), highlight_color: *highlight_color, highlight_opacity: *highlight_opacity,
shadow_blend: blend_mode_from_string(shadow_blend), shadow_color: *shadow_color, shadow_opacity: *shadow_opacity,
contour: contour.as_ref().map(|c| ContourCurve { points: c.points.clone() }),
}
}
effects::LayerEffect::Satin { enabled, blend_mode, color, opacity, angle, distance, size, invert, contour } => {
LayerEffect::Satin {
enabled: *enabled, blend_mode: blend_mode_from_string(blend_mode), color: *color,
opacity: *opacity, angle: *angle, distance: *distance, size: *size, invert: *invert,
contour: contour.as_ref().map(|c| ContourCurve { points: c.points.clone() }),
}
}
effects::LayerEffect::ColorOverlay { enabled, blend_mode, color, opacity } => {
LayerEffect::ColorOverlay {
enabled: *enabled, blend_mode: blend_mode_from_string(blend_mode), color: *color, opacity: *opacity,
}
}
effects::LayerEffect::GradientOverlay { enabled, blend_mode, opacity, angle, scale, gradient } => {
LayerEffect::GradientOverlay {
enabled: *enabled, blend_mode: blend_mode_from_string(blend_mode), opacity: *opacity,
angle: *angle, scale: *scale,
gradient: GradientDef {
color_stops: gradient.color_stops.clone(),
transparency_stops: gradient.transparency_stops.clone(),
midpoint: gradient.midpoint,
angle: gradient.angle,
scale: gradient.scale,
gradient_type: gradient.gradient_type,
},
}
}
effects::LayerEffect::PatternOverlay { enabled, blend_mode, opacity, scale, pattern_id } => {
LayerEffect::PatternOverlay {
enabled: *enabled, blend_mode: blend_mode_from_string(blend_mode), opacity: *opacity,
scale: *scale, pattern_id: pattern_id.clone(),
}
}
effects::LayerEffect::Stroke { enabled, position, fill_type, blend_mode, opacity, size, color } => {
LayerEffect::Stroke {
enabled: *enabled,
position: match position { effects::StrokePosition::Inside => StrokePosition::Inside, effects::StrokePosition::Center => StrokePosition::Center, effects::StrokePosition::Outside => StrokePosition::Outside },
fill_type: match fill_type { effects::StrokeFillType::Color => StrokeFillType::Color, effects::StrokeFillType::Gradient => StrokeFillType::Gradient, effects::StrokeFillType::Pattern => StrokeFillType::Pattern },
blend_mode: blend_mode_from_string(blend_mode), opacity: *opacity, size: *size, color: *color,
}
}
}
}
/// Converts hcie-fx effect (rendering format with BlendMode enum)
/// to protocol-layer effect (serialization format with String blend_mode).
pub fn hcie_fx_effect_to_protocol(fx: &LayerEffect) -> effects::LayerEffect {
match fx {
LayerEffect::DropShadow { enabled, blend_mode, color, opacity, angle, distance, spread, size, noise, contour } => {
effects::LayerEffect::DropShadow {
enabled: *enabled, blend_mode: blend_mode_to_string(blend_mode), color: *color,
opacity: *opacity, angle: *angle, distance: *distance, spread: *spread, size: *size,
noise: *noise, contour: contour.as_ref().map(|c| effects::ContourCurve { points: c.points.clone() }),
}
}
LayerEffect::InnerShadow { enabled, blend_mode, color, opacity, angle, distance, choke, size, noise, contour } => {
effects::LayerEffect::InnerShadow {
enabled: *enabled, blend_mode: blend_mode_to_string(blend_mode), color: *color,
opacity: *opacity, angle: *angle, distance: *distance, choke: *choke, size: *size,
noise: *noise, contour: contour.as_ref().map(|c| effects::ContourCurve { points: c.points.clone() }),
}
}
LayerEffect::OuterGlow { enabled, blend_mode, color, opacity, spread, size, noise, contour } => {
effects::LayerEffect::OuterGlow {
enabled: *enabled, blend_mode: blend_mode_to_string(blend_mode), color: *color,
opacity: *opacity, spread: *spread, size: *size,
noise: *noise, contour: contour.as_ref().map(|c| effects::ContourCurve { points: c.points.clone() }),
}
}
LayerEffect::InnerGlow { enabled, blend_mode, color, opacity, choke, size, noise, contour, source } => {
effects::LayerEffect::InnerGlow {
enabled: *enabled, blend_mode: blend_mode_to_string(blend_mode), color: *color,
opacity: *opacity, choke: *choke, size: *size,
noise: *noise, contour: contour.as_ref().map(|c| effects::ContourCurve { points: c.points.clone() }),
source: *source,
}
}
LayerEffect::BevelEmboss { enabled, style, technique, depth, direction, size, soften, angle, altitude, highlight_blend, highlight_color, highlight_opacity, shadow_blend, shadow_color, shadow_opacity, contour } => {
effects::LayerEffect::BevelEmboss {
enabled: *enabled,
style: match style { BevelStyle::InnerBevel => effects::BevelStyle::InnerBevel, BevelStyle::OuterBevel => effects::BevelStyle::OuterBevel, BevelStyle::Emboss => effects::BevelStyle::Emboss, BevelStyle::PillowEmboss => effects::BevelStyle::PillowEmboss, BevelStyle::StrokeEmboss => effects::BevelStyle::StrokeEmboss },
technique: match technique { Technique::Smooth => effects::Technique::Smooth, Technique::ChiselHard => effects::Technique::ChiselHard, Technique::ChiselSoft => effects::Technique::ChiselSoft },
depth: *depth,
direction: match direction { Direction::Up => effects::Direction::Up, Direction::Down => effects::Direction::Down },
size: *size, soften: *soften, angle: *angle, altitude: *altitude,
highlight_blend: blend_mode_to_string(highlight_blend), highlight_color: *highlight_color, highlight_opacity: *highlight_opacity,
shadow_blend: blend_mode_to_string(shadow_blend), shadow_color: *shadow_color, shadow_opacity: *shadow_opacity,
contour: contour.as_ref().map(|c| effects::ContourCurve { points: c.points.clone() }),
}
}
LayerEffect::Satin { enabled, blend_mode, color, opacity, angle, distance, size, invert, contour } => {
effects::LayerEffect::Satin {
enabled: *enabled, blend_mode: blend_mode_to_string(blend_mode), color: *color,
opacity: *opacity, angle: *angle, distance: *distance, size: *size, invert: *invert,
contour: contour.as_ref().map(|c| effects::ContourCurve { points: c.points.clone() }),
}
}
LayerEffect::ColorOverlay { enabled, blend_mode, color, opacity } => {
effects::LayerEffect::ColorOverlay {
enabled: *enabled, blend_mode: blend_mode_to_string(blend_mode), color: *color, opacity: *opacity,
}
}
LayerEffect::GradientOverlay { enabled, blend_mode, opacity, angle, scale, gradient } => {
effects::LayerEffect::GradientOverlay {
enabled: *enabled, blend_mode: blend_mode_to_string(blend_mode), opacity: *opacity,
angle: *angle, scale: *scale,
gradient: effects::GradientDef {
color_stops: gradient.color_stops.clone(),
transparency_stops: gradient.transparency_stops.clone(),
midpoint: gradient.midpoint,
angle: gradient.angle,
scale: gradient.scale,
gradient_type: gradient.gradient_type,
},
}
}
LayerEffect::PatternOverlay { enabled, blend_mode, opacity, scale, pattern_id } => {
effects::LayerEffect::PatternOverlay {
enabled: *enabled, blend_mode: blend_mode_to_string(blend_mode), opacity: *opacity,
scale: *scale, pattern_id: pattern_id.clone(),
}
}
LayerEffect::Stroke { enabled, position, fill_type, blend_mode, opacity, size, color } => {
effects::LayerEffect::Stroke {
enabled: *enabled,
position: match position { StrokePosition::Inside => effects::StrokePosition::Inside, StrokePosition::Center => effects::StrokePosition::Center, StrokePosition::Outside => effects::StrokePosition::Outside },
fill_type: match fill_type { StrokeFillType::Color => effects::StrokeFillType::Color, StrokeFillType::Gradient => effects::StrokeFillType::Gradient, StrokeFillType::Pattern => effects::StrokeFillType::Pattern },
blend_mode: blend_mode_to_string(blend_mode), opacity: *opacity, size: *size, color: *color,
}
}
}
}
/// Converts protocol `LayerStyle` (simplified style format with String blend_mode)
/// to hcie-fx `LayerEffect` (rendering format with BlendMode enum).
/// Used by engine-api before calling apply_layer_effects().
pub fn layer_style_to_effect(style: &hcie_protocol::LayerStyle) -> Option<LayerEffect> {
use hcie_protocol::LayerStyle;
match style {
LayerStyle::DropShadow { enabled, opacity, angle, distance, spread, size, color, blend_mode } => Some(LayerEffect::DropShadow {
enabled: *enabled,
blend_mode: blend_mode_from_string(blend_mode),
color: *color,
opacity: *opacity,
angle: *angle,
distance: *distance,
spread: *spread,
size: *size,
noise: 0.0,
contour: None,
}),
LayerStyle::InnerShadow { enabled, opacity, angle, distance, spread, size, color, blend_mode } => Some(LayerEffect::InnerShadow {
enabled: *enabled,
blend_mode: blend_mode_from_string(blend_mode),
color: *color,
opacity: *opacity,
angle: *angle,
distance: *distance,
choke: *spread, // spread maps to choke for inner shadow
size: *size,
noise: 0.0,
contour: None,
}),
LayerStyle::OuterGlow { enabled, opacity, spread, size, color, blend_mode } => Some(LayerEffect::OuterGlow {
enabled: *enabled,
blend_mode: blend_mode_from_string(blend_mode),
color: *color,
opacity: *opacity,
spread: *spread,
size: *size,
noise: 0.0,
contour: None,
}),
LayerStyle::InnerGlow { enabled, opacity, spread, size, color, blend_mode } => Some(LayerEffect::InnerGlow {
enabled: *enabled,
blend_mode: blend_mode_from_string(blend_mode),
color: *color,
opacity: *opacity,
choke: *spread,
size: *size,
noise: 0.0,
contour: None,
source: 0,
}),
LayerStyle::BevelEmboss { enabled, depth, size, angle, altitude, highlight_opacity, shadow_opacity, direction, style, technique, soften, highlight_blend_mode, highlight_color, shadow_blend_mode, shadow_color } => Some(LayerEffect::BevelEmboss {
enabled: *enabled,
style: match style.as_str() {
"InnerBevel" => BevelStyle::InnerBevel,
"OuterBevel" => BevelStyle::OuterBevel,
"Emboss" => BevelStyle::Emboss,
"PillowEmboss" => BevelStyle::PillowEmboss,
"StrokeEmboss" => BevelStyle::StrokeEmboss,
_ => BevelStyle::InnerBevel,
},
technique: match technique.as_str() {
"Smooth" => Technique::Smooth,
"ChiselHard" => Technique::ChiselHard,
"ChiselSoft" => Technique::ChiselSoft,
_ => Technique::Smooth,
},
depth: *depth,
direction: match direction.as_str() {
"Up" => Direction::Up,
"Down" => Direction::Down,
_ => Direction::Up,
},
size: *size,
soften: *soften,
angle: *angle,
altitude: *altitude,
highlight_blend: blend_mode_from_string(highlight_blend_mode),
highlight_color: *highlight_color,
highlight_opacity: *highlight_opacity,
shadow_blend: blend_mode_from_string(shadow_blend_mode),
shadow_color: *shadow_color,
shadow_opacity: *shadow_opacity,
contour: None,
}),
LayerStyle::Satin { enabled, opacity, angle, distance, size, color, invert } => Some(LayerEffect::Satin {
enabled: *enabled,
blend_mode: BlendMode::Multiply,
color: *color,
opacity: *opacity,
angle: *angle,
distance: *distance,
size: *size,
invert: *invert,
contour: None,
}),
LayerStyle::ColorOverlay { enabled, opacity, color, blend_mode } => Some(LayerEffect::ColorOverlay {
enabled: *enabled,
blend_mode: blend_mode_from_string(blend_mode),
color: *color,
opacity: *opacity,
}),
LayerStyle::GradientOverlay { enabled, opacity, blend_mode, angle, scale, gradient_type: _ } => Some(LayerEffect::GradientOverlay {
enabled: *enabled,
blend_mode: blend_mode_from_string(blend_mode),
opacity: *opacity,
angle: *angle,
scale: *scale,
gradient: GradientDef {
color_stops: vec![],
transparency_stops: vec![],
midpoint: 0.5,
angle: *angle,
scale: *scale,
gradient_type: 0,
},
}),
LayerStyle::PatternOverlay { enabled, opacity, blend_mode, scale, pattern_name } => Some(LayerEffect::PatternOverlay {
enabled: *enabled,
blend_mode: blend_mode_from_string(blend_mode),
opacity: *opacity,
scale: *scale,
pattern_id: pattern_name.clone(),
}),
LayerStyle::Stroke { enabled, size, position, opacity, color, blend_mode } => Some(LayerEffect::Stroke {
enabled: *enabled,
position: match position.as_str() {
"Inside" => StrokePosition::Inside,
"Center" => StrokePosition::Center,
"Outside" => StrokePosition::Outside,
_ => StrokePosition::Outside,
},
fill_type: StrokeFillType::Color,
blend_mode: blend_mode_from_string(blend_mode),
opacity: *opacity,
size: *size,
color: *color,
}),
}
}