How to Implement a Hysteresis Controller for Tier Transition Stability in Clypra

Clypra's HysteresisController prevents visual jitter during zoom transitions by requiring a 10% overshoot threshold, maintaining a 5% dead-band, and enforcing a 200ms debounce window before committing tier changes.

Clypra's rendering pipeline dynamically selects spatial tiers (L0, L1, etc.) based on zoom levels, but rapid boundary crossings can cause flickering and unnecessary re-renders. Implementing a hysteresis controller for tier transition stability ensures smooth visual performance by introducing deliberate delays and buffers that filter out noise. The HysteresisController class in src/lib/renderEngine/hysteresis.ts provides a pure TypeScript solution that integrates seamlessly with the Spatial Rendering Pipeline (SRP).

Why Tier Transitions Need Hysteresis

Without protection, rapid zooming can cause the spatial tier to flip back and forth at exact boundaries, producing visual jitter and thrashing the render engine. When a user hovers near a tier boundary, minute input variations trigger continuous SRP recomputations that degrade performance and create a jarring experience. The hysteresis controller solves this by making tier transitions "sticky"—once entered, a tier resists exit until the zoom level moves sufficiently far from the threshold.

Core Safeguards in the HysteresisController

The HysteresisController implemented in src/lib/renderEngine/hysteresis.ts introduces three specific safeguards to stabilize tier transitions:

  • Overshoot threshold (OVERSHOOT_THRESHOLD = 0.10): The controller only accepts a tier change when the zoom level has moved 10% beyond the current tier's boundary. This prevents an immediate flip when the pointer hovers right on the boundary edge.

  • Dead-band (DEAD_BAND = 0.05): While the zoom stays within ±5% of the current tier's min/max limits, the candidate tier is forced back to the current tier. This keeps the system stable when users slowly nudge around a threshold.

  • Debounce and rate-limit (COMMIT_DEBOUNCE_MS = 200): After a candidate tier is first recognized, it must remain stable for 200ms before it is committed, and commits are limited to one per 200ms. This eliminates short-lived spikes that would otherwise cause rapid tier thrashing.

Internal Workflow of the Controller

When the update() method receives a new zoom level, the controller executes a six-step validation process before committing to a new SpatialTier:

  1. Current boundary lookup: Using the active SpatialTier, the controller retrieves the configured min and max zoom values from the SRP config (DEFAULT_SRP_CONFIG).

  2. Dead-band check: If the zoom lies inside the dead-band range, the candidate tier resets to the current tier and the method returns null (no commit).

  3. Overshoot evaluation: If the target tier (the tier SRP would pick without hysteresis) differs from the current tier, the controller calls _computeCandidateWithOvershoot. This checks whether the zoom has crossed the 10% overshoot threshold and promotes the candidate only if true.

  4. Stability window: The candidate must stay unchanged for COMMIT_DEBOUNCE_MS. The elapsed time is measured with the injected now() function (defaulting to performance.now).

  5. Rate limiting: A second guard ensures that commits are spaced at least COMMIT_DEBOUNCE_MS apart, preventing rapid successive transitions.

  6. Commit: When both conditions are satisfied, _commit updates currentTier, clears the candidate state, and records the commit timestamp.

Implementation Example

To implement the hysteresis controller for tier transition stability in your Clypra application, instantiate the controller once per render engine and feed zoom updates through the update() method:

import { HysteresisController } from '@/lib/renderEngine/hysteresis';
import { SpatialTier, DEFAULT_SRP_CONFIG } from '@/lib/renderEngine/types';

// Create a controller – typically once per RenderEngine instance
const hysteresis = new HysteresisController(
  SpatialTier.L0,               // initial tier
  DEFAULT_SRP_CONFIG           // zoom-to-tier map
);

// Feed zoom updates (e.g. from a pinch/scroll handler)
function onZoomChange(zoomLevel: number) {
  // Determine the tier SRP would pick without hysteresis
  const targetTier = computeTierFromZoom(zoomLevel, DEFAULT_SRP_CONFIG);

  // Let the controller decide if we should actually switch tiers
  const committedTier = hysteresis.update(zoomLevel, targetTier);

  if (committedTier !== null) {
    // Tier change accepted – trigger a re-render or SRP recompute
    renderEngine.setSpatialTier(committedTier);
  }
}

// Reset when the whole view mode changes (e.g., switching to a different scene)
function onModeChange(newTier: SpatialTier) {
  hysteresis.reset(newTier);
}

The controller is pure TypeScript with no browser API dependencies, making it straightforward to unit-test and reuse anywhere a tier decision is needed.

Testing the Hysteresis Logic

The test suite in src/lib/renderEngine/__tests__/hysteresis.test.ts validates the overshoot and debounce behavior. Use the following pattern to verify that tier transitions require both threshold crossing and time stability:

test('requires 10% overshoot before committing', () => {
  const ctrl = new HysteresisController(SpatialTier.L1, DEFAULT_SRP_CONFIG, () => 0);
  
  // Zoom just inside the upper boundary – should stay at L1
  expect(ctrl.update(0.95, SpatialTier.L2)).toBeNull();

  // Zoom 11% past the boundary – candidate becomes L2 but needs debounce
  expect(ctrl.update(1.11, SpatialTier.L2)).toBeNull();

  // After 200ms debounce passes, commit occurs
  jest.advanceTimersByTime(200);
  expect(ctrl.update(1.11, SpatialTier.L2)).toBe(SpatialTier.L2);
});

Integration with the Render Engine

In src/lib/renderEngine/renderEngine.ts, the main render engine creates an instance of HysteresisController and calls update each time the zoom changes. The returned tier (or null) determines whether the engine should recompute the Spatial Rendering Pipeline (SRP) and re-render clips.

Adjusting the SrpConfig ranges indirectly tweaks the hysteresis behavior—larger min/max ranges provide a broader effective dead-band, while the OVERSHOOT_THRESHOLD and COMMIT_DEBOUNCE_MS constants in the controller source fine-tune the responsiveness.

Summary

  • Implement the HysteresisController from src/lib/renderEngine/hysteresis.ts to eliminate tier-transition jitter in Clypra.
  • Configure the overshoot threshold (10%), dead-band (5%), and debounce window (200ms) to match your performance requirements.
  • Call update(zoomLevel, targetTier) on every zoom change and only re-render when the method returns a non-null SpatialTier.
  • Use reset(newTier) when switching scenes or view modes to clear internal state.
  • Leverage the pure TypeScript implementation for straightforward unit testing without browser dependencies.

Frequently Asked Questions

What is the optimal dead-band percentage for preventing tier flickering?

The default DEAD_BAND = 0.05 (5%) in src/lib/renderEngine/hysteresis.ts provides a balanced buffer for most zoom interfaces. If users report sluggish transitions, reduce this value; if flickering persists at boundaries, increase it to 0.08 or 0.10 while maintaining the 10% overshoot threshold.

How does the hysteresis controller affect performance during rapid zoom gestures?

The controller improves performance by returning null from update() when the zoom fluctuates within the dead-band or stability window, preventing unnecessary SRP recomputations. According to the Clypra source, the rate-limiting guard ensures commits are spaced at least COMMIT_DEBOUNCE_MS (200ms) apart, effectively throttling render calls during rapid pinch-to-zoom gestures.

Can I use the HysteresisController for non-spatial tier transitions?

Yes, the controller is generic and pure TypeScript. While designed for SpatialTier transitions in src/lib/renderEngine/hysteresis.ts, you can adapt it for any discrete state machine requiring hysteresis by replacing the SrpConfig with your own threshold definitions and using the same update()/reset() API pattern.

Where are the hysteresis constants configured in the Clypra codebase?

The three key constants—OVERSHOOT_THRESHOLD, DEAD_BAND, and COMMIT_DEBOUNCE_MS—are defined as private static readonly properties directly in src/lib/renderEngine/hysteresis.ts. The tier-specific ranges used for boundary calculations are configured in DEFAULT_SRP_CONFIG within src/lib/renderEngine/types.ts.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →