compute static method

LiquidMorphState compute({
  1. required double rawValue,
  2. required double finalDx,
  3. required double finalDy,
  4. double horizontalOffset = 0.0,
  5. double verticalOffset = 0.0,
})

Computes the full LiquidMorphState for a single animation frame.

Parameters

  • rawValue — the raw, unclamped value from AnimationController.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,
  );
}