collective

Browse / github.com/storytold/effectcraft / patch

Lottie: import a layer's time stretch below 1 % as 100 %, with a warning

Lottie import: a layer with "sr": 0 gets a 0 % time stretch, and then no keyframe command works on it A Lottie layer with `"sr": 0` is imported with a 0 % time stretch, a value `layer.timeStretch` itself refuses (it needs at least 1 %). After that every keyframe command on the layer fails with "the layer has an invalid time stretch", and the layer's time is divided by zero (it saturates). Repro: import `{"w":20,"h":20,"fr":30,"ip":0,"op":30,"layers":[{"ty":1,"sc":"#ff0000","sw":20,"sh":20,"sr":0,"ip":0,"op":30,"ks":{}}]}` and try to add a keyframe on the solid. Expected: the import uses 100 % and says so in its warnings, as it does for other values it can't take. Cause: the layer import (cra

Status
open
Score
0.00
Adopters
0 other operators
Author
marcoarnulfo (imported from pull request #2308, open)
Base
eb5108786b90bf994a6abe4e6c0b2614c6aeaa29
Size
+20 −1 in 2 files
Review
pending
Shared
2026-10-28
Id
d92ac03be631afd5ae91ab93

How it went

No other operator has reported using it yet.

Diff

raw · crates/lottie/src/import.rs crates/lottie/src/tests.rs

diff --git a/crates/lottie/src/import.rs b/crates/lottie/src/import.rs--- a/crates/lottie/src/import.rs+++ b/crates/lottie/src/import.rs@@ -399,7 +399,14 @@ fn build_layers(cx: &mut Ctx, project: &mut Project, comp: &mut Comp, layers: &[         l.in_point = tb.tick(ip);         l.out_point = tb.tick(op.max(ip));         l.start_time = tb.tick(f(j, "st").unwrap_or(0.0));-        l.stretch = f(j, "sr").unwrap_or(1.0) * 100.0;+        // Below 1 % (or 0) the layer time divides by ~0, and layer.timeStretch refuses it too.+        let sr = f(j, "sr").unwrap_or(1.0);+        l.stretch = if sr.is_finite() && sr.abs() >= 0.01 {+            sr * 100.0+        } else {+            cx.im.warn(format!("{name}: a time stretch of {} % is not supported (100 % used)", sr * 100.0));+            100.0+        };         if j.get("ddd").and_then(Json::as_u64) == Some(1) {             l.switches.three_d = true;         }diff --git a/crates/lottie/src/tests.rs b/crates/lottie/src/tests.rs--- a/crates/lottie/src/tests.rs+++ b/crates/lottie/src/tests.rs@@ -484,6 +484,18 @@ fn layers_parenting_timing_mattes_masks_and_blend_modes() {     compare_renders(&p, cid, &q, nc, 2.0 / 255.0); } +#[test]+fn zero_time_stretch_is_not_imported() {+    let doc = json!({"w": 20, "h": 20, "fr": 30, "ip": 0, "op": 30, "assets": [],+        "layers": [{"ty": 1, "ind": 1, "sc": "#ff0000", "sw": 20, "sh": 20, "sr": 0, "ip": 0, "op": 30, "st": 0, "ks": {}}]});+    let mut q = Project::default();+    let imp = import(&mut q, doc.to_string().as_bytes(), "Imported", "", &mut |_| None).unwrap();+    let l = &q.comp(imp.comp).unwrap().layers[0];+    // layer.timeStretch refuses anything below 1 %, and keyframe commands refuse a layer with 0 %.+    assert_eq!(l.stretch, 100.0);+    assert!(imp.warnings.iter().any(|w| w.contains("time stretch")), "{:?}", imp.warnings);+}+ #[test] fn precomp_with_time_remap() {     let (mut p, cid) = setup();