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.
- package/.github/workflows/pages.yml +68 -0
- package/AGENTS.md +179 -0
- package/README.md +222 -0
- package/demo-app/README.md +70 -0
- package/demo-app/angular.json +78 -0
- package/demo-app/e2e/benchmark.spec.ts +53 -0
- package/demo-app/e2e/demo-page.spec.ts +77 -0
- package/demo-app/e2e/grid-features.spec.ts +269 -0
- package/demo-app/package-lock.json +14023 -0
- package/demo-app/package.json +36 -0
- package/demo-app/playwright-test-menu.js +19 -0
- package/demo-app/playwright.config.ts +23 -0
- package/demo-app/src/app/app.component.ts +10 -0
- package/demo-app/src/app/app.config.ts +13 -0
- package/demo-app/src/app/app.routes.ts +7 -0
- package/demo-app/src/app/demo-page/demo-page.component.css +313 -0
- package/demo-app/src/app/demo-page/demo-page.component.html +124 -0
- package/demo-app/src/app/demo-page/demo-page.component.ts +366 -0
- package/demo-app/src/index.html +19 -0
- package/demo-app/src/main.ts +6 -0
- package/demo-app/tsconfig.json +31 -0
- package/ng-package.json +8 -0
- package/package.json +60 -0
- package/plan.md +131 -0
- package/setup-vitest.ts +18 -0
- package/src/lib/argent-grid.module.ts +21 -0
- package/src/lib/components/argent-grid.component.css +483 -0
- package/src/lib/components/argent-grid.component.html +320 -0
- package/src/lib/components/argent-grid.component.spec.ts +189 -0
- package/src/lib/components/argent-grid.component.ts +1188 -0
- package/src/lib/directives/ag-grid-compatibility.directive.ts +92 -0
- package/src/lib/rendering/canvas-renderer.ts +962 -0
- package/src/lib/rendering/render/blit.spec.ts +453 -0
- package/src/lib/rendering/render/blit.ts +393 -0
- package/src/lib/rendering/render/cells.ts +369 -0
- package/src/lib/rendering/render/index.ts +105 -0
- package/src/lib/rendering/render/lines.ts +363 -0
- package/src/lib/rendering/render/theme.spec.ts +282 -0
- package/src/lib/rendering/render/theme.ts +201 -0
- package/src/lib/rendering/render/types.ts +279 -0
- package/src/lib/rendering/render/walk.spec.ts +360 -0
- package/src/lib/rendering/render/walk.ts +360 -0
- package/src/lib/rendering/utils/damage-tracker.spec.ts +444 -0
- package/src/lib/rendering/utils/damage-tracker.ts +423 -0
- package/src/lib/rendering/utils/index.ts +7 -0
- package/src/lib/services/grid.service.spec.ts +1039 -0
- package/src/lib/services/grid.service.ts +1284 -0
- package/src/lib/types/ag-grid-types.ts +970 -0
- package/src/public-api.ts +22 -0
- package/tsconfig.json +32 -0
- package/tsconfig.lib.json +11 -0
- package/tsconfig.spec.json +8 -0
- 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
|
+
}
|