Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import { RATE_LIMITED_RENDER_FPS } from "../constants";
 
/**
 * A utility class that provides rate-limited rendering functionality.
 * Handles both debounced zoom operations and rate-limited scrolling.
 */
export class RateLimitedRenderer {
  private lastDrawTime = 0;
  private pendingDraw: {
    getContext: () => any;
    drawFn: (context: any) => void;
  } | null = null;
  private drawScheduled = false;
  private _fps = RATE_LIMITED_RENDER_FPS;
  private _minFrameTime: number = 1000 / RATE_LIMITED_RENDER_FPS;
  private currentWindowStart = 0;
  private zoomDebounce: number | null = null;
 
  /**
   * Creates a new RateLimitedRenderer
   * @param fps The minimum frames per second to maintain (default: 30)
   * @param zoomDebounceMs The debounce time for zoom operations in milliseconds (default: 50)
   */
  constructor(
    fps = RATE_LIMITED_RENDER_FPS,
    private readonly zoomDebounceMs: number = 50,
  ) {
    this.fps = fps;
    this.currentWindowStart = performance.now();
  }
 
  set fps(fps: number) {
    this._fps = fps;
    this._minFrameTime = 1000 / this._fps;
  }
 
  get fps() {
    return this._fps;
  }
 
  get minFrameTime() {
    return this._minFrameTime;
  }
 
  /**
   * Schedules a draw operation with rate limiting.
   * For zoom operations, applies debouncing.
   * For regular operations, applies rate limiting.
   * @param context The context to draw with
   * @param drawFn The function to call for drawing
   * @param isZoom Whether this is a zoom operation
   */
  scheduleDraw<T>(context: T, drawFn: (context: T) => void, isZoom = false) {
    if (isZoom) {
      // Handle zoom operations with debouncing
      if (this.zoomDebounce) {
        clearTimeout(this.zoomDebounce);
      }
      this.zoomDebounce = setTimeout(() => {
        drawFn(context);
        this.zoomDebounce = null;
      }, this.zoomDebounceMs);
      return;
    }
 
    // Handle regular operations with rate limiting
    const now = performance.now();
 
    // Store the function that will get fresh context values
    this.pendingDraw = { getContext: () => context, drawFn };
 
    // Check if we're in a new time window
    const timeSinceWindowStart = now - this.currentWindowStart;
 
    if (timeSinceWindowStart >= this.minFrameTime) {
      // We're in a new time window, draw immediately
      this.executeDraw();
      this.lastDrawTime = now;
      this.currentWindowStart = now;
    } else if (!this.drawScheduled) {
      // Schedule the draw for the start of the next time window
      this.drawScheduled = true;
      const timeUntilNextWindow = this.minFrameTime - timeSinceWindowStart;
 
      setTimeout(() => {
        if (this.pendingDraw) {
          requestAnimationFrame(() => {
            this.executeDraw();
            this.lastDrawTime = performance.now();
            this.currentWindowStart = this.lastDrawTime;
          });
        }
      }, timeUntilNextWindow);
    }
  }
 
  private executeDraw() {
    if (this.pendingDraw) {
      const { getContext, drawFn } = this.pendingDraw;
      // Get fresh context values at draw time
      const context = getContext();
      drawFn(context);
      this.pendingDraw = null;
      this.drawScheduled = false;
    }
  }
 
  /**
   * Resets the renderer state
   */
  reset() {
    this.lastDrawTime = 0;
    this.pendingDraw = null;
    this.drawScheduled = false;
    this.currentWindowStart = performance.now();
    if (this.zoomDebounce) {
      clearTimeout(this.zoomDebounce);
      this.zoomDebounce = null;
    }
  }
}