Text: Justify All balances the lines with the every-line composer, as After Effects

With Justify All After Effects stretches the paragraph's last line too, and
its every-line composer breaks the paragraph so that line is not a lone
word: a 400 px box that the other justifications break into 6 / 6 / 1 words
breaks 4 / 4 / 4 (AE 2026, EXPORTER read-back t6). EffectCraft's composer
left the last line's slack out of its cost, so "twelve" stood alone on a
stretched line; it now counts that slack for Justify All and every word
starts within 1 px of AE's (what remains in that box is the known box-text
baseline offset). Other justifications and the single-line composer are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76
2026-10-19 16:41:03 +02:00
co-authored by Claude Opus 5.5
parent a7255f7608
commit 8bc57bc7ad
2 changed files with 31 additions and 2 deletions
+22 -2
View File
@@ -761,7 +761,10 @@ fn paragraph(text: &str, base: usize, runs: &[(Range<usize>, TextStyle)], primar
let opps: Vec<usize> = unicode_linebreak::linebreaks(text).map(|(p, _)| p).collect();
let avail = |line: usize| avail_for(line == 0).unwrap_or(f32::MAX);
if para.every_line && opps.len() > 1 && opps.len() <= 1500 {
// Minimise the summed squared slack of every line but the last.
// Minimise the summed squared slack of every line but the last; with Justify All
// the last line is stretched too, so its slack counts (After Effects then balances
// the lines: 4 / 4 / 4 words where the other modes break 6 / 6 / 1).
let last_counts = para.align == Align::Justify && para.justify_last == Align::Justify;
let nodes: Vec<usize> = std::iter::once(0).chain(opps.iter().copied()).collect();
let m = nodes.len();
let mut best = vec![(f32::INFINITY, 0usize); m];
@@ -781,7 +784,7 @@ fn paragraph(text: &str, base: usize, runs: &[(Range<usize>, TextStyle)], primar
let slack = maxw - w;
let cost = if over {
1e12 + w
} else if j == m - 1 {
} else if j == m - 1 && !last_counts {
0.0
} else {
slack * slack
@@ -1464,6 +1467,23 @@ mod tests {
assert!(every.lines.iter().all(|ln| ln.width <= 320.0 + 0.5));
}
/// After Effects 2026 (EXPORTER t6): with Justify All and the every-line composer, a box that
/// the other justifications break into lines of 6, 6 and 1 words breaks 4 / 4 / 4, so the
/// stretched last line is not a lone word; word positions then match AE to 1 px.
#[test]
fn justify_all_balances_the_lines_with_the_every_line_composer() {
let text = "one two three four five six seven eight nine ten eleven twelve";
let s = st(30.0);
let words = |l: &Layout| -> Vec<usize> { l.lines.iter().map(|ln| text[ln.range.clone()].split_whitespace().count()).collect() };
let p = |last: Align| ParagraphStyle { width: Some(450.0), align: Align::Justify, justify_last: last, every_line: true, ..Default::default() };
let left = layout_rich_uncached(text, &[(0..text.len(), s.clone())], &[p(Align::Left)]);
let all = layout_rich_uncached(text, &[(0..text.len(), s)], &[p(Align::Justify)]);
let (wl, wa) = (words(&left), words(&all));
assert!(wl.last().copied().unwrap_or(0) < *wl.first().unwrap_or(&0), "{wl:?}");
let (lo, hi) = (wa.iter().min().copied().unwrap_or(0), wa.iter().max().copied().unwrap_or(0));
assert!(hi - lo <= 1, "Justify All balances the lines: {wa:?} (left-justified: {wl:?})");
}
#[test]
fn hanging_punctuation_overhangs_the_box() {
let s = st(40.0);
+9
View File
@@ -71,6 +71,15 @@ After the fix, all ten tangents equal AE's to the printed 0.001 px, and the two
test `spatial_auto_bezier_tangents_follow_after_effects` checks the rule on other original keys,
including a timing change. AE's output stays local; only the script and these scores are tracked.
## Justify All with the every-line composer
Measured on 19 October 2026 with After Effects 2026 (26.2) on box text (EXPORTER read-back t6):
with Justify All, AE stretches the paragraph's last line too and breaks the lines so that it is not
a lone word (4 / 4 / 4 words where the other justifications break 6 / 6 / 1). EffectCraft's
every-line composer ignored the last line's slack and left "twelve" alone on a stretched line; it now
counts that slack for Justify All, and every word starts within 1 px of AE's. Regression test:
`justify_all_balances_the_lines_with_the_every_line_composer`.
## Still unmeasured
Rendered frame comparisons now have a deterministic `effectcraft_effects::fidelity::compare_frames`