lottie_fixup 1.2.0
lottie_fixup: ^1.2.0 copied to clipboard
Fixes Lottie/Bodymovin exports that crash, freeze or warn in the lottie Flutter package: malformed layers, assets, masks, shapes and keyframes, unexecuted expressions, and ignored layer effects.
lottie_fixup #
Fixes Lottie/Bodymovin exports that crash, freeze or misrender in the
lottie Flutter package: malformed
layers/assets/masks/shape content/keyframes, expressions that lottie
doesn't execute (loopOut()/loopIn(), wiggle(), random(),
time-based motion, cross-layer links), auto-oriented layers it sets
spinning, and layer effects it ignores.
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. - Stops mid-playback
Missing values for keyframe.crashes — an animated property left with a keyframe that has no end value and no next keyframe to borrow one from (typically a lone keyframe withoute) throws the first timelottiedraws it, i.e. only once its layer comes into view. Such a keyframe becomes a hold keyframe, keeping its value, as in After Effects. - 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. - Stops auto-oriented layers spinning —
lottieimplements After Effects' Auto-Orient ("ao": 1) but hands the direction of travel toMatrix4.rotateZin degrees where it takes radians, so a layer following a curved motion path spins round and round instead of banking along it (and the layer's own rotation is ignored meanwhile). This bakes the orientation After Effects shows — the motion path's tangent, honoring each segment's easing, plus the layer's own rotation — into plain rotation keyframes and turns auto-orient off. - Removes layer effects
lottieignores — Expression Controls (Slider/Angle/Checkbox Control...) and effects it can't draw (Brightness & Contrast, Tint...) — which otherwise put "Lottie doesn't support layer effects" in the composition'swarnings. Gaussian Blur and Drop Shadow, whichlottiedoes render, are kept. - 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.2.0
Only using the CLI? Add it under dev_dependencies
instead, so it stays out of your app:
flutter pub add --dev lottie_fixup
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. Run the CLI with dart run from a
Flutter project that has lottie_fixup in its dependencies or
dev_dependencies:
dart run lottie_fixup diagnose assets/animations/*.json # report only, no changes
dart run lottie_fixup fix assets/animations/*.json # fix in place
It can't be installed with dart pub global activate: the package depends on
the Flutter SDK (for fixupLottieDecoder), and pub doesn't run global
executables from such packages.
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
dart run 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. - Auto-orient on a 3D layer (
"ddd": 1), which After Effects orients in 3D, or on a layer whose position/rotation still has an expression this package couldn't bake, is reported (FixResult.autoOrient.skippedLayers) rather than baked.
Additional information #
If this package saved you a debugging session, consider buying me a coffee.
