mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Render: text layers are always continuously rasterised (#194)
A text layer was drawn at its font size and the bitmap resampled by the layer's Scale unless its Continuously Rasterize switch was on, so text grown with Scale (the usual way to animate it) went soft. Text layers now always rasterise at their on-screen scale, as they would with the switch; shape layers and vector footage still follow the switch. Text at 10 px scaled 800% now renders identically to text set at 80 px (795 partially covered pixels each, maximum difference 0; it was 5854 against 795 before). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
608d1b077b
commit
9b26c268b9
3 files changed
+44
-11
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
@@ -317,8 +317,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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user