LiquidMorphPhysics class

Pure, stateless math engine for liquid glass morphing animations.

This class implements the J-curve back-out position curve and Curves.linearToEaseOut / smootherstep size curve that together create the iOS 26 liquid teardrop morphing effect.

It is intentionally stateless — feed it the raw animation value plus the source/destination geometry and it returns a fully computed LiquidMorphState for that frame. No BuildContext, no State, no ChangeNotifier dependencies.

Design

Two conceptual blobs drive the morph:

  • Blob A (anchor / ghost trigger) stays at the trigger position and shrinks away over the first 40 % of the animation to cleanly break the liquid bridge.
  • Blob B (menu body) travels from the trigger center to the menu center along a J-curve overshoot trajectory, expanding from trigger size to menu size.

The metaball SDF shader automatically creates the teardrop neck between the two blobs — there is no explicit neck geometry.

Auto-Scaling

When compute receives a scaleDelta greater than 1.5, the engine engages adaptive mode:

  • The J-curve back-out amplitude is reduced inversely with vertical travel so the physical overshoot stays within ~10–14 px regardless of the destination size. This eliminates the 35–40 px "plunge" seen when a large menu or full-sheet morph uses the small-menu constant.
  • The size curve switches from Curves.linearToEaseOut to a smootherstep sigmoid (for scaleDelta > 3.0) that keeps the droplet compact through the detach/travel phases and blossoms rapidly only as it lands.
  • Blend attenuation prevents excessive SDF merging at extreme scale ratios.
  • Push momentum is bounded to a physical maximum so the trigger bounces with native-grade subtlety rather than a jarring multi-pixel jolt.

Usage

// Typically called from an AnimatedBuilder or addListener callback.
final state = LiquidMorphPhysics.compute(
  rawValue: controller.value,
  finalDx: finalDx,
  finalDy: finalDy,
  horizontalOffset: _horizontalOffset,
  verticalOffset: _verticalOffset,
);

// Apply to the UI:
Positioned(
  left: triggerX + state.pushDx,
  top:  triggerY + state.pushDy,
  child: Transform.scale(scale: state.anchorScale, child: blobA),
)

Properties

hashCode → int
The hash code for this object.
no setterinherited
runtimeType → Type
A representation of the runtime type of the object.
no setterinherited

Methods

noSuchMethod(Invocation invocation) → dynamic
Invoked when a nonexistent method or property is accessed.
inherited
toString() → String
A string representation of this object.
inherited

Operators

operator ==(Object other) → bool
The equality operator.
inherited

Static Methods

compute({required double rawValue, required double finalDx, required double finalDy, double horizontalOffset = 0.0, double verticalOffset = 0.0, double scaleDelta = 1.0, bool? adaptiveDamping, bool isClosing = false}) → LiquidMorphState
Computes the full LiquidMorphState for a single animation frame.
computeAdaptiveBackOutAmplitude({required double finalDy, double baseAmplitude = _backOutAmplitude}) → double
Computes the adaptive J-curve back-out amplitude for a given vertical travel distance.
computeBlendAttenuation(double scaleDelta) → double
Computes the SDF blend attenuation factor for a given scale ratio.
computeScaleDelta({required Size sourceSize, required Size targetSize}) → double
Computes the geometric scale ratio from sourceSize to targetSize.

Constants

closeSpring → const SpringDescription
Spring profile for the iOS 26 liquid morph closing animation.
closeVelocityHint → const double
Initial velocity hint injected when the close animation starts.
openSpring → const SpringDescription
Spring profile for the iOS 26 liquid morph opening animation.