bevel and emboss geliştirme
This commit is contained in:
@@ -42,6 +42,14 @@ pub fn apply_layer_effects(
|
||||
|
||||
let alpha = extract_alpha(pixels, px_count);
|
||||
|
||||
// Storage for Emboss/OuterBevel outside portions (applied to `behind` later)
|
||||
let mut bevel_hl_outside: Option<Vec<u8>> = None;
|
||||
let mut bevel_sh_outside: Option<Vec<u8>> = None;
|
||||
let mut bevel_hl_blend: hcie_blend::BlendMode = hcie_blend::BlendMode::Screen;
|
||||
let mut bevel_sh_blend: hcie_blend::BlendMode = hcie_blend::BlendMode::Multiply;
|
||||
let mut bevel_hl_op: f32 = 1.0;
|
||||
let mut bevel_sh_op: f32 = 1.0;
|
||||
|
||||
// 1. Prepare inside content (original pixels + inside effects)
|
||||
let mut inside = vec![0u8; px_count * 4];
|
||||
inside.copy_from_slice(pixels);
|
||||
@@ -67,8 +75,38 @@ pub fn apply_layer_effects(
|
||||
*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);
|
||||
|
||||
// For Emboss and OuterBevel, the effect extends outside the shape.
|
||||
// Inside pixels go to `inside`, outside pixels go to `behind`.
|
||||
let has_outer = matches!(*style, crate::types::BevelStyle::Emboss | crate::types::BevelStyle::OuterBevel);
|
||||
if has_outer {
|
||||
// Split highlight/shadow into inside and outside portions
|
||||
let mut hl_inside = vec![0u8; px_count * 4];
|
||||
let mut hl_outside = vec![0u8; px_count * 4];
|
||||
let mut sh_inside = vec![0u8; px_count * 4];
|
||||
let mut sh_outside = vec![0u8; px_count * 4];
|
||||
for i in 0..px_count {
|
||||
if alpha[i] > 0 {
|
||||
hl_inside[i * 4..i * 4 + 4].copy_from_slice(&highlight[i * 4..i * 4 + 4]);
|
||||
sh_inside[i * 4..i * 4 + 4].copy_from_slice(&shadow[i * 4..i * 4 + 4]);
|
||||
} else {
|
||||
hl_outside[i * 4..i * 4 + 4].copy_from_slice(&highlight[i * 4..i * 4 + 4]);
|
||||
sh_outside[i * 4..i * 4 + 4].copy_from_slice(&shadow[i * 4..i * 4 + 4]);
|
||||
}
|
||||
}
|
||||
composite_effect(&mut inside, &hl_inside, *highlight_blend, *highlight_opacity, px_count);
|
||||
composite_effect(&mut inside, &sh_inside, *shadow_blend, *shadow_opacity, px_count);
|
||||
// Store outside portions to apply to `behind` later
|
||||
bevel_hl_outside = Some(hl_outside);
|
||||
bevel_sh_outside = Some(sh_outside);
|
||||
bevel_hl_blend = *highlight_blend;
|
||||
bevel_sh_blend = *shadow_blend;
|
||||
bevel_hl_op = *highlight_opacity;
|
||||
bevel_sh_op = *shadow_opacity;
|
||||
} else {
|
||||
composite_effect(&mut inside, &highlight, *highlight_blend, *highlight_opacity, px_count);
|
||||
composite_effect(&mut inside, &shadow, *shadow_blend, *shadow_opacity, px_count);
|
||||
}
|
||||
}
|
||||
|
||||
// b. Satin
|
||||
@@ -159,6 +197,15 @@ pub fn apply_layer_effects(
|
||||
}
|
||||
}
|
||||
|
||||
// d. Emboss/OuterBevel outside portion (highlight then shadow)
|
||||
// Applied to `behind` so the outer bevel band renders behind the shape.
|
||||
if let Some(hl_out) = &bevel_hl_outside {
|
||||
composite_effect(&mut behind, hl_out, bevel_hl_blend, bevel_hl_op, px_count);
|
||||
}
|
||||
if let Some(sh_out) = &bevel_sh_outside {
|
||||
composite_effect(&mut behind, sh_out, bevel_sh_blend, bevel_sh_op, 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]];
|
||||
|
||||
@@ -68,8 +68,6 @@ pub(crate) fn generate_shadow(
|
||||
|
||||
// MAE-optimized against Krita ground truth (768 samples, 350 combos, 800x800)
|
||||
// Best: blur_factor=5.0, passes=5, spread_scale=1.00
|
||||
// Actually use the spread parameter (0.0 to 100.0) from the user
|
||||
apply_spread(&mut offset_alpha, spread);
|
||||
|
||||
let radius = size.max(0.0).round() as i32;
|
||||
if radius > 0 {
|
||||
@@ -81,6 +79,9 @@ pub(crate) fn generate_shadow(
|
||||
offset_alpha = f32_buf.iter().map(|&a| (a as u8).min(255)).collect();
|
||||
}
|
||||
|
||||
// Actually use the spread parameter (0.0 to 100.0) from the user AFTER the blur!
|
||||
apply_spread(&mut offset_alpha, spread);
|
||||
|
||||
for i in 0..px {
|
||||
let a = offset_alpha[i];
|
||||
if a > 0 {
|
||||
|
||||
+195
-30
@@ -1,5 +1,44 @@
|
||||
use crate::types;
|
||||
use crate::helpers::box_blur_f32;
|
||||
|
||||
/// Generate bevel & emboss highlight and shadow buffers.
|
||||
///
|
||||
/// **Purpose:** Renders Photoshop-style Bevel & Emboss layer effect. Produces
|
||||
/// two separate RGBA buffers (highlight + shadow) that the caller composites
|
||||
/// over the layer content using Screen and Multiply blend modes respectively.
|
||||
///
|
||||
/// **Logic & Workflow:**
|
||||
/// 1. Compute inside/outside Euclidean Distance Transforms (EDT) from the
|
||||
/// layer alpha mask. For Emboss and OuterBevel, also track which opaque
|
||||
/// pixel is nearest to each outside pixel so the outer bevel can inherit
|
||||
/// the alpha of the nearest content pixel.
|
||||
/// 2. Build a continuous height field from the EDT values. The profile shape
|
||||
/// depends on the BevelStyle:
|
||||
/// - InnerBevel: ramps up from edge to center inside the shape.
|
||||
/// - OuterBevel: ramps down from edge outward outside the shape.
|
||||
/// - Emboss: combines both — ramps inside (raised) and outside (lowered).
|
||||
/// 3. Apply a box-blur soften pass on the height field.
|
||||
/// 4. Compute per-pixel lighting via central-difference normals · light vector.
|
||||
/// 5. Apply a lighting blur to smooth wrinkles at sharp corners.
|
||||
/// 6. Map lighting to highlight/shadow alpha. For outside pixels, use the
|
||||
/// nearest opaque pixel's alpha instead of the (zero) local alpha.
|
||||
///
|
||||
/// **Arguments:**
|
||||
/// * `alpha` — Layer alpha mask (one byte per pixel)
|
||||
/// * `w`, `h` — Canvas dimensions
|
||||
/// * `depth` — Bevel depth (0–1000+, controls slope steepness)
|
||||
/// * `size` — Bevel size in pixels (radius of the bevel band)
|
||||
/// * `soften` — Soften amount in pixels (extra blur on height field)
|
||||
/// * `angle` — Light azimuth angle in degrees
|
||||
/// * `altitude` — Light altitude angle in degrees
|
||||
/// * `direction` — Up or Down (flips lighting)
|
||||
/// * `technique` — Smooth, ChiselHard, ChiselSoft
|
||||
/// * `style` — InnerBevel, OuterBevel, Emboss, etc.
|
||||
/// * `highlight_color` — RGBA color for the highlight channel
|
||||
/// * `shadow_color` — RGBA color for the shadow channel
|
||||
///
|
||||
/// **Returns:**
|
||||
/// `(highlight_buf, shadow_buf)` — two RGBA buffers, same size as canvas.
|
||||
pub fn generate_bevel_emboss(
|
||||
alpha: &[u8],
|
||||
w: u32,
|
||||
@@ -27,6 +66,16 @@ pub fn generate_bevel_emboss(
|
||||
let dist_inside_sq = edt_inside(alpha, iw, ih);
|
||||
let dist_outside_sq = edt_outside(alpha, iw, ih);
|
||||
|
||||
// For Emboss and OuterBevel, track the nearest opaque source pixel so
|
||||
// the outer bevel band can inherit the alpha of the nearest content pixel.
|
||||
// This is critical: without it, alpha[i]==0 outside the shape produces
|
||||
// zero-strength highlight/shadow, making Emboss/OuterBevel invisible.
|
||||
let outside_src = if matches!(style, types::BevelStyle::Emboss | types::BevelStyle::OuterBevel) {
|
||||
edt_outside_src(alpha, iw, ih)
|
||||
} else {
|
||||
vec![0usize; px]
|
||||
};
|
||||
|
||||
// Compute shape center for Emboss tilt
|
||||
let (cx, cy) = {
|
||||
let mut min_x = iw;
|
||||
@@ -76,15 +125,17 @@ pub fn generate_bevel_emboss(
|
||||
}
|
||||
}
|
||||
types::BevelStyle::Emboss => {
|
||||
// Emboss = inner bevel (inside) + outer bevel (outside) + tilt.
|
||||
// tilt_scale controls the global slope for 3D emboss effect.
|
||||
// Negate tilt so direction=Down correctly produces highlight on
|
||||
// the light side and shadow on the dark side.
|
||||
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;
|
||||
let tilt = ((px_x - cx) * light_x + (px_y - cy) * light_y) * radius * 0.05;
|
||||
if alpha[i] > 0 {
|
||||
let edge_factor = (d_in / radius).min(1.0);
|
||||
radius * edge_factor + tilt
|
||||
(d_in / radius).min(1.0) * radius + tilt
|
||||
} else {
|
||||
let t = (d_out / radius).min(1.0);
|
||||
(radius * (1.0 - t) * 0.5 + tilt).max(0.0)
|
||||
((radius - d_out).max(0.0) / radius) * radius + tilt
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -155,13 +206,7 @@ pub fn generate_bevel_emboss(
|
||||
|
||||
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
|
||||
};
|
||||
let factor = ndl;
|
||||
|
||||
lighting[i] = match direction {
|
||||
types::Direction::Up => factor,
|
||||
@@ -171,46 +216,59 @@ pub fn generate_bevel_emboss(
|
||||
}
|
||||
|
||||
// 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
|
||||
// Emboss uses tilt for 3D effect and needs sharp lighting (no blur).
|
||||
// Inner/Outer bevel need blur for smooth corners.
|
||||
// MAE-optimized against Photoshop ground truth (288 emboss samples, 256×256).
|
||||
// BEGIN_TUNING_ZONE
|
||||
let lighting_blur = (soften_radius + 5).max(1);
|
||||
let lighting_blur = if matches!(style, crate::types::BevelStyle::Emboss) { 0 } else { 5 };
|
||||
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,
|
||||
crate::types::BevelStyle::InnerBevel => is_inner,
|
||||
crate::types::BevelStyle::OuterBevel => !is_inner,
|
||||
crate::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,
|
||||
crate::types::Technique::Smooth => true,
|
||||
_ => dist_abs <= radius,
|
||||
};
|
||||
if !technique_show { continue; }
|
||||
|
||||
let ndl = lighting[i];
|
||||
let is_flat = is_inner && dist_abs > radius;
|
||||
|
||||
// Emboss has tilt so the interior is never truly flat — skip the
|
||||
// is_flat shortcut for emboss to allow tilt-based highlight/shadow
|
||||
// across the entire shape interior.
|
||||
let is_flat = is_inner && dist_abs > radius && !matches!(style, crate::types::BevelStyle::Emboss);
|
||||
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_scale, sh_scale) = if matches!(style, crate::types::BevelStyle::Emboss) {
|
||||
(0.05, 16.00)
|
||||
} else {
|
||||
(0.05, 2.50)
|
||||
};
|
||||
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;
|
||||
// For outside pixels (alpha[i]==0), use the nearest opaque pixel's
|
||||
// alpha as the strength base. This is what Photoshop does: the outer
|
||||
// bevel band fades out based on distance to the shape edge, and the
|
||||
// effect intensity is proportional to the nearest content alpha.
|
||||
let base_alpha = if is_inner {
|
||||
alpha[i] as f32
|
||||
} else {
|
||||
alpha[outside_src[i]] as f32
|
||||
};
|
||||
|
||||
let hl_a = (base_alpha * hl_pred).round() as u8;
|
||||
let sh_a = (base_alpha * sh_pred).round() as u8;
|
||||
|
||||
if hl_a > 0 {
|
||||
highlight_buf[i * 4] = highlight_color[0];
|
||||
@@ -267,6 +325,33 @@ fn edt_outside(alpha: &[u8], w: usize, h: usize) -> Vec<f32> {
|
||||
dt
|
||||
}
|
||||
|
||||
/// EDT for outside pixels with source index tracking.
|
||||
///
|
||||
/// **Purpose:** Same as `edt_outside` but also returns, for each pixel, the
|
||||
/// index of the nearest opaque pixel. This is needed by Emboss and OuterBevel
|
||||
/// to give outside (transparent) pixels the alpha of the nearest content pixel.
|
||||
///
|
||||
/// **Returns:** `Vec<usize>` where each element is the pixel index of the
|
||||
/// nearest opaque source pixel.
|
||||
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
|
||||
}
|
||||
|
||||
/// 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];
|
||||
@@ -336,4 +421,84 @@ fn edt_1d(f: &mut [f32]) {
|
||||
}
|
||||
|
||||
f.copy_from_slice(&d);
|
||||
}
|
||||
|
||||
/// 2D EDT with source index tracking (Felzenszwalb & Huttenlocher).
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 1D EDT with source index tracking (Felzenszwalb & Huttenlocher).
|
||||
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);
|
||||
}
|
||||
@@ -64,8 +64,6 @@ pub(crate) fn generate_inner_shadow(
|
||||
}
|
||||
}
|
||||
|
||||
apply_spread(&mut offset_alpha, choke);
|
||||
|
||||
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;
|
||||
@@ -75,6 +73,8 @@ pub(crate) fn generate_inner_shadow(
|
||||
offset_alpha = f32_buf.iter().map(|&a| (a as u8).min(255)).collect();
|
||||
}
|
||||
|
||||
apply_spread(&mut offset_alpha, choke);
|
||||
|
||||
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 {
|
||||
|
||||
+35
-17
@@ -19,6 +19,7 @@ pub fn generate_bevel_tuned(
|
||||
hl_scale: f32,
|
||||
sh_scale: f32,
|
||||
profile_exp: f32,
|
||||
tilt_scale: f32,
|
||||
) -> (Vec<u8>, Vec<u8>) {
|
||||
let hl_color = [255u8; 4];
|
||||
let sh_color = [0u8; 4];
|
||||
@@ -33,6 +34,14 @@ pub fn generate_bevel_tuned(
|
||||
let dist_inside_sq = edt_inside(alpha, iw, ih);
|
||||
let dist_outside_sq = edt_outside(alpha, iw, ih);
|
||||
|
||||
// For Emboss and OuterBevel, track the nearest opaque source pixel so
|
||||
// the outer bevel band can inherit the alpha of the nearest content pixel.
|
||||
let outside_src = if matches!(*style, types::BevelStyle::Emboss | types::BevelStyle::OuterBevel) {
|
||||
edt_outside_src(alpha, iw, ih)
|
||||
} else {
|
||||
vec![0usize; px]
|
||||
};
|
||||
|
||||
// Compute shape center for Emboss tilt
|
||||
let (cx, cy) = {
|
||||
let mut min_x = iw;
|
||||
@@ -81,15 +90,22 @@ pub fn generate_bevel_tuned(
|
||||
}
|
||||
}
|
||||
types::BevelStyle::Emboss => {
|
||||
// Emboss = inner bevel (inside) + outer bevel (outside) + tilt.
|
||||
// tilt_scale controls how much global slope is added across the
|
||||
// shape based on light direction. The tilt makes one side of
|
||||
// the shape raised (toward light = highlight) and the other
|
||||
// lowered (away from light = shadow), producing the 3D effect.
|
||||
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;
|
||||
// Negate tilt so direction=Down (default) correctly produces
|
||||
// highlight on the light side and shadow on the dark side.
|
||||
let tilt = ((px_x - cx) * light_x + (px_y - cy) * light_y) * radius * tilt_scale;
|
||||
if alpha[i] > 0 {
|
||||
let t = (d_in / radius).min(1.0);
|
||||
radius * t.powf(profile_exp) + tilt
|
||||
t.powf(profile_exp) * radius + tilt
|
||||
} else {
|
||||
let t = (d_out / radius).min(1.0);
|
||||
(radius * (1.0 - t).powf(profile_exp) * 0.5 + tilt).max(0.0)
|
||||
let t = ((radius - d_out).max(0.0) / radius).min(1.0);
|
||||
(1.0 - t).powf(profile_exp) * radius + tilt
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -142,13 +158,7 @@ pub fn generate_bevel_tuned(
|
||||
|
||||
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
|
||||
};
|
||||
let factor = ndl;
|
||||
|
||||
lighting[i] = match direction {
|
||||
types::Direction::Up => factor,
|
||||
@@ -171,19 +181,27 @@ pub fn generate_bevel_tuned(
|
||||
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;
|
||||
// Emboss has tilt so the interior is never truly flat — skip the
|
||||
// is_flat shortcut for emboss to allow tilt-based highlight/shadow
|
||||
// across the entire shape interior.
|
||||
let is_flat = is_inner && dist_abs > radius && !matches!(*style, types::BevelStyle::Emboss);
|
||||
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;
|
||||
// For outside pixels (alpha[i]==0), use the nearest opaque pixel's
|
||||
// alpha as the strength base, matching Photoshop behavior.
|
||||
let base_alpha = if is_inner {
|
||||
alpha[i] as f32
|
||||
} else {
|
||||
alpha[outside_src[i]] as f32
|
||||
};
|
||||
|
||||
let hl_a = (base_alpha * hl_pred).round() as u8;
|
||||
let sh_a = (base_alpha * sh_pred).round() as u8;
|
||||
|
||||
if hl_a > 0 {
|
||||
highlight_buf[i * 4] = hl_color[0];
|
||||
|
||||
Reference in New Issue
Block a user