mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
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:
co-authored by
Claude Opus 5.5
parent
a7255f7608
commit
8bc57bc7ad
@@ -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);
|
||||
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user