text_morph_flutter 1.0.0 copy "text_morph_flutter: ^1.0.0" to clipboard
text_morph_flutter: ^1.0.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.

Changelog #

All notable changes to this project will be documented in this file.

This project adheres to Semantic Versioning.

1.0.0 #

Initial release.

Core #

  • Morph, a widget that animates one string's glyph outlines into another's — or into an arbitrary vector shape — instead of cross-fading. Changing target (e.g. via setState) diffs it against whatever is currently displayed and animates between them, continuing smoothly from the on-screen shape if a new target/style/stagger/options/ fontSize/font change interrupts a transition already in flight.
  • TextMorph / PathMorph, the lower-level, widget-free geometry engine Morph is built on, exported for callers who want to drive their own CustomPainter.
  • MorphSource, a morph endpoint, with two implementations:
    • TextSource — renders a string in a glyph_path 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. TextSource.rtlScript opts a pure, non-cursive right-to-left string (the realistic case: Hebrew) into mirrored glyph order and paired-punctuation swapping (()/[]/{}/<>/«»/‹›) — see Known limitations for exactly what this does and doesn't cover.
    • ShapeSource — renders an arbitrary vector shape (a list of glyph_path Contours) fitted to the surrounding text's size, so target can switch between TextSource and ShapeSource freely using the same underlying mechanism. ShapeSource.fromPath builds one directly from a dart:ui Path by sampling it via PathMetric, since Path doesn't expose the drawing commands it was built from.
  • MorphOptions, tuning knobs layered under MorphStyle and stagger (contour timing offset, hole-area threshold, dissimilarity thresholds, alignment strategy, idle pulse, wobble — see the README's MorphOptions reference for the full field list) — value-comparable (==/hashCode) so passing a freshly-built but equal instance doesn't interrupt an in-progress transition.
  • GlyphAlignment, how glyphs from the two strings are paired when they don't correspond one-to-one: byIndex (purely positional), diff (the default — minimizes pop-in/out and keeps identical characters in place), or wholePath (pools both strings' contours by area rank, skipping per-glyph pairing entirely — useful for text↔complex-shape morphs, at the cost of stagger having no effect).
  • MorphStyle, shape (always morph outlines), crossFade (never morph outlines, fade in place instead), or auto (shape-morph glyph pairs similar enough per dissimilarityThreshold and the optional matchedShapeDissimilarityThreshold, cross-fade the rest).

Bidi text #

  • TextSource.embedLtr, for marking a substring of rtlScript text as an embedded left-to-right run (e.g. a Latin brand name or phone number inside an otherwise-RTL sentence) — its own glyph order stays intact while it still takes its place in the surrounding line's mirroring, the same as a real bidi engine's left-to-right isolate. Wraps the text in the Unicode left-to-right isolate pair (U+2066/U+2069), which TextSource strips back out before layout.

Styling & layout #

  • Morph.color/strokeColor/strokeWidth morph over the same duration/timeline as the shape, including continuing correctly from the actual on-screen color/stroke if a change interrupts one already in flight, and fading smoothly in/out when strokeColor changes to/from null. Morph.paintBuilder/strokePaintBuilder let a caller build its own Paint (gradients, shadows) that morphs the same way, given (Rect bounds, double t).
  • Morph.textAlign — horizontal alignment (left/right/center; start/end resolve against the ambient Directionality, justify falls back to start). For multi-line TextSource content, the same value also controls inter-line alignment, so a paragraph's internal alignment always matches how the whole block is placed in its box. Defaults to TextAlign.center.
  • TextMorph.between's fromFontSize parameter lets from resolve at a different size than to, so a target/fontSize change animates the size alongside the shape instead of the two only ever being compared at one shared size.
  • Morph respects the ambient MediaQuery.textScaler, the same way Text does.

Accessibility #

  • Morph reports its current target's text as a Semantics label (a ShapeSource target can supply one explicitly via semanticLabel, exposed the same way as TextSource.semanticLabel).
  • Morph honors MediaQuery.disableAnimations ("reduce motion"): a transition still completes, but in a single frame instead of animating over duration.
  • Morph.announceChanges, an opt-in flag (Semantics.liveRegion) for a target change that happens off-focus, e.g. a live search preview.
  • Morph.highContrastColor/highContrastStrokeColor/highContrastStrokeWidth, opt-in overrides for color/strokeColor/strokeWidth that take effect while the platform's "increase contrast" setting (MediaQuery.highContrast) is on — including a runtime flip of the setting itself, morphed the same way a plain color change is. highContrastStrokeColor alone can add an outline under high contrast to a shape that's otherwise strokeless.

Performance & caching #

  • MorphCache, an opt-in, cacherine-backed cache for PathMorph's one-time contour pairing/alignment cost, reused across repeated transitions between the same glyph pair(s) under the same MorphOptions — aimed at high-churn content like a dashboard numeric ticker or a search-suggestion list. Pass one to Morph.cache or TextMorph.between's cache parameter; the default (null) never caches. Internally split into two independently-sized pools — one for per-glyph entries (maxSize, default 64), one for whole-transition ("wholePath") entries (maxWholePathSize, default 8) — since the two hold entries of very different weight (per-glyph entries scale with character variety rather than candidate count, while wholePath entries are far heavier per transition); filling one pool no longer evicts entries from the other. The wholePath pool isn't limited to an explicit GlyphAlignment.wholePath: any transition TextMorph can't align by glyph (e.g. a ShapeSource on either side) or silently downgrades for being too long also lands there. MorphCache.perGlyphSize/wholePathSize report each pool's count on its own, since the existing size (now a combined total) can no longer answer "is my per-glyph pool sized correctly" once a cache holds both kinds of entry. The wholePath pool additionally bounds its combined estimated weight, not only its entry count: maxWholePathBytes (default 256 * 1024 * 1024, calibrated against measured process memory growth for real wholePath content — see estimateWholePathBytes's doc) evicts least-recently-used entries once a rough, deliberately approximate per-entry weight estimate — scaled from the transition's total input contour command count, since the built PathMorph's actual ui.Paths are opaque, natively-backed Skia objects with no queryable size — sums past the limit, independent of maxWholePathSize. This directly addresses the risk maxWholePathSize alone couldn't: a handful of unusually heavy wholePath entries (e.g. long strings, or detailed ShapeSources) no longer accumulate unbounded memory just because they're still under the entry-count cap. MorphCache.wholePathBytes reports the pool's current combined weight. MorphCache's single getOrBuild (@internal) is now two separate methods, getOrBuildPerGlyph/getOrBuildWholePath — the latter takes weightBytes as a required positional parameter rather than an optional/defaulted one, so a future wholePath call site can't silently forget it and admit a zero-weight entry that evades maxWholePathBytes entirely.
  • Settled-endpoint caching at two layers, so only geometry still actively transitioning is recomputed each frame: TextMorph caches a glyph slot's positioned outline (plus the union of every currently-settled slot) once its stagger window reaches 0/1; PathMorph caches a contour pair's interpolated outline once its own windowed progress settles, which can happen mid-transition when contourTimingOffset is non-zero.
  • PathMorph's hole-nesting classification checks a cheap bounding-box containment before paying for the O(vertices) ray-cast it used to run unconditionally for every contour pair.
  • Built-in ceilings on glyph count, contour-pool size, per-contour vertex count, and ShapeSource.fromPath's path-sampling density, each falling back to a cheaper approximation once exceeded — so a very long string, a high-vertex-count shape, or a very fine sampleSpacing can't block the UI thread with unbounded O(n·m)/O(n²)/O(n³) work.
  • PathMorph's settled-endpoint contour cache (a contour pinned at 0/1 mid-transition by contourTimingOffset) now also caches the ui.Path and bounds built from that contour, so per-frame compositing (_solidsMinusHoles) reuses them instead of replaying the same settled contour's commands into a fresh Path and re-measuring it every frame.
  • TextMorph's per-frame slot compositing now scopes each Path.combine union to the specific cluster of overlapping slots it belongs to, mirroring PathMorph._solidsMinusHoles's clustering, instead of unioning against one ever-growing accumulator over every slot placed so far — a non-zero stagger used to make this scale superlinearly with glyph count (640 glyphs took 6.3s for a single frame); it now stays close to linear (roughly 280ms for the same case, across a full 61-frame transition).
  • Morph now reads highContrast/disableAnimations via MediaQuery's aspect-scoped accessors (MediaQuery.highContrastOf/ MediaQuery.disableAnimationsOf) instead of MediaQuery.of, which subscribed to every MediaQueryData aspect — an unrelated change (e.g. viewInsets during a keyboard show/hide animation) no longer triggers a rebuild.

Known limitations #

See the README's Known limitations section for the current state of RTL/bidi text support, accessibility coverage, and performance scaling.

1
likes
160
points
119
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