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
sourceSizetotargetSize.
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.