@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.
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  ReportService,
3
3
  ScreenshotStudioService
4
- } from "./chunk-BN7JTXL7.mjs";
4
+ } from "./chunk-DNKRSL6D.mjs";
5
5
  import {
6
6
  CanvasRenderer
7
- } from "./chunk-WMKPCJFI.mjs";
7
+ } from "./chunk-UOGTEMIJ.mjs";
8
8
  import {
9
9
  DEFAULT_SCREENSHOT_STUDIO_CONFIG
10
10
  } from "./chunk-Z6IB4BQV.mjs";
@@ -13,7 +13,7 @@ import {
13
13
  } from "./chunk-MC6PNV2F.mjs";
14
14
 
15
15
  // src/presentation/hooks/useScreenshotStudio.ts
16
- import { useState, useCallback, useRef } from "react";
16
+ import { useState, useCallback, useRef, useEffect } from "react";
17
17
  var DEFAULT_SCREENS = [
18
18
  {
19
19
  id: "1",
@@ -107,9 +107,16 @@ function useScreenshotStudio(options = {}) {
107
107
  ...DEFAULT_SCREENSHOT_STUDIO_CONFIG,
108
108
  ...config
109
109
  };
110
- const serviceRef = useRef(
111
- new ScreenshotStudioService(fullConfig)
112
- );
110
+ const serviceRef = useRef(null);
111
+ if (!serviceRef.current) {
112
+ serviceRef.current = new ScreenshotStudioService(fullConfig);
113
+ }
114
+ useEffect(() => {
115
+ return () => {
116
+ if (serviceRef.current) {
117
+ }
118
+ };
119
+ }, []);
113
120
  const [screens, setScreens] = useState(DEFAULT_SCREENS);
114
121
  const [design, setDesign] = useState(DEFAULT_DESIGN);
115
122
  const [currentLanguage, setCurrentLanguage] = useState(
@@ -230,6 +237,7 @@ function useScreenshotStudio(options = {}) {
230
237
  );
231
238
  const generateDesignedScreenshot = useCallback(
232
239
  async (screenId) => {
240
+ if (!serviceRef.current) return;
233
241
  const screen = screens.find((s) => s.id === screenId);
234
242
  if (!screen) return;
235
243
  setIsProcessing(true);
@@ -250,7 +258,9 @@ function useScreenshotStudio(options = {}) {
250
258
  );
251
259
  const exportAllScreensAsZip = useCallback(
252
260
  async (format = "png") => {
261
+ if (!serviceRef.current) return;
253
262
  setIsProcessing(true);
263
+ let url = null;
254
264
  try {
255
265
  const project = {
256
266
  id: projectId || "default-project",
@@ -265,12 +275,11 @@ function useScreenshotStudio(options = {}) {
265
275
  updatedAt: /* @__PURE__ */ new Date()
266
276
  };
267
277
  const blob = await serviceRef.current.exportProjectAsZip(project);
268
- const url = URL.createObjectURL(blob);
278
+ url = URL.createObjectURL(blob);
269
279
  const link = document.createElement("a");
270
280
  link.href = url;
271
281
  link.download = `Studio_${currentLanguage}_${Date.now()}.zip`;
272
282
  link.click();
273
- URL.revokeObjectURL(url);
274
283
  onShowToast(
275
284
  "Success",
276
285
  `Exported ${screens.length} screens in ZIP archive.`
@@ -279,6 +288,14 @@ function useScreenshotStudio(options = {}) {
279
288
  onShowToast("Error", "ZIP export failed");
280
289
  } finally {
281
290
  setIsProcessing(false);
291
+ if (url) {
292
+ setTimeout(() => {
293
+ try {
294
+ URL.revokeObjectURL(url);
295
+ } catch (e) {
296
+ }
297
+ }, 100);
298
+ }
282
299
  }
283
300
  },
284
301
  [
@@ -358,21 +375,40 @@ function useScreenshotStudio(options = {}) {
358
375
  }
359
376
 
360
377
  // src/presentation/hooks/useCanvasExport.ts
361
- import { useState as useState2, useCallback as useCallback2 } from "react";
378
+ import { useState as useState2, useCallback as useCallback2, useRef as useRef2, useEffect as useEffect2 } from "react";
362
379
  function useCanvasExport(options = {}) {
363
380
  const { config = DEFAULT_CANVAS_CONFIG } = options;
364
381
  const [isExporting, setIsExporting] = useState2(false);
365
382
  const [progress, setProgress] = useState2(0);
366
383
  const [error, setError] = useState2(null);
367
- const renderer = new CanvasRenderer(config);
384
+ const rendererRef = useRef2(null);
385
+ const urlCleanupRef = useRef2([]);
386
+ if (!rendererRef.current) {
387
+ rendererRef.current = new CanvasRenderer(config);
388
+ }
389
+ useEffect2(() => {
390
+ return () => {
391
+ urlCleanupRef.current.forEach((url) => {
392
+ try {
393
+ URL.revokeObjectURL(url);
394
+ } catch (e) {
395
+ }
396
+ });
397
+ urlCleanupRef.current = [];
398
+ };
399
+ }, []);
368
400
  const exportToBlob = useCallback2(
369
401
  async (canvas, options2) => {
402
+ if (!rendererRef.current) return null;
370
403
  setIsExporting(true);
371
404
  setProgress(0);
372
405
  setError(null);
373
406
  try {
374
- const result = await renderer.exportToBlob(canvas, options2);
407
+ const result = await rendererRef.current.exportToBlob(canvas, options2);
375
408
  setProgress(100);
409
+ if (result?.url) {
410
+ urlCleanupRef.current.push(result.url);
411
+ }
376
412
  return result;
377
413
  } catch (err) {
378
414
  const errorMessage = err instanceof Error ? err.message : "Export failed";
@@ -382,19 +418,20 @@ function useCanvasExport(options = {}) {
382
418
  setIsExporting(false);
383
419
  }
384
420
  },
385
- [renderer]
421
+ []
386
422
  );
387
423
  const exportToDataURL = useCallback2(
388
424
  (canvas, format, quality) => {
425
+ if (!rendererRef.current) return null;
389
426
  try {
390
- return renderer.exportToDataURL(canvas, format, quality);
427
+ return rendererRef.current.exportToDataURL(canvas, format, quality);
391
428
  } catch (err) {
392
429
  const errorMessage = err instanceof Error ? err.message : "Export failed";
393
430
  setError(errorMessage);
394
431
  return null;
395
432
  }
396
433
  },
397
- [renderer]
434
+ []
398
435
  );
399
436
  const downloadCanvas = useCallback2(
400
437
  async (canvas, filename, options2) => {
@@ -414,6 +451,10 @@ function useCanvasExport(options = {}) {
414
451
  setProgress(100);
415
452
  setTimeout(() => {
416
453
  URL.revokeObjectURL(result.url);
454
+ const index = urlCleanupRef.current.indexOf(result.url);
455
+ if (index > -1) {
456
+ urlCleanupRef.current.splice(index, 1);
457
+ }
417
458
  }, 100);
418
459
  } catch (err) {
419
460
  const errorMessage = err instanceof Error ? err.message : "Download failed";
@@ -426,15 +467,16 @@ function useCanvasExport(options = {}) {
426
467
  );
427
468
  const cloneCanvas = useCallback2(
428
469
  (canvas) => {
470
+ if (!rendererRef.current) return null;
429
471
  try {
430
- return renderer.cloneCanvas(canvas);
472
+ return rendererRef.current.cloneCanvas(canvas);
431
473
  } catch (err) {
432
474
  const errorMessage = err instanceof Error ? err.message : "Clone failed";
433
475
  setError(errorMessage);
434
476
  return null;
435
477
  }
436
478
  },
437
- [renderer]
479
+ []
438
480
  );
439
481
  return {
440
482
  isExporting,
@@ -448,17 +490,23 @@ function useCanvasExport(options = {}) {
448
490
  }
449
491
 
450
492
  // src/presentation/hooks/useReportExport.ts
451
- import { useState as useState3, useCallback as useCallback3 } from "react";
493
+ import { useState as useState3, useCallback as useCallback3, useRef as useRef3 } from "react";
452
494
  function useReportExport() {
453
495
  const [isExporting, setIsExporting] = useState3(false);
454
496
  const [error, setError] = useState3(null);
455
- const reportService = ReportService.getInstance();
497
+ const reportServiceRef = useRef3(null);
498
+ if (!reportServiceRef.current) {
499
+ reportServiceRef.current = ReportService.getInstance();
500
+ }
456
501
  const exportToPDF = useCallback3(
457
502
  async (elementId, options) => {
503
+ if (!reportServiceRef.current) {
504
+ throw new Error("ReportService not initialized");
505
+ }
458
506
  setIsExporting(true);
459
507
  setError(null);
460
508
  try {
461
- await reportService.exportToPDF(elementId, options);
509
+ await reportServiceRef.current.exportToPDF(elementId, options);
462
510
  } catch (err) {
463
511
  const errorMessage = err instanceof Error ? err.message : "PDF export failed";
464
512
  setError(errorMessage);
@@ -467,14 +515,17 @@ function useReportExport() {
467
515
  setIsExporting(false);
468
516
  }
469
517
  },
470
- [reportService]
518
+ []
471
519
  );
472
520
  const exportCanvasToPDF = useCallback3(
473
521
  async (canvas, options) => {
522
+ if (!reportServiceRef.current) {
523
+ throw new Error("ReportService not initialized");
524
+ }
474
525
  setIsExporting(true);
475
526
  setError(null);
476
527
  try {
477
- await reportService.exportCanvasToPDF(canvas, options);
528
+ await reportServiceRef.current.exportCanvasToPDF(canvas, options);
478
529
  } catch (err) {
479
530
  const errorMessage = err instanceof Error ? err.message : "Canvas PDF export failed";
480
531
  setError(errorMessage);
@@ -483,14 +534,17 @@ function useReportExport() {
483
534
  setIsExporting(false);
484
535
  }
485
536
  },
486
- [reportService]
537
+ []
487
538
  );
488
539
  const exportMultipleToPDF = useCallback3(
489
540
  async (elementIds, options) => {
541
+ if (!reportServiceRef.current) {
542
+ throw new Error("ReportService not initialized");
543
+ }
490
544
  setIsExporting(true);
491
545
  setError(null);
492
546
  try {
493
- await reportService.exportMultipleToPDF(elementIds, options);
547
+ await reportServiceRef.current.exportMultipleToPDF(elementIds, options);
494
548
  } catch (err) {
495
549
  const errorMessage = err instanceof Error ? err.message : "Multi-page PDF export failed";
496
550
  setError(errorMessage);
@@ -499,14 +553,17 @@ function useReportExport() {
499
553
  setIsExporting(false);
500
554
  }
501
555
  },
502
- [reportService]
556
+ []
503
557
  );
504
558
  const exportToExcel = useCallback3(
505
559
  async (data, options) => {
560
+ if (!reportServiceRef.current) {
561
+ throw new Error("ReportService not initialized");
562
+ }
506
563
  setIsExporting(true);
507
564
  setError(null);
508
565
  try {
509
- await reportService.exportToExcel(data, options);
566
+ await reportServiceRef.current.exportToExcel(data, options);
510
567
  } catch (err) {
511
568
  const errorMessage = err instanceof Error ? err.message : "Excel export failed";
512
569
  setError(errorMessage);
@@ -515,19 +572,22 @@ function useReportExport() {
515
572
  setIsExporting(false);
516
573
  }
517
574
  },
518
- [reportService]
575
+ []
519
576
  );
520
577
  const exportToCSV = useCallback3(
521
578
  (data, filename, delimiter) => {
579
+ if (!reportServiceRef.current) {
580
+ throw new Error("ReportService not initialized");
581
+ }
522
582
  try {
523
- reportService.exportToCSV(data, filename, delimiter);
583
+ reportServiceRef.current.exportToCSV(data, filename, delimiter);
524
584
  } catch (err) {
525
585
  const errorMessage = err instanceof Error ? err.message : "CSV export failed";
526
586
  setError(errorMessage);
527
587
  throw err;
528
588
  }
529
589
  },
530
- [reportService]
590
+ []
531
591
  );
532
592
  return {
533
593
  isExporting,
@@ -7,6 +7,7 @@ import {
7
7
  var CanvasRenderer = class {
8
8
  constructor(config = DEFAULT_CANVAS_CONFIG) {
9
9
  this.config = { ...DEFAULT_CANVAS_CONFIG, ...config };
10
+ this.imageCache = /* @__PURE__ */ new Map();
10
11
  }
11
12
  /**
12
13
  * Create a new canvas element
@@ -108,17 +109,37 @@ var CanvasRenderer = class {
108
109
  }
109
110
  }
110
111
  /**
111
- * Load image from URL
112
+ * Load image from URL with caching
112
113
  */
113
114
  loadImage(url) {
115
+ if (this.imageCache.has(url)) {
116
+ return Promise.resolve(this.imageCache.get(url));
117
+ }
114
118
  return new Promise((resolve, reject) => {
115
119
  const img = new Image();
116
- img.crossOrigin = this.config.useCORS ? "anonymous" : void 0;
117
- img.onload = () => resolve(img);
120
+ if (this.config.useCORS) {
121
+ img.crossOrigin = "anonymous";
122
+ }
123
+ img.onload = () => {
124
+ this.imageCache.set(url, img);
125
+ resolve(img);
126
+ };
118
127
  img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
119
128
  img.src = url;
120
129
  });
121
130
  }
131
+ /**
132
+ * Clear image cache to free memory
133
+ */
134
+ clearImageCache() {
135
+ this.imageCache.clear();
136
+ }
137
+ /**
138
+ * Get cache size for monitoring
139
+ */
140
+ getCacheSize() {
141
+ return this.imageCache.size;
142
+ }
122
143
  /**
123
144
  * Create gradient
124
145
  */
@@ -200,10 +221,15 @@ var CanvasRenderer = class {
200
221
 
201
222
  // src/infrastructure/renderers/PatternRenderer.ts
202
223
  var PatternRenderer = class {
224
+ constructor() {
225
+ this.patternCanvasCache = /* @__PURE__ */ new Map();
226
+ this.MAX_CACHE_SIZE = 10;
227
+ }
203
228
  /**
204
229
  * Render pattern on canvas
205
230
  */
206
231
  renderPattern(ctx, pattern, dimensions, color = "#000000") {
232
+ if (pattern === "none") return;
207
233
  ctx.save();
208
234
  ctx.globalAlpha = 0.1;
209
235
  switch (pattern) {
@@ -222,62 +248,65 @@ var PatternRenderer = class {
222
248
  case "checkers":
223
249
  this.renderCheckerPattern(ctx, dimensions, color);
224
250
  break;
225
- case "none":
226
- default:
227
- break;
228
251
  }
229
252
  ctx.restore();
230
253
  }
231
254
  /**
232
- * Render dot pattern
255
+ * Clear pattern cache to free memory
256
+ */
257
+ clearCache() {
258
+ this.patternCanvasCache.clear();
259
+ }
260
+ /**
261
+ * Render dot pattern - optimized with batch rendering
233
262
  */
234
263
  renderDotPattern(ctx, dimensions, color) {
235
264
  ctx.fillStyle = color;
236
265
  const spacing = 30;
266
+ const radius = 2;
267
+ ctx.beginPath();
237
268
  for (let x = spacing; x < dimensions.width; x += spacing) {
238
269
  for (let y = spacing; y < dimensions.height; y += spacing) {
239
- ctx.beginPath();
240
- ctx.arc(x, y, 2, 0, Math.PI * 2);
241
- ctx.fill();
270
+ ctx.moveTo(x + radius, y);
271
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
242
272
  }
243
273
  }
274
+ ctx.fill();
244
275
  }
245
276
  /**
246
- * Render grid pattern
277
+ * Render grid pattern - optimized with single path
247
278
  */
248
279
  renderGridPattern(ctx, dimensions, color) {
249
280
  ctx.strokeStyle = color;
250
281
  ctx.lineWidth = 1;
251
282
  const spacing = 30;
283
+ ctx.beginPath();
252
284
  for (let x = 0; x <= dimensions.width; x += spacing) {
253
- ctx.beginPath();
254
285
  ctx.moveTo(x, 0);
255
286
  ctx.lineTo(x, dimensions.height);
256
- ctx.stroke();
257
287
  }
258
288
  for (let y = 0; y <= dimensions.height; y += spacing) {
259
- ctx.beginPath();
260
289
  ctx.moveTo(0, y);
261
290
  ctx.lineTo(dimensions.width, y);
262
- ctx.stroke();
263
291
  }
292
+ ctx.stroke();
264
293
  }
265
294
  /**
266
- * Render line pattern
295
+ * Render line pattern - optimized with single path
267
296
  */
268
297
  renderLinePattern(ctx, dimensions, color) {
269
298
  ctx.strokeStyle = color;
270
299
  ctx.lineWidth = 2;
271
300
  const spacing = 20;
301
+ ctx.beginPath();
272
302
  for (let y = spacing; y < dimensions.height; y += spacing) {
273
- ctx.beginPath();
274
303
  ctx.moveTo(0, y);
275
304
  ctx.lineTo(dimensions.width, y);
276
- ctx.stroke();
277
305
  }
306
+ ctx.stroke();
278
307
  }
279
308
  /**
280
- * Render wave pattern
309
+ * Render wave pattern - optimized with pre-calculated values
281
310
  */
282
311
  renderWavePattern(ctx, dimensions, color) {
283
312
  ctx.strokeStyle = color;
@@ -285,15 +314,15 @@ var PatternRenderer = class {
285
314
  const amplitude = 10;
286
315
  const frequency = 0.1;
287
316
  const spacing = 40;
317
+ const step = 5;
288
318
  for (let y = spacing; y < dimensions.height; y += spacing) {
289
319
  ctx.beginPath();
290
- for (let x = 0; x <= dimensions.width; x += 5) {
320
+ const startX = 0;
321
+ const startY = y + Math.sin(startX * frequency) * amplitude;
322
+ ctx.moveTo(startX, startY);
323
+ for (let x = step; x <= dimensions.width; x += step) {
291
324
  const waveY = y + Math.sin(x * frequency) * amplitude;
292
- if (x === 0) {
293
- ctx.moveTo(x, waveY);
294
- } else {
295
- ctx.lineTo(x, waveY);
296
- }
325
+ ctx.lineTo(x, waveY);
297
326
  }
298
327
  ctx.stroke();
299
328
  }
@@ -316,6 +345,9 @@ var PatternRenderer = class {
316
345
  // src/infrastructure/services/DeviceConfigService.ts
317
346
  var DeviceConfigService = class _DeviceConfigService {
318
347
  constructor() {
348
+ this.phonesCache = null;
349
+ this.tabletsCache = null;
350
+ this.allDevicesCache = null;
319
351
  this.devices = /* @__PURE__ */ new Map();
320
352
  this.initializeDevices();
321
353
  }
@@ -412,13 +444,22 @@ var DeviceConfigService = class _DeviceConfigService {
412
444
  return this.devices.get(deviceType);
413
445
  }
414
446
  getAllDevices() {
415
- return Array.from(this.devices.values());
447
+ if (!this.allDevicesCache) {
448
+ this.allDevicesCache = Array.from(this.devices.values());
449
+ }
450
+ return this.allDevicesCache;
416
451
  }
417
452
  getPhones() {
418
- return this.getAllDevices().filter((d) => d.isPhone);
453
+ if (!this.phonesCache) {
454
+ this.phonesCache = this.getAllDevices().filter((d) => d.isPhone);
455
+ }
456
+ return this.phonesCache;
419
457
  }
420
458
  getTablets() {
421
- return this.getAllDevices().filter((d) => d.isTablet);
459
+ if (!this.tabletsCache) {
460
+ this.tabletsCache = this.getAllDevices().filter((d) => d.isTablet);
461
+ }
462
+ return this.tabletsCache;
422
463
  }
423
464
  isPhone(deviceType) {
424
465
  const config = this.getDeviceConfig(deviceType);
@@ -437,10 +478,20 @@ var DeviceConfigService = class _DeviceConfigService {
437
478
  // src/infrastructure/renderers/ScreenshotRenderer.ts
438
479
  var ScreenshotRenderer = class {
439
480
  constructor() {
481
+ this.renderCache = /* @__PURE__ */ new Map();
482
+ this.MAX_CACHE_SIZE = 5;
440
483
  this.canvasRenderer = new CanvasRenderer();
441
484
  this.patternRenderer = new PatternRenderer();
442
485
  this.deviceConfigService = DeviceConfigService.getInstance();
443
486
  }
487
+ /**
488
+ * Clear render cache to free memory
489
+ */
490
+ clearCache() {
491
+ this.renderCache.clear();
492
+ this.canvasRenderer.clearImageCache?.();
493
+ this.patternRenderer.clearCache?.();
494
+ }
444
495
  /**
445
496
  * Render device frame
446
497
  */
@@ -492,7 +543,7 @@ var ScreenshotRenderer = class {
492
543
  ctx.fill();
493
544
  }
494
545
  /**
495
- * Render background
546
+ * Render background - optimized
496
547
  */
497
548
  renderBackground(ctx, design, dimensions) {
498
549
  if (design.backgroundGradientEnd) {