Skip to main content
Wave provides several public utility functions that are commonly used in gesture-driven animations and interactive interfaces.

Projection

project(value:velocity:decelerationRate:)

Projects a scalar value based on its velocity, simulating where the value would end up after natural deceleration.
value
CGFloat
required
The current value
velocity
CGFloat
required
The current velocity (points per second)
decelerationRate
CGFloat
default:"0.998"
The deceleration rate (0-1). Higher values = slower deceleration
Returns: The projected final value
let currentPosition: CGFloat = 100
let velocity: CGFloat = 500 // points per second

let projectedPosition = project(value: currentPosition, velocity: velocity)
// Use projectedPosition to determine snap target

project(point:velocity:decelerationRate:)

Projects a 2D point based on its 2D velocity.
point
CGPoint
required
The current point
velocity
CGPoint
required
The current velocity (points per second in x and y)
decelerationRate
CGFloat
default:"0.998"
The deceleration rate (0-1)
Returns: The projected final point
let currentCenter = view.center
let gestureVelocity = panGesture.velocity(in: view.superview)

let projectedCenter = project(point: currentCenter, velocity: gestureVelocity)

// Determine which snap target is closest to projected position
let snapTarget = closestSnapPoint(to: projectedCenter)
Projection is commonly used to determine snap targets in gesture-driven animations. When a user releases a dragged object, you can project where it would naturally end up and snap to the nearest valid position.

Value mapping

mapRange(value:inMin:inMax:outMin:outMax:clip:)

Maps a value from one range to another using linear interpolation.
value
T: FloatingPoint
required
The value to map
inMin
T
required
The minimum of the input range
inMax
T
required
The maximum of the input range
outMin
T
required
The minimum of the output range
outMax
T
required
The maximum of the output range
clip
Bool
default:"false"
If true, clamps the result to the output range
Returns: The mapped value
// Map touch Y position to scale (0.5x to 2.0x)
let touchY = gesture.location(in: view).y
let scale = mapRange(
    value: touchY,
    inMin: 0,
    inMax: view.bounds.height,
    outMin: 0.5,
    outMax: 2.0
)

// Map progress (0-1) to sheet height
let progress: CGFloat = 0.75
let height = mapRange(
    value: progress,
    inMin: 0,
    inMax: 1,
    outMin: 100,    // collapsed height
    outMax: 600,    // full height
    clip: true      // ensure result stays in bounds
)

mapRange(::::_:clip:)

Convenience overload that omits parameter labels for more concise syntax.
let scale = mapRange(touchY, 0, screenHeight, 0.5, 2.0)
The clip parameter is useful when the input value might exceed the input range. Without clipping, values outside the input range will extrapolate beyond the output range.

Boundary functions

clip(value:lower:upper:)

Clamps a value to a specified range.
value
T: FloatingPoint
required
The value to clamp
lower
T
required
The minimum allowed value (inclusive)
upper
T
required
The maximum allowed value (inclusive)
Returns: The clamped value
let progress = clip(value: rawProgress, lower: 0, upper: 1)

clipUnit(value:)

Clamps a value to the range [0, 1].
value
T: FloatingPoint
required
The value to clamp
Returns: The clamped value between 0 and 1
let normalizedProgress = clipUnit(value: rawProgress)

Rubber-banding

rubberband(value:range:interval:c:)

Applies a rubber-band effect to values outside a specified range, similar to UIScrollView’s overscroll behavior.
value
CGFloat
required
The input value
range
ClosedRange<CGFloat>
required
The range within which no rubber-banding occurs
interval
CGFloat
required
The dimension used for rubber-band calculations (e.g., view width or height)
c
CGFloat
default:"0.55"
The rubber-band constant. UIScrollView uses 0.55. Lower values = stronger resistance
Returns: The rubber-banded value
let contentHeight: CGFloat = 1000
let viewHeight: CGFloat = 400
let validRange: ClosedRange<CGFloat> = 0...contentHeight

// Apply rubber-banding to scroll offset
let scrollOffset = gesture.translation(in: view).y
let rubberbandedOffset = rubberband(
    value: scrollOffset,
    range: validRange,
    interval: viewHeight,
    c: 0.55
)
Values within the specified range are returned unchanged. Values outside the range are progressively compressed using the rubber-band formula, creating a resistance effect.

Usage in examples

These utilities are demonstrated in the examples:

See also

Build docs developers (and LLMs) love