mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-01 14:08:38 -06:00
Shape rectangles start at the top-right, as After Effects, so Trim Paths cut the same segment
After Effects draws a Rectangle Path clockwise from its top-right corner, or with round corners from the end of the top-right arc on the right edge; Trim Paths measures from there (AE 2026, case 930: 400 x 300 rectangles, Trim 0-25 % and 70 / 30). EffectCraft built the path with kurbo, starting at the top-left corner, so trimmed rectangles drew another segment: case 930 was 86 of 5184 grid points off (max 255), now 5 at stroke edges. Start > End was already drawn swapped, as in AE. Convert to Bezier now gives AE's vertex order (top-right first); the engine test follows. Lottie rectangles share the path and now start where lottie players start them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
710ffd9f7e
commit
d57b73d4cb
@@ -519,9 +519,10 @@ fn convert_to_bezier_turns_a_parametric_shape_into_an_editable_path() {
|
||||
};
|
||||
let radius = |v: [f64; 2]| v[0].hypot(v[1]);
|
||||
|
||||
// A 120 x 80 rectangle is its four corners, joined by straight edges.
|
||||
// A 120 x 80 rectangle is its four corners, joined by straight edges, from the top-right
|
||||
// corner clockwise as After Effects draws it.
|
||||
let (mut s, l, was, rect) = convert(json!({"kind": "rect", "size": [120, 80]}));
|
||||
assert_eq!(rect.vertices, vec![[-60.0, -40.0], [60.0, -40.0], [60.0, 40.0], [-60.0, 40.0]]);
|
||||
assert_eq!(rect.vertices, vec![[60.0, -40.0], [60.0, 40.0], [-60.0, 40.0], [-60.0, -40.0]]);
|
||||
assert!(rect.in_tangents.iter().chain(&rect.out_tangents).all(|t| *t == [0.0, 0.0]), "straight edges: {rect:?}");
|
||||
// Undo brings the parametric shape back.
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
|
||||
+60
-4
@@ -130,11 +130,36 @@ pub fn from_kurbo(path: &BezPath) -> Vec<ShapePath> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Rectangle path centred at `pos` with corner `roundness` (AE draws clockwise from the top-right).
|
||||
/// Rectangle path centred at `pos` with corner `roundness`, drawn as After Effects draws it
|
||||
/// (Trim Paths measures from its first point): clockwise from the top-right corner, or with
|
||||
/// round corners from the end of the top-right arc on the right edge (After Effects 2026,
|
||||
/// 400 × 300 rectangles trimmed 0–25 %, case 930).
|
||||
pub fn rect(size: [f64; 2], pos: [f64; 2], roundness: f64) -> BezPath {
|
||||
let r = kurbo::Rect::from_center_size(Point::new(pos[0], pos[1]), (size[0].abs(), size[1].abs()));
|
||||
let rad = roundness.clamp(0.0, size[0].abs().min(size[1].abs()) / 2.0);
|
||||
if rad > 0.0 { r.to_rounded_rect(rad).to_path(0.1) } else { r.to_path(0.1) }
|
||||
let (w, h) = (size[0].abs(), size[1].abs());
|
||||
let (x0, y0, x1, y1) = (pos[0] - w / 2.0, pos[1] - h / 2.0, pos[0] + w / 2.0, pos[1] + h / 2.0);
|
||||
let rad = if roundness.is_finite() { roundness.clamp(0.0, w.min(h) / 2.0) } else { 0.0 };
|
||||
let mut p = BezPath::new();
|
||||
if rad <= 0.0 {
|
||||
p.move_to((x1, y0));
|
||||
p.line_to((x1, y1));
|
||||
p.line_to((x0, y1));
|
||||
p.line_to((x0, y0));
|
||||
p.close_path();
|
||||
return p;
|
||||
}
|
||||
// Quarter circles as cubics (handle 0.5523 × radius).
|
||||
let k = rad * 0.552_284_749_8;
|
||||
p.move_to((x1, y0 + rad));
|
||||
p.line_to((x1, y1 - rad));
|
||||
p.curve_to((x1, y1 - rad + k), (x1 - rad + k, y1), (x1 - rad, y1));
|
||||
p.line_to((x0 + rad, y1));
|
||||
p.curve_to((x0 + rad - k, y1), (x0, y1 - rad + k), (x0, y1 - rad));
|
||||
p.line_to((x0, y0 + rad));
|
||||
p.curve_to((x0, y0 + rad - k), (x0 + rad - k, y0), (x0 + rad, y0));
|
||||
p.line_to((x1 - rad, y0));
|
||||
p.curve_to((x1 - rad + k, y0), (x1, y0 + rad - k), (x1, y0 + rad));
|
||||
p.close_path();
|
||||
p
|
||||
}
|
||||
|
||||
pub fn ellipse(size: [f64; 2], pos: [f64; 2]) -> BezPath {
|
||||
@@ -354,6 +379,37 @@ pub fn transform(paths: &[BezPath], m: &Mat3) -> Vec<BezPath> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// After Effects 2026 (case 930): 400 × 300 rectangles at the origin with Trim Paths. Plain:
|
||||
/// 0–25 % runs from the top-right corner down the right edge and 50 px along the bottom.
|
||||
/// Round corners 40: from the end of the top-right arc (200, −110) down, round the
|
||||
/// bottom-right corner, to x 110 on the bottom. Start 70 / End 30 draws 30–70 %.
|
||||
#[test]
|
||||
fn rectangles_start_where_after_effects_starts_them() {
|
||||
let ends = |p: &BezPath| {
|
||||
let mut first = None;
|
||||
let mut last = None;
|
||||
for el in p.elements() {
|
||||
match *el {
|
||||
PathEl::MoveTo(q) if first.is_none() => first = Some(q),
|
||||
PathEl::MoveTo(q) | PathEl::LineTo(q) | PathEl::CurveTo(_, _, q) | PathEl::QuadTo(_, q) => last = Some(q),
|
||||
PathEl::ClosePath => {}
|
||||
}
|
||||
}
|
||||
(first.unwrap_or_default(), last.unwrap_or_default())
|
||||
};
|
||||
let near = |a: Point, b: (f64, f64)| (a.x - b.0).abs() < 1.0 && (a.y - b.1).abs() < 1.0;
|
||||
let plain = ops::trim(&[rect([400.0, 300.0], [0.0, 0.0], 0.0)], 0.0, 25.0, 0.0);
|
||||
let (a, b) = ends(&plain[0]);
|
||||
assert!(near(a, (200.0, -150.0)) && near(b, (150.0, 150.0)), "{a:?} {b:?}");
|
||||
let round = ops::trim(&[rect([400.0, 300.0], [0.0, 0.0], 40.0)], 0.0, 25.0, 0.0);
|
||||
let (a, b) = ends(&round[0]);
|
||||
assert!(near(a, (200.0, -110.0)) && near(b, (110.0, 150.0)), "{a:?} {b:?}");
|
||||
// Start > End: AE draws 30..70 % (plain: from x 80 on the bottom up to y −130 on the left).
|
||||
let swapped = ops::trim(&[rect([400.0, 300.0], [0.0, 0.0], 0.0)], 70.0, 30.0, 0.0);
|
||||
let (a, b) = ends(&swapped[0]);
|
||||
assert!(near(a, (80.0, 150.0)) && near(b, (-200.0, -130.0)), "{a:?} {b:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_vertex_model() {
|
||||
let sp = ShapePath::ellipse([10.0, 20.0], 40.0, 30.0);
|
||||
|
||||
@@ -71,6 +71,16 @@ 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.
|
||||
|
||||
## Rectangle path start point
|
||||
|
||||
Measured on 19 October 2026 with After Effects 2026 (26.2) on 400 × 300 shape rectangles with Trim
|
||||
Paths: a plain rectangle starts at its top-right corner and runs clockwise (0–25 % covers the right
|
||||
edge and 50 px of the bottom); with Roundness 40 it starts where the top-right arc meets the right
|
||||
edge. Start 70 / End 30 draws 30–70 %. EffectCraft started at the top-left corner (and the rounded
|
||||
rectangle elsewhere), so trimmed rectangles drew the wrong segment (86 of 5184 grid points off,
|
||||
now 5 at stroke edges). `effectcraft_path::rect` now draws AE's order, which also gives Convert to
|
||||
Bezier AE's vertex order. Regression test: `rectangles_start_where_after_effects_starts_them`.
|
||||
|
||||
## Still unmeasured
|
||||
|
||||
Rendered frame comparisons now have a deterministic `effectcraft_effects::fidelity::compare_frames`
|
||||
|
||||
Reference in New Issue
Block a user