morphnext 0.7.1
morphnext: ^0.7.1 copied to clipboard
Interruptible, spring-driven vector icon morphing for Flutter without per-pair animation assets.
morphnext #
Morph into what’s next.
morphnext turns bundled Flutter IconData glyphs into interruptible,
spring-driven vector morphs without requiring per-pair animation assets.
Its outline engine is public, so text and other font glyphs
can morph too.

Install #
flutter pub add morphnext
Requires Dart >=3.9.0 and Flutter >=3.32.0.
Implicit morph #
Change icon; AnimatedMorphIcon starts from the shape currently on screen,
even when a new target interrupts an active transition.
import 'package:flutter/material.dart';
import 'package:morphnext/morphnext.dart';
IconButton(
onPressed: () => setState(() => open = !open),
icon: AnimatedMorphIcon(
icon: open ? Icons.close : Icons.menu,
semanticLabel: open ? 'Close menu' : 'Open menu',
),
)
Use onEnd when the next action must wait for the spring to finish and the
target to settle on its final vector frame.
AnimatedMorphIcon uses MorphSprings.snappy by default. Select
MorphSprings.smooth for a critically damped transition, or pass Flutter's
SpringDescription directly for custom physics.
The widgets accept the same visual font controls as Flutter's Icon:
fill, weight, grade, opticalSize, shadows, blendMode, and
fontWeight. Values not supplied directly are inherited from IconTheme.
Controlled morph #
Use any Animation<double> for gestures, scrolling, or a timeline. Progress is
clamped to 0–1.
MorphIcon(
from: Icons.favorite_border,
to: Icons.favorite,
progress: controller,
size: 32,
color: Colors.red,
semanticLabel: 'Favorite',
)
Glyph outlines #
The outline engine is public, so you can morph more than icons. Read any glyph from font bytes by code point and plan a morph between two glyphs:
final font = MorphFont.parse(bytes); // TrueType or CFF
final a = font.glyph(0x61)!; // 'a', in ems about its pen position and baseline
final g = font.glyph(0x67)!; // 'g'
final plan = MorphOutlinePlan(a, g);
final contours = plan.allocateOutput();
plan.interpolate(0.5, contours); // closed polygons, interleaved x and y
Fill the result with the non-zero rule. The baseline is at y = 0, so glyphs
from different fonts, weights, and variable-font instances share one frame.
font.axes lists a variable font's axes, and glyph takes weight, fill,
grade, and opticalSize.
To morph a whole word as one outline, place each glyph at its pen position and
size, then combine them:
final hi = MorphOutline.combine([
h.placed(Offset.zero, scale: 32),
i.placed(Offset(h.advance * 32, 0), scale: 32),
])!;
Nothing is cached, so keep the fonts, glyphs, and plans you reuse. This API is experimental and may change in a minor release before 1.0. morphtext builds text morphing on it.
Font compatibility #
There is no fontAsset argument. IconData identifies a code point, font
family, package, and fallback families—not an asset path. morphnext uses those
fields and Flutter's font manifest to resolve fonts bundled through
pubspec.yaml.
If a font is loaded only at runtime, missing, malformed, or unsupported, morphnext preserves the widget layout and displays regular Flutter icons instead. The UI remains usable, but that transition is not a continuous vector morph.
Flutter conventions #
size, color, opacity, variable axes, shadows, optional text scaling, and
directionality follow IconTheme, MediaQuery, and Directionality like
Flutter's Icon.
matchTextDirection is honored in RTL layouts. Set semanticLabel for one
semantic image node; leave it null for a decorative icon. When
MediaQuery.disableAnimations is true, AnimatedMorphIcon settles immediately.
Performance and caching #
Fonts and morph plans are loaded lazily and cached with bounded memory use. Animation repaints directly without rebuilding the widget on every frame.
The application-wide morph cache has limits similar to Flutter's image cache.
It is shared by morphnext widgets in the current Dart isolate. The defaults
work without configuration. Applications that need explicit limits can set
both before runApp:
MorphCache.configure(
maxMorphs: 100,
maxBytes: 16 * 1024 * 1024,
);
maxMorphs counts completed morphs retained for reuse. A → B and B → A, LTR and RTL, and different font parameters are separate morphs. One-off
morphs from an interrupted intermediate shape are not retained. A morph larger
than maxBytes is still built and used by the current animation, but it is not
cached and does not evict existing entries.
currentMorphs and currentBytes expose the current cache statistics. Pending
morphs are not counted. currentBytes is the size of vector buffers owned by
retained morphs; it excludes decoded fonts, sampled source shapes, Dart object
overhead, and memory owned by Flutter. Use MorphCache.clear() to empty the
cache, MorphCache.disable() to empty and disable it, and MorphCache.reset()
to restore defaultMaxMorphs and defaultMaxBytes. configure throws
ArgumentError unless both limits are positive.
Prepare a known pair before the first interaction, for example from
didChangeDependencies or an event handler:
await precacheMorph(
context,
from: Icons.menu,
to: Icons.close,
bidirectional: true,
);
The helper shares the widgets' bounded caches and inherits the asset bundle,
direction, and font axes from the context. Pass the same textDirection,
fill, weight, grade, opticalSize, and fontWeight overrides as the
widget when applicable. Reverse plans are separate; bidirectional prepares
both. It respects MorphCache limits, clearing and disabling; with caching
disabled no completed plan is retained. Eviction can make later preparation
necessary again. The returned future completes with an error if preparation
fails; catch it if unavailable
fonts are expected. Equal icons require no loading.
Both widgets accept onFallback for diagnostics in debug and release:
AnimatedMorphIcon(
icon: currentIcon,
onFallback: (details) {
debugPrint('Morph to ${details.to} unavailable: ${details.error}');
},
)
The callback receives MorphFallbackDetails with the source, target, original
error, and stack trace. It runs once per failed current preparation request,
replaces default debug error reporting, and ignores superseded requests or
completion after disposal. Native-icon fallback still works and an implicit
transition still calls onEnd when it settles. No callback means the existing
fallback behavior is unchanged.
See the performance guide for geometry benchmarks and a standalone Flutter frame-timing harness.
Limitations #
morphnext preserves filled contour topology and keeps holes open, but arbitrary
icon pairs can still produce an artistically surprising intermediate shape.
The renderer is monochrome and accepts bundled font-backed IconData; it does
not morph arbitrary Flutter Path objects or SVG artwork.
Examples #
example/example.dart is the compact pub.dev example. The app in
example/lib/main.dart powers the GitHub Pages showcase and interactive
playground.
Built with morphnext #
- morphtext morphs text letter by letter, outline into outline. A light word turns bold, a serif word turns sans, and one word becomes another or flows into an icon, using the glyph outline API. Try it in the browser.
Built something with morphnext? Open a pull request to add it here.
License and attribution #
morphnext is MIT licensed. Parts of its geometry behavior are adapted from
MIT-licensed Morphicons, and its font reader is derived from the MIT-licensed
icon_font_generator; exact revisions and notices are in
THIRD_PARTY_NOTICES.md.
This is an independent project. It is not affiliated with or endorsed by the Flutter project or the Morphicons project.