anim_svg 0.0.5 copy "anim_svg: ^0.0.5" to clipboard
anim_svg: ^0.0.5 copied to clipboard

Flutter widget for animated SVG. Transpiles SMIL, CSS @keyframes, and Svgator animations to Lottie JSON, rendered by the native thorvg engine.

anim_svg

Animated SVG for Flutter — transpiled to Lottie, rendered by thorvg.

pub version pub likes platforms Flutter ≥3.24 experimental MIT license

anim_svg turns an animated SVG — SMIL, CSS @keyframes, motion paths — into a Lottie 5.7 document at runtime and hands the result to thorvg, a fast C++ vector + Lottie renderer. It's shipped to Flutter via package:thorvg_plus — a source-built fork of package:thorvg that restores iOS simulator support (see Why thorvg_plus below). Conversion runs entirely inside a native Rust core (anim_svg_core) invoked through dart:ffi.

Experimental (v0.0.4). Public API may change between patch releases. Coverage grows with real-world input — if an SVG renders wrong, open an issue with the file attached and we'll add it to the fixture suite.


Why #

Flutter has no first-class runtime for SMIL- or CSS-animated SVG. Lottie does, and thorvg is a production-grade open-source renderer that already ships it to Flutter. anim_svg bridges the gap: it reads the animated SVG, emits a valid Lottie JSON document, and lets thorvg do what it does best — draw it, fast.

Pipeline #

┌──────────────────────────────────────────┐   ┌────────────────────┐   ┌─────────────────────┐
│  SVG                                     │   │  anim_svg_core     │   │  thorvg             │
│   • SMIL (<animate>, <animateTransform>) │──▶│  (native Rust,     │──▶│  (native C++        │
│   • CSS @keyframes + motion path         │   │   via dart:ffi)    │   │   Lottie renderer)  │
│   • Inline images (data URI)             │   │  → Lottie 5.7 JSON │   │                     │
└──────────────────────────────────────────┘   └────────────────────┘   └─────────────────────┘

The Rust core streams every stage (parse → map → serialize) through a structured log envelope, so nothing produced by the pipeline is dropped silently. See ADR-024 for the design rationale.

Supported platforms #

Platform Status Notes
iOS 13+ arm64 device + simulator; static xcframework built from Rust
Android 24+ arm64-v8a, armeabi-v7a, x86_64, x86; built via cargo-ndk + CMake
macOS / Linux / Windows not attempted yet — contributions welcome
Web not attempted yet — would require a WASM build of the Rust core

Install #

flutter pub add anim_svg

Why we depend on thorvg_plus instead of thorvg #

Upstream thorvg: ^1.0.0 on pub.dev ships a device-only libthorvg.dylib for iOS, so any consumer building for the iOS simulator hits a linker error (tracked at thorvg.flutter#22). dependency_overrides work only in the root app, not transitively from a plugin — so anim_svg can't fix this for its consumers by overriding upstream thorvg internally.

Instead we depend on thorvg_plus, a source-built fork published from this same repository. thorvg_plus compiles ThorVG per-platform (no prebuilt dylib at all), so the right architecture slice is always produced. The Dart API matches upstream byte-for-byte — only the native build pipeline differs.

We will migrate back to upstream thorvg as soon as #22 is resolved and a fixed version reaches pub.dev.

Native binaries #

anim_svg ships a Rust core that targets iOS and Android. The published package does not embed prebuilt binaries (they'd blow past pub.dev's 100 MB limit). Instead:

  1. Default path (zero setup). On first pod install / Gradle build, the plugin's prepare_command downloads prebuilt artifacts for the current plugin version from GitHub Releases and verifies them via SHA256. No Rust toolchain required.
  2. Fallback path (source build). If the download fails (offline, corporate firewall, missing release asset) the plugin falls back to building from the Rust source that ships with the package. See the Building from source section below for toolchain requirements.

Environment variables:

Variable Effect
ANIM_SVG_SKIP_DOWNLOAD=1 Skip the remote fetch, go straight to local build. Useful behind corporate proxies.
FORCE_RUST_REBUILD=1 Ignore any cached or downloaded artifacts and rebuild from source.
ANIM_SVG_RELEASE_BASE_URL Override the release host (for mirrors / self-hosting).

Building from source #

Only needed if the download path is disabled or unreachable. Requires a working Rust toolchain (rustup.rs).

Android: install the NDK via Android Studio and make sure cargo-ndk is on your PATH (cargo install cargo-ndk). Gradle invokes it automatically during build.

iOS: install Xcode, then add the iOS Rust targets once:

rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios

CocoaPods runs the Rust build script during pod install.

Swift Package Manager (iOS, AnimSvgView.network only) #

AnimSvgView.network depends on flutter_cache_manager, whose transitive path_provider_foundation needs Swift Package Manager to link its ObjC runtime on iOS. Enable SPM once per machine; Flutter then integrates SPM into your Xcode project automatically alongside CocoaPods:

flutter config --enable-swift-package-manager

Without this flag iOS will crash at startup with Couldn't resolve native function 'DOBJC_initializeApi' the first time AnimSvgView.network is built. You don't need SPM if you only use AnimSvgView.asset / .string.

Usage #

Widget #

import 'package:anim_svg/anim_svg.dart';

AnimSvgView.asset(
  'assets/sticker.svg',
  width: 300,
  height: 300,
  controller: AnimSvgController(),
);

In-memory SVG:

AnimSvgView.string(svgXml, width: 240, height: 240);

Remote SVG (cached on disk for 7 days):

AnimSvgView.network(
  'https://example.com/sticker.svg',
  width: 300,
  height: 300,
  fit: BoxFit.contain,         // default; same semantics as Image
  alignment: Alignment.center, // default
  loadingBuilder: (ctx) => const CircularProgressIndicator(),
  errorBuilder: (ctx, err, stack) => const Icon(Icons.broken_image),
);

fit / alignment are accepted by every factory (.asset, .string, .network) and behave the same way as on Image — the rendered Lottie surface is wrapped in a FittedBox because thorvg itself paints at 1:1.

Parameters #

The same parameter set is accepted by all three factories (.asset / .string / .network); only the first positional arg — the source — differs. Everything else is keyword-only.

Source (per factory)

Factory First arg Type What it loads
AnimSvgView.asset(path) assetPath String Bundled asset path ('assets/foo.svg'). Resolved via rootBundle.
AnimSvgView.string(svg) svgXml String Raw SVG XML already in memory. For runtime-generated or fetched-by-yourself sources.
AnimSvgView.network(url) url String HTTP(S) URL. Converted Lottie cached on disk for 7 days; replays skip both network and FFI.

Layout (required)

Param Type What it does
width double Logical width of the widget. For portrait sources, drives lateral padding of the render buffer.
height double Logical height. Drives effective rasterization cost (∝ height² for portrait sources) — capping height is the single most effective lever for fitting more animations on screen at 60 FPS. See Performance tuning below.

Layout (optional)

Param Type Default What it does
fit BoxFit BoxFit.contain How the rendered Lottie surface fits inside width × height. Same semantics as Image.
alignment AlignmentGeometry Alignment.center Where to position the surface inside its box when fit leaves slack. Same semantics as Image.

Playback

Param Type Default What it does
animate bool true If false, the engine paints frame 0 once and never advances. Useful for hero stills and golden-snapshot tests.
repeat bool true If false, the animation plays once and freezes on the last frame.
controller AnimSvgController? null Programmatic handle for play() / pause() / seek(progress). Note: thorvg 1.0 has no native pause/seek primitives — pause() currently no-ops with a warning, kept as a forward-compat hook.
startDelay Duration? null Delays mounting of the thorvg engine. Use to stagger initial tvg.load calls across many list items in the same frame — a common cause of first-frame jank with 8+ visible animations. Set index * 20.ms in your itemBuilder. While pending, the widget renders its loadingBuilder.

Performance

Param Type Default What it does
renderScale double 1.0 Multiplier on width / height when sizing the native render buffer. 1.0 renders at logical pixels (softer on high-DPR but cheap); device DPR (2.0–3.0) is crisp but ~scale²× more CPU per frame. Full deep-dive: Performance tuning: renderScale below.
disposeWhenInvisible bool true Tear down the native handle once the widget is fully off-screen for disposeDelay, re-create after showDelay of returning. Full deep-dive: Off-screen disposal below.
disposeDelay Duration Duration(milliseconds: 700) Debounce: how long to wait after invisibility before tearing down. Lower = reclaim sooner; higher = mask flap during fast scrolls.
showDelay Duration Duration(milliseconds: 150) Symmetric debounce: how long to wait after returning to visibility before re-creating. Lower = snappier re-show; higher = avoid native creates for fleeting glances.

Builders (fallback UI)

Param Type Default When it renders
loadingBuilder WidgetBuilder? centered CircularProgressIndicator While the source is loading or converting. In practice only the .network path reaches this — assets resolve fast enough that the loading state usually doesn't render.
placeholderBuilder WidgetBuilder? neutral grey "no renderable content" box Conversion succeeded but produced zero renderable layers — e.g. an SVG made entirely of filter primitives or features we don't yet support.
errorBuilder (ctx, err, stack) → Widget? broken-image icon Conversion or load failed. The error and stack trace are passed in so callers can surface their own diagnostics.

Diagnostics

Param Type Default What it does
logger AnimSvgLogger? DeveloperLogger() Receives every pipeline event (load, convert, network, engine, visibility, hide/show), each tagged with [anim_svg] [stage]. Pass PrintLogger() for raw stdout, or your own implementation for telemetry — see Debugging below.
onLottieReady (Uint8List) → void? null Called once the converter produces Lottie JSON bytes. Paste the bytes into lottiefiles.com/preview to isolate render bugs from conversion bugs.

.network-only

Param Type Default What it does
cacheManager BaseCacheManager? LottieCacheManager.instance Override the default cache (TTL, capacity, location). See Networking & caching below.
loader NetworkSvgLoader? injected default Inject a custom HTTP loader. Useful for tests, mocks, or non-http transports.

Performance tuning: renderScale #

The thorvg renderer is software-only (CPU SwCanvas), so rasterization cost scales with the rendered pixel count. Every factory accepts an optional renderScale (default 1.0) — a multiplier applied to logical width / height when sizing the native render buffer.

AnimSvgView.network(
  url,
  width: 300,
  height: 300,
  renderScale: 1.0, // default: render at logical pixels — cheapest
);

AnimSvgView.network(
  url,
  width: 300,
  height: 300,
  renderScale: 2.0, // crisper on retina, ~4× more rasterization cost
);
renderScale When to use
1.0 (default) Many simultaneous animations, scrollable lists, low-end Android. Output is upscaled by Flutter's compositor; expect visible softness on high-DPR screens.
1.52.0 Hero animations, single visible item, high-DPR target. Costs ~2.25–4× more CPU per frame than the default.
device DPR (≈ 2.5–3.0) Crispest output. Only feasible for one or two simultaneous animations on modern hardware.

The native render path runs on a single shared producer thread (Android HandlerThread, iOS DispatchQueue) so the Flutter UI isolate is never blocked, but lifting renderScale past what the producer thread can sustain causes dropped frames in the Texture compositor — animations stutter visually even though the UI thread stays at 60 FPS. When in doubt, profile.

How width and height actually affect cost

Worth knowing for portrait-source SVGs (most slot-machine and mobile-app art): thorvg scales the source by height / source_height, leaving lateral padding on either side when width is wider than source_width × scale. height therefore drives effectively all rasterization cost (∝ height² for square widget bounds); changing width only resizes the side padding of the buffer.

This means:

  • Match width to (source_aspect × height) to avoid wasted padding.
  • Capping height is the single most effective lever when you need to fit more animations on screen at 60 FPS.

Off-screen disposal #

Each mounted AnimSvgView holds a thorvg scene tree, an RGBA frame buffer (renderScale² × W × H × 4 bytes — ≈720 KB for a 300×300 tile at renderScale: 2.0), and a platform texture surface. Inside long lists this adds up quickly.

By default AnimSvgView watches its own viewport visibility and tears down the native handle once the widget has been fully off-screen for disposeDelay, then re-creates it after showDelay of returning to visibility — symmetric debounce that keeps fast scrolls from churning native resources.

AnimSvgView.network(
  url,
  width: 300,
  height: 300,
  // Defaults shown here for clarity; you can omit them.
  disposeWhenInvisible: true,
  disposeDelay: const Duration(milliseconds: 700),
  showDelay: const Duration(milliseconds: 150),
);

On Android the texture lifecycle is driven by TextureRegistry.createSurfaceProducer (since thorvg_plus 1.1.0). On API 28+ this is backed by ImageReader / HardwareBuffer, so create/destroy cycles stay cheap even under sustained fast-scroll workloads. On API < 28 the engine transparently falls back to SurfaceTexture — fine for typical use, but very long fast-scroll sessions on those older devices can in principle hit the legacy BufferQueue fence-FD pressure documented in flutter/flutter#94916. If you observe FD growth in /proc/<pid>/fd during long scrolls on those targets, opt out via disposeWhenInvisible: false.

Tuning:

Knob When to change
disposeWhenInvisible: false Keep the native handle alive while the widget stays mounted. Useful for tests, golden snapshots, tightly-scoped lists where every item is on-screen at rest, or as a safety opt-out on Android API < 28 (see above).
disposeDelay (lower, e.g. 200 ms) Memory-constrained device or very long lists — reclaim sooner at the cost of more re-create churn during scrolling.
disposeDelay (higher, e.g. 1500 ms) User scrolls the list back and forth a lot; mask the small re-mount cost behind a longer grace window.
showDelay (lower, e.g. 0 ms) Fewer than ~10 simultaneous animations and you want the snappiest re-show; you accept paying one MethodChannel round-trip per fleeting scroll glance.

Limitations:

  • Visibility is detected geometrically against the viewport. Items obscured by a Stack overlay in the same layer tree are NOT considered invisible — the overlay covers them on screen but they're still painted underneath. If your UI flips between full-screen pages with a Stack, wrap the underlay branch in Offstage or Visibility(visible: false) at the call site to take it out of the tree entirely; that triggers our normal unmount path.
  • TabBarView inactive tabs are handled — we treat TickerMode.of(context) == false the same as zero visibility.
  • Re-showing pays roughly one MethodChannel create round-trip plus thorvg's per-instance native init. The Lottie JSON bytes are reused from the outer State, so no re-conversion happens.

Direct conversion (no widget) #

import 'package:anim_svg/anim_svg.dart';

final converter  = ConvertSvgToLottie();
final lottieMap  = converter.convertToMap(svgXml);   // Map<String, dynamic>
final lottieJson = converter.convertToJson(svgXml);  // String

Networking & caching #

AnimSvgView.network(url) runs a tiny three-step pipeline:

  1. Cache lookup. Ask LottieCacheManager (a flutter_cache_manager instance) for an entry keyed by the URL string. If a fresh entry exists, its bytes are returned immediately — no HTTP, no FFI.
  2. HTTP GET. On a miss, fetch the SVG with package:http. Non-200 responses raise NetworkSvgException(url, statusCode: …) and are logged at error via the active AnimSvgLogger (defaults to DeveloperLogger).
  3. Convert + store. Feed the SVG body to the Rust core (ConvertSvgToLottie) and write the resulting Lottie JSON into the cache.

What the cache stores and where:

Property Value
Payload the converted Lottie JSON (not the raw SVG) — replays skip both the network and the FFI converter
Key the URL string (one cached file per unique URL)
TTL 7 days (Config.stalePeriod)
Capacity 200 entries, LRU eviction
Location platform getTemporaryDirectory() — managed by flutter_cache_manager
Store key anim_svg_lottie_v2 — bumped if the converter output format changes

Hooks for advanced cases:

// Pre-warm the cache at app start (no widget needed).
final loader = NetworkSvgLoader();
await loader.loadLottieBytes('https://example.com/hero.svg');

// Wipe everything (e.g. on user "Clear cache" action).
await LottieCacheManager.instance.emptyCache();

// Use a custom CacheManager for tests or stricter eviction.
AnimSvgView.network(url, width: 200, height: 200, cacheManager: myManager);

Debugging #

Plug a logger in and every stage of the pipeline streams back into your app's logs — including the network path (network.fetch, network.cache.hit/store) and NetworkSvgException (URL + status):

AnimSvgView.asset(
  'assets/sticker.svg',
  width: 300, height: 300,
  logger: DeveloperLogger(),
  onLottieReady: (bytes) {
    // Paste into https://lottiefiles.com/preview to isolate render vs. conversion issues.
  },
);

DeveloperLogger is the default if no logger is passed, so network failures (DNS, 404, parse) always surface in the anim_svg channel of DevTools / IDE console even with the silent Icon(Icons.broken_image) fallback.

Supported input #

The matrix below reflects what the Rust core actually emits today — no aspirational rows.

SVG elements #

Recognized Behavior
<svg>, <g>, <defs>, <use> <use> resolves local #id refs; recursion depth ≤ 32
<path>, <rect>, <circle>, <ellipse>, <line>, <polyline>, <polygon> Emitted as Lottie shape layers (sh / rc / el)
<linearGradient>, <radialGradient>, <stop> gradientUnits, gradientTransform, focal points, animated stops
<filter> Only the primitives listed in the Filters table below
<mask> Luminance + alpha matte, baked at t=0
<image> Inline data: URI only (see Images)
<style>, <script> Parsed (<style> for CSS); <script> extracted but not executed
<title>, <desc>, <metadata>, <clipPath>, <pattern>, <marker>, <symbol> Silently skipped (decorative or out-of-scope)

SMIL animation #

Element Attributes supported
<animate> attributeName, dur, from / to / by / values, keyTimes, keySplines, calcMode (linear | spline | discrete | paced), repeatCount="indefinite", additive
<animateTransform> All of the above + type (translate, scale, rotate, skewX, skewY, matrix)
<animateMotion> path, values, keyPoints / keyTimes, rotate="auto | auto-reverse | Ndeg"

Not supported: <set>, <mpath>, explicit begin / end offsets.

CSS animation #

  • @keyframes name { 0%/from ... 100%/to ... } including per-keyframe animation-timing-function overrides.
  • Shorthand animation: and every longhand: animation-name, animation-duration, animation-delay, animation-timing-function, animation-iteration-count, animation-direction, animation-fill-mode.
  • Timing functions: linear, ease, ease-in, ease-out, ease-in-out, cubic-bezier(x1,y1,x2,y2), step-start, step-end, steps(n).
  • Selectors: #id, .class, and compound lists (#a, #b, .c).
  • Whitelisted static properties: fill, fill-opacity, opacity.
  • Keyframe properties: transform, opacity, offset-distance, stroke-dashoffset.

Transforms #

All 2D: translate, translateX/Y, scale, scaleX/Y, rotate / rotateZ, skewX, skewY, matrix(a,b,c,d,e,f). translate3d and scale3d are tolerated (z ignored). rotateX, rotateY, rotate3d warn and skip. transform-origin accepts pixel values only — percentages and keywords (center, top) are not yet resolved.

Motion path #

CSS offset-path: path('M…Z'), offset-distance, offset-rotate: auto | reverse | N(deg|rad|turn|grad). SMIL <animateMotion> with path data, values point lists, or keyPoints / keyTimes sampling. ray() and url(#id) motion path forms are not supported.

Gradients #

<linearGradient> and <radialGradient> with gradientUnits (userSpaceOnUse | objectBoundingBox), gradientTransform, and animated <stop> color / opacity. Radial focal points (fx, fy) are respected. Switching fill between gradient IDs at runtime is not supported.

Filters → Lottie effects #

Filter primitive Lottie output
<feGaussianBlur stdDeviation> (animatable) Gaussian Blur (ty:29)
<feComponentTransfer> with R/G/B type="linear" slope="N" Brightness & Contrast (ty:22)
<feColorMatrix type="saturate" values> (animatable) Saturation (approximate — mapping still firming up)
Anything else (feBlend, feOffset, feFlood, feDisplacementMap, feTurbulence, …) ⚠️ skipped with warning

Images #

  • data:image/png;base64,... — passes through verbatim.
  • data:image/jpeg;base64,... — passes through verbatim.
  • data:image/webp;base64,... — decoded with pure-rust image-webp and re-encoded as PNG before handing to thorvg (thorvg 1.0's Flutter build ships PNG/JPG loaders only). On decode failure the original URI is kept, a warning is logged under map.raster, and that asset renders blank — the rest of the document still converts.
  • External href="http(s)://..."not fetched. Conversion fails with UnsupportedFeatureException by design.

Lottie output #

Schema 5.7. Layer types emitted: image (ty:2), null (ty:3), shape (ty:4). Shape items emitted: sh, rc, el, fl, st, gf, tm, tr, gr.

Svgator #

Svgator-exported SVGs embed a <script> tag containing a JavaScript runtime. anim_svg does not execute that script — a static transpiler can't faithfully emulate a JS engine, and partial parsing would produce wrong animation timing more often than right.

If you need to render Svgator output, use Svgator's own Flutter-compatible Dart package — per their docs it ships native Dart support. Happy path: animate everything else with anim_svg, drop Svgator-exported assets through their SDK.

The thorvg.flutter/ directory #

This repo hosts the source of our thorvg_plus fork under thorvg.flutter/. It's published to pub.dev as a separate package (cd thorvg.flutter && dart pub publish) and consumed by anim_svg like any other hosted dependency. See the fork's own README for what was changed and why.

Once upstream thorvg/thorvg.flutter#22 ships a fix, anim_svg will depend on upstream thorvg directly, thorvg_plus will be deprecated, and the thorvg.flutter/ directory will be removed. Huge thanks to the thorvg team — the bug is narrow, the renderer itself is excellent.

Example #

cd example
flutter run

example/ ships six representative fixtures exercising SMIL, CSS @keyframes, gradients, filters, and inline images — a good starting point for eyeballing conversion results.

Contributing #

The single most useful thing you can do: when an SVG breaks, open an issue and attach the file. Every accepted fixture becomes a permanent regression test and usually unlocks a small feature for every other user.

  • Bugs / unsupported features → open an issue with the SVG attached and a one-liner on expected behavior.
  • PRs adding element, SMIL, CSS, or filter mappings are welcome — update the matrix in this README in the same PR.
  • Dev setup: install Rust + cargo-ndk, then tool/prepare_rust.sh ios|android builds the native core. Architecture notes live in brain/adr.md (ADR-024 covers the Rust core).

Acknowledgements #

License #

MIT © 2026 Yeftifeyev Konstantin — see LICENSE.

2
likes
140
points
333
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter widget for animated SVG. Transpiles SMIL, CSS @keyframes, and Svgator animations to Lottie JSON, rendered by the native thorvg engine.

Repository (GitHub)
View/report issues

Topics

#svg #animation #lottie #thorvg #smil

License

MIT (license)

Dependencies

ffi, flutter, flutter_cache_manager, http, image, meta, thorvg_plus, visibility_detector, xml

More

Packages that depend on anim_svg

Packages that implement anim_svg