compute static method
Computes the full LiquidMorphState for a single animation frame.
Parameters
rawValue— the raw, unclamped value fromAnimationController.unbounded. Legitimately exceeds[0, 1]during spring overshoot phases.finalDx— target horizontal displacement from the trigger center to the menu center, in logical pixels.finalDy— target vertical displacement from the trigger center to the menu center, in logical pixels.horizontalOffset— screen-edge clamping correction for horizontal overflow, accumulated by the menu positioning logic.verticalOffset— screen-edge clamping correction for vertical overflow.
Implementation
static LiquidMorphState compute({
required double rawValue,
required double finalDx,
required double finalDy,
double horizontalOffset = 0.0,
double verticalOffset = 0.0,
}) {
final clampedValue = rawValue.clamp(0.0, 1.0);
// Inject the close undershoot so Blob B bounces past the anchor on close.
// The open-side overshoot (rawValue > 1) is intentionally excluded because
// it causes an unwanted size wobble during the initial expansion.
final closeUndershoot = rawValue < 0.0 ? rawValue : 0.0;
// ── J-Curve Position ──────────────────────────────────────────────────────
// The back-out curve overshoots far past 1.0 before snapping back,
// creating the "string pull" teardrop neck at maximum separation.
final pathT = _BackOutCurve(_backOutAmplitude).transform(clampedValue) +
closeUndershoot;
// ── Size ─────────────────────────────────────────────────────────────────
// Size grows steadily then decelerates so the teardrop bulge is clearly
// visible before the container reaches its final dimensions.
final sizeT =
Curves.linearToEaseOut.transform(clampedValue) + closeUndershoot;
// ── Closing Momentum Push (Blob A displacement) ───────────────────────────
// When the spring overshoots past 0 (rawValue < 0), Blob A is displaced
// proportionally to mirror the closing momentum.
final pushDx =
rawValue < 0.0 ? (finalDx + horizontalOffset) * rawValue : 0.0;
final pushDy = rawValue < 0.0 ? (finalDy + verticalOffset) * rawValue : 0.0;
// ── Blob B Displacement ───────────────────────────────────────────────────
final currentDx = finalDx * pathT;
final currentDy = finalDy * pathT;
// ── Anchor Scale ─────────────────────────────────────────────────────────
// Shrinks the ghost trigger (Blob A) to 0 over the first 40 % of the
// animation. Grows back during close so the real trigger "catches" the menu.
final anchorScale =
(1.0 - (clampedValue / _anchorEaseDuration)).clamp(0.0, 1.0);
// ── Metaball Blend ────────────────────────────────────────────────────────
// Separation between pathT (position) and sizeT (size) represents how far
// Blob B has pulled away from its anchor. Blend naturally scales with this.
final separation = (pathT - sizeT).abs();
final blend = (separation * _blendMultiplier).clamp(0.0, _maxBlend);
// ── Container Scale Pulse ─────────────────────────────────────────────────
// Subtle squeeze/swell during spring overshoot phases.
final containerScale = rawValue > 1.0
? 1.0 + (rawValue - 1.0) * 0.10 // open overshoot (negligible)
: rawValue < 0.0
? 1.0 + rawValue * 0.55 // close undershoot → visible squeeze
: 1.0;
// ── Phase ─────────────────────────────────────────────────────────────────
final phase = _derivePhase(rawValue, clampedValue);
return LiquidMorphState(
pathT: pathT,
sizeT: sizeT,
currentDx: currentDx,
currentDy: currentDy,
pushDx: pushDx,
pushDy: pushDy,
anchorScale: anchorScale,
blend: blend,
containerScale: containerScale,
phase: phase,
);
}