mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-27 19:47:12 -06:00
Shape layers: always rasterise at the on-screen scale
Shape layers are always continuously rasterised, as in After Effects and as text layers already are here: the contents are drawn at the layer's effective scale (own transform and parents) whether or not the Continuously Rasterize switch is on, so scaled-up shapes stay sharp. Fixes #751 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
67434e9038
commit
183d052c6a
@@ -732,15 +732,14 @@ impl<'a> Renderer<'a> {
|
||||
Region::Full
|
||||
}
|
||||
|
||||
/// Rasterisation scale of a layer's source: the output scale, or — for text layers, which are
|
||||
/// always continuously rasterised, and Continuously Rasterize shape layers and vector footage —
|
||||
/// the output scale times the layer's on-screen scale (quarter-octave steps, rounded up, so
|
||||
/// vector content is drawn at its final size and stays sharp when scaled up).
|
||||
/// Rasterisation scale of a layer's source: the output scale, or — for text and shape layers,
|
||||
/// which are always continuously rasterised (#390, #751), and Continuously Rasterize vector
|
||||
/// footage — the output scale times the layer's on-screen scale (quarter-octave steps, rounded
|
||||
/// up, so vector content is drawn at its final size and stays sharp when scaled up).
|
||||
pub fn raster_scale(&self, ctx: &EvalCtx, layer: &Layer) -> f64 {
|
||||
let s = self.opts.scale;
|
||||
let continuous = match layer.source {
|
||||
LayerSource::Text => true,
|
||||
LayerSource::Shape => layer.switches.collapse,
|
||||
LayerSource::Text | LayerSource::Shape => true,
|
||||
LayerSource::Footage { item } => {
|
||||
layer.switches.collapse && matches!(self.project.item(item).map(|i| &i.kind), Some(ItemKind::Footage(f)) if is_vector_footage(f))
|
||||
}
|
||||
|
||||
@@ -145,36 +145,53 @@ fn edge_width(img: &Image, y: i64, x0: i64, x1: i64) -> usize {
|
||||
(x0..x1).filter(|&x| (0.02..0.98).contains(&img.get(x, y)[3])).count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn continuously_rasterized_shape_stays_sharp_when_scaled_up() {
|
||||
let run = |cr: bool| {
|
||||
let mut p = project();
|
||||
let comp = Comp::new(200, 200, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||
let mut l = build::layer(&mut p, &comp, "Shape", LayerSource::Shape, (200, 200), None);
|
||||
let mut next = p.next_id;
|
||||
let mut ids = Ids(&mut next);
|
||||
let rect = build::shape_rect(&mut ids, [10.0, 10.0], [0.0, 0.0], 0.0);
|
||||
let fill = build::shape_fill(&mut ids, [1.0, 1.0, 1.0, 1.0]);
|
||||
let g = build::shape_group(&mut ids, "Rectangle 1", vec![rect, fill]);
|
||||
p.next_id = next;
|
||||
l.props.sub_mut("contents").unwrap().children.push(g.into());
|
||||
set(&mut l, "transform/anchor", Value::Vec3([0.0, 0.0, 0.0]));
|
||||
/// A 10 px white square in a 200×200 comp, scaled 800% either by its own Scale or by a parent
|
||||
/// null's, with the Continuously Rasterize switch `cr`.
|
||||
fn scaled_square(cr: bool, via_parent: bool) -> Image {
|
||||
let mut p = project();
|
||||
let comp = Comp::new(200, 200, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||
let mut l = build::layer(&mut p, &comp, "Shape", LayerSource::Shape, (200, 200), None);
|
||||
let mut next = p.next_id;
|
||||
let mut ids = Ids(&mut next);
|
||||
let rect = build::shape_rect(&mut ids, [10.0, 10.0], [0.0, 0.0], 0.0);
|
||||
let fill = build::shape_fill(&mut ids, [1.0, 1.0, 1.0, 1.0]);
|
||||
let g = build::shape_group(&mut ids, "Rectangle 1", vec![rect, fill]);
|
||||
p.next_id = next;
|
||||
l.props.sub_mut("contents").unwrap().children.push(g.into());
|
||||
set(&mut l, "transform/anchor", Value::Vec3([0.0, 0.0, 0.0]));
|
||||
l.switches.collapse = cr;
|
||||
let mut c = comp.clone();
|
||||
if via_parent {
|
||||
let mut null = build::layer(&mut p, &comp, "Null", LayerSource::Null, (100, 100), None);
|
||||
set(&mut null, "transform/anchor", Value::Vec3([0.0, 0.0, 0.0]));
|
||||
set(&mut null, "transform/position", Value::Vec3([100.3, 100.3, 0.0]));
|
||||
set(&mut null, "transform/scale", Value::Vec3([800.0, 800.0, 100.0]));
|
||||
set(&mut l, "transform/position", Value::Vec3([0.0, 0.0, 0.0]));
|
||||
l.parent = Some(null.id);
|
||||
c.layers = vec![l, null];
|
||||
} else {
|
||||
set(&mut l, "transform/position", Value::Vec3([100.3, 100.3, 0.0]));
|
||||
set(&mut l, "transform/scale", Value::Vec3([800.0, 800.0, 100.0]));
|
||||
l.switches.collapse = cr;
|
||||
let mut c = comp;
|
||||
c.layers = vec![l];
|
||||
let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(c.into()));
|
||||
render_frame(&p, cid, Tick::ZERO, 1.0)
|
||||
};
|
||||
}
|
||||
let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(c.into()));
|
||||
render_frame(&p, cid, Tick::ZERO, 1.0)
|
||||
}
|
||||
|
||||
/// Shape layers are always continuously rasterised, as in After Effects (#751): a shape scaled up
|
||||
/// by its own transform or a parent's is drawn at its on-screen size, with a 1 px anti-aliased
|
||||
/// edge, whether or not the Continuously Rasterize switch is on.
|
||||
#[test]
|
||||
fn shape_scaled_up_stays_sharp_without_the_switch() {
|
||||
// The 10 px square scaled 800% spans 60.3..140.3.
|
||||
let soft = run(false);
|
||||
let sharp = run(true);
|
||||
let ws = edge_width(&soft, 100, 50, 75);
|
||||
let wc = edge_width(&sharp, 100, 50, 75);
|
||||
assert!(ws >= 4, "bitmap scaling blurs the edge: {ws}");
|
||||
assert!(wc <= 1, "continuously rasterized edge: {wc}");
|
||||
assert!(sharp.get(100, 100)[3] > 0.99 && sharp.get(55, 100)[3] < 0.01);
|
||||
for cr in [false, true] {
|
||||
for via_parent in [false, true] {
|
||||
let img = scaled_square(cr, via_parent);
|
||||
let w = edge_width(&img, 100, 50, 75);
|
||||
assert!(w <= 1, "switch {cr}, scaled by parent {via_parent}: edge {w} px wide");
|
||||
assert!(img.get(100, 100)[3] > 0.99 && img.get(55, 100)[3] < 0.01);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -285,3 +302,38 @@ fn unscaled_text_rasterises_at_the_output_scale() {
|
||||
assert_eq!(k, 1.0, "text at {pos:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Effects on a shape layer run on its continuously rasterised pixels, still in layer space as
|
||||
/// with the switch off in After Effects: a 10 px square scaled 800% with a 2 px blur looks like
|
||||
/// an 80 px square with a 16 px blur, not a blurred bitmap enlarged afterwards (#751).
|
||||
#[test]
|
||||
fn effects_on_a_scaled_shape_follow_its_scale() {
|
||||
let run = |size: f64, scale: f64, blur: f64| {
|
||||
let mut p = project();
|
||||
let comp = Comp::new(200, 200, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||
let mut l = build::layer(&mut p, &comp, "Shape", LayerSource::Shape, (200, 200), None);
|
||||
let mut next = p.next_id;
|
||||
let mut ids = Ids(&mut next);
|
||||
let rect = build::shape_rect(&mut ids, [size, size], [0.0, 0.0], 0.0);
|
||||
let fill = build::shape_fill(&mut ids, [1.0, 1.0, 1.0, 1.0]);
|
||||
let g = build::shape_group(&mut ids, "Rectangle 1", vec![rect, fill]);
|
||||
let spec = effectcraft_effects::find("ec.blur.gaussian").unwrap();
|
||||
let mut fx = effectcraft_effects::instantiate(spec, &mut ids, spec.name, [200.0, 200.0]);
|
||||
p.next_id = next;
|
||||
fx.prop_mut("blurriness").unwrap().value = Value::Scalar(blur);
|
||||
l.props.sub_mut("contents").unwrap().children.push(g.into());
|
||||
l.props.sub_mut("effects").unwrap().children.push(fx.into());
|
||||
set(&mut l, "transform/anchor", Value::Vec3([0.0, 0.0, 0.0]));
|
||||
set(&mut l, "transform/position", Value::Vec3([100.0, 100.0, 0.0]));
|
||||
set(&mut l, "transform/scale", Value::Vec3([scale, scale, 100.0]));
|
||||
let mut c = comp;
|
||||
c.layers = vec![l];
|
||||
let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(c.into()));
|
||||
render_frame(&p, cid, Tick::ZERO, 1.0)
|
||||
};
|
||||
let scaled = run(10.0, 800.0, 2.0);
|
||||
let native = run(80.0, 100.0, 16.0);
|
||||
let d = max_diff(&scaled, &native);
|
||||
assert!(scaled.get(100, 100)[3] > 0.99 && scaled.get(70, 100)[3] > 0.05, "the blur spreads the edge");
|
||||
assert!(d < 0.05, "scaled blurred shape differs from the native one by {d}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user