@umituz/web-canvas 1.0.6 → 1.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 (41) hide show
  1. package/README.md +33 -0
  2. package/dist/CanvasConfig-HFgH7irb.d.mts +310 -0
  3. package/dist/CanvasConfig-HFgH7irb.d.ts +310 -0
  4. package/dist/application/index.d.mts +78 -0
  5. package/dist/application/index.d.ts +78 -0
  6. package/dist/application/index.js +1146 -0
  7. package/dist/application/index.mjs +11 -0
  8. package/dist/chunk-DNKRSL6D.mjs +358 -0
  9. package/dist/chunk-HJ24NSQA.mjs +777 -0
  10. package/dist/chunk-MC6PNV2F.mjs +34 -0
  11. package/dist/chunk-TZGTTYTZ.mjs +0 -0
  12. package/dist/chunk-UOGTEMIJ.mjs +712 -0
  13. package/dist/chunk-Z6IB4BQV.mjs +31 -0
  14. package/dist/domain/config/index.d.mts +87 -0
  15. package/dist/domain/config/index.d.ts +87 -0
  16. package/dist/domain/config/index.js +92 -0
  17. package/dist/domain/config/index.mjs +15 -0
  18. package/dist/domain/index.d.mts +45 -0
  19. package/dist/domain/index.d.ts +45 -0
  20. package/dist/domain/index.js +137 -0
  21. package/dist/domain/index.mjs +55 -0
  22. package/dist/index.d.mts +7 -0
  23. package/dist/index.d.ts +7 -0
  24. package/dist/index.js +1927 -0
  25. package/dist/index.mjs +40 -0
  26. package/dist/infrastructure/index.d.mts +198 -0
  27. package/dist/infrastructure/index.d.ts +198 -0
  28. package/dist/infrastructure/index.js +765 -0
  29. package/dist/infrastructure/index.mjs +13 -0
  30. package/dist/presentation/index.d.mts +145 -0
  31. package/dist/presentation/index.d.ts +145 -0
  32. package/dist/presentation/index.js +1911 -0
  33. package/dist/presentation/index.mjs +16 -0
  34. package/dist/report.types-DWDnprBx.d.mts +76 -0
  35. package/dist/report.types-DWDnprBx.d.ts +76 -0
  36. package/package.json +8 -2
  37. package/src/domain/types/freehand.types.ts +42 -0
  38. package/src/domain/types/index.ts +1 -0
  39. package/src/index.ts +10 -0
  40. package/src/presentation/hooks/useFreehandCanvas.ts +244 -0
  41. package/src/presentation/index.ts +1 -0
@@ -0,0 +1,1146 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/application/index.ts
31
+ var application_exports = {};
32
+ __export(application_exports, {
33
+ ReportService: () => ReportService,
34
+ ScreenshotStudioService: () => ScreenshotStudioService
35
+ });
36
+ module.exports = __toCommonJS(application_exports);
37
+
38
+ // src/domain/config/ScreenshotStudioConfig.ts
39
+ var DEFAULT_SCREENSHOT_STUDIO_CONFIG = {
40
+ canvas: {
41
+ defaultFormat: "png",
42
+ defaultQuality: 0.9,
43
+ scale: 2,
44
+ useCORS: true
45
+ },
46
+ device: {
47
+ devices: ["iphone-15-pro"],
48
+ defaultDevice: "iphone-15-pro",
49
+ showFrame: true,
50
+ frameColor: "#1f2937",
51
+ frameThickness: 8
52
+ },
53
+ export: {
54
+ includeMetadata: true,
55
+ folderStructure: "by-language",
56
+ defaultFormat: "png"
57
+ },
58
+ ai: {
59
+ enabled: true,
60
+ provider: "gemini"
61
+ },
62
+ languages: ["en-US", "tr-TR", "de-DE", "fr-FR", "es-ES", "ja-JP"],
63
+ defaultLanguage: "en-US"
64
+ };
65
+
66
+ // src/domain/config/CanvasConfig.ts
67
+ var DEFAULT_CANVAS_CONFIG = {
68
+ defaultFormat: "png",
69
+ defaultQuality: 0.9,
70
+ maxDimension: 4096,
71
+ scale: 2,
72
+ backgroundColor: "#ffffff",
73
+ useCORS: true
74
+ };
75
+
76
+ // src/infrastructure/renderers/CanvasRenderer.ts
77
+ var CanvasRenderer = class {
78
+ constructor(config = DEFAULT_CANVAS_CONFIG) {
79
+ this.config = { ...DEFAULT_CANVAS_CONFIG, ...config };
80
+ this.imageCache = /* @__PURE__ */ new Map();
81
+ }
82
+ /**
83
+ * Create a new canvas element
84
+ */
85
+ createCanvas(dimensions) {
86
+ const canvas = document.createElement("canvas");
87
+ canvas.width = dimensions.width;
88
+ canvas.height = dimensions.height;
89
+ return canvas;
90
+ }
91
+ /**
92
+ * Get canvas 2D context
93
+ */
94
+ getContext(canvas) {
95
+ return canvas.getContext("2d");
96
+ }
97
+ /**
98
+ * Apply context configuration
99
+ */
100
+ configureContext(ctx, config) {
101
+ if (config.fillStyle !== void 0) {
102
+ ctx.fillStyle = config.fillStyle;
103
+ }
104
+ if (config.strokeStyle !== void 0) {
105
+ ctx.strokeStyle = config.strokeStyle;
106
+ }
107
+ if (config.lineWidth !== void 0) {
108
+ ctx.lineWidth = config.lineWidth;
109
+ }
110
+ if (config.lineCap !== void 0) {
111
+ ctx.lineCap = config.lineCap;
112
+ }
113
+ if (config.lineJoin !== void 0) {
114
+ ctx.lineJoin = config.lineJoin;
115
+ }
116
+ if (config.globalAlpha !== void 0) {
117
+ ctx.globalAlpha = config.globalAlpha;
118
+ }
119
+ if (config.font !== void 0) {
120
+ ctx.font = config.font;
121
+ }
122
+ if (config.textAlign !== void 0) {
123
+ ctx.textAlign = config.textAlign;
124
+ }
125
+ if (config.textBaseline !== void 0) {
126
+ ctx.textBaseline = config.textBaseline;
127
+ }
128
+ }
129
+ /**
130
+ * Clear canvas
131
+ */
132
+ clearCanvas(ctx, dimensions) {
133
+ ctx.clearRect(0, 0, dimensions.width, dimensions.height);
134
+ }
135
+ /**
136
+ * Fill rectangle
137
+ */
138
+ fillRect(ctx, x, y, width, height, color) {
139
+ if (color) {
140
+ ctx.fillStyle = color;
141
+ }
142
+ ctx.fillRect(x, y, width, height);
143
+ }
144
+ /**
145
+ * Stroke rectangle
146
+ */
147
+ strokeRect(ctx, x, y, width, height, color, lineWidth) {
148
+ if (color) {
149
+ ctx.strokeStyle = color;
150
+ }
151
+ if (lineWidth !== void 0) {
152
+ ctx.lineWidth = lineWidth;
153
+ }
154
+ ctx.strokeRect(x, y, width, height);
155
+ }
156
+ /**
157
+ * Draw text
158
+ */
159
+ drawText(ctx, text, x, y, config) {
160
+ if (config) {
161
+ this.configureContext(ctx, config);
162
+ }
163
+ ctx.fillText(text, x, y);
164
+ }
165
+ /**
166
+ * Draw image
167
+ */
168
+ async drawImage(ctx, imageSource, x, y, width, height) {
169
+ let img;
170
+ if (typeof imageSource === "string") {
171
+ img = await this.loadImage(imageSource);
172
+ } else {
173
+ img = imageSource;
174
+ }
175
+ if (width && height) {
176
+ ctx.drawImage(img, x, y, width, height);
177
+ } else {
178
+ ctx.drawImage(img, x, y);
179
+ }
180
+ }
181
+ /**
182
+ * Load image from URL with caching
183
+ */
184
+ loadImage(url) {
185
+ if (this.imageCache.has(url)) {
186
+ return Promise.resolve(this.imageCache.get(url));
187
+ }
188
+ return new Promise((resolve, reject) => {
189
+ const img = new Image();
190
+ if (this.config.useCORS) {
191
+ img.crossOrigin = "anonymous";
192
+ }
193
+ img.onload = () => {
194
+ this.imageCache.set(url, img);
195
+ resolve(img);
196
+ };
197
+ img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
198
+ img.src = url;
199
+ });
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
+ }
213
+ /**
214
+ * Create gradient
215
+ */
216
+ createLinearGradient(ctx, x1, y1, x2, y2, colorStops) {
217
+ const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
218
+ colorStops.forEach(({ offset, color }) => {
219
+ gradient.addColorStop(offset, color);
220
+ });
221
+ return gradient;
222
+ }
223
+ /**
224
+ * Export canvas to blob
225
+ */
226
+ async exportToBlob(canvas, options = {}) {
227
+ const format = options.format || this.config.defaultFormat;
228
+ const quality = options.quality || this.config.defaultQuality;
229
+ const scale = options.scale || this.config.scale;
230
+ const scaledCanvas = (scale || 1) !== 1 ? this.scaleCanvas(canvas, scale || 1) : canvas;
231
+ const mimeType = `image/${format || "png"}`;
232
+ const blob = await new Promise((resolve) => {
233
+ scaledCanvas.toBlob(
234
+ (result) => resolve(result),
235
+ mimeType,
236
+ quality
237
+ );
238
+ });
239
+ if (!blob) {
240
+ throw new Error("Failed to export canvas to blob");
241
+ }
242
+ const url = URL.createObjectURL(blob);
243
+ return {
244
+ blob,
245
+ url,
246
+ dimensions: {
247
+ width: scaledCanvas.width,
248
+ height: scaledCanvas.height
249
+ },
250
+ format: format || "png",
251
+ size: blob.size
252
+ };
253
+ }
254
+ /**
255
+ * Export canvas to data URL
256
+ */
257
+ exportToDataURL(canvas, format, quality) {
258
+ const mimeType = format || `image/${this.config.defaultFormat}`;
259
+ const q = quality || this.config.defaultQuality;
260
+ return canvas.toDataURL(mimeType, q);
261
+ }
262
+ /**
263
+ * Scale canvas
264
+ */
265
+ scaleCanvas(canvas, scale) {
266
+ const scaled = document.createElement("canvas");
267
+ scaled.width = canvas.width * scale;
268
+ scaled.height = canvas.height * scale;
269
+ const ctx = scaled.getContext("2d");
270
+ if (ctx) {
271
+ ctx.imageSmoothingEnabled = true;
272
+ ctx.imageSmoothingQuality = "high";
273
+ ctx.drawImage(canvas, 0, 0, scaled.width, scaled.height);
274
+ }
275
+ return scaled;
276
+ }
277
+ /**
278
+ * Clone canvas
279
+ */
280
+ cloneCanvas(canvas) {
281
+ const clone = document.createElement("canvas");
282
+ clone.width = canvas.width;
283
+ clone.height = canvas.height;
284
+ const ctx = clone.getContext("2d");
285
+ if (ctx) {
286
+ ctx.drawImage(canvas, 0, 0);
287
+ }
288
+ return clone;
289
+ }
290
+ };
291
+
292
+ // src/infrastructure/renderers/PatternRenderer.ts
293
+ var PatternRenderer = class {
294
+ constructor() {
295
+ this.patternCanvasCache = /* @__PURE__ */ new Map();
296
+ this.MAX_CACHE_SIZE = 10;
297
+ }
298
+ /**
299
+ * Render pattern on canvas
300
+ */
301
+ renderPattern(ctx, pattern, dimensions, color = "#000000") {
302
+ if (pattern === "none") return;
303
+ ctx.save();
304
+ ctx.globalAlpha = 0.1;
305
+ switch (pattern) {
306
+ case "dots":
307
+ this.renderDotPattern(ctx, dimensions, color);
308
+ break;
309
+ case "grid":
310
+ this.renderGridPattern(ctx, dimensions, color);
311
+ break;
312
+ case "lines":
313
+ this.renderLinePattern(ctx, dimensions, color);
314
+ break;
315
+ case "waves":
316
+ this.renderWavePattern(ctx, dimensions, color);
317
+ break;
318
+ case "checkers":
319
+ this.renderCheckerPattern(ctx, dimensions, color);
320
+ break;
321
+ }
322
+ ctx.restore();
323
+ }
324
+ /**
325
+ * Clear pattern cache to free memory
326
+ */
327
+ clearCache() {
328
+ this.patternCanvasCache.clear();
329
+ }
330
+ /**
331
+ * Render dot pattern - optimized with batch rendering
332
+ */
333
+ renderDotPattern(ctx, dimensions, color) {
334
+ ctx.fillStyle = color;
335
+ const spacing = 30;
336
+ const radius = 2;
337
+ ctx.beginPath();
338
+ for (let x = spacing; x < dimensions.width; x += spacing) {
339
+ for (let y = spacing; y < dimensions.height; y += spacing) {
340
+ ctx.moveTo(x + radius, y);
341
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
342
+ }
343
+ }
344
+ ctx.fill();
345
+ }
346
+ /**
347
+ * Render grid pattern - optimized with single path
348
+ */
349
+ renderGridPattern(ctx, dimensions, color) {
350
+ ctx.strokeStyle = color;
351
+ ctx.lineWidth = 1;
352
+ const spacing = 30;
353
+ ctx.beginPath();
354
+ for (let x = 0; x <= dimensions.width; x += spacing) {
355
+ ctx.moveTo(x, 0);
356
+ ctx.lineTo(x, dimensions.height);
357
+ }
358
+ for (let y = 0; y <= dimensions.height; y += spacing) {
359
+ ctx.moveTo(0, y);
360
+ ctx.lineTo(dimensions.width, y);
361
+ }
362
+ ctx.stroke();
363
+ }
364
+ /**
365
+ * Render line pattern - optimized with single path
366
+ */
367
+ renderLinePattern(ctx, dimensions, color) {
368
+ ctx.strokeStyle = color;
369
+ ctx.lineWidth = 2;
370
+ const spacing = 20;
371
+ ctx.beginPath();
372
+ for (let y = spacing; y < dimensions.height; y += spacing) {
373
+ ctx.moveTo(0, y);
374
+ ctx.lineTo(dimensions.width, y);
375
+ }
376
+ ctx.stroke();
377
+ }
378
+ /**
379
+ * Render wave pattern - optimized with pre-calculated values
380
+ */
381
+ renderWavePattern(ctx, dimensions, color) {
382
+ ctx.strokeStyle = color;
383
+ ctx.lineWidth = 2;
384
+ const amplitude = 10;
385
+ const frequency = 0.1;
386
+ const spacing = 40;
387
+ const step = 5;
388
+ for (let y = spacing; y < dimensions.height; y += spacing) {
389
+ ctx.beginPath();
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) {
394
+ const waveY = y + Math.sin(x * frequency) * amplitude;
395
+ ctx.lineTo(x, waveY);
396
+ }
397
+ ctx.stroke();
398
+ }
399
+ }
400
+ /**
401
+ * Render checker pattern
402
+ */
403
+ renderCheckerPattern(ctx, dimensions, color) {
404
+ ctx.fillStyle = color;
405
+ const size = 20;
406
+ for (let x = 0; x < dimensions.width; x += size * 2) {
407
+ for (let y = 0; y < dimensions.height; y += size * 2) {
408
+ ctx.fillRect(x, y, size, size);
409
+ ctx.fillRect(x + size, y + size, size, size);
410
+ }
411
+ }
412
+ }
413
+ };
414
+
415
+ // src/domain/config/DeviceConfig.ts
416
+ var DEVICE_DIMENSIONS = {
417
+ "iphone-15-pro": { width: 1290, height: 2796 },
418
+ "iphone-15": { width: 1179, height: 2556 },
419
+ "iphone-se": { width: 1080, height: 2340 },
420
+ "ipad-pro": { width: 2048, height: 2732 },
421
+ "ipad": { width: 1620, height: 2160 },
422
+ "android-phone": { width: 1440, height: 3120 },
423
+ "android-tablet": { width: 2e3, height: 2200 },
424
+ "desktop": { width: 1920, height: 1080 }
425
+ };
426
+ var DEFAULT_DEVICE_CONFIG = {
427
+ devices: Object.keys(DEVICE_DIMENSIONS),
428
+ defaultDevice: "iphone-15-pro",
429
+ showFrame: true,
430
+ frameColor: "#1f2937",
431
+ frameThickness: 8
432
+ };
433
+
434
+ // src/infrastructure/services/DeviceConfigService.ts
435
+ var DeviceConfigService = class _DeviceConfigService {
436
+ constructor() {
437
+ this.phonesCache = null;
438
+ this.tabletsCache = null;
439
+ this.allDevicesCache = null;
440
+ this.devices = /* @__PURE__ */ new Map();
441
+ this.initializeDevices();
442
+ }
443
+ static getInstance() {
444
+ if (!_DeviceConfigService.instance) {
445
+ _DeviceConfigService.instance = new _DeviceConfigService();
446
+ }
447
+ return _DeviceConfigService.instance;
448
+ }
449
+ initializeDevices() {
450
+ const deviceConfigs = [
451
+ {
452
+ name: "iPhone 15 Pro",
453
+ dimensions: { width: 1290, height: 2796, scale: 3 },
454
+ isPhone: true,
455
+ hasNotch: true,
456
+ cornerRadius: 20,
457
+ frameThickness: 8
458
+ },
459
+ {
460
+ name: "iPhone 15",
461
+ dimensions: { width: 1179, height: 2556, scale: 3 },
462
+ isPhone: true,
463
+ hasNotch: true,
464
+ cornerRadius: 20,
465
+ frameThickness: 8
466
+ },
467
+ {
468
+ name: "iPhone SE",
469
+ dimensions: { width: 1080, height: 2340, scale: 3 },
470
+ isPhone: true,
471
+ hasNotch: false,
472
+ cornerRadius: 16,
473
+ frameThickness: 6
474
+ },
475
+ {
476
+ name: "iPad Pro",
477
+ dimensions: { width: 2048, height: 2732, scale: 2 },
478
+ isTablet: true,
479
+ hasNotch: false,
480
+ cornerRadius: 24,
481
+ frameThickness: 10
482
+ },
483
+ {
484
+ name: "iPad",
485
+ dimensions: { width: 1620, height: 2160, scale: 2 },
486
+ isTablet: true,
487
+ hasNotch: false,
488
+ cornerRadius: 20,
489
+ frameThickness: 10
490
+ },
491
+ {
492
+ name: "Android Phone",
493
+ dimensions: { width: 1440, height: 3120, scale: 3 },
494
+ isPhone: true,
495
+ hasNotch: false,
496
+ cornerRadius: 24,
497
+ frameThickness: 8
498
+ },
499
+ {
500
+ name: "Android Tablet",
501
+ dimensions: { width: 2e3, height: 2200, scale: 2 },
502
+ isTablet: true,
503
+ hasNotch: false,
504
+ cornerRadius: 24,
505
+ frameThickness: 10
506
+ },
507
+ {
508
+ name: "Desktop",
509
+ dimensions: { width: 1920, height: 1080, scale: 1 },
510
+ isPhone: false,
511
+ isTablet: false,
512
+ hasNotch: false,
513
+ cornerRadius: 0,
514
+ frameThickness: 0
515
+ }
516
+ ];
517
+ Object.entries(DEVICE_DIMENSIONS).forEach(([type, dimensions]) => {
518
+ const deviceType = type;
519
+ const config = deviceConfigs.find((_, index) => {
520
+ const types = Object.keys(DEVICE_DIMENSIONS);
521
+ return types[index] === type;
522
+ });
523
+ if (config) {
524
+ this.devices.set(deviceType, {
525
+ type: deviceType,
526
+ ...config,
527
+ dimensions
528
+ });
529
+ }
530
+ });
531
+ }
532
+ getDeviceConfig(deviceType) {
533
+ return this.devices.get(deviceType);
534
+ }
535
+ getAllDevices() {
536
+ if (!this.allDevicesCache) {
537
+ this.allDevicesCache = Array.from(this.devices.values());
538
+ }
539
+ return this.allDevicesCache;
540
+ }
541
+ getPhones() {
542
+ if (!this.phonesCache) {
543
+ this.phonesCache = this.getAllDevices().filter((d) => d.isPhone);
544
+ }
545
+ return this.phonesCache;
546
+ }
547
+ getTablets() {
548
+ if (!this.tabletsCache) {
549
+ this.tabletsCache = this.getAllDevices().filter((d) => d.isTablet);
550
+ }
551
+ return this.tabletsCache;
552
+ }
553
+ isPhone(deviceType) {
554
+ const config = this.getDeviceConfig(deviceType);
555
+ return config?.isPhone || false;
556
+ }
557
+ isTablet(deviceType) {
558
+ const config = this.getDeviceConfig(deviceType);
559
+ return config?.isTablet || false;
560
+ }
561
+ hasNotch(deviceType) {
562
+ const config = this.getDeviceConfig(deviceType);
563
+ return config?.hasNotch || false;
564
+ }
565
+ };
566
+
567
+ // src/infrastructure/renderers/ScreenshotRenderer.ts
568
+ var ScreenshotRenderer = class {
569
+ constructor() {
570
+ this.renderCache = /* @__PURE__ */ new Map();
571
+ this.MAX_CACHE_SIZE = 5;
572
+ this.canvasRenderer = new CanvasRenderer();
573
+ this.patternRenderer = new PatternRenderer();
574
+ this.deviceConfigService = DeviceConfigService.getInstance();
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
+ }
584
+ /**
585
+ * Render device frame
586
+ */
587
+ renderDeviceFrame(ctx, dimensions, deviceType, frameStyle, frameColor) {
588
+ const deviceConfig = this.deviceConfigService.getDeviceConfig(deviceType);
589
+ if (!deviceConfig) return;
590
+ if (frameStyle === "none") return;
591
+ const isPhone = this.deviceConfigService.isPhone(deviceType);
592
+ const isTablet = this.deviceConfigService.isTablet(deviceType);
593
+ if (!isPhone && !isTablet) return;
594
+ const frameThickness = deviceConfig.frameThickness || 8;
595
+ if (frameStyle === "realistic") {
596
+ this.renderRealisticFrame(ctx, dimensions, frameColor, frameThickness);
597
+ if (isPhone && this.deviceConfigService.hasNotch(deviceType)) {
598
+ this.renderNotch(ctx, dimensions);
599
+ }
600
+ } else if (frameStyle === "minimal") {
601
+ this.renderMinimalFrame(ctx, dimensions, frameColor, frameThickness);
602
+ }
603
+ }
604
+ /**
605
+ * Render realistic device frame
606
+ */
607
+ renderRealisticFrame(ctx, dimensions, color, thickness) {
608
+ ctx.strokeStyle = color;
609
+ ctx.lineWidth = thickness;
610
+ ctx.strokeRect(
611
+ thickness / 2,
612
+ thickness / 2,
613
+ dimensions.width - thickness,
614
+ dimensions.height - thickness
615
+ );
616
+ }
617
+ /**
618
+ * Render minimal device frame
619
+ */
620
+ renderMinimalFrame(ctx, dimensions, color, thickness) {
621
+ ctx.strokeStyle = color;
622
+ ctx.lineWidth = thickness;
623
+ ctx.strokeRect(0, 0, dimensions.width, dimensions.height);
624
+ }
625
+ /**
626
+ * Render device notch
627
+ */
628
+ renderNotch(ctx, dimensions) {
629
+ ctx.fillStyle = "#1f2937";
630
+ ctx.beginPath();
631
+ ctx.arc(dimensions.width / 2, 20, 10, 0, Math.PI * 2);
632
+ ctx.fill();
633
+ }
634
+ /**
635
+ * Render background - optimized
636
+ */
637
+ renderBackground(ctx, design, dimensions) {
638
+ if (design.backgroundGradientEnd) {
639
+ const gradient = this.canvasRenderer.createLinearGradient(
640
+ ctx,
641
+ 0,
642
+ 0,
643
+ dimensions.width,
644
+ dimensions.height,
645
+ [
646
+ { offset: 0, color: design.backgroundColor },
647
+ { offset: 1, color: design.backgroundGradientEnd }
648
+ ]
649
+ );
650
+ ctx.fillStyle = gradient;
651
+ } else {
652
+ ctx.fillStyle = design.backgroundColor;
653
+ }
654
+ ctx.fillRect(0, 0, dimensions.width, dimensions.height);
655
+ if (design.backgroundPattern && design.backgroundPattern !== "none") {
656
+ this.patternRenderer.renderPattern(
657
+ ctx,
658
+ design.backgroundPattern,
659
+ dimensions
660
+ );
661
+ }
662
+ }
663
+ /**
664
+ * Render text content
665
+ */
666
+ renderTextContent(ctx, content, design, dimensions) {
667
+ const titleConfig = {
668
+ fillStyle: design.textColor,
669
+ font: `bold ${design.titleFontSize}px ${design.fontFamily}`,
670
+ textAlign: design.textAlign
671
+ };
672
+ const subtitleConfig = {
673
+ fillStyle: design.textColor,
674
+ font: `${design.subtitleFontSize}px ${design.fontFamily}`,
675
+ textAlign: design.textAlign
676
+ };
677
+ const titleY = design.layout === "text-bottom" ? dimensions.height - 150 : 100 + (design.offsetY || 0);
678
+ this.canvasRenderer.drawText(
679
+ ctx,
680
+ this.formatText(content.title, design.textCase),
681
+ dimensions.width / 2,
682
+ titleY,
683
+ titleConfig
684
+ );
685
+ const subtitleY = titleY + design.titleFontSize + (design.gap || 20);
686
+ this.canvasRenderer.drawText(
687
+ ctx,
688
+ this.formatText(content.subtitle, design.textCase),
689
+ dimensions.width / 2,
690
+ subtitleY,
691
+ subtitleConfig
692
+ );
693
+ }
694
+ /**
695
+ * Format text case
696
+ */
697
+ formatText(text, textCase = "none") {
698
+ switch (textCase) {
699
+ case "uppercase":
700
+ return text.toUpperCase();
701
+ case "lowercase":
702
+ return text.toLowerCase();
703
+ default:
704
+ return text;
705
+ }
706
+ }
707
+ /**
708
+ * Render screenshot image
709
+ */
710
+ async renderScreenshotImage(ctx, imageUrl, design, dimensions) {
711
+ if (!imageUrl) return;
712
+ const deviceConfig = this.deviceConfigService.getDeviceConfig(
713
+ design.deviceType
714
+ );
715
+ if (!deviceConfig) return;
716
+ const frameThickness = deviceConfig.frameThickness || 8;
717
+ const padding = 60 + frameThickness;
718
+ const screenshotWidth = dimensions.width - padding * 2;
719
+ const screenshotHeight = dimensions.height - padding * 2;
720
+ try {
721
+ await this.canvasRenderer.drawImage(
722
+ ctx,
723
+ imageUrl,
724
+ padding,
725
+ padding,
726
+ screenshotWidth,
727
+ screenshotHeight
728
+ );
729
+ } catch (error) {
730
+ console.error("Failed to render screenshot image:", error);
731
+ }
732
+ }
733
+ /**
734
+ * Render complete screenshot
735
+ */
736
+ async renderDesignedScreenshot(content, design) {
737
+ const deviceConfig = this.deviceConfigService.getDeviceConfig(design.deviceType);
738
+ if (!deviceConfig) {
739
+ throw new Error(`Device config not found: ${design.deviceType}`);
740
+ }
741
+ const dimensions = deviceConfig.dimensions;
742
+ const canvas = this.canvasRenderer.createCanvas(dimensions);
743
+ const ctx = canvas.getContext("2d");
744
+ if (!ctx) {
745
+ throw new Error("Failed to get canvas context");
746
+ }
747
+ this.renderBackground(ctx, design, dimensions);
748
+ if (content.screenshot) {
749
+ await this.renderScreenshotImage(ctx, content.screenshot, design, dimensions);
750
+ }
751
+ this.renderTextContent(ctx, content, design, dimensions);
752
+ this.renderDeviceFrame(
753
+ ctx,
754
+ dimensions,
755
+ design.deviceType,
756
+ design.frameStyle,
757
+ design.deviceColor
758
+ );
759
+ return canvas;
760
+ }
761
+ /**
762
+ * Render placeholder content
763
+ */
764
+ renderPlaceholderContent(ctx, dimensions, deviceType) {
765
+ this.canvasRenderer.fillRect(ctx, 0, 0, dimensions.width, dimensions.height, "#ffffff");
766
+ this.canvasRenderer.fillRect(ctx, 0, 0, dimensions.width, 60, "#f3f4f6");
767
+ this.canvasRenderer.fillRect(
768
+ ctx,
769
+ 0,
770
+ dimensions.height - 80,
771
+ dimensions.width,
772
+ 80,
773
+ "#f3f4f6"
774
+ );
775
+ ctx.fillStyle = "#e5e7eb";
776
+ for (let i = 0; i < 5; i++) {
777
+ const y = 80 + i * 120;
778
+ const height = Math.min(100, dimensions.height - y - 100);
779
+ if (y < dimensions.height - 100) {
780
+ this.canvasRenderer.fillRect(ctx, 20, y, dimensions.width - 40, height);
781
+ }
782
+ }
783
+ const deviceConfig = this.deviceConfigService.getDeviceConfig(deviceType);
784
+ if (deviceConfig) {
785
+ this.renderDeviceFrame(
786
+ ctx,
787
+ dimensions,
788
+ deviceType,
789
+ "realistic",
790
+ "#1f2937"
791
+ );
792
+ }
793
+ }
794
+ };
795
+
796
+ // src/application/services/ScreenshotStudioService.ts
797
+ var ScreenshotStudioService = class {
798
+ constructor(config) {
799
+ this.config = {
800
+ ...DEFAULT_SCREENSHOT_STUDIO_CONFIG,
801
+ ...config
802
+ };
803
+ this.renderer = new ScreenshotRenderer();
804
+ }
805
+ /**
806
+ * Generate screenshot
807
+ */
808
+ async generateScreenshot(options) {
809
+ const deviceConfig = this.config.device?.devices.find(
810
+ (d) => d === options.deviceType
811
+ );
812
+ if (!deviceConfig) {
813
+ throw new Error(`Device configuration not found for ${options.deviceType}`);
814
+ }
815
+ const canvas = this.renderer["canvasRenderer"].createCanvas(
816
+ options.customDimensions || { width: 1290, height: 2796 }
817
+ );
818
+ const ctx = canvas.getContext("2d");
819
+ if (!ctx) {
820
+ throw new Error("Failed to get canvas context");
821
+ }
822
+ this.renderer["renderPlaceholderContent"](
823
+ ctx,
824
+ { width: canvas.width, height: canvas.height },
825
+ options.deviceType
826
+ );
827
+ const blob = await this.renderer["canvasRenderer"].exportToBlob(canvas, {
828
+ format: options.format || this.config.export?.defaultFormat || "png",
829
+ quality: options.quality || this.config.canvas?.defaultQuality || 0.9
830
+ });
831
+ return {
832
+ id: this.generateId(),
833
+ deviceType: options.deviceType,
834
+ dimensions: { width: canvas.width, height: canvas.height },
835
+ format: options.format || this.config.export?.defaultFormat || "png",
836
+ timestamp: /* @__PURE__ */ new Date(),
837
+ fileSize: blob.blob.size,
838
+ url: blob.url
839
+ };
840
+ }
841
+ /**
842
+ * Generate batch screenshots
843
+ */
844
+ async generateBatchScreenshots(deviceTypes) {
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;
856
+ }
857
+ /**
858
+ * Generate designed screenshot
859
+ */
860
+ async generateDesignedScreenshot(content, design) {
861
+ const canvas = await this.renderer.renderDesignedScreenshot(content, design);
862
+ const blob = await this.renderer["canvasRenderer"].exportToBlob(canvas, {
863
+ format: "png"
864
+ });
865
+ return {
866
+ id: this.generateId(),
867
+ deviceType: design.deviceType,
868
+ dimensions: { width: canvas.width, height: canvas.height },
869
+ format: "png",
870
+ timestamp: /* @__PURE__ */ new Date(),
871
+ fileSize: blob.blob.size,
872
+ url: blob.url
873
+ };
874
+ }
875
+ /**
876
+ * Export project as ZIP
877
+ */
878
+ async exportProjectAsZip(project) {
879
+ const JSZip = (await import("jszip")).default;
880
+ const zip = new JSZip();
881
+ const folder = zip.folder(project.currentLanguage);
882
+ if (!folder) {
883
+ throw new Error("Failed to create folder in ZIP");
884
+ }
885
+ if (this.config.export?.includeMetadata) {
886
+ const metadataText = `APP NAME: ${project.appMetadata.appName}
887
+ SUBTITLE: ${project.appMetadata.subtitle}
888
+ DESCRIPTION:
889
+ ${project.appMetadata.description}
890
+
891
+ KEYWORDS: ${project.appMetadata.keywords}
892
+ WHATS NEW:
893
+ ${project.appMetadata.whatsNew}`;
894
+ folder.file("aso_metadata.txt", metadataText);
895
+ }
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 }) => {
901
+ const base64Data = screenshot.url.split(",")[1];
902
+ folder.file(
903
+ `screen_${index + 1}_${project.design.deviceType}.png`,
904
+ base64Data,
905
+ { base64: true }
906
+ );
907
+ });
908
+ return await zip.generateAsync({ type: "blob" });
909
+ }
910
+ /**
911
+ * Create project
912
+ */
913
+ createProject(name) {
914
+ return {
915
+ id: this.generateId(),
916
+ name,
917
+ design: this.getDefaultDesign(),
918
+ screens: [this.getDefaultScreen()],
919
+ currentLanguage: this.config.defaultLanguage || "en-US",
920
+ selectedScreenId: "1",
921
+ appMetadata: {
922
+ appName: "My App",
923
+ subtitle: "App Description",
924
+ promotionalText: "Promotional text",
925
+ keywords: "keyword1, keyword2",
926
+ description: "App description",
927
+ whatsNew: "What is new in this version"
928
+ },
929
+ appIcon: null,
930
+ createdAt: /* @__PURE__ */ new Date(),
931
+ updatedAt: /* @__PURE__ */ new Date()
932
+ };
933
+ }
934
+ /**
935
+ * Get default design
936
+ */
937
+ getDefaultDesign() {
938
+ return {
939
+ backgroundColor: "#6366f1",
940
+ backgroundGradientEnd: "#a855f7",
941
+ backgroundImage: null,
942
+ backgroundBlur: 0,
943
+ backgroundOverlayOpacity: 0,
944
+ backgroundPattern: "none",
945
+ textColor: "#ffffff",
946
+ highlightColor: "#fbbf24",
947
+ textAlign: "center",
948
+ deviceColor: "#1f2937",
949
+ deviceType: this.config.device?.defaultDevice || "iphone-15-pro",
950
+ frameStyle: "realistic",
951
+ layout: "text-top",
952
+ scale: 0.85,
953
+ fontFamily: "Inter",
954
+ titleFontSize: 60,
955
+ subtitleFontSize: 24,
956
+ titleFontWeight: "900",
957
+ subtitleFontWeight: "500",
958
+ textCase: "uppercase",
959
+ lineHeight: 0.9,
960
+ letterSpacing: -1,
961
+ rotation: 0,
962
+ rotationX: 0,
963
+ rotationY: 0,
964
+ rotationZ: 0,
965
+ offsetY: 0,
966
+ shadowIntensity: 0.5,
967
+ padding: 40,
968
+ gap: 20,
969
+ isPanoramic: false
970
+ };
971
+ }
972
+ /**
973
+ * Get default screen
974
+ */
975
+ getDefaultScreen() {
976
+ return {
977
+ id: "1",
978
+ title: "MY *APP*",
979
+ subtitle: "Your app description here",
980
+ screenshot: null,
981
+ translations: {
982
+ "en-US": { title: "MY *APP*", subtitle: "Your app description here" }
983
+ },
984
+ imageFit: "cover",
985
+ imageZoom: 1,
986
+ imageAnchorX: 50,
987
+ imageAnchorY: 50
988
+ };
989
+ }
990
+ /**
991
+ * Generate unique ID - optimized version
992
+ */
993
+ generateId() {
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)}`;
998
+ }
999
+ };
1000
+
1001
+ // src/application/services/ReportService.ts
1002
+ var ReportService = class _ReportService {
1003
+ static getInstance() {
1004
+ if (!_ReportService.instance) {
1005
+ _ReportService.instance = new _ReportService();
1006
+ }
1007
+ return _ReportService.instance;
1008
+ }
1009
+ /**
1010
+ * Export DOM element to PDF
1011
+ */
1012
+ async exportToPDF(elementId, options = {}) {
1013
+ const html2canvas = (await import("html2canvas")).default;
1014
+ const jsPDF = (await import("jspdf")).default;
1015
+ const element = document.getElementById(elementId);
1016
+ if (!element) {
1017
+ throw new Error("Report element not found");
1018
+ }
1019
+ const canvas = await html2canvas(element, {
1020
+ scale: options.scale || 2,
1021
+ useCORS: options.useCORS !== false,
1022
+ backgroundColor: options.backgroundColor || "#ffffff"
1023
+ });
1024
+ const imgData = canvas.toDataURL("image/png");
1025
+ const pdf = new jsPDF("p", "mm", "a4");
1026
+ const imgWidth = 210;
1027
+ const imgHeight = canvas.height * imgWidth / canvas.width;
1028
+ pdf.addImage(imgData, "PNG", 0, 0, imgWidth, imgHeight);
1029
+ pdf.save(options.filename || "report.pdf");
1030
+ }
1031
+ /**
1032
+ * Export canvas to PDF
1033
+ */
1034
+ async exportCanvasToPDF(canvas, options = {}) {
1035
+ const jsPDF = (await import("jspdf")).default;
1036
+ const imgData = canvas.toDataURL("image/png");
1037
+ const pdf = new jsPDF("p", "mm", "a4");
1038
+ const imgWidth = 210;
1039
+ const imgHeight = canvas.height * imgWidth / canvas.width;
1040
+ pdf.addImage(imgData, "PNG", 0, 0, imgWidth, imgHeight);
1041
+ pdf.save(options.filename || "canvas-export.pdf");
1042
+ }
1043
+ /**
1044
+ * Export data to Excel
1045
+ */
1046
+ async exportToExcel(data, options = {}) {
1047
+ const XLSX = await import("xlsx");
1048
+ const wb = XLSX.utils.book_new();
1049
+ const apps = data.items.filter((i) => i.type === "app").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
1050
+ const campaigns = data.items.filter((i) => i.type === "campaign").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
1051
+ const posts = data.items.filter((i) => i.type === "post").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
1052
+ if (apps.length > 0) {
1053
+ XLSX.utils.book_append_sheet(
1054
+ wb,
1055
+ XLSX.utils.json_to_sheet(apps),
1056
+ options.sheetName || "Apps"
1057
+ );
1058
+ }
1059
+ if (campaigns.length > 0) {
1060
+ XLSX.utils.book_append_sheet(
1061
+ wb,
1062
+ XLSX.utils.json_to_sheet(campaigns),
1063
+ "Campaigns"
1064
+ );
1065
+ }
1066
+ if (posts.length > 0) {
1067
+ XLSX.utils.book_append_sheet(
1068
+ wb,
1069
+ XLSX.utils.json_to_sheet(posts),
1070
+ "Posts"
1071
+ );
1072
+ }
1073
+ if (options.includeSummary !== false) {
1074
+ const summary = [
1075
+ { Metric: "Total Reach", Value: data.summary.totalReach },
1076
+ {
1077
+ Metric: "Total Engagement",
1078
+ Value: data.summary.totalEngagement
1079
+ },
1080
+ {
1081
+ Metric: "Average Score",
1082
+ Value: data.summary.averageScore
1083
+ },
1084
+ {
1085
+ Metric: "Export Date",
1086
+ Value: (/* @__PURE__ */ new Date()).toLocaleDateString()
1087
+ }
1088
+ ];
1089
+ XLSX.utils.book_append_sheet(
1090
+ wb,
1091
+ XLSX.utils.json_to_sheet(summary),
1092
+ "Summary"
1093
+ );
1094
+ }
1095
+ XLSX.writeFile(wb, options.filename || "report.xlsx");
1096
+ }
1097
+ /**
1098
+ * Export data to CSV
1099
+ */
1100
+ exportToCSV(data, filename = "report.csv", delimiter = ",") {
1101
+ const headers = ["Type", "Name", "Metrics"];
1102
+ const rows = data.items.map((item) => [
1103
+ item.type,
1104
+ item.name,
1105
+ JSON.stringify(item.metrics)
1106
+ ]);
1107
+ const csvContent = [headers, ...rows].map((row) => row.map((cell) => `"${cell}"`).join(delimiter)).join("\n");
1108
+ const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
1109
+ const url = URL.createObjectURL(blob);
1110
+ const link = document.createElement("a");
1111
+ link.href = url;
1112
+ link.download = filename;
1113
+ link.click();
1114
+ URL.revokeObjectURL(url);
1115
+ }
1116
+ /**
1117
+ * Export multiple elements to PDF
1118
+ */
1119
+ async exportMultipleToPDF(elementIds, options = {}) {
1120
+ const html2canvas = (await import("html2canvas")).default;
1121
+ const jsPDF = (await import("jspdf")).default;
1122
+ const pdf = new jsPDF("p", "mm", "a4");
1123
+ for (let i = 0; i < elementIds.length; i++) {
1124
+ const element = document.getElementById(elementIds[i]);
1125
+ if (!element) continue;
1126
+ const canvas = await html2canvas(element, {
1127
+ scale: options.scale || 2,
1128
+ useCORS: options.useCORS !== false,
1129
+ backgroundColor: options.backgroundColor || "#ffffff"
1130
+ });
1131
+ const imgData = canvas.toDataURL("image/png");
1132
+ const imgWidth = 210;
1133
+ const imgHeight = canvas.height * imgWidth / canvas.width;
1134
+ if (i > 0) {
1135
+ pdf.addPage();
1136
+ }
1137
+ pdf.addImage(imgData, "PNG", 0, 0, imgWidth, imgHeight);
1138
+ }
1139
+ pdf.save(options.filename || "multi-page-report.pdf");
1140
+ }
1141
+ };
1142
+ // Annotate the CommonJS export names for ESM import in node:
1143
+ 0 && (module.exports = {
1144
+ ReportService,
1145
+ ScreenshotStudioService
1146
+ });