@umituz/web-canvas 1.0.1 → 1.0.3

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.
@@ -13,9 +13,11 @@ import { DEFAULT_CANVAS_CONFIG } from '../../domain/config/CanvasConfig';
13
13
 
14
14
  export class CanvasRenderer {
15
15
  private config: typeof DEFAULT_CANVAS_CONFIG;
16
+ private imageCache: Map<string, HTMLImageElement>;
16
17
 
17
18
  constructor(config = DEFAULT_CANVAS_CONFIG) {
18
19
  this.config = { ...DEFAULT_CANVAS_CONFIG, ...config };
20
+ this.imageCache = new Map();
19
21
  }
20
22
 
21
23
  /**
@@ -164,18 +166,43 @@ export class CanvasRenderer {
164
166
  }
165
167
 
166
168
  /**
167
- * Load image from URL
169
+ * Load image from URL with caching
168
170
  */
169
171
  private loadImage(url: string): Promise<HTMLImageElement> {
172
+ // Check cache first
173
+ if (this.imageCache.has(url)) {
174
+ return Promise.resolve(this.imageCache.get(url)!);
175
+ }
176
+
170
177
  return new Promise((resolve, reject) => {
171
178
  const img = new Image();
172
- img.crossOrigin = this.config.useCORS ? 'anonymous' : (undefined as any);
173
- img.onload = () => resolve(img);
179
+ if (this.config.useCORS) {
180
+ img.crossOrigin = 'anonymous';
181
+ }
182
+ img.onload = () => {
183
+ // Cache the loaded image
184
+ this.imageCache.set(url, img);
185
+ resolve(img);
186
+ };
174
187
  img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
175
188
  img.src = url;
176
189
  });
177
190
  }
178
191
 
192
+ /**
193
+ * Clear image cache to free memory
194
+ */
195
+ public clearImageCache(): void {
196
+ this.imageCache.clear();
197
+ }
198
+
199
+ /**
200
+ * Get cache size for monitoring
201
+ */
202
+ public getCacheSize(): number {
203
+ return this.imageCache.size;
204
+ }
205
+
179
206
  /**
180
207
  * Create gradient
181
208
  */
@@ -7,6 +7,9 @@ import type { CanvasDimensions } from '../../domain/types/canvas.types';
7
7
  import type { BackgroundPattern } from '../../domain/types/screenshot.types';
8
8
 
9
9
  export class PatternRenderer {
10
+ private patternCanvasCache: Map<string, HTMLCanvasElement> = new Map();
11
+ private readonly MAX_CACHE_SIZE = 10;
12
+
10
13
  /**
11
14
  * Render pattern on canvas
12
15
  */
@@ -16,6 +19,8 @@ export class PatternRenderer {
16
19
  dimensions: CanvasDimensions,
17
20
  color: string = '#000000'
18
21
  ): void {
22
+ if (pattern === 'none') return;
23
+
19
24
  ctx.save();
20
25
  ctx.globalAlpha = 0.1;
21
26
 
@@ -35,17 +40,20 @@ export class PatternRenderer {
35
40
  case 'checkers':
36
41
  this.renderCheckerPattern(ctx, dimensions, color);
37
42
  break;
38
- case 'none':
39
- default:
40
- // Do nothing
41
- break;
42
43
  }
43
44
 
44
45
  ctx.restore();
45
46
  }
46
47
 
47
48
  /**
48
- * Render dot pattern
49
+ * Clear pattern cache to free memory
50
+ */
51
+ public clearCache(): void {
52
+ this.patternCanvasCache.clear();
53
+ }
54
+
55
+ /**
56
+ * Render dot pattern - optimized with batch rendering
49
57
  */
50
58
  private renderDotPattern(
51
59
  ctx: CanvasRenderingContext2D,
@@ -54,18 +62,21 @@ export class PatternRenderer {
54
62
  ): void {
55
63
  ctx.fillStyle = color;
56
64
  const spacing = 30;
65
+ const radius = 2;
57
66
 
67
+ // Batch rendering for better performance
68
+ ctx.beginPath();
58
69
  for (let x = spacing; x < dimensions.width; x += spacing) {
59
70
  for (let y = spacing; y < dimensions.height; y += spacing) {
60
- ctx.beginPath();
61
- ctx.arc(x, y, 2, 0, Math.PI * 2);
62
- ctx.fill();
71
+ ctx.moveTo(x + radius, y);
72
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
63
73
  }
64
74
  }
75
+ ctx.fill();
65
76
  }
66
77
 
67
78
  /**
68
- * Render grid pattern
79
+ * Render grid pattern - optimized with single path
69
80
  */
70
81
  private renderGridPattern(
71
82
  ctx: CanvasRenderingContext2D,
@@ -76,23 +87,25 @@ export class PatternRenderer {
76
87
  ctx.lineWidth = 1;
77
88
  const spacing = 30;
78
89
 
90
+ ctx.beginPath();
91
+
92
+ // Vertical lines
79
93
  for (let x = 0; x <= dimensions.width; x += spacing) {
80
- ctx.beginPath();
81
94
  ctx.moveTo(x, 0);
82
95
  ctx.lineTo(x, dimensions.height);
83
- ctx.stroke();
84
96
  }
85
97
 
98
+ // Horizontal lines
86
99
  for (let y = 0; y <= dimensions.height; y += spacing) {
87
- ctx.beginPath();
88
100
  ctx.moveTo(0, y);
89
101
  ctx.lineTo(dimensions.width, y);
90
- ctx.stroke();
91
102
  }
103
+
104
+ ctx.stroke();
92
105
  }
93
106
 
94
107
  /**
95
- * Render line pattern
108
+ * Render line pattern - optimized with single path
96
109
  */
97
110
  private renderLinePattern(
98
111
  ctx: CanvasRenderingContext2D,
@@ -103,16 +116,19 @@ export class PatternRenderer {
103
116
  ctx.lineWidth = 2;
104
117
  const spacing = 20;
105
118
 
119
+ ctx.beginPath();
120
+
121
+ // Draw all lines in a single path
106
122
  for (let y = spacing; y < dimensions.height; y += spacing) {
107
- ctx.beginPath();
108
123
  ctx.moveTo(0, y);
109
124
  ctx.lineTo(dimensions.width, y);
110
- ctx.stroke();
111
125
  }
126
+
127
+ ctx.stroke();
112
128
  }
113
129
 
114
130
  /**
115
- * Render wave pattern
131
+ * Render wave pattern - optimized with pre-calculated values
116
132
  */
117
133
  private renderWavePattern(
118
134
  ctx: CanvasRenderingContext2D,
@@ -124,17 +140,21 @@ export class PatternRenderer {
124
140
  const amplitude = 10;
125
141
  const frequency = 0.1;
126
142
  const spacing = 40;
143
+ const step = 5;
127
144
 
128
145
  for (let y = spacing; y < dimensions.height; y += spacing) {
129
146
  ctx.beginPath();
130
- for (let x = 0; x <= dimensions.width; x += 5) {
147
+
148
+ // Pre-calculate wave points and use more efficient path drawing
149
+ const startX = 0;
150
+ const startY = y + Math.sin(startX * frequency) * amplitude;
151
+ ctx.moveTo(startX, startY);
152
+
153
+ for (let x = step; x <= dimensions.width; x += step) {
131
154
  const waveY = y + Math.sin(x * frequency) * amplitude;
132
- if (x === 0) {
133
- ctx.moveTo(x, waveY);
134
- } else {
135
- ctx.lineTo(x, waveY);
136
- }
155
+ ctx.lineTo(x, waveY);
137
156
  }
157
+
138
158
  ctx.stroke();
139
159
  }
140
160
  }
@@ -22,6 +22,8 @@ export class ScreenshotRenderer {
22
22
  private canvasRenderer: CanvasRenderer;
23
23
  private patternRenderer: PatternRenderer;
24
24
  private deviceConfigService: DeviceConfigService;
25
+ private renderCache: Map<string, HTMLCanvasElement> = new Map();
26
+ private readonly MAX_CACHE_SIZE = 5;
25
27
 
26
28
  constructor() {
27
29
  this.canvasRenderer = new CanvasRenderer();
@@ -29,6 +31,15 @@ export class ScreenshotRenderer {
29
31
  this.deviceConfigService = DeviceConfigService.getInstance();
30
32
  }
31
33
 
34
+ /**
35
+ * Clear render cache to free memory
36
+ */
37
+ public clearCache(): void {
38
+ this.renderCache.clear();
39
+ this.canvasRenderer.clearImageCache?.();
40
+ this.patternRenderer.clearCache?.();
41
+ }
42
+
32
43
  /**
33
44
  * Render device frame
34
45
  */
@@ -108,13 +119,14 @@ export class ScreenshotRenderer {
108
119
  }
109
120
 
110
121
  /**
111
- * Render background
122
+ * Render background - optimized
112
123
  */
113
124
  public renderBackground(
114
125
  ctx: CanvasRenderingContext2D,
115
126
  design: ScreenshotDesign,
116
127
  dimensions: CanvasDimensions
117
128
  ): void {
129
+ // Fill background
118
130
  if (design.backgroundGradientEnd) {
119
131
  const gradient = this.canvasRenderer.createLinearGradient(
120
132
  ctx,
@@ -134,6 +146,7 @@ export class ScreenshotRenderer {
134
146
 
135
147
  ctx.fillRect(0, 0, dimensions.width, dimensions.height);
136
148
 
149
+ // Render pattern if specified (and not none)
137
150
  if (design.backgroundPattern && design.backgroundPattern !== 'none') {
138
151
  this.patternRenderer.renderPattern(
139
152
  ctx,
@@ -12,6 +12,9 @@ import { DEVICE_DIMENSIONS } from '../../domain/config/DeviceConfig';
12
12
  export class DeviceConfigService {
13
13
  private static instance: DeviceConfigService;
14
14
  private devices: Map<DeviceType, DeviceConfig>;
15
+ private phonesCache: DeviceConfig[] | null = null;
16
+ private tabletsCache: DeviceConfig[] | null = null;
17
+ private allDevicesCache: DeviceConfig[] | null = null;
15
18
 
16
19
  private constructor() {
17
20
  this.devices = new Map();
@@ -116,15 +119,27 @@ export class DeviceConfigService {
116
119
  }
117
120
 
118
121
  public getAllDevices(): DeviceConfig[] {
119
- return Array.from(this.devices.values());
122
+ // Use cached value if available
123
+ if (!this.allDevicesCache) {
124
+ this.allDevicesCache = Array.from(this.devices.values());
125
+ }
126
+ return this.allDevicesCache;
120
127
  }
121
128
 
122
129
  public getPhones(): DeviceConfig[] {
123
- return this.getAllDevices().filter((d) => d.isPhone);
130
+ // Use cached value if available
131
+ if (!this.phonesCache) {
132
+ this.phonesCache = this.getAllDevices().filter((d) => d.isPhone);
133
+ }
134
+ return this.phonesCache;
124
135
  }
125
136
 
126
137
  public getTablets(): DeviceConfig[] {
127
- return this.getAllDevices().filter((d) => d.isTablet);
138
+ // Use cached value if available
139
+ if (!this.tabletsCache) {
140
+ this.tabletsCache = this.getAllDevices().filter((d) => d.isTablet);
141
+ }
142
+ return this.tabletsCache;
128
143
  }
129
144
 
130
145
  public isPhone(deviceType: DeviceType): boolean {
@@ -3,7 +3,7 @@
3
3
  * React hook for canvas export operations
4
4
  */
5
5
 
6
- import { useState, useCallback } from 'react';
6
+ import { useState, useCallback, useRef, useEffect } from 'react';
7
7
  import type { CanvasExportOptions, CanvasResult } from '../../domain/types/canvas.types';
8
8
  import type { CanvasConfig } from '../../domain/config/CanvasConfig';
9
9
  import { DEFAULT_CANVAS_CONFIG } from '../../domain/config/CanvasConfig';
@@ -52,20 +52,50 @@ export function useCanvasExport(
52
52
  const [progress, setProgress] = useState(0);
53
53
  const [error, setError] = useState<string | null>(null);
54
54
 
55
- const renderer = new CanvasRenderer(config);
55
+ // Use ref to prevent recreating renderer on every render
56
+ const rendererRef = useRef<CanvasRenderer | null>(null);
57
+ const urlCleanupRef = useRef<string[]>([]);
58
+
59
+ // Initialize renderer once
60
+ if (!rendererRef.current) {
61
+ rendererRef.current = new CanvasRenderer(config);
62
+ }
63
+
64
+ // Cleanup URLs on unmount
65
+ useEffect(() => {
66
+ return () => {
67
+ // Clean up any remaining URLs
68
+ urlCleanupRef.current.forEach(url => {
69
+ try {
70
+ URL.revokeObjectURL(url);
71
+ } catch (e) {
72
+ // Ignore errors during cleanup
73
+ }
74
+ });
75
+ urlCleanupRef.current = [];
76
+ };
77
+ }, []);
56
78
 
57
79
  const exportToBlob = useCallback(
58
80
  async (
59
81
  canvas: HTMLCanvasElement,
60
82
  options?: CanvasExportOptions
61
83
  ): Promise<CanvasResult | null> => {
84
+ if (!rendererRef.current) return null;
85
+
62
86
  setIsExporting(true);
63
87
  setProgress(0);
64
88
  setError(null);
65
89
 
66
90
  try {
67
- const result = await renderer.exportToBlob(canvas, options);
91
+ const result = await rendererRef.current.exportToBlob(canvas, options);
68
92
  setProgress(100);
93
+
94
+ // Track URL for cleanup
95
+ if (result?.url) {
96
+ urlCleanupRef.current.push(result.url);
97
+ }
98
+
69
99
  return result;
70
100
  } catch (err) {
71
101
  const errorMessage =
@@ -76,7 +106,7 @@ export function useCanvasExport(
76
106
  setIsExporting(false);
77
107
  }
78
108
  },
79
- [renderer]
109
+ []
80
110
  );
81
111
 
82
112
  const exportToDataURL = useCallback(
@@ -85,8 +115,10 @@ export function useCanvasExport(
85
115
  format?: string,
86
116
  quality?: number
87
117
  ): string | null => {
118
+ if (!rendererRef.current) return null;
119
+
88
120
  try {
89
- return renderer.exportToDataURL(canvas, format, quality);
121
+ return rendererRef.current.exportToDataURL(canvas, format, quality);
90
122
  } catch (err) {
91
123
  const errorMessage =
92
124
  err instanceof Error ? err.message : 'Export failed';
@@ -94,7 +126,7 @@ export function useCanvasExport(
94
126
  return null;
95
127
  }
96
128
  },
97
- [renderer]
129
+ []
98
130
  );
99
131
 
100
132
  const downloadCanvas = useCallback(
@@ -122,8 +154,14 @@ export function useCanvasExport(
122
154
 
123
155
  setProgress(100);
124
156
 
157
+ // Schedule URL cleanup after a delay
125
158
  setTimeout(() => {
126
159
  URL.revokeObjectURL(result.url);
160
+ // Remove from cleanup tracking
161
+ const index = urlCleanupRef.current.indexOf(result.url);
162
+ if (index > -1) {
163
+ urlCleanupRef.current.splice(index, 1);
164
+ }
127
165
  }, 100);
128
166
  } catch (err) {
129
167
  const errorMessage =
@@ -138,8 +176,10 @@ export function useCanvasExport(
138
176
 
139
177
  const cloneCanvas = useCallback(
140
178
  (canvas: HTMLCanvasElement): HTMLCanvasElement | null => {
179
+ if (!rendererRef.current) return null;
180
+
141
181
  try {
142
- return renderer.cloneCanvas(canvas);
182
+ return rendererRef.current.cloneCanvas(canvas);
143
183
  } catch (err) {
144
184
  const errorMessage =
145
185
  err instanceof Error ? err.message : 'Clone failed';
@@ -147,7 +187,7 @@ export function useCanvasExport(
147
187
  return null;
148
188
  }
149
189
  },
150
- [renderer]
190
+ []
151
191
  );
152
192
 
153
193
  return {
@@ -3,7 +3,7 @@
3
3
  * React hook for report export operations
4
4
  */
5
5
 
6
- import { useState, useCallback } from 'react';
6
+ import { useState, useCallback, useRef, useEffect } from 'react';
7
7
  import type { ReportData } from '../../domain/types/report.types';
8
8
  import type {
9
9
  PDFExportOptions,
@@ -47,15 +47,25 @@ export function useReportExport(): UseReportExportReturn {
47
47
  const [isExporting, setIsExporting] = useState(false);
48
48
  const [error, setError] = useState<string | null>(null);
49
49
 
50
- const reportService = ReportService.getInstance();
50
+ // Use ref to prevent recreating service instance on every render
51
+ const reportServiceRef = useRef<ReportService | null>(null);
52
+
53
+ // Initialize service once
54
+ if (!reportServiceRef.current) {
55
+ reportServiceRef.current = ReportService.getInstance();
56
+ }
51
57
 
52
58
  const exportToPDF = useCallback(
53
59
  async (elementId: string, options?: PDFExportOptions): Promise<void> => {
60
+ if (!reportServiceRef.current) {
61
+ throw new Error('ReportService not initialized');
62
+ }
63
+
54
64
  setIsExporting(true);
55
65
  setError(null);
56
66
 
57
67
  try {
58
- await reportService.exportToPDF(elementId, options);
68
+ await reportServiceRef.current.exportToPDF(elementId, options);
59
69
  } catch (err) {
60
70
  const errorMessage =
61
71
  err instanceof Error ? err.message : 'PDF export failed';
@@ -65,7 +75,7 @@ export function useReportExport(): UseReportExportReturn {
65
75
  setIsExporting(false);
66
76
  }
67
77
  },
68
- [reportService]
78
+ []
69
79
  );
70
80
 
71
81
  const exportCanvasToPDF = useCallback(
@@ -73,11 +83,15 @@ export function useReportExport(): UseReportExportReturn {
73
83
  canvas: HTMLCanvasElement,
74
84
  options?: PDFExportOptions
75
85
  ): Promise<void> => {
86
+ if (!reportServiceRef.current) {
87
+ throw new Error('ReportService not initialized');
88
+ }
89
+
76
90
  setIsExporting(true);
77
91
  setError(null);
78
92
 
79
93
  try {
80
- await reportService.exportCanvasToPDF(canvas, options);
94
+ await reportServiceRef.current.exportCanvasToPDF(canvas, options);
81
95
  } catch (err) {
82
96
  const errorMessage =
83
97
  err instanceof Error ? err.message : 'Canvas PDF export failed';
@@ -87,7 +101,7 @@ export function useReportExport(): UseReportExportReturn {
87
101
  setIsExporting(false);
88
102
  }
89
103
  },
90
- [reportService]
104
+ []
91
105
  );
92
106
 
93
107
  const exportMultipleToPDF = useCallback(
@@ -95,11 +109,15 @@ export function useReportExport(): UseReportExportReturn {
95
109
  elementIds: string[],
96
110
  options?: PDFExportOptions
97
111
  ): Promise<void> => {
112
+ if (!reportServiceRef.current) {
113
+ throw new Error('ReportService not initialized');
114
+ }
115
+
98
116
  setIsExporting(true);
99
117
  setError(null);
100
118
 
101
119
  try {
102
- await reportService.exportMultipleToPDF(elementIds, options);
120
+ await reportServiceRef.current.exportMultipleToPDF(elementIds, options);
103
121
  } catch (err) {
104
122
  const errorMessage =
105
123
  err instanceof Error ? err.message : 'Multi-page PDF export failed';
@@ -109,16 +127,20 @@ export function useReportExport(): UseReportExportReturn {
109
127
  setIsExporting(false);
110
128
  }
111
129
  },
112
- [reportService]
130
+ []
113
131
  );
114
132
 
115
133
  const exportToExcel = useCallback(
116
134
  async (data: ReportData, options?: ExcelExportOptions): Promise<void> => {
135
+ if (!reportServiceRef.current) {
136
+ throw new Error('ReportService not initialized');
137
+ }
138
+
117
139
  setIsExporting(true);
118
140
  setError(null);
119
141
 
120
142
  try {
121
- await reportService.exportToExcel(data, options);
143
+ await reportServiceRef.current.exportToExcel(data, options);
122
144
  } catch (err) {
123
145
  const errorMessage =
124
146
  err instanceof Error ? err.message : 'Excel export failed';
@@ -128,7 +150,7 @@ export function useReportExport(): UseReportExportReturn {
128
150
  setIsExporting(false);
129
151
  }
130
152
  },
131
- [reportService]
153
+ []
132
154
  );
133
155
 
134
156
  const exportToCSV = useCallback(
@@ -137,8 +159,12 @@ export function useReportExport(): UseReportExportReturn {
137
159
  filename?: string,
138
160
  delimiter?: ',' | ';'
139
161
  ): void => {
162
+ if (!reportServiceRef.current) {
163
+ throw new Error('ReportService not initialized');
164
+ }
165
+
140
166
  try {
141
- reportService.exportToCSV(data, filename, delimiter);
167
+ reportServiceRef.current.exportToCSV(data, filename, delimiter);
142
168
  } catch (err) {
143
169
  const errorMessage =
144
170
  err instanceof Error ? err.message : 'CSV export failed';
@@ -146,7 +172,7 @@ export function useReportExport(): UseReportExportReturn {
146
172
  throw err;
147
173
  }
148
174
  },
149
- [reportService]
175
+ []
150
176
  );
151
177
 
152
178
  return {
@@ -3,7 +3,7 @@
3
3
  * React hook for screenshot studio functionality
4
4
  */
5
5
 
6
- import { useState, useCallback, useRef } from 'react';
6
+ import { useState, useCallback, useRef, useEffect } from 'react';
7
7
  import type {
8
8
  ScreenshotContent,
9
9
  ScreenshotDesign,
@@ -198,9 +198,22 @@ export function useScreenshotStudio(
198
198
  ...config,
199
199
  };
200
200
 
201
- const serviceRef = useRef(
202
- new ScreenshotStudioService(fullConfig)
203
- );
201
+ const serviceRef = useRef<ScreenshotStudioService | null>(null);
202
+
203
+ // Initialize service once
204
+ if (!serviceRef.current) {
205
+ serviceRef.current = new ScreenshotStudioService(fullConfig);
206
+ }
207
+
208
+ // Cleanup on unmount
209
+ useEffect(() => {
210
+ return () => {
211
+ // Clean up service resources if needed
212
+ if (serviceRef.current) {
213
+ // Any cleanup logic can go here
214
+ }
215
+ };
216
+ }, []);
204
217
 
205
218
  const [screens, setScreens] = useState<ScreenshotContent[]>(DEFAULT_SCREENS);
206
219
  const [design, setDesign] = useState<ScreenshotDesign>(DEFAULT_DESIGN);
@@ -342,6 +355,8 @@ export function useScreenshotStudio(
342
355
 
343
356
  const generateDesignedScreenshot = useCallback(
344
357
  async (screenId: string) => {
358
+ if (!serviceRef.current) return;
359
+
345
360
  const screen = screens.find((s) => s.id === screenId);
346
361
  if (!screen) return;
347
362
 
@@ -364,7 +379,11 @@ export function useScreenshotStudio(
364
379
 
365
380
  const exportAllScreensAsZip = useCallback(
366
381
  async (format: ScreenshotFormat = 'png') => {
382
+ if (!serviceRef.current) return;
383
+
367
384
  setIsProcessing(true);
385
+ let url: string | null = null;
386
+
368
387
  try {
369
388
  const project: ScreenshotProject = {
370
389
  id: projectId || 'default-project',
@@ -380,12 +399,11 @@ export function useScreenshotStudio(
380
399
  };
381
400
 
382
401
  const blob = await serviceRef.current.exportProjectAsZip(project);
383
- const url = URL.createObjectURL(blob);
402
+ url = URL.createObjectURL(blob);
384
403
  const link = document.createElement('a');
385
404
  link.href = url;
386
405
  link.download = `Studio_${currentLanguage}_${Date.now()}.zip`;
387
406
  link.click();
388
- URL.revokeObjectURL(url);
389
407
 
390
408
  onShowToast(
391
409
  'Success',
@@ -395,6 +413,17 @@ export function useScreenshotStudio(
395
413
  onShowToast('Error', 'ZIP export failed');
396
414
  } finally {
397
415
  setIsProcessing(false);
416
+
417
+ // Clean up object URL after a delay
418
+ if (url) {
419
+ setTimeout(() => {
420
+ try {
421
+ URL.revokeObjectURL(url!);
422
+ } catch (e) {
423
+ // Ignore cleanup errors
424
+ }
425
+ }, 100);
426
+ }
398
427
  }
399
428
  },
400
429
  [