mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-13 23:17:30 +00:00
Effects: CC Smear and CC Tiler on text and shape layers reach past the content's box (#659)
A text or shape layer's effect bounds are comp-sized (as in After Effects), but its buffer surrounds only its content, and each effect grows the buffer itself when it needs room. CC Smear and CC Tiler never did, so a smear was cut off where the glyphs end and tiles filled only the text's box. CC Tiler now covers the whole layer rectangle; CC Smear grows by how far it moves pixels (Reach x From->To), at most to the layer bounds, so footage layers, whose buffer is their bounds already, are unchanged. CPU and GPU alike. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
This commit is contained in:
5 files changed
+81
-6
No files matched your search
@@ -20,7 +20,7 @@ use effectcraft_raster::{Image, Px};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::generate::value_noise;
|
||||
use crate::util::{SRC_NAMES, Src, fit_layer, layer_rect, lerp4, map_xy, pick, premul, remap, smoothstep, src_at, unpremul};
|
||||
use crate::util::{SRC_NAMES, Src, fit_layer, layer_cover_pad, layer_rect, lerp4, map_xy, pick, premul, remap, smoothstep, src_at, unpremul};
|
||||
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||||
|
||||
fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {
|
||||
@@ -678,6 +678,9 @@ fn page_turn(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
|
||||
fn cc_tiler(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let s = (ctx.params.f("scale") / 100.0).max(0.001);
|
||||
if !ctx.adjustment {
|
||||
b.pad(layer_cover_pad(ctx, [b.img.width, b.img.height], b.offset, b.scale));
|
||||
}
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let blend = (ctx.params.f("blendWithOriginal") / 100.0) as f32;
|
||||
let (x0, y0, w, h) = layer_rect(ctx, &b);
|
||||
|
||||
@@ -330,9 +330,12 @@ fn cc_slant(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
// ---------------------------------------------------------------- CC Smear
|
||||
|
||||
fn cc_smear(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let reach = ctx.params.f("reach") / 100.0;
|
||||
if !ctx.adjustment {
|
||||
b.pad(cc_smear_pad(ctx, [b.img.width, b.img.height], b.offset, b.scale));
|
||||
}
|
||||
let from = b.to_px(ctx.params.v2("from"));
|
||||
let to = b.to_px(ctx.params.v2("to"));
|
||||
let reach = ctx.params.f("reach") / 100.0;
|
||||
let radius = (ctx.params.f("radius") * b.scale).max(0.5);
|
||||
let (dx, dy) = (to.0 - from.0, to.1 - from.1);
|
||||
let l2 = dx * dx + dy * dy;
|
||||
@@ -351,6 +354,21 @@ fn cc_smear(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
b
|
||||
}
|
||||
|
||||
/// The transparent pixels to add on every side of a buffer `size` pixels big (with `offset` and
|
||||
/// `scale`) for CC Smear: it moves pixels by up to Reach × From→To, and they may move out to the
|
||||
/// layer bounds. A text or shape layer's buffer ends at its content while its bounds are
|
||||
/// comp-sized, so the smear was cut off at the content's box (#659).
|
||||
pub fn cc_smear_pad(ctx: &EffectCtx, size: [u32; 2], offset: [f64; 2], scale: f64) -> u32 {
|
||||
let reach = ctx.params.f("reach") / 100.0;
|
||||
let (from, to) = (ctx.params.v2("from"), ctx.params.v2("to"));
|
||||
let len = ((to[0] - from[0]).powi(2) + (to[1] - from[1]).powi(2)).sqrt() * scale;
|
||||
let moved = (reach.abs() * len).ceil();
|
||||
if !moved.is_finite() || moved <= 0.0 {
|
||||
return 0;
|
||||
}
|
||||
(moved.min(4096.0) as u32).min(crate::util::layer_cover_pad(ctx, size, offset, scale))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Split / CC Split 2
|
||||
|
||||
fn split_impl(ctx: &EffectCtx, mut b: Buf, s1: f64, s2: f64) -> Buf {
|
||||
@@ -1109,6 +1127,43 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A text layer's buffer: 20 × 10 opaque pixels at (90, 45)–(110, 55) of its comp-sized
|
||||
/// (200 × 100) layer bounds, after `id` with `set`.
|
||||
fn on_text(id: &str, set: &[(&str, Value)]) -> Buf {
|
||||
let s = crate::find(id).unwrap();
|
||||
let mut params = Params { values: s.params.iter().map(|p| (p.id.to_string(), p.default.clone())).collect() };
|
||||
for (k, v) in set {
|
||||
assert!(params.values.contains_key(*k), "{id}: {k}");
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
let mut im = Image::new(20, 10);
|
||||
im.data.iter_mut().for_each(|p| *p = [1.0; 4]);
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [200.0, 100.0], seed: 1, adjustment: false, env: Default::default() };
|
||||
crate::apply(s, &ctx, Buf { img: im, offset: [-90.0, -45.0], scale: 1.0 })
|
||||
}
|
||||
|
||||
/// Alpha at layer point `p` (0 outside the buffer).
|
||||
fn alpha_at(b: &Buf, p: [f64; 2]) -> f32 {
|
||||
let (x, y) = b.to_px(p);
|
||||
b.img.get(x.floor() as i64, y.floor() as i64)[3]
|
||||
}
|
||||
|
||||
/// #659: CC Smear and CC Tiler on a text layer reach past the text's box to the layer bounds
|
||||
/// (comp-sized, as in After Effects), not cut off where the glyphs end.
|
||||
#[test]
|
||||
fn effects_on_text_reach_past_its_box() {
|
||||
let smear = on_text("ec.distort.ccsmear", &[("from", pt(100.0, 50.0)), ("to", pt(140.0, 50.0)), ("reach", num(100.0)), ("radius", num(30.0))]);
|
||||
assert!(alpha_at(&smear, [130.0, 50.0]) > 0.9, "the smear is cut off at the text's box");
|
||||
assert!(smear.img.width < 200, "it grows only as far as pixels move");
|
||||
let tiles = on_text("ec.distort.cctiler", &[("scale", num(50.0)), ("center", pt(100.0, 50.0))]);
|
||||
assert!(alpha_at(&tiles, [100.0, 50.0]) > 0.9);
|
||||
assert!(alpha_at(&tiles, [2.0, 50.0]) > 0.9, "tiles fill the layer bounds");
|
||||
assert!(alpha_at(&tiles, [50.0, 50.0]) < 0.1);
|
||||
// A footage layer's buffer is its bounds already: no change.
|
||||
let o = run("ec.distort.ccsmear", &[], img(40, 30), &[]);
|
||||
assert_eq!((o.img.width, o.img.height, o.offset), (40, 30, [0.0, 0.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_warp_style_runs_and_bends() {
|
||||
let im = img(40, 30);
|
||||
|
||||
@@ -78,7 +78,7 @@ pub use color2::{Curve, auto_correct_settings, color_link_mode, color_link_sampl
|
||||
pub use color3::OffsetCurve;
|
||||
pub use distort::transform_shutter;
|
||||
pub use distort2::{MAGNIFY_MODES, bezier_grid, parse_mesh};
|
||||
pub use distort3::{RESHAPE_POINTS, ReshapeSetup, SmearSetup, reshape_setup, smear_setup, warp_inverse_map};
|
||||
pub use distort3::{RESHAPE_POINTS, ReshapeSetup, SmearSetup, cc_smear_pad, reshape_setup, smear_setup, warp_inverse_map};
|
||||
pub use distort4::liquify_mesh;
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::build::Ids;
|
||||
|
||||
@@ -433,6 +433,17 @@ pub fn layer_rect_at(ctx: &crate::EffectCtx, offset: [f64; 2], scale: f64) -> (f
|
||||
(offset[0], offset[1], ctx.layer_size[0] * scale, ctx.layer_size[1] * scale)
|
||||
}
|
||||
|
||||
/// Transparent pixels to add on every side ([`crate::Buf::pad`]) of a buffer `size` pixels big,
|
||||
/// with `offset` and `scale`, so it covers the whole layer rectangle (at most 4096). A text or
|
||||
/// shape layer's buffer only surrounds its content, but its layer bounds are comp-sized (see
|
||||
/// the renderer's `effect_bounds`): an effect that fills the layer, such as CC Tiler, fills
|
||||
/// those as in After Effects (#659).
|
||||
pub fn layer_cover_pad(ctx: &crate::EffectCtx, size: [u32; 2], offset: [f64; 2], scale: f64) -> u32 {
|
||||
let (x0, y0, w, h) = layer_rect_at(ctx, offset, scale);
|
||||
let need = [-x0, -y0, x0 + w - f64::from(size[0]), y0 + h - f64::from(size[1])].into_iter().fold(0.0, f64::max);
|
||||
need.ceil().min(4096.0) as u32
|
||||
}
|
||||
|
||||
/// Resample another layer (a layer parameter) into `b`'s pixel grid. With `stretch` the other
|
||||
/// layer is scaled to this layer's size ("Stretch to Fit"); otherwise it is centred.
|
||||
pub fn fit_layer(ctx: &crate::EffectCtx, b: &crate::Buf, other: &crate::LayerPixels, stretch: bool) -> Image {
|
||||
|
||||
@@ -299,10 +299,13 @@ fn cc_slant(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
)
|
||||
}
|
||||
|
||||
fn cc_smear(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
fn cc_smear(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let reach = ctx.params.f("reach") / 100.0;
|
||||
if !ctx.adjustment {
|
||||
b.pad(e, effectcraft_effects::cc_smear_pad(ctx, [b.img.width, b.img.height], b.offset, b.scale))?;
|
||||
}
|
||||
let from = b.to_px(ctx.params.v2("from"));
|
||||
let to = b.to_px(ctx.params.v2("to"));
|
||||
let reach = ctx.params.f("reach") / 100.0;
|
||||
let radius = (ctx.params.f("radius") * b.scale).max(0.5);
|
||||
let (dx, dy) = (to.0 - from.0, to.1 - from.1);
|
||||
let l2 = dx * dx + dy * dy;
|
||||
@@ -585,8 +588,11 @@ fn offset(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
table(e, b, &data, false, blend.max(0.0))
|
||||
}
|
||||
|
||||
fn cc_tiler(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
fn cc_tiler(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let s = (ctx.params.f("scale") / 100.0).max(0.001);
|
||||
if !ctx.adjustment {
|
||||
b.pad(e, effectcraft_effects::util::layer_cover_pad(ctx, [b.img.width, b.img.height], b.offset, b.scale))?;
|
||||
}
|
||||
let c = b.to_px(ctx.params.v2("center"));
|
||||
let blend = (ctx.params.f("blendWithOriginal") / 100.0) as f32;
|
||||
let (x0, y0, w, h) = layer_rect(ctx, &b);
|
||||
|
||||
Reference in new issue
Block a user