diff --git a/crates/effects/src/generate3/paths.rs b/crates/effects/src/generate3/paths.rs index 319368a82..03f041673 100644 --- a/crates/effects/src/generate3/paths.rs +++ b/crates/effects/src/generate3/paths.rs @@ -387,6 +387,13 @@ pub(super) fn vegas(ctx: &EffectCtx, mut b: Buf) -> Buf { b } +/// Sub-samples per Vegas segment: at least 12 (opacity ramp), more for long segments so each straight piece stays +/// short compared with the stroke width and the segment follows rounded corners smoothly. +pub(super) fn vegas_substeps(each: f64, width: f64) -> usize { + let step = width.max(2.0); + ((each / step).ceil().max(12.0) as usize).min(512) +} + fn vegas_plan(ctx: &EffectCtx, b: &Buf) -> PaintPlan { let pr = ctx.params; let (w, h) = (b.img.width as usize, b.img.height as usize); @@ -459,7 +466,7 @@ fn vegas_plan(ctx: &EffectCtx, b: &Buf) -> PaintPlan { let base = if bunched { (i as f64 + (hash1(ci as u32, i as u32 + 7, seed) as f64 - 0.5) * 0.8) / nseg as f64 } else { i as f64 / nseg as f64 }; let s0 = (base + phase).rem_euclid(1.0) * len; // Sub-sample the segment so opacity can ramp along it. - let sub = 12usize; + let sub = vegas_substeps(each, width); let mut prev: Option<[f64; 2]> = None; for k in 0..=sub { let f = k as f64 / sub as f64; diff --git a/crates/effects/src/generate3/tests.rs b/crates/effects/src/generate3/tests.rs index 0edad2f2d..cf05842f5 100644 --- a/crates/effects/src/generate3/tests.rs +++ b/crates/effects/src/generate3/tests.rs @@ -395,3 +395,44 @@ fn scribble_caps_and_start_end_apply_to() { let each = run_env("ec.generate.scribble", &[base.as_slice(), &seq, &[("startEndApplyTo", Value::Enum(1))]].concat(), img, 0.0, env2); assert_ne!(all.img.data, each.img.data); } + +#[test] +fn vegas_long_segments_are_sampled_finely() { + // A short segment keeps the 12 samples; a long one gets more so it bends smoothly round rounded corners. + assert_eq!(super::paths::vegas_substeps(10.0, 4.0), 12); + let n = super::paths::vegas_substeps(400.0, 4.0); + assert!(n > 12, "{n}"); + assert!(400.0 / n as f64 <= 4.0 + 1e-9); + assert_eq!(super::paths::vegas_substeps(1.0e9, 4.0), 512); +} + +#[test] +fn vegas_long_segment_follows_curved_mask() { + // One segment as long as a round mask (radius 90 px): with only 12 straight pieces the + // chords cut ~3 px inside the curve, leaving gaps on the mask outline. + let (c, r) = (100.0, 90.0); + let mut points = Vec::new(); + for i in 0..256 { + let a = i as f64 / 256.0 * std::f64::consts::TAU; + points.push([c + r * a.cos(), c + r * a.sin()]); + } + let masks = vec![MaskShape { name: "Mask 1".into(), points, closed: true, inverted: false }]; + let env = EffectEnv { masks: &masks, ..Default::default() }; + let set = [ + ("stroke", Value::Enum(1)), + ("segments", num(1.0)), + ("length", num(1.0)), + ("segmentDistribution", Value::Enum(1)), + ("blendMode", Value::Enum(0)), + ("hardness", num(1.0)), + ("midpointOpacity", num(1.0)), + ("endOpacity", num(1.0)), + ]; + let out = run_env("ec.generate.vegas", &set, Image::new(200, 200), 0.5, env); + for k in 0..720 { + let a = k as f64 / 720.0 * std::f64::consts::TAU; + let (x, y) = ((c + r * a.cos()).floor() as usize, (c + r * a.sin()).floor() as usize); + let alpha = out.img.data[y * 200 + x][3]; + assert!(alpha > 0.5, "gap on the curve at {k}/720 turn: alpha {alpha}"); + } +}