blobRect static method
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,
);
}