init
This commit is contained in:
@@ -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"]
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.0–100.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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user