argent-grid 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.github/workflows/pages.yml +68 -0
  2. package/AGENTS.md +179 -0
  3. package/README.md +222 -0
  4. package/demo-app/README.md +70 -0
  5. package/demo-app/angular.json +78 -0
  6. package/demo-app/e2e/benchmark.spec.ts +53 -0
  7. package/demo-app/e2e/demo-page.spec.ts +77 -0
  8. package/demo-app/e2e/grid-features.spec.ts +269 -0
  9. package/demo-app/package-lock.json +14023 -0
  10. package/demo-app/package.json +36 -0
  11. package/demo-app/playwright-test-menu.js +19 -0
  12. package/demo-app/playwright.config.ts +23 -0
  13. package/demo-app/src/app/app.component.ts +10 -0
  14. package/demo-app/src/app/app.config.ts +13 -0
  15. package/demo-app/src/app/app.routes.ts +7 -0
  16. package/demo-app/src/app/demo-page/demo-page.component.css +313 -0
  17. package/demo-app/src/app/demo-page/demo-page.component.html +124 -0
  18. package/demo-app/src/app/demo-page/demo-page.component.ts +366 -0
  19. package/demo-app/src/index.html +19 -0
  20. package/demo-app/src/main.ts +6 -0
  21. package/demo-app/tsconfig.json +31 -0
  22. package/ng-package.json +8 -0
  23. package/package.json +60 -0
  24. package/plan.md +131 -0
  25. package/setup-vitest.ts +18 -0
  26. package/src/lib/argent-grid.module.ts +21 -0
  27. package/src/lib/components/argent-grid.component.css +483 -0
  28. package/src/lib/components/argent-grid.component.html +320 -0
  29. package/src/lib/components/argent-grid.component.spec.ts +189 -0
  30. package/src/lib/components/argent-grid.component.ts +1188 -0
  31. package/src/lib/directives/ag-grid-compatibility.directive.ts +92 -0
  32. package/src/lib/rendering/canvas-renderer.ts +962 -0
  33. package/src/lib/rendering/render/blit.spec.ts +453 -0
  34. package/src/lib/rendering/render/blit.ts +393 -0
  35. package/src/lib/rendering/render/cells.ts +369 -0
  36. package/src/lib/rendering/render/index.ts +105 -0
  37. package/src/lib/rendering/render/lines.ts +363 -0
  38. package/src/lib/rendering/render/theme.spec.ts +282 -0
  39. package/src/lib/rendering/render/theme.ts +201 -0
  40. package/src/lib/rendering/render/types.ts +279 -0
  41. package/src/lib/rendering/render/walk.spec.ts +360 -0
  42. package/src/lib/rendering/render/walk.ts +360 -0
  43. package/src/lib/rendering/utils/damage-tracker.spec.ts +444 -0
  44. package/src/lib/rendering/utils/damage-tracker.ts +423 -0
  45. package/src/lib/rendering/utils/index.ts +7 -0
  46. package/src/lib/services/grid.service.spec.ts +1039 -0
  47. package/src/lib/services/grid.service.ts +1284 -0
  48. package/src/lib/types/ag-grid-types.ts +970 -0
  49. package/src/public-api.ts +22 -0
  50. package/tsconfig.json +32 -0
  51. package/tsconfig.lib.json +11 -0
  52. package/tsconfig.spec.json +8 -0
  53. package/vitest.config.ts +55 -0
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Blitting Optimization for Canvas Renderer
3
+ *
4
+ * Reuses pixels from previous frame to minimize redraws.
5
+ * Based on Glide Data Grid's blitting architecture.
6
+ */
7
+
8
+ import { Rectangle, BlitResult, BufferPair } from './types';
9
+
10
+ // ============================================================================
11
+ // BLIT THRESHOLDS
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Minimum scroll delta to trigger blitting
16
+ * Small scrolls are faster to just redraw
17
+ */
18
+ export const MIN_BLIT_DELTA = 2;
19
+
20
+ /**
21
+ * Maximum scroll delta before full redraw
22
+ * Large scrolls would copy too much garbage
23
+ */
24
+ export const MAX_BLIT_DELTA_RATIO = 0.8; // 80% of viewport
25
+
26
+ // ============================================================================
27
+ // BLIT CALCULATIONS
28
+ // ============================================================================
29
+
30
+ /**
31
+ * Determine if blitting is worthwhile for the given scroll delta
32
+ */
33
+ export function shouldBlit(
34
+ deltaX: number,
35
+ deltaY: number,
36
+ viewportWidth: number,
37
+ viewportHeight: number
38
+ ): boolean {
39
+ const absDeltaX = Math.abs(deltaX);
40
+ const absDeltaY = Math.abs(deltaY);
41
+
42
+ // No blitting if no scroll
43
+ if (absDeltaX === 0 && absDeltaY === 0) {
44
+ return false;
45
+ }
46
+
47
+ // Don't blit tiny movements
48
+ if (absDeltaX < MIN_BLIT_DELTA && absDeltaY < MIN_BLIT_DELTA) {
49
+ return false;
50
+ }
51
+
52
+ // Don't blit if scrolling in both directions
53
+ if (absDeltaX > MIN_BLIT_DELTA && absDeltaY > MIN_BLIT_DELTA) {
54
+ return false;
55
+ }
56
+
57
+ // Don't blit if scroll is too large (would copy mostly garbage)
58
+ if (absDeltaX > viewportWidth * MAX_BLIT_DELTA_RATIO) {
59
+ return false;
60
+ }
61
+ if (absDeltaY > viewportHeight * MAX_BLIT_DELTA_RATIO) {
62
+ return false;
63
+ }
64
+
65
+ return true;
66
+ }
67
+
68
+ /**
69
+ * Calculate blit parameters
70
+ */
71
+ export function calculateBlit(
72
+ currentScroll: { x: number; y: number },
73
+ lastScroll: { x: number; y: number },
74
+ viewportSize: { width: number; height: number },
75
+ pinnedWidths: { left: number; right: number }
76
+ ): {
77
+ canBlit: boolean;
78
+ sourceRect: Rectangle;
79
+ destRect: Rectangle;
80
+ dirtyRegions: Rectangle[];
81
+ deltaX: number;
82
+ deltaY: number;
83
+ } {
84
+ const deltaX = currentScroll.x - lastScroll.x;
85
+ const deltaY = currentScroll.y - lastScroll.y;
86
+
87
+ const { left: leftPinnedWidth, right: rightPinnedWidth } = pinnedWidths;
88
+ const centerWidth = viewportSize.width - leftPinnedWidth - rightPinnedWidth;
89
+
90
+ // Check if blitting is worthwhile
91
+ if (!shouldBlit(deltaX, deltaY, viewportSize.width, viewportSize.height)) {
92
+ return {
93
+ canBlit: false,
94
+ sourceRect: { x: 0, y: 0, width: 0, height: 0 },
95
+ destRect: { x: 0, y: 0, width: 0, height: 0 },
96
+ dirtyRegions: [{ x: 0, y: 0, width: viewportSize.width, height: viewportSize.height }],
97
+ deltaX,
98
+ deltaY,
99
+ };
100
+ }
101
+
102
+ const dirtyRegions: Rectangle[] = [];
103
+
104
+ // Vertical scroll (most common)
105
+ if (Math.abs(deltaY) >= MIN_BLIT_DELTA && Math.abs(deltaX) < MIN_BLIT_DELTA) {
106
+ const absDelta = Math.abs(deltaY);
107
+ const copyHeight = viewportSize.height - absDelta;
108
+
109
+ // Source and destination depend on scroll direction
110
+ const sourceY = deltaY > 0 ? 0 : absDelta;
111
+ const destY = deltaY > 0 ? absDelta : 0;
112
+
113
+ // Blit the entire width (center region)
114
+ return {
115
+ canBlit: true,
116
+ sourceRect: {
117
+ x: leftPinnedWidth,
118
+ y: sourceY,
119
+ width: centerWidth,
120
+ height: copyHeight,
121
+ },
122
+ destRect: {
123
+ x: leftPinnedWidth,
124
+ y: destY,
125
+ width: centerWidth,
126
+ height: copyHeight,
127
+ },
128
+ dirtyRegions: [
129
+ // Top strip that needs redraw
130
+ ...(deltaY > 0 ? [{
131
+ x: leftPinnedWidth,
132
+ y: 0,
133
+ width: centerWidth,
134
+ height: absDelta,
135
+ }] : []),
136
+ // Bottom strip that needs redraw
137
+ ...(deltaY < 0 ? [{
138
+ x: leftPinnedWidth,
139
+ y: viewportSize.height - absDelta,
140
+ width: centerWidth,
141
+ height: absDelta,
142
+ }] : []),
143
+ ],
144
+ deltaX,
145
+ deltaY,
146
+ };
147
+ }
148
+
149
+ // Horizontal scroll
150
+ if (Math.abs(deltaX) >= MIN_BLIT_DELTA && Math.abs(deltaY) < MIN_BLIT_DELTA) {
151
+ const absDelta = Math.abs(deltaX);
152
+ const copyWidth = centerWidth - absDelta;
153
+
154
+ // Source and destination depend on scroll direction
155
+ const sourceX = deltaX > 0 ? leftPinnedWidth : leftPinnedWidth + absDelta;
156
+ const destX = deltaX > 0 ? leftPinnedWidth + absDelta : leftPinnedWidth;
157
+
158
+ return {
159
+ canBlit: true,
160
+ sourceRect: {
161
+ x: sourceX,
162
+ y: 0,
163
+ width: copyWidth,
164
+ height: viewportSize.height,
165
+ },
166
+ destRect: {
167
+ x: destX,
168
+ y: 0,
169
+ width: copyWidth,
170
+ height: viewportSize.height,
171
+ },
172
+ dirtyRegions: [
173
+ // Left strip that needs redraw
174
+ ...(deltaX > 0 ? [{
175
+ x: leftPinnedWidth,
176
+ y: 0,
177
+ width: absDelta,
178
+ height: viewportSize.height,
179
+ }] : []),
180
+ // Right strip that needs redraw
181
+ ...(deltaX < 0 ? [{
182
+ x: viewportSize.width - rightPinnedWidth - absDelta,
183
+ y: 0,
184
+ width: absDelta,
185
+ height: viewportSize.height,
186
+ }] : []),
187
+ ],
188
+ deltaX,
189
+ deltaY,
190
+ };
191
+ }
192
+
193
+ // Diagonal scroll - just do full redraw
194
+ return {
195
+ canBlit: false,
196
+ sourceRect: { x: 0, y: 0, width: 0, height: 0 },
197
+ destRect: { x: 0, y: 0, width: 0, height: 0 },
198
+ dirtyRegions: [{ x: 0, y: 0, width: viewportSize.width, height: viewportSize.height }],
199
+ deltaX,
200
+ deltaY,
201
+ };
202
+ }
203
+
204
+ // ============================================================================
205
+ // BLIT EXECUTION
206
+ // ============================================================================
207
+
208
+ /**
209
+ * Perform blit operation on canvas
210
+ */
211
+ export function blitLastFrame(
212
+ ctx: CanvasRenderingContext2D,
213
+ lastCanvas: HTMLCanvasElement,
214
+ currentScroll: { x: number; y: number },
215
+ lastScroll: { x: number; y: number },
216
+ viewportSize: { width: number; height: number },
217
+ pinnedWidths: { left: number; right: number }
218
+ ): BlitResult {
219
+ const blit = calculateBlit(currentScroll, lastScroll, viewportSize, pinnedWidths);
220
+
221
+ if (blit.canBlit) {
222
+ // Use drawImage to copy pixels from last frame
223
+ ctx.drawImage(
224
+ lastCanvas,
225
+ blit.sourceRect.x,
226
+ blit.sourceRect.y,
227
+ blit.sourceRect.width,
228
+ blit.sourceRect.height,
229
+ blit.destRect.x,
230
+ blit.destRect.y,
231
+ blit.destRect.width,
232
+ blit.destRect.height
233
+ );
234
+ }
235
+
236
+ return {
237
+ blitted: blit.canBlit,
238
+ regionsToDraw: blit.dirtyRegions,
239
+ deltaX: blit.deltaX,
240
+ deltaY: blit.deltaY,
241
+ };
242
+ }
243
+
244
+ // ============================================================================
245
+ // DOUBLE BUFFERING
246
+ // ============================================================================
247
+
248
+ /**
249
+ * Create a buffer pair for double buffering
250
+ */
251
+ export function createBufferPair(
252
+ width: number,
253
+ height: number,
254
+ dpr: number = 1
255
+ ): BufferPair {
256
+ const front = document.createElement('canvas');
257
+ const back = document.createElement('canvas');
258
+
259
+ [front, back].forEach(canvas => {
260
+ canvas.width = Math.floor(width * dpr);
261
+ canvas.height = Math.floor(height * dpr);
262
+ canvas.style.width = `${width}px`;
263
+ canvas.style.height = `${height}px`;
264
+ });
265
+
266
+ const frontCtx = front.getContext('2d')!;
267
+ const backCtx = back.getContext('2d')!;
268
+
269
+ // Set up DPR scaling
270
+ if (dpr !== 1) {
271
+ frontCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
272
+ backCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
273
+ }
274
+
275
+ return { front, back, frontCtx, backCtx };
276
+ }
277
+
278
+ /**
279
+ * Swap front and back buffers
280
+ */
281
+ export function swapBuffers(buffers: BufferPair): void {
282
+ const temp = buffers.front;
283
+ buffers.front = buffers.back;
284
+ buffers.back = temp;
285
+
286
+ const tempCtx = buffers.frontCtx;
287
+ buffers.frontCtx = buffers.backCtx;
288
+ buffers.backCtx = tempCtx;
289
+ }
290
+
291
+ /**
292
+ * Copy back buffer to front buffer (for display)
293
+ */
294
+ export function displayBuffer(
295
+ displayCtx: CanvasRenderingContext2D,
296
+ buffer: HTMLCanvasElement
297
+ ): void {
298
+ displayCtx.drawImage(buffer, 0, 0);
299
+ }
300
+
301
+ /**
302
+ * Resize buffer pair
303
+ */
304
+ export function resizeBufferPair(
305
+ buffers: BufferPair,
306
+ width: number,
307
+ height: number,
308
+ dpr: number = 1
309
+ ): void {
310
+ [buffers.front, buffers.back].forEach(canvas => {
311
+ canvas.width = Math.floor(width * dpr);
312
+ canvas.height = Math.floor(height * dpr);
313
+ canvas.style.width = `${width}px`;
314
+ canvas.style.height = `${height}px`;
315
+ });
316
+
317
+ if (dpr !== 1) {
318
+ buffers.frontCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
319
+ buffers.backCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
320
+ }
321
+ }
322
+
323
+ // ============================================================================
324
+ // BLIT STATE TRACKING
325
+ // ============================================================================
326
+
327
+ /**
328
+ * Track scroll state for blitting
329
+ */
330
+ export class BlitState {
331
+ private lastScrollX: number = 0;
332
+ private lastScrollY: number = 0;
333
+ private lastCanvas: HTMLCanvasElement | null = null;
334
+
335
+ /**
336
+ * Update scroll position and return previous position
337
+ */
338
+ updateScroll(x: number, y: number): { x: number; y: number } {
339
+ const last = { x: this.lastScrollX, y: this.lastScrollY };
340
+ this.lastScrollX = x;
341
+ this.lastScrollY = y;
342
+ return last;
343
+ }
344
+
345
+ /**
346
+ * Get current scroll position
347
+ */
348
+ getScroll(): { x: number; y: number } {
349
+ return { x: this.lastScrollX, y: this.lastScrollY };
350
+ }
351
+
352
+ /**
353
+ * Store the last rendered canvas for blitting
354
+ */
355
+ setLastCanvas(canvas: HTMLCanvasElement): void {
356
+ if (!this.lastCanvas) {
357
+ this.lastCanvas = document.createElement('canvas');
358
+ }
359
+
360
+ // Only resize if dimensions changed, as setting width/height clears the canvas
361
+ // and is an expensive operation.
362
+ if (this.lastCanvas.width !== canvas.width || this.lastCanvas.height !== canvas.height) {
363
+ this.lastCanvas.width = canvas.width;
364
+ this.lastCanvas.height = canvas.height;
365
+ }
366
+
367
+ const ctx = this.lastCanvas.getContext('2d')!;
368
+ ctx.drawImage(canvas, 0, 0);
369
+ }
370
+
371
+ /**
372
+ * Get the last rendered canvas
373
+ */
374
+ getLastCanvas(): HTMLCanvasElement | null {
375
+ return this.lastCanvas;
376
+ }
377
+
378
+ /**
379
+ * Check if we have a previous frame to blit from
380
+ */
381
+ hasLastFrame(): boolean {
382
+ return this.lastCanvas !== null;
383
+ }
384
+
385
+ /**
386
+ * Reset blit state
387
+ */
388
+ reset(): void {
389
+ this.lastScrollX = 0;
390
+ this.lastScrollY = 0;
391
+ this.lastCanvas = null;
392
+ }
393
+ }