buildJellyTransform static method

Matrix4 buildJellyTransform({
  1. required Offset velocity,
  2. double maxDistortion = 0.7,
  3. double velocityScale = 1000.0,
})

Creates a jelly transform matrix based on velocity.

Applies organic squash and stretch effects that create a satisfying "jelly" animation:

  • Squashes in the direction of movement
  • Stretches perpendicular to movement
  • Intensity scales with velocity magnitude

This creates the iOS-style elastic effect seen when dragging indicators.

Parameters:

  • velocity: Direction and speed of movement as an Offset
  • maxDistortion: Maximum distortion factor 0-1 (default: 0.7)
  • velocityScale: Scale factor for velocity; higher = less distortion (default: 1000.0)

Returns: A Matrix4 transform that can be applied to a widget.

Example:

Transform(
  alignment: Alignment.center,
  transform: DraggableIndicatorPhysics.buildJellyTransform(
    velocity: Offset(velocityX, 0),
    maxDistortion: 0.8,
    velocityScale: 10,
  ),
  child: indicator,
)

Implementation

static Matrix4 buildJellyTransform({
  required Offset velocity,
  double maxDistortion = 0.7,
  double velocityScale = 1000.0,
}) {
  final speed = velocity.distance;
  if (speed == 0 || !speed.isFinite) {
    // speed == 0: no movement, avoid division by zero.
    // !isFinite: NaN or Infinity from synthetic/custom velocity — fall back
    // to a neutral transform. Keep a sub-pixel translation so the
    // TransformLayer stays mounted and avoids edge-snapping artifacts.
    return Matrix4.identity()..translate(0.0001, 0.0);
  }

  // Normalize velocity direction
  final direction = velocity / speed;

  // Calculate distortion intensity based on speed
  final distortionFactor =
      (speed / velocityScale).clamp(0.0, 1.0) * maxDistortion;

  // Squash in direction of movement
  final squashX = 1.0 - (direction.dx.abs() * distortionFactor * 0.5);
  final squashY = 1.0 - (direction.dy.abs() * distortionFactor * 0.5);

  // Stretch perpendicular to movement
  final stretchX = 1.0 + (direction.dy.abs() * distortionFactor * 0.3);
  final stretchY = 1.0 + (direction.dx.abs() * distortionFactor * 0.3);

  // Combine effects
  final scaleX = squashX * stretchX;
  final scaleY = squashY * stretchY;

  final matrix = Matrix4.identity()..scale(scaleX, scaleY);
  if (matrix.isIdentity()) {
    // Catch floating-point rounding rendering drops when speed is > 0 but microscopic
    matrix.translate(0.0001, 0.0);
  }
  return matrix;
}