blobRect static method

Rect blobRect({
  1. required Rect trigger,
  2. required Rect destination,
  3. required double pathT,
  4. required double sizeT,
  5. double scaleDelta = 1.0,
  6. bool isClosing = false,
  7. double travelDx = 0.0,
  8. double travelDy = 0.0,
})

The frame of the travelling droplet (Blob B) for one morph frame.

Size follows LiquidMorphState.sizeT and position follows LiquidMorphState.pathT; keeping them on separate curves is what opens the gap the metaball neck stretches across.

Center displacement is driven by sizeT with anchor-drift clamped to at most 8 px (rawAnchorDrift.clamp(-8, 8)), pinning the base of the expanding blob to the trigger button during flight. This achieves GlassMenu's cohesive "one animated blob" feel that grows and shrinks smoothly.

On close undershoot (sizeT <= 0.0), the droplet squeezes relative to the trigger's own dimensions rather than subtracting destination delta, preventing 0 px collapse.

Implementation

static Rect blobRect({
  required Rect trigger,
  required Rect destination,
  required double pathT,
  required double sizeT,
  double scaleDelta = 1.0,
  bool isClosing = false,
  double travelDx = 0.0,
  double travelDy = 0.0,
}) {
  final double width;
  final double height;

  if (sizeT <= 0.0) {
    // Close undershoot bounce: droplet is at the trigger, squeeze relative
    // to the trigger's own dimensions — never subtract destination delta!
    // Subtracting an 852 px destination delta with negative sizeT would collapse
    // the container to 0 px and cause it to vanish from the screen.
    final squeeze = (1.0 + sizeT * 0.25).clamp(0.5, 1.0);
    width = trigger.width * squeeze;
    height = trigger.height * squeeze;
  } else {
    // Continuous fluid expansion: droplet expands smoothly and continuously
    // from the trigger into the destination bounds. Combined with the
    // anchor-drift clamping below, the base of the droplet stays rooted at
    // the trigger while inflating outward and upward — achieving GlassMenu's
    // cohesive "one animated blob" feel without artificial elongation artifacts.
    width =
        lerpDouble(trigger.width, destination.width, sizeT)!.clamp(0.0, 1e6);
    height = lerpDouble(trigger.height, destination.height, sizeT)!
        .clamp(0.0, 1e6);
  }

  // Anchor-drift clamping (parity with GlassMenu):
  // For large menus and sheets, clamp the anchor edge displacement so the
  // blob's pinned edge never drifts more than 8 px from the trigger button
  // during flight. By driving center displacement by sizeT (with clamped
  // J-curve drift), the blob's edges stay rooted at the trigger while it grows
  // outward — matching GlassMenu's unified-blob feel rather than a detached
  // flying droplet.
  const double maxAnchorDrift = 8.0;
  final double finalDx = destination.center.dx - trigger.center.dx;
  final double finalDy = destination.center.dy - trigger.center.dy;

  final double rawAnchorDriftX = finalDx * (pathT - sizeT);
  final double anchorDriftX =
      rawAnchorDriftX.clamp(-maxAnchorDrift, maxAnchorDrift);
  final double effectiveDx = finalDx * sizeT + anchorDriftX;

  final double rawAnchorDriftY = finalDy * (pathT - sizeT);
  final double anchorDriftY =
      rawAnchorDriftY.clamp(-maxAnchorDrift, maxAnchorDrift);
  final double effectiveDy = finalDy * sizeT + anchorDriftY;

  final double centerX = trigger.center.dx + effectiveDx;
  final double centerY = trigger.center.dy + effectiveDy;

  return Rect.fromLTWH(
    centerX - width / 2.0,
    centerY - height / 2.0,
    width,
    height,
  );
}