@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 umituz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # @umituz/web-canvas
2
+
3
+ Comprehensive canvas and screenshot generation library with device frames, patterns, and PDF export.
4
+
5
+ ## Features
6
+
7
+ - Canvas-based screenshot generation
8
+ - Device frame templates (iOS, Android)
9
+ - PDF export functionality
10
+ - Pattern generation
11
+ - HTML2Canvas integration
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @umituz/web-canvas
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```typescript
22
+ import { generateScreenshot } from '@umituz/web-canvas';
23
+
24
+ const screenshot = await generateScreenshot({
25
+ url: 'https://example.com',
26
+ device: 'iphone-14-pro',
27
+ });
28
+ ```
29
+
30
+ ## License
31
+
32
+ MIT
@@ -40,7 +40,7 @@ declare class ScreenshotStudioService {
40
40
  */
41
41
  private getDefaultScreen;
42
42
  /**
43
- * Generate unique ID
43
+ * Generate unique ID - optimized version
44
44
  */
45
45
  private generateId;
46
46
  }
@@ -40,7 +40,7 @@ declare class ScreenshotStudioService {
40
40
  */
41
41
  private getDefaultScreen;
42
42
  /**
43
- * Generate unique ID
43
+ * Generate unique ID - optimized version
44
44
  */
45
45
  private generateId;
46
46
  }
@@ -77,6 +77,7 @@ var DEFAULT_CANVAS_CONFIG = {
77
77
  var CanvasRenderer = class {
78
78
  constructor(config = DEFAULT_CANVAS_CONFIG) {
79
79
  this.config = { ...DEFAULT_CANVAS_CONFIG, ...config };
80
+ this.imageCache = /* @__PURE__ */ new Map();
80
81
  }
81
82
  /**
82
83
  * Create a new canvas element
@@ -178,17 +179,37 @@ var CanvasRenderer = class {
178
179
  }
179
180
  }
180
181
  /**
181
- * Load image from URL
182
+ * Load image from URL with caching
182
183
  */
183
184
  loadImage(url) {
185
+ if (this.imageCache.has(url)) {
186
+ return Promise.resolve(this.imageCache.get(url));
187
+ }
184
188
  return new Promise((resolve, reject) => {
185
189
  const img = new Image();
186
- img.crossOrigin = this.config.useCORS ? "anonymous" : void 0;
187
- img.onload = () => resolve(img);
190
+ if (this.config.useCORS) {
191
+ img.crossOrigin = "anonymous";
192
+ }
193
+ img.onload = () => {
194
+ this.imageCache.set(url, img);
195
+ resolve(img);
196
+ };
188
197
  img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
189
198
  img.src = url;
190
199
  });
191
200
  }
201
+ /**
202
+ * Clear image cache to free memory
203
+ */
204
+ clearImageCache() {
205
+ this.imageCache.clear();
206
+ }
207
+ /**
208
+ * Get cache size for monitoring
209
+ */
210
+ getCacheSize() {
211
+ return this.imageCache.size;
212
+ }
192
213
  /**
193
214
  * Create gradient
194
215
  */
@@ -270,10 +291,15 @@ var CanvasRenderer = class {
270
291
 
271
292
  // src/infrastructure/renderers/PatternRenderer.ts
272
293
  var PatternRenderer = class {
294
+ constructor() {
295
+ this.patternCanvasCache = /* @__PURE__ */ new Map();
296
+ this.MAX_CACHE_SIZE = 10;
297
+ }
273
298
  /**
274
299
  * Render pattern on canvas
275
300
  */
276
301
  renderPattern(ctx, pattern, dimensions, color = "#000000") {
302
+ if (pattern === "none") return;
277
303
  ctx.save();
278
304
  ctx.globalAlpha = 0.1;
279
305
  switch (pattern) {
@@ -292,62 +318,65 @@ var PatternRenderer = class {
292
318
  case "checkers":
293
319
  this.renderCheckerPattern(ctx, dimensions, color);
294
320
  break;
295
- case "none":
296
- default:
297
- break;
298
321
  }
299
322
  ctx.restore();
300
323
  }
301
324
  /**
302
- * Render dot pattern
325
+ * Clear pattern cache to free memory
326
+ */
327
+ clearCache() {
328
+ this.patternCanvasCache.clear();
329
+ }
330
+ /**
331
+ * Render dot pattern - optimized with batch rendering
303
332
  */
304
333
  renderDotPattern(ctx, dimensions, color) {
305
334
  ctx.fillStyle = color;
306
335
  const spacing = 30;
336
+ const radius = 2;
337
+ ctx.beginPath();
307
338
  for (let x = spacing; x < dimensions.width; x += spacing) {
308
339
  for (let y = spacing; y < dimensions.height; y += spacing) {
309
- ctx.beginPath();
310
- ctx.arc(x, y, 2, 0, Math.PI * 2);
311
- ctx.fill();
340
+ ctx.moveTo(x + radius, y);
341
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
312
342
  }
313
343
  }
344
+ ctx.fill();
314
345
  }
315
346
  /**
316
- * Render grid pattern
347
+ * Render grid pattern - optimized with single path
317
348
  */
318
349
  renderGridPattern(ctx, dimensions, color) {
319
350
  ctx.strokeStyle = color;
320
351
  ctx.lineWidth = 1;
321
352
  const spacing = 30;
353
+ ctx.beginPath();
322
354
  for (let x = 0; x <= dimensions.width; x += spacing) {
323
- ctx.beginPath();
324
355
  ctx.moveTo(x, 0);
325
356
  ctx.lineTo(x, dimensions.height);
326
- ctx.stroke();
327
357
  }
328
358
  for (let y = 0; y <= dimensions.height; y += spacing) {
329
- ctx.beginPath();
330
359
  ctx.moveTo(0, y);
331
360
  ctx.lineTo(dimensions.width, y);
332
- ctx.stroke();
333
361
  }
362
+ ctx.stroke();
334
363
  }
335
364
  /**
336
- * Render line pattern
365
+ * Render line pattern - optimized with single path
337
366
  */
338
367
  renderLinePattern(ctx, dimensions, color) {
339
368
  ctx.strokeStyle = color;
340
369
  ctx.lineWidth = 2;
341
370
  const spacing = 20;
371
+ ctx.beginPath();
342
372
  for (let y = spacing; y < dimensions.height; y += spacing) {
343
- ctx.beginPath();
344
373
  ctx.moveTo(0, y);
345
374
  ctx.lineTo(dimensions.width, y);
346
- ctx.stroke();
347
375
  }
376
+ ctx.stroke();
348
377
  }
349
378
  /**
350
- * Render wave pattern
379
+ * Render wave pattern - optimized with pre-calculated values
351
380
  */
352
381
  renderWavePattern(ctx, dimensions, color) {
353
382
  ctx.strokeStyle = color;
@@ -355,15 +384,15 @@ var PatternRenderer = class {
355
384
  const amplitude = 10;
356
385
  const frequency = 0.1;
357
386
  const spacing = 40;
387
+ const step = 5;
358
388
  for (let y = spacing; y < dimensions.height; y += spacing) {
359
389
  ctx.beginPath();
360
- for (let x = 0; x <= dimensions.width; x += 5) {
390
+ const startX = 0;
391
+ const startY = y + Math.sin(startX * frequency) * amplitude;
392
+ ctx.moveTo(startX, startY);
393
+ for (let x = step; x <= dimensions.width; x += step) {
361
394
  const waveY = y + Math.sin(x * frequency) * amplitude;
362
- if (x === 0) {
363
- ctx.moveTo(x, waveY);
364
- } else {
365
- ctx.lineTo(x, waveY);
366
- }
395
+ ctx.lineTo(x, waveY);
367
396
  }
368
397
  ctx.stroke();
369
398
  }
@@ -405,6 +434,9 @@ var DEFAULT_DEVICE_CONFIG = {
405
434
  // src/infrastructure/services/DeviceConfigService.ts
406
435
  var DeviceConfigService = class _DeviceConfigService {
407
436
  constructor() {
437
+ this.phonesCache = null;
438
+ this.tabletsCache = null;
439
+ this.allDevicesCache = null;
408
440
  this.devices = /* @__PURE__ */ new Map();
409
441
  this.initializeDevices();
410
442
  }
@@ -501,13 +533,22 @@ var DeviceConfigService = class _DeviceConfigService {
501
533
  return this.devices.get(deviceType);
502
534
  }
503
535
  getAllDevices() {
504
- return Array.from(this.devices.values());
536
+ if (!this.allDevicesCache) {
537
+ this.allDevicesCache = Array.from(this.devices.values());
538
+ }
539
+ return this.allDevicesCache;
505
540
  }
506
541
  getPhones() {
507
- return this.getAllDevices().filter((d) => d.isPhone);
542
+ if (!this.phonesCache) {
543
+ this.phonesCache = this.getAllDevices().filter((d) => d.isPhone);
544
+ }
545
+ return this.phonesCache;
508
546
  }
509
547
  getTablets() {
510
- return this.getAllDevices().filter((d) => d.isTablet);
548
+ if (!this.tabletsCache) {
549
+ this.tabletsCache = this.getAllDevices().filter((d) => d.isTablet);
550
+ }
551
+ return this.tabletsCache;
511
552
  }
512
553
  isPhone(deviceType) {
513
554
  const config = this.getDeviceConfig(deviceType);
@@ -526,10 +567,20 @@ var DeviceConfigService = class _DeviceConfigService {
526
567
  // src/infrastructure/renderers/ScreenshotRenderer.ts
527
568
  var ScreenshotRenderer = class {
528
569
  constructor() {
570
+ this.renderCache = /* @__PURE__ */ new Map();
571
+ this.MAX_CACHE_SIZE = 5;
529
572
  this.canvasRenderer = new CanvasRenderer();
530
573
  this.patternRenderer = new PatternRenderer();
531
574
  this.deviceConfigService = DeviceConfigService.getInstance();
532
575
  }
576
+ /**
577
+ * Clear render cache to free memory
578
+ */
579
+ clearCache() {
580
+ this.renderCache.clear();
581
+ this.canvasRenderer.clearImageCache?.();
582
+ this.patternRenderer.clearCache?.();
583
+ }
533
584
  /**
534
585
  * Render device frame
535
586
  */
@@ -581,7 +632,7 @@ var ScreenshotRenderer = class {
581
632
  ctx.fill();
582
633
  }
583
634
  /**
584
- * Render background
635
+ * Render background - optimized
585
636
  */
586
637
  renderBackground(ctx, design, dimensions) {
587
638
  if (design.backgroundGradientEnd) {
@@ -791,10 +842,17 @@ var ScreenshotStudioService = class {
791
842
  * Generate batch screenshots
792
843
  */
793
844
  async generateBatchScreenshots(deviceTypes) {
794
- const promises = deviceTypes.map(
795
- (deviceType) => this.generateScreenshot({ deviceType })
796
- );
797
- return Promise.all(promises);
845
+ const BATCH_SIZE = 3;
846
+ const results = [];
847
+ for (let i = 0; i < deviceTypes.length; i += BATCH_SIZE) {
848
+ const batch = deviceTypes.slice(i, i + BATCH_SIZE);
849
+ const batchPromises = batch.map(
850
+ (deviceType) => this.generateScreenshot({ deviceType })
851
+ );
852
+ const batchResults = await Promise.all(batchPromises);
853
+ results.push(...batchResults);
854
+ }
855
+ return results;
798
856
  }
799
857
  /**
800
858
  * Generate designed screenshot
@@ -835,16 +893,18 @@ WHATS NEW:
835
893
  ${project.appMetadata.whatsNew}`;
836
894
  folder.file("aso_metadata.txt", metadataText);
837
895
  }
838
- for (let i = 0; i < project.screens.length; i++) {
839
- const screen = project.screens[i];
840
- const screenshot = await this.generateDesignedScreenshot(screen, project.design);
896
+ const screenshotPromises = project.screens.map(
897
+ (screen, index) => this.generateDesignedScreenshot(screen, project.design).then((screenshot) => ({ screenshot, index }))
898
+ );
899
+ const screenshots = await Promise.all(screenshotPromises);
900
+ screenshots.forEach(({ screenshot, index }) => {
841
901
  const base64Data = screenshot.url.split(",")[1];
842
902
  folder.file(
843
- `screen_${i + 1}_${project.design.deviceType}.png`,
903
+ `screen_${index + 1}_${project.design.deviceType}.png`,
844
904
  base64Data,
845
905
  { base64: true }
846
906
  );
847
- }
907
+ });
848
908
  return await zip.generateAsync({ type: "blob" });
849
909
  }
850
910
  /**
@@ -928,10 +988,13 @@ ${project.appMetadata.whatsNew}`;
928
988
  };
929
989
  }
930
990
  /**
931
- * Generate unique ID
991
+ * Generate unique ID - optimized version
932
992
  */
933
993
  generateId() {
934
- return Math.random().toString(36).substr(2, 9);
994
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
995
+ return crypto.randomUUID();
996
+ }
997
+ return `${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 9)}`;
935
998
  }
936
999
  };
937
1000
 
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  ReportService,
3
3
  ScreenshotStudioService
4
- } from "../chunk-BN7JTXL7.mjs";
5
- import "../chunk-WMKPCJFI.mjs";
4
+ } from "../chunk-DNKRSL6D.mjs";
5
+ import "../chunk-UOGTEMIJ.mjs";
6
6
  import "../chunk-Z6IB4BQV.mjs";
7
7
  import "../chunk-MC6PNV2F.mjs";
8
8
  export {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ScreenshotRenderer
3
- } from "./chunk-WMKPCJFI.mjs";
3
+ } from "./chunk-UOGTEMIJ.mjs";
4
4
  import {
5
5
  DEFAULT_SCREENSHOT_STUDIO_CONFIG
6
6
  } from "./chunk-Z6IB4BQV.mjs";
@@ -54,10 +54,17 @@ var ScreenshotStudioService = class {
54
54
  * Generate batch screenshots
55
55
  */
56
56
  async generateBatchScreenshots(deviceTypes) {
57
- const promises = deviceTypes.map(
58
- (deviceType) => this.generateScreenshot({ deviceType })
59
- );
60
- return Promise.all(promises);
57
+ const BATCH_SIZE = 3;
58
+ const results = [];
59
+ for (let i = 0; i < deviceTypes.length; i += BATCH_SIZE) {
60
+ const batch = deviceTypes.slice(i, i + BATCH_SIZE);
61
+ const batchPromises = batch.map(
62
+ (deviceType) => this.generateScreenshot({ deviceType })
63
+ );
64
+ const batchResults = await Promise.all(batchPromises);
65
+ results.push(...batchResults);
66
+ }
67
+ return results;
61
68
  }
62
69
  /**
63
70
  * Generate designed screenshot
@@ -98,16 +105,18 @@ WHATS NEW:
98
105
  ${project.appMetadata.whatsNew}`;
99
106
  folder.file("aso_metadata.txt", metadataText);
100
107
  }
101
- for (let i = 0; i < project.screens.length; i++) {
102
- const screen = project.screens[i];
103
- const screenshot = await this.generateDesignedScreenshot(screen, project.design);
108
+ const screenshotPromises = project.screens.map(
109
+ (screen, index) => this.generateDesignedScreenshot(screen, project.design).then((screenshot) => ({ screenshot, index }))
110
+ );
111
+ const screenshots = await Promise.all(screenshotPromises);
112
+ screenshots.forEach(({ screenshot, index }) => {
104
113
  const base64Data = screenshot.url.split(",")[1];
105
114
  folder.file(
106
- `screen_${i + 1}_${project.design.deviceType}.png`,
115
+ `screen_${index + 1}_${project.design.deviceType}.png`,
107
116
  base64Data,
108
117
  { base64: true }
109
118
  );
110
- }
119
+ });
111
120
  return await zip.generateAsync({ type: "blob" });
112
121
  }
113
122
  /**
@@ -191,10 +200,13 @@ ${project.appMetadata.whatsNew}`;
191
200
  };
192
201
  }
193
202
  /**
194
- * Generate unique ID
203
+ * Generate unique ID - optimized version
195
204
  */
196
205
  generateId() {
197
- return Math.random().toString(36).substr(2, 9);
206
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
207
+ return crypto.randomUUID();
208
+ }
209
+ return `${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 9)}`;
198
210
  }
199
211
  };
200
212