lottie_fixup 1.0.1
lottie_fixup: ^1.0.1 copied to clipboard
Fixes Lottie/Bodymovin exports that crash or freeze the lottie Flutter package: malformed layers, assets, masks, shapes, and unexecuted expressions.
lottie_fixup #
Fixes Lottie/Bodymovin exports that crash or freeze the
lottie Flutter package: malformed
layers/assets/masks/shape content, and expressions that lottie doesn't
execute (loopOut()/loopIn(), wiggle(), random(), time-based
motion, cross-layer links).
Features #
- Stops structural crashes — a range of JSON shapes the schema allows
but
lottie's own parser/render-tree builder doesn't defensively guard against, each grounded in a specific non-null-assertion or unassigned- field crash confirmed inlottie's source: audio layers ("ty": 6, which ship without the transform blocklottieexpects —Null check operator used on a null value), a precomp layer ("ty": 0) whoserefIddoesn't resolve to any asset, a text layer ("ty": 5) missing its document-data block, an asset with no usableid, a malformedmasksPropertiesentry, and a gradient-fill/gradient-stroke/solid-stroke shape item missing a required field (confirmed bylottie's own source to be a real shape non-After-Effects tools like Telegram's Lottie export ship) — this package removes each one specifically, leaving everything else in the file untouched. An out-of-range stroke line-cap/line-join value is patched in place instead of removed. An animatable value with no actual keyframes (which crasheslottiethe same way, but has no safe default to substitute) is reported rather than guessed at — seeSanitizeResult.propertiesWithEmptyKeyframes. - Bakes
loopOut()/loopIn()expressions into real keyframes, so looping animations don't freeze after their first cycle (lottiedoesn't execute expressions). All four After Effects loop modes are supported in both directions —'cycle','pingpong','offset','continue'— the latter two on any numeric property (position, scale, rotation, opacity...) — plus the duration-basedloopOutDuration()/loopInDuration()variants in'cycle'/'pingpong'mode. - Bakes other expressions, on a never-animated property or one that's
already keyframed (the expression's result is authoritative, same as After
Effects — the original curve is only available through
value/valueAtTime()): continuoustime-based motion (e.g.time * 180for constant rotation),if/elsebranching with comparisons/booleans, localvarbindings, cross-layer links (thisComp.layer('Name').transform.position, across all transform properties includingskew/skewAxis, copied exactly when that's the whole expression on a never-animated property, sampled when combined with other math or via.valueAtTime(t)) and the same-layer equivalent (thisLayer.transform.position, or baretransform.position), theMath.*namespace,linear()/ease()/easeIn()/easeOut()/clamp(),add()/sub()/mul()/div()/value,posterizeTime(), andrandom()/wiggle()(a deterministic, seeded approximation — After Effects' own noise/PRNG can't be reproduced bit-for-bit, but this is reproducible across builds and beats a frozen property). Awiggle()-only expression on a shape path wiggles each vertex independently. - Prunes empty precomps and now-unreferenced assets left behind by the fixes above.
- Use it at load time (drop-in decoder, no build step) or ahead of time (CLI, zero runtime cost).
Getting started #
Add the dependency:
dependencies:
lottie_fixup: ^1.0.0
Usage #
At load time — no build step #
Drop fixupLottieDecoder into any lottie loading API that takes a
decoder:
import 'package:lottie/lottie.dart';
import 'package:lottie_fixup/lottie_fixup.dart';
Lottie.asset('assets/character.json', decoder: fixupLottieDecoder)
Safe to apply unconditionally, even to files already fixed ahead of
time — fix is a no-op when there's nothing left to do. This adds a JSON
decode/walk/re-encode once per composition load, not per frame. For larger
files, pass backgroundLoading: true to move that work off the UI isolate:
Lottie.asset(
'assets/character.json',
decoder: fixupLottieDecoder,
backgroundLoading: true,
)
Ahead of time — CLI #
For an animation that ships in every build and never changes, fix it once and skip the runtime cost entirely:
dart pub global activate lottie_fixup
lottie_fixup diagnose assets/animations/*.json # report only, no changes
lottie_fixup fix assets/animations/*.json # fix in place
Library #
import 'dart:convert';
import 'dart:io';
import 'package:lottie_fixup/lottie_fixup.dart';
final file = File('animation.json');
final doc = jsonDecode(file.readAsStringSync()) as Map<String, dynamic>;
final result = fix(doc); // mutates doc in place
if (result.changed) {
file.writeAsStringSync(jsonEncode(doc));
}
Configuration: opting out of approximations #
A few parts of expression baking are an approximation or a judgment call
rather than an exact match to what After Effects would render — see
Features above. BakeOptions lets you turn any of them off
individually; every option defaults to true (bake everything), and turning
one off only ever makes baking more conservative — the affected
expressions are reported as unsupported instead of altered:
| Option | Default | Turn off to... |
|---|---|---|
bakeRandomAndWiggle |
true |
Leave random()/wiggle() unbaked (also disables bakeShapePathWiggle). |
bakeOnKeyframedProperties |
true |
Only bake never-keyframed ("a": 0) properties, matching versions before 0.3.0. |
bakeShapePathWiggle |
true |
Leave wiggle() on a shape path unbaked. |
bakeApproximateEasing |
true |
Leave ease()/easeIn()/easeOut() unbaked (linear() is unaffected — it's an exact formula, not an approximation). |
It plugs into every entry point:
const options = BakeOptions(bakeOnKeyframedProperties: false);
// Library
fix(doc, options: options);
diagnose(rawJson, doc, options: options); // pass the same options you'll fix() with
// At load time
Lottie.asset(
'assets/character.json',
decoder: fixupLottieDecoderWithOptions(options),
)
# CLI
lottie_fixup fix --no-keyframed-properties assets/animations/*.json
What this does not fix #
- Expressions this package's small evaluator doesn't understand are reported
(
diagnose, orFixResult.propertyBake.skippedExpressions) but left untouched, notably:effect(...)(Effects Controller references, e.g. a Slider/Angle/Checkbox control used as a rig parameter); a reference into a comp more than onethisComp.layer(...)hop away;for/whileloops or user-defined functions (no such grammar exists); text-layer/sourceTextanimator expressions; compmarker.*references;Math.random()(as opposed to the supported top-levelrandom()); the vector-math helperslength()/normalize()/cross()/dot(); andlookAt(). 'offset'/'continue'on a non-numeric value (a shape path, for example, rather than position/scale/rotation/opacity) is reported rather than baked, since those modes work by doing arithmetic directly on the value.- A non-
wiggle()expression on a shape path (arithmetic directly on a path value) is reported rather than baked — After Effects doesn't support that either. - A duration variant (
loopOutDuration/loopInDuration) whose duration is shorter than the keyframed segment itself is reported rather than baked — that would need interpolating a cut point in the middle of the real animation, which isn't implemented. - A property that calls both
loopInandloopOut(a manual "loop both ways" expression) is reported rather than guessed at. random()/wiggle()are baked as a plausible approximation, not a bit-exact match to After Effects — see Features above.- A layer missing
ksfor a reason other than being an audio layer is flagged (SanitizeResult.layersMissingTransform) rather than silently removed, since that could be a real authoring mistake worth checking by hand. - Repeater (
ty: "rp") and merge-paths (ty: "mm") shape-content items missing their own required fields have the same crash shape as the gradient/stroke items this package does check (an unguarded non-null assertion inlottie's parser) but aren't checked yet. - An animatable-value-shaped object with a missing/empty
kis flagged (SanitizeResult.propertiesWithEmptyKeyframes) rather than fixed — see Features above.
Additional information #
If this package saved you a debugging session, consider buying me a coffee.
