# How Smooth Scrolling Modifies Trackpad Input in Vorssaint-utils

> Discover how Vorssaint-utils smooth scrolling modifies trackpad input with physics-based gliding. Learn about delta normalization and frame-by-frame animation for a seamless user experience.

- Repository: [vorssaint/vorssaint-utils](https://github.com/vorssaint/vorssaint-utils)
- Tags: deep-dive
- Published: 2026-09-12

---

**Vorssaint-utils transforms discrete trackpad wheel events into a continuous, physics-based glide by normalizing deltas through `continuousDistance`, accumulating them in the `Engine` class, and emitting frame-by-frame animation steps via `advance(elapsed:response:)` regardless of display refresh rate.**

The open-source utility **Vorssaint-utils** enhances macOS scrolling by intercepting raw hardware events and applying a custom interpolation algorithm. Unlike standard system scrolling, which emits discrete jumps per trackpad gesture, this implementation converts wheel deltas into smooth, momentum-based motion that remains consistent across 30 Hz, 60 Hz, or 144 Hz displays.

## Normalizing Raw Trackpad Events with `continuousDistance`

Before any physics calculations occur, the system must convert hardware-specific wheel units into standard pixel distances. In [`Sources/Vorssaint/Services/SmoothScrollSupport.swift`](https://github.com/vorssaint/vorssaint-utils/blob/main/Sources/Vorssaint/Services/SmoothScrollSupport.swift), the helper function `continuousDistance` (lines 9‑17) performs this normalization using the user-defined *step* setting.

This conversion ensures that a trackpad flick produces the same scroll distance regardless of whether the hardware reports in lines, pixels, or arbitrary fixed-point values. The method signature accepts `fixedPointDelta`, `pointDelta`, and the configurable `step` parameter to produce a hardware-agnostic output.

```swift
let normalized = SmoothScrollSupport.continuousDistance(
    fixedPointDelta: event.fixedPointDelta,
    pointDelta: event.pointDelta,
    step: Double(smoothScrollStep)
)

```

## Accumulating Scroll Deltas in the Physics Engine

Normalized distances feed into the `Engine` class via the `add(vertical:horizontal:)` method. This engine maintains per-axis “budgets” tracked as `remainingVertical` and `remainingHorizontal` properties that represent the total distance yet to be scrolled.

When a new gesture arrives, the engine checks if the new direction opposes the current momentum using the `directionsOppose` logic. If the user reverses direction, the old scroll tail is immediately discarded so the first opposite tick registers without delay.

```swift
var scrollEngine = SmoothScrollSupport.Engine()
scrollEngine.add(vertical: normalized, horizontal: 0)

```

## Frame-Independent Animation with `advance(elapsed:response:)`

The smooth scrolling animation runs on a display-link callback (approximately 60 Hz by default). On each frame, the engine’s `advance(elapsed:response:)` method calculates how much of the remaining budget to emit.

Inside `frameDelta` (lines 71‑82), the code applies an exponential-decay curve whose time constant derives from the *response* setting. Crucially, the calculation uses the actual `elapsed` time passed into `advance`, making the scroll shape identical on 30 Hz, 60 Hz, or 144 Hz displays.

The computed delta is then split into `wholePixels` and a fractional carry (lines 19‑28) to prevent cumulative rounding errors. On the final frame, `finalPixels` flushes any remaining fraction to ensure the view lands exactly at the target position.

```swift
let frame = scrollEngine.advance(
    elapsed: displayLink.timestamp - lastTimestamp,
    response: smoothScrollResponse
)
applyScroll(deltaX: frame.horizontal, deltaY: frame.vertical)

```

## Handling Direction Reversals and Shift-Modified Scrolling

The engine respects immediate user intent during gesture reversals. When `directionsOppose` detects a direction change, it discards the existing momentum buffer so the scroll responds instantly rather than fighting the new input.

For accessibility, Vorssaint-utils implements axis swapping in `axes(vertical:horizontal:shiftPressed:)` (lines 51‑63). When the user holds the **Shift** key, vertical wheel ticks redirect to horizontal scrolling while preserving the original sign and magnitude.

## Configuring Smooth Scrolling in SettingsView.swift

User preferences bind directly to the engine’s behavior through [`Sources/Vorssaint/UI/Settings/SettingsView.swift`](https://github.com/vorssaint/vorssaint-utils/blob/main/Sources/Vorssaint/UI/Settings/SettingsView.swift) (lines 906‑1022). Three `@AppStorage` properties control the feature:

- `smoothScrollEnabled` – Toggles the interpolation on or off
- `smoothScrollStep` – Adjusts the distance multiplier applied during normalization
- `smoothScrollResponse` – Controls the exponential decay rate (higher values feel snappier)

Changes to these values update the engine’s parameters in real-time without requiring an app restart.

```swift
@AppStorage(DefaultsKey.smoothScrollEnabled) private var smoothScrollEnabled = false
@AppStorage(DefaultsKey.smoothScrollStep) private var smoothScrollStep = SmoothScrollSupport.defaultStep
@AppStorage(DefaultsKey.smoothScrollResponse) private var smoothScrollResponse = SmoothScrollSupport.defaultResponse

```

## Summary

- **Normalization**: `continuousDistance` converts hardware-specific trackpad units to pixels using the configurable `step` value.
- **Accumulation**: The `Engine` class adds deltas to per-axis budgets and discards stale momentum when `directionsOppose` detects a reversal.
- **Frame Independence**: `advance(elapsed:response:)` uses elapsed time calculations to ensure consistent animation curves across all display refresh rates.
- **Precision**: Rounding logic splits deltas into `wholePixels` and fractional carries, flushing remainders via `finalPixels` on the last frame.
- **Accessibility**: `axes(vertical:horizontal:shiftPressed:)` redirects vertical input to horizontal scrolling when Shift is held.
- **Configuration**: [`SettingsView.swift`](https://github.com/vorssaint/vorssaint-utils/blob/main/SettingsView.swift) binds UI controls to `DefaultsKey` storage for instant updates.

## Frequently Asked Questions

### What file contains the core smooth scrolling logic in Vorssaint-utils?

The physics engine lives in [`Sources/Vorssaint/Services/SmoothScrollSupport.swift`](https://github.com/vorssaint/vorssaint-utils/blob/main/Sources/Vorssaint/Services/SmoothScrollSupport.swift), which defines the `Engine` class, `continuousDistance` normalization, and `advance(elapsed:response:)` animation logic. The public façade that connects this engine to the app’s event loop resides in [`Sources/Vorssaint/Services/SmoothScrollService.swift`](https://github.com/vorssaint/vorssaint-utils/blob/main/Sources/Vorssaint/Services/SmoothScrollService.swift).

### How does Vorssaint-utils ensure smooth scrolling feels the same on 144Hz and 60Hz monitors?

The `advance(elapsed:response:)` method accepts an `elapsed` time parameter rather than assuming a fixed frame duration. The `frameDelta` calculation (lines 71‑82) uses this elapsed value to apply the exponential decay curve, ensuring the same gesture produces identical scroll shapes regardless of whether the display refreshes at 30 Hz, 60 Hz, or 144 Hz.

### What happens when I reverse scroll direction during a smooth scroll gesture?

The engine checks if new input `directionsOppose` the current momentum. If so, it immediately discards the remaining scroll budget (`remainingVertical` or `remainingHorizontal`) so the trackpad responds to the reverse gesture without fighting the previous momentum’s tail.

### How can I disable smooth scrolling or adjust its speed?

Open the settings UI defined in [`Sources/Vorssaint/UI/Settings/SettingsView.swift`](https://github.com/vorssaint/vorssaint-utils/blob/main/Sources/Vorssaint/UI/Settings/SettingsView.swift). The toggle bound to `smoothScrollEnabled` disables the feature entirely, while sliders for `smoothScrollStep` (distance per tick) and `smoothScrollResponse` (decay speed) adjust the physics in real-time via `AppStorage` bindings to `DefaultsKey` values.