mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-13 13:43:20 +00:00
Port behavior and regression coverage into current modules; preserve current upstream behavior. Added bounded Softer outer-glow tail in current CPU paint and GPU glow-builder modules, retaining original tent convolution and current program dispatch; registered exact tail/kernel regression and expanded render margin only for outer Softer glows.
This commit is contained in:
commit
f369874ddb
8 files changed
+194
-5
No files matched your search
@@ -41,7 +41,7 @@ mod paint;
|
||||
pub use bevel::{BEVEL_H_EPS, BevelGeom, BevelPaint, TextureCtx, bevel_chisel_h, bevel_geom, satin_distance};
|
||||
#[cfg(test)]
|
||||
use bevel::{bevel_maps, satin_map};
|
||||
pub use blur::tent_kernel;
|
||||
pub use blur::{tent_kernel, softer_glow_tail_width, softer_glow_tail_amplitude};
|
||||
#[cfg(test)]
|
||||
use blur::{blur, box_line, dilate, tent, tent_direct};
|
||||
pub(crate) use composite::{VectorStroke, build_maps_prepared, composite_with_effects_prepared, map_count};
|
||||
@@ -89,7 +89,14 @@ pub fn margin(layer: &Layer) -> i32 {
|
||||
for e in layer.effects.items.iter().filter(|e| e.enabled()) {
|
||||
let r = match e {
|
||||
Effect::DropShadow(s) | Effect::InnerShadow(s) => s.distance + s.size,
|
||||
Effect::OuterGlow(g) | Effect::InnerGlow(g) => g.size,
|
||||
Effect::OuterGlow(g) => {
|
||||
if g.technique == GlowTechnique::Softer {
|
||||
g.size + softer_glow_tail_width(g.size)
|
||||
} else {
|
||||
g.size
|
||||
}
|
||||
}
|
||||
Effect::InnerGlow(g) => g.size,
|
||||
Effect::Stroke(s) => s.size,
|
||||
Effect::Satin(s) => satin_distance(s) + s.size,
|
||||
Effect::BevelEmboss(b) => b.size + b.soften,
|
||||
|
||||
@@ -210,3 +210,14 @@ fn tent_fast(m: &mut Map, w: f32) {
|
||||
rows_par(&t, &mut b, mh, twice);
|
||||
m.v = transpose_map(&b, mh, mw);
|
||||
}
|
||||
|
||||
/// Raw coverage at the Size boundary, scaled through the glow's Range to a subtle one-percent tail.
|
||||
pub fn softer_glow_tail_amplitude(range: f32) -> f32 {
|
||||
let range = if range.is_finite() { range.clamp(0.01, 1.0) } else { 1.0 };
|
||||
0.005 * range
|
||||
}
|
||||
|
||||
/// Width of the added fading tail on a Softer outer glow, capped so large settings stay local.
|
||||
pub fn softer_glow_tail_width(size: f32) -> f32 {
|
||||
if size.is_finite() && size > 0.0 { (size * 0.1).clamp(1.0, 6.0) } else { 0.0 }
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::blur::{blur, dilate};
|
||||
use super::blur::{blur, dilate, softer_glow_tail_width, softer_glow_tail_amplitude};
|
||||
use super::distance::{dist_inside, dist_outside};
|
||||
use super::{
|
||||
BlendMode, Buffer, Contour, Effect, FxPaint, GlobalLight, Glow, GlowSource, GlowTechnique, Gradient, GradientStyle, Map, Placement, PreparedPatterns, Rect,
|
||||
@@ -303,6 +303,9 @@ pub(super) fn glow_map(shape: &Map, g: &Glow, inner: bool, origin: (i32, i32)) -
|
||||
let (r, bw) = spread_split(g.size, g.spread);
|
||||
let mut m = dilate(&src, r);
|
||||
blur(&mut m, bw);
|
||||
if !inner {
|
||||
add_softer_glow_tail(&mut m, &src, g.size, g.range);
|
||||
}
|
||||
m
|
||||
}
|
||||
};
|
||||
@@ -402,3 +405,26 @@ pub(super) fn paint_glow(dst: &mut Buffer, m: &Map, g: &Glow, shape_bounds: Rect
|
||||
let strength = Map { w: m.w, h: m.h, v: m.v.iter().map(|v| (v * gain).min(1.0)).collect() };
|
||||
paint(dst, &strength, |i| prepared.sample(1.0 - m.v.get(i).copied().unwrap_or(0.0).clamp(0.0, 1.0)), g.common.blend, g.common.opacity);
|
||||
}
|
||||
|
||||
fn add_softer_glow_tail(map: &mut Map, shape: &Map, size: f32, range: f32) {
|
||||
let width = softer_glow_tail_width(size);
|
||||
if width == 0.0 {
|
||||
return;
|
||||
}
|
||||
let amp = softer_glow_tail_amplitude(range);
|
||||
let distance = dist_outside(shape);
|
||||
for (coverage, distance) in map.v.iter_mut().zip(distance) {
|
||||
let t = (distance + 0.5 - (size - width)) / width;
|
||||
let tail = if (0.0..=1.0).contains(&t) {
|
||||
amp * t * t * (3.0 - 2.0 * t)
|
||||
} else if (1.0..=2.0).contains(&t) {
|
||||
let u = t - 1.0;
|
||||
let remaining = 1.0 - u;
|
||||
amp * remaining * remaining * (1.0 + 2.0 * u)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
*coverage = coverage.max(tail);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -858,6 +858,56 @@ fn glows_match_photoshop() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn softer_outer_glow_keeps_a_fading_tail_past_its_size() {
|
||||
let mut shape = Map::new(220, 220, 0.0);
|
||||
for y in 0..shape.h {
|
||||
for x in 20..100 {
|
||||
shape.v[y * shape.w + x] = 1.0;
|
||||
}
|
||||
}
|
||||
let glow = Glow {
|
||||
common: photocraft_doc::FxCommon::new(BlendMode::Screen, 1.0),
|
||||
paint: FxPaint::Color(photocraft_color::Color::WHITE),
|
||||
technique: GlowTechnique::Softer,
|
||||
spread: 0.0,
|
||||
size: 60.0,
|
||||
contour: Contour::Linear,
|
||||
anti_alias: false,
|
||||
range: 0.5,
|
||||
jitter: 0.0,
|
||||
noise: 0.0,
|
||||
source: GlowSource::Edge,
|
||||
};
|
||||
let m = glow_map(&shape, &glow, false, (0, 0));
|
||||
let row = shape.h / 2;
|
||||
let at = |distance: usize| m.v[row * m.w + 100 + distance];
|
||||
assert!(at(50) > 0.024, "the softer shoulder should stay visible: {}", at(50));
|
||||
assert!(at(60) > 1.0 / 255.0, "the glow must retain a visible tail at its Size: {}", at(60));
|
||||
assert!(at(62) > 0.5 / 255.0, "the glow must extend beyond its Size: {}", at(62));
|
||||
assert!(at(66) < 1e-6, "the added tail should fade to zero: {}", at(66));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn softer_outer_glow_margin_includes_its_tail_support() {
|
||||
let glow = Glow {
|
||||
common: photocraft_doc::FxCommon::new(BlendMode::Screen, 1.0),
|
||||
paint: FxPaint::Color(photocraft_color::Color::WHITE),
|
||||
technique: GlowTechnique::Softer,
|
||||
spread: 0.0,
|
||||
size: 60.0,
|
||||
contour: Contour::Linear,
|
||||
anti_alias: false,
|
||||
range: 0.5,
|
||||
jitter: 0.0,
|
||||
noise: 0.0,
|
||||
source: GlowSource::Edge,
|
||||
};
|
||||
let mut layer = Layer::raster("glow", photocraft_color::PixelFormat::RGBA8);
|
||||
layer.effects.items.push(Effect::OuterGlow(glow));
|
||||
assert_eq!(margin(&layer), 68);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offsets_follow_light() {
|
||||
let (dx, dy) = offset(120.0, 10.0);
|
||||
|
||||
@@ -1449,6 +1449,18 @@ fn fs_mblur(in: VOut) -> @location(0) vec4<f32> {
|
||||
@fragment
|
||||
fn fs_mglow(in: VOut) -> @location(0) vec4<f32> {
|
||||
let d = textureLoad(tex_a, local(in.pos), 0).r + 0.5;
|
||||
if (op.p0.w > 0.5) {
|
||||
let t = (d - op.p0.x) / op.p0.y;
|
||||
var tail = 0.0;
|
||||
if (t >= 0.0 && t <= 1.0) {
|
||||
tail = op.p0.z * t * t * (3.0 - 2.0 * t);
|
||||
} else if (t > 1.0 && t <= 2.0) {
|
||||
let u = t - 1.0;
|
||||
let remaining = 1.0 - u;
|
||||
tail = op.p0.z * remaining * remaining * (1.0 + 2.0 * u);
|
||||
}
|
||||
return mout(tail);
|
||||
}
|
||||
var m = 0.5 * clamp(1.0 - (d - op.p0.x) / op.p0.y, 0.0, 1.0);
|
||||
if (d <= op.p0.x) { m = 1.0; }
|
||||
return mout(m);
|
||||
@@ -1460,7 +1472,11 @@ fn fs_mglow(in: VOut) -> @location(0) vec4<f32> {
|
||||
fn fs_mfinish(in: VOut) -> @location(0) vec4<f32> {
|
||||
let p = local(in.pos);
|
||||
var v = textureLoad(tex_a, p, 0).r;
|
||||
if (op.p0.x > 0.5) { v = abs(v - textureLoad(tex_b, p, 0).r); }
|
||||
if (op.p0.x > 1.5) {
|
||||
v = max(v, textureLoad(tex_b, p, 0).r);
|
||||
} else if (op.p0.x > 0.5) {
|
||||
v = abs(v - textureLoad(tex_b, p, 0).r);
|
||||
}
|
||||
if (op.p0.y > 0.5 && op.p0.y < 1.5) { v = 1.0 - v; }
|
||||
if (op.p0.z > 0.5) { v = lut(0, v); }
|
||||
if (op.p0.y > 1.5) { v = 1.0 - v; }
|
||||
|
||||
@@ -226,6 +226,24 @@ impl B {
|
||||
let Some(k) = blur_kernel(size) else { return src };
|
||||
self.conv(src, k)
|
||||
}
|
||||
fn max(&mut self, a: In, b: In) -> In {
|
||||
self.push(stage(Kernel::MFinish, Some(a), Some(b), [2.0, 0.0, 0.0, 0.0], 0))
|
||||
}
|
||||
fn softer_glow_tail(&mut self, blur: In, size: f32, range: f32) -> In {
|
||||
let width = photocraft_compose::effects::softer_glow_tail_width(size);
|
||||
if width == 0.0 {
|
||||
return blur;
|
||||
}
|
||||
let distance = self.field(FieldKind::Outside, size + width);
|
||||
let tail = self.push(stage(
|
||||
Kernel::MGlow,
|
||||
Some(distance),
|
||||
None,
|
||||
[size - width, width, photocraft_compose::effects::softer_glow_tail_amplitude(range), 1.0],
|
||||
0,
|
||||
));
|
||||
self.max(blur, tail)
|
||||
}
|
||||
/// Separable convolution with a symmetric kernel `(radius, weights)` (two `MBlur` passes).
|
||||
fn conv(&mut self, src: In, (r, w): (i32, Vec<f32>)) -> In {
|
||||
if r <= 0 {
|
||||
|
||||
@@ -55,7 +55,8 @@ impl B {
|
||||
GlowTechnique::Softer => {
|
||||
// Inner glows spread 1 - alpha.
|
||||
let src = if inner { self.shift(In::Shape, 0.0, 0.0, 0.0, true) } else { In::Shape };
|
||||
self.spread_blur(src, (g.size, g.spread), inner, 0.0, 0.0)
|
||||
let m=self.spread_blur(src, (g.size,g.spread),inner,0.0,0.0);
|
||||
if inner {m} else {self.softer_glow_tail(m,g.size,g.range)}
|
||||
}
|
||||
};
|
||||
self.finish_glow(m, fx::glow_lut(g), inner, center);
|
||||
|
||||
@@ -51,6 +51,36 @@ fn programs_key_on_geometry_not_colour() {
|
||||
assert_ne!(a.key, program(&Effect::DropShadow(s), &light, false).key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn softer_outer_glow_program_keeps_the_tent_and_adds_a_distance_tail() {
|
||||
let effect = Effect::OuterGlow(photocraft_doc::Glow {
|
||||
common: FxCommon::new(BlendMode::Screen, 1.0),
|
||||
paint: photocraft_doc::FxPaint::Color(photocraft_color::Color::WHITE),
|
||||
technique: GlowTechnique::Softer,
|
||||
spread: 0.0,
|
||||
size: 60.0,
|
||||
contour: Contour::Linear,
|
||||
anti_alias: false,
|
||||
range: 0.5,
|
||||
jitter: 0.0,
|
||||
noise: 0.0,
|
||||
source: GlowSource::Edge,
|
||||
});
|
||||
let kernel = blur_kernel(60.0).unwrap();
|
||||
let p = program(&effect, &GlobalLight::default(), false);
|
||||
let blur: Vec<_> = p.stages.iter().filter(|s| s.kernel == Kernel::MBlur).collect();
|
||||
assert_eq!(blur.len(), 2);
|
||||
for stage in blur {
|
||||
assert_eq!(stage.radius, kernel.0);
|
||||
assert_eq!(stage.lut.as_deref().unwrap(), kernel.1.as_slice());
|
||||
}
|
||||
let tail = p.stages.iter().find(|s| s.kernel == Kernel::MGlow).unwrap();
|
||||
assert_eq!(tail.a, Some(In::Field(FieldKind::Outside)));
|
||||
assert_eq!(tail.p0, [54.0, 6.0, 0.0025, 1.0]);
|
||||
assert!(p.stages.iter().any(|s| s.kernel == Kernel::MFinish && s.p0[0] == 2.0));
|
||||
assert!(p.fields.contains(&(FieldKind::Outside, reach_for(66.0))));
|
||||
}
|
||||
|
||||
/// A styled group's effect shape follows its own vector mask and its children's Blend If and
|
||||
/// Advanced Blending, so the cached shape must be rebuilt when any of them changes.
|
||||
#[test]
|
||||
@@ -308,3 +338,33 @@ fn shape_tiles_and_group_pins_hold_addresses_not_pixels() {
|
||||
drop(group);
|
||||
assert!(pins.iter().all(|w| w.strong_count() == 0), "a group's pins keep none of its pixels");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn softer_outer_glow_program_keeps_the_tent_and_adds_a_distance_tail() {
|
||||
let effect = Effect::OuterGlow(photocraft_doc::Glow {
|
||||
common: FxCommon::new(BlendMode::Screen, 1.0),
|
||||
paint: photocraft_doc::FxPaint::Color(photocraft_color::Color::WHITE),
|
||||
technique: GlowTechnique::Softer,
|
||||
spread: 0.0,
|
||||
size: 60.0,
|
||||
contour: Contour::Linear,
|
||||
anti_alias: false,
|
||||
range: 0.5,
|
||||
jitter: 0.0,
|
||||
noise: 0.0,
|
||||
source: GlowSource::Edge,
|
||||
});
|
||||
let kernel = blur_kernel(60.0).unwrap();
|
||||
let p = program(&effect, &GlobalLight::default(), false);
|
||||
let blur: Vec<_> = p.stages.iter().filter(|s| s.kernel == Kernel::MBlur).collect();
|
||||
assert_eq!(blur.len(), 2);
|
||||
for stage in blur {
|
||||
assert_eq!(stage.radius, kernel.0);
|
||||
assert_eq!(stage.lut.as_deref().unwrap(), kernel.1.as_slice());
|
||||
}
|
||||
let tail = p.stages.iter().find(|s| s.kernel == Kernel::MGlow).unwrap();
|
||||
assert_eq!(tail.a, Some(In::Field(FieldKind::Outside)));
|
||||
assert_eq!(tail.p0, [54.0, 6.0, 0.0025, 1.0]);
|
||||
assert!(p.stages.iter().any(|s| s.kernel == Kernel::MFinish && s.p0[0] == 2.0));
|
||||
assert!(p.fields.contains(&(FieldKind::Outside, reach_for(66.0))));
|
||||
}
|
||||
Reference in new issue
Block a user