text_morph_flutter 1.1.0 copy "text_morph_flutter: ^1.1.0" to clipboard
text_morph_flutter: ^1.1.0 copied to clipboard

Shape-level text morphing animations for Flutter, built on glyph_path and glyph_path_flutter — animates one string's glyph outlines into another's.

text_morph_flutter #

Shape-level text morphing for Flutter: instead of cross-fading, Morph animates one string's glyph outlines into another's (or into an arbitrary vector shape), built on top of glyph_path and glyph_path_flutter.

pub package pub points CI License: BSD-3-Clause

Demos #

Two things to notice across the demos below: glyphs morph as vector outlines instead of cross-fading, and the same engine drives text→text, text→shape, and shape→shape alike.

Text ↔ shape #

The clearest look at what sets this apart from a typical text transition.

Word to icon — ShapeSource.fromPath morphing a multi-contour shape with a hole

Word → icon — text morphing into a multi-contour shape with a hole, via ShapeSource.fromPath.

App logo reveal — shape-cloud burst into a large ShapeSource mark

App logo reveal — a shape-cloud burst settling into a large ShapeSource mark. A template for a splash screen or empty-state flourish.

Everyday UI states #

Drop-in replacements for interactions you're already building.

Send to Sending to done — text morphing into a checkmark shape

Send → Sending → done — the state change itself becomes the animation, morphing the label straight into a checkmark instead of swapping icons.

Dashboard ticker — only the digits that change animate

Dashboard ticker — only the digits that actually changed animate (MorphCache), so a refreshing number doesn't flicker.

Search suggestions — a shared prefix stays put while the rest morphs in

Search suggestions — the shared prefix stays put; only the diverging suffix morphs in as the query changes.

Quote carousel — multi-line reflow across a changing line count

Quote carousel — line count changes between quotes, and the paragraph reflows instead of jump-cutting.

Getting oriented #

Onboarding walkthrough — multi-line TextSource, textAlign, color morph

Onboarding walkthrough — start here to see multi-line TextSource, textAlign, and color morph working together before diving into the API.

All seven demos live in example/ — see Running the example app to try them yourself.

Installation #

flutter pub add text_morph_flutter

Or add to pubspec.yaml manually:

dependencies:
  text_morph_flutter: ^1.0.0

Morph renders text via a glyph_path Font, so most apps also want glyph_path directly (to parse font bytes into a Font) — flutter pub add glyph_path if it isn't already a dependency.

Quick start #

import 'package:flutter/material.dart';
import 'package:glyph_path/glyph_path.dart';
import 'package:text_morph_flutter/text_morph_flutter.dart';

class Example extends StatefulWidget {
  const Example({super.key, required this.font});
  final Font font;

  @override
  State<Example> createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  String _text = 'Hello';

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _text = _text == 'Hello' ? 'World' : 'Hello'),
      child: SizedBox(
        width: 300,
        height: 120,
        child: Morph(
          font: widget.font,
          target: TextSource(font: widget.font, text: _text),
          fontSize: 64,
        ),
      ),
    );
  }
}

font is a glyph_path Font — parse one once (e.g. from a loaded font asset's bytes via Font.parse) and reuse it; see glyph_path's own docs for details on loading fonts.

Changing target (here, via setState) triggers the morph animation automatically — Morph diffs the new target against what's currently displayed and animates between them.

Key concepts #

  • MorphSource — a morph endpoint. target (and, implicitly, whatever was displayed before it) is one. Two implementations ship today:
    • TextSource — renders a string in a Font. text may contain explicit \n line breaks, and maxWidth additionally word-wraps against a fixed width — both flow through the same per-glyph alignment/ stagger/caching pipeline as single-line text. rtlScript opts a pure, non-cursive right-to-left string (e.g. Hebrew) into mirrored glyph order and punctuation — see Known limitations for exactly what it does and doesn't cover.
    • ShapeSource — renders an arbitrary vector shape (a list of glyph_path Contours, e.g. parsed from SVG path data or built by hand) fitted to the surrounding text's size. target can switch between TextSource and ShapeSource freely — morphing text into a shape and back uses the exact same mechanism as morphing text into different text. ShapeSource.fromPath builds one directly from a dart:ui Path instead, sampling it via PathMetric since Path doesn't expose the drawing commands it was built from.
  • MorphOptions — tuning knobs layered under MorphStyle and stagger; every field defaults to a value that reproduces the look as if the option didn't exist. See MorphOptions reference below for what each one does.
  • TextMorph / PathMorph — the lower-level, widget-free geometry engine Morph is built on, exported for callers who want to drive their own CustomPainter instead of using the widget directly.
  • Morph.textAlign — horizontal alignment (left/right/center; start/end resolve against the ambient Directionality, and justify falls back to start, matching how Text itself treats a single/last line). For multi-line TextSource content, the same value also controls how the lines align relative to each other, so a paragraph's internal alignment always matches how the whole block is placed in the box.
  • MorphCache — an opt-in, cacherine-backed cache for PathMorph's most expensive per-transition work (contour pairing/alignment). Pass one instance to Morph.cache (or TextMorph.between's cache parameter) when your content replays the same glyph-pair transitions often — a dashboard numeric ticker (the same digit-to-digit transitions recur every tick) or a search-suggestion list (the same candidate substrings recur as a query is edited back and forth) are the two cases it's aimed at. Construct and reuse one instance the same way you reuse a Font. null (the default) never caches.

MorphOptions reference #

Two enums it builds on, first:

  • MorphStyleshape (always morph outlines, the default look), crossFade (never morph outlines; fade in place instead), or auto (shape-morph glyph pairs similar enough per the two thresholds below, cross-fade the rest).
  • GlyphAlignment (MorphOptions.alignment) — how glyphs from the two strings are paired when they don't correspond one-to-one: byIndex pairs purely by position; diff (the default) minimizes pop-in/out glyphs and prefers keeping identical characters in place; wholePath skips per-glyph pairing entirely and matches the two strings' contours as one pool by area rank — useful for text↔complex-shape morphs (see the "word → icon" demo above), at the cost of stagger having no effect.
Field Default Effect
staggerCurve Curves.linear Distributes per-glyph start times across the string when TextMorph.stagger > 0. Must be a const Curve — a freshly-built non-const curve breaks MorphOptions's value-equality change detection.
contourTimingOffset 0.0 (-1..1) Shifts a glyph's hole/counter contours' timing relative to its solid contour's. Positive delays holes so the outer silhouette settles first; negative has holes settle first.
holeAreaRatioThreshold 0.02 (0..1) Minimum area (relative to the glyph's largest contour) for a nested opposite-winding contour to count as a real hole/counter rather than a hinting artifact.
dissimilarityThreshold 0.6 (0..1) Under MorphStyle.auto: the fraction of a pair's contours allowed to end up unmatched (grow/shrink from a point) before falling back to cross-fade. Measures pairing coverage only, not shape similarity.
matchedShapeDissimilarityThreshold null (0..1) Under MorphStyle.auto: an additional check on how different a pair's matched contours actually look (silhouette overlap once centered/normalized). Opt-in — catches cases like "O" -> "L" that dissimilarityThreshold alone can't, since both sides have one contour each.
lengthMismatchFallback null (int) If the "from"/"to" glyph counts differ by at least this many, the whole transition renders as MorphStyle.crossFade regardless of the requested style.
useAreaCorrectedPop true Unmatched contours grow/shrink from a point at a constant rate of visible-area change, instead of a plain vertex lerp (which pops in suddenly near the end).
alignment GlyphAlignment.diff See GlyphAlignment above.
idlePulseAmount 0.0 A uniform scale "breath" applied to every glyph (1 + amount * sin(pi * t)), so glyphs diff leaves shape-unchanged still get shared motion instead of sitting frozen. 0.050.1 is a subtle pulse.
selfWobbleDetour 0.0 Warps every matched contour pair into a genuinely different wavy shape at the animation's midpoint before settling into to — unlike idlePulseAmount, this isn't just a scaled copy. 0.10.3 is already a strong wobble.
selfWobbleSeed 0 Seeds the wobble's random lobe count/phase per contour pair, so glyphs don't all trace the identical wobble. Vary it (e.g. a counter incremented per transition) to avoid replaying the same pattern every time.

API Reference #

Type Description
Morph The widget — a StatefulWidget that animates a shape-morph transition whenever its target changes.
MorphSource Abstract base for a morph endpoint (text or shape); resolves into positioned glyph-like elements plus a reference size/label.
TextSource A MorphSource rendering a string in a glyph_path Font, with optional word-wrap (maxWidth) and RTL mirroring (rtlScript).
ShapeSource A MorphSource for one arbitrary vector shape, built from Contours or sampled from a dart:ui Path via ShapeSource.fromPath.
MorphOptions Configuration bundle for TextMorph/Morph — see the reference table above.
MorphStyle Enum selecting whether glyphs render as direct outline morphs, cross-fades, or a similarity-based mix of both (auto).
GlyphAlignment Enum for how "from" glyphs pair with "to" glyphs when string lengths differ — byIndex/diff/wholePath.
TextMorph The core, widget-free engine interpolating a whole string's/shape's glyph outlines between two MorphSources; exposes pathAt/frameAt per animation t.
PathMorph Builds/holds the contour pairing and interpolation state for morphing one glyph outline into another.
MorphFrame Result of TextMorph.frameAt(t) — a combined shapePath for directly-morphed glyphs plus a crossFades list.
CrossFadeGlyph One glyph slot's cross-fade at a frame — fromPath/toPath (positioned outlines) and blend alpha (0..1).
MorphCache Opt-in, cacherine-backed cache of built PathMorphs, reused across repeated transitions between the same glyph pairs.

Running the example app #

cd example
flutter run -d macos   # or chrome, ios, etc.

main.dart's app bar links out to every demo shown above, plus a free-form playground (presets, alignment modes, idle pulse/wobble sliders, shape morphing).

Known limitations #

  • LTR text order only, plus opt-in mirroring for pure RTL scripts. Glyph order follows the font's left-to-right advance widths by default. TextSource.rtlScript reverses glyph order and swaps paired punctuation (()/[]/{}/<>/«»/‹›) for a line — correct for a pure, non-cursive RTL script like Hebrew — and TextSource.embedLtr lets a substring (e.g. a Latin brand name) keep its own glyph order within an RTL line, though the caller must mark it manually (no automatic script detection, no nesting). This isn't full Unicode Bidi (UAX #9) reordering or contextual shaping, so cursive/joining scripts like Arabic, Farsi, or Urdu — which need each letter substituted with its own initial/medial/ final/isolated form — aren't supported.

  • Accessibility. Morph exposes its current target's text as a Semantics label (or an explicit semanticLabel for a ShapeSource), respects the ambient MediaQuery.textScaler, and honors MediaQuery.disableAnimations ("reduce motion") by completing a transition in a single frame. An opt-in announceChanges flag sets Semantics.liveRegion for content that changes off-focus. The highContrastColor/highContrastStrokeColor/highContrastStrokeWidth fields, when set, replace color/strokeColor/strokeWidth while MediaQuery.highContrast is on; they default to null since Morph has no way to judge a given color's contrast on its own.

  • Performance scales with contour/vertex count. An actively transitioning contour re-runs its cubic lerp, winding computation, and path-boolean compositing from scratch every frame — fine for the short strings and moderate shape complexity typical UI morphs involve, but a very long wholePath string or high-vertex ShapeSource has no incremental-update path for whatever is genuinely mid-transition. Settled endpoints (a glyph done with its stagger window, a contour pair settled at 0/1) are cached and reused instead of recomposited each frame, and a PathMorph's one-time construction cost can itself be cached across repeated transitions via MorphCache. A few internal ceilings also bound the worst case for inputs well past typical use, falling back to a cheaper approximation once exceeded:

    Ceiling Bound Fallback once exceeded
    TextSource UTF-16 code units laid out 4096 Excess characters (and any dangling surrogate/combining mark at the cut point) are dropped rather than laid out.
    Glyphs per side, per-glyph slot alignment (stagger/GlyphAlignment.byIndex/.diff) 2000 Falls back to GlyphAlignment.wholePath instead of building per-glyph slots.
    Glyphs per side, GlyphAlignment.diff 400 Skips the O(n·m) edit-distance alignment and pairs by index (byIndex) instead.
    Contours per pool, hole classification 300 Skips the O(n²) nesting test; every contour in the pool is treated as solid.
    Contours per role group, proximity match 200 Skips the O(m·n) cost-matrix search and pairs purely by area rank instead.
    Matched pairs per role group 2000 Excess contours (however many more a pool has) fall back to the same cheap grow/shrink-from-centroid treatment an outright from/to count mismatch already gets, instead of each paying full per-pair alignment and silhouette-dissimilarity cost — the two ceilings above only cheapen how a large pool is searched, not how many pairs a search of it produces.
    Vertices per contour 2000, shrinking (to a floor of 8) as contour count on either side rises past 200 Decimates the contour to a lower-fidelity, straight-segment version with the same winding. Scaling the per-contour cap down keeps total alignment cost (pool size × cap²) bounded even for a pool with many high-vertex contours, which the count-based ceilings above don't limit on their own.
    Sampled vertices per sub-path, ShapeSource.fromPath 2000 Widens the effective sample spacing past what was requested, instead of letting vertex count grow further.
    Sub-paths sampled, ShapeSource.fromPath 500 Drops remaining sub-paths instead of letting total sampled vertices grow without limit.

See CHANGELOG.md for the full history of features and fixes.

Contributing #

After cloning, flutter pub get sets up all dependencies, including the example app's own (cd example && flutter pub get).

# 1. Format the code
dart format lib test example

# 2. Run static analysis
flutter analyze --fatal-infos

# 3. Run the test suite (with coverage)
flutter test --coverage

This mirrors what CI (.github/workflows/ci.yml) checks on every pull request. Feel free to open an issue or submit a pull request for any suggestions or bug fixes.

License #

BSD 3-Clause — see LICENSE.

Dependencies #

Package License Notes
glyph_path BSD-3-Clause Runtime dependency — font parsing and glyph outline generation.
glyph_path_flutter BSD-3-Clause Runtime dependency — converts glyph_path output into dart:ui Paths.
cacherine MIT Runtime dependency — backs MorphCache.

flutter_test is a dev-only dependency (bundled with the Flutter SDK) and is not included in the published package.

0
likes
160
points
117
downloads
screenshot

Documentation

API reference

Publisher

verified publishercrossapplication.members.co.jp

Weekly Downloads

Shape-level text morphing animations for Flutter, built on glyph_path and glyph_path_flutter — animates one string's glyph outlines into another's.

Repository (GitHub)
View/report issues

Topics

#animation #text #transition #shape #vector-graphics

License

BSD-3-Clause (license)

Dependencies

cacherine, flutter, glyph_path, glyph_path_flutter, meta

More

Packages that depend on text_morph_flutter