diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index ac120165..e6f6620c 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -667,18 +667,21 @@ impl<'a> Renderer<'a> { Region::Full } - /// Rasterisation scale of a layer's source: the output scale, or — for Continuously - /// Rasterize text and shape layers — 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 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). pub fn raster_scale(&self, ctx: &EvalCtx, layer: &Layer) -> f64 { let s = self.opts.scale; - let vector = match layer.source { - LayerSource::Text | LayerSource::Shape => true, - LayerSource::Footage { item } => matches!(self.project.item(item).map(|i| &i.kind), Some(ItemKind::Footage(f)) if is_vector_footage(f)), + let continuous = match layer.source { + LayerSource::Text => true, + LayerSource::Shape => layer.switches.collapse, + LayerSource::Footage { item } => { + layer.switches.collapse && matches!(self.project.item(item).map(|i| &i.kind), Some(ItemKind::Footage(f)) if is_vector_footage(f)) + } _ => false, }; - if !layer.switches.collapse || !vector { + if !continuous { return s; } let (l2c, _) = ctx.layer_to_comp(layer); diff --git a/crates/render/src/tests_collapse.rs b/crates/render/src/tests_collapse.rs index 7d7e28ae..0ab11b3f 100644 --- a/crates/render/src/tests_collapse.rs +++ b/crates/render/src/tests_collapse.rs @@ -1,7 +1,7 @@ //! Collapse Transformations, Continuously Rasterize and the Quality switch. use effectcraft_color::{BlendMode, Label}; -use effectcraft_keyframe::Value; +use effectcraft_keyframe::{TextDoc, Value}; use effectcraft_project::build::{self, Ids}; use effectcraft_project::{BitDepth, Comp, ItemId, ItemKind, Layer, LayerSource, Project, Quality, Solid}; use effectcraft_time::{FrameRate, Tick}; @@ -234,3 +234,32 @@ fn collapsed_precomp_of_another_size_renders_through_the_parent_camera() { assert!(img.get(100, 100)[3] < 0.01, "only the 40 px solid is drawn"); } } + +/// Text layers are always continuously rasterised (#194): text scaled up with the layer's Scale is drawn +/// at its on-screen size, as sharp as text set at that font size, without the switch. +#[test] +fn text_scaled_up_stays_sharp_without_the_switch() { + let run = |size: f64, scale: f64| { + let mut p = project(); + let comp = Comp::new(240, 160, FrameRate::FPS_30, Tick::from_seconds_f64(2.0)); + let mut l = build::layer(&mut p, &comp, "Text", LayerSource::Text, (240, 160), None); + l.props.prop_mut("text/sourceText").unwrap().value = Value::Text(Box::new(TextDoc { text: "HE".into(), size, ..Default::default() })); + set(&mut l, "transform/anchor", Value::Vec3([0.0, 0.0, 0.0])); + set(&mut l, "transform/position", Value::Vec3([40.0, 120.0, 0.0])); + set(&mut l, "transform/scale", Value::Vec3([scale, scale, 100.0])); + assert!(!l.switches.collapse); + 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 partial = |img: &Image| img.data.iter().filter(|px| (0.02..0.98).contains(&px[3])).count(); + let scaled = run(10.0, 800.0); + let native = run(80.0, 100.0); + let (ps, pn) = (partial(&scaled), partial(&native)); + let d = max_diff(&scaled, &native); + eprintln!("partially covered pixels: scaled {ps}, native {pn}; max difference {d}"); + assert!(native.data.iter().filter(|px| px[3] > 0.98).count() > 1000, "the text renders"); + assert!(ps <= pn + pn / 2, "scaled text is blurred: {ps} soft pixels against {pn}"); + assert!(d < 0.05, "scaled text differs from text set at that size by {d}"); +} diff --git a/docs/architecture.md b/docs/architecture.md index 6d29b294..52f8897d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -318,8 +318,9 @@ hierarchical block-matching optical flow and a bidirectional warp (Pixel Motion, its nested layers straight into the parent with concatenated transforms (one resample), so nested blend modes and adjustment layers act on the parent's layers; nested 3D layers use the parent's camera and lights and, when the precomp layer is 3D, are depth-sorted with the parent's -3D run. Masks, effects or styles force a flattened render. On text and shape layers the switch is -Continuously Rasterize: the source is rasterised at its on-screen scale. Quality: Draft samples +3D run. Masks, effects or styles force a flattened render. Text layers are always rasterised at +their on-screen scale; on shape layers and vector footage the switch is Continuously Rasterize and +does the same. Quality: Draft samples nearest-neighbour, Wireframe draws the layer bounds. Slip edit (`layer.slip`, Alt+PageUp/Down, dragging the source bar in the timeline) moves the source under fixed in/out points.