@umituz/web-canvas 1.0.7 → 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
package/dist/index.js ADDED
@@ -0,0 +1,1927 @@
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/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ CanvasRenderer: () => CanvasRenderer,
34
+ DEFAULT_CANVAS_CONFIG: () => DEFAULT_CANVAS_CONFIG,
35
+ DEFAULT_DEVICE_CONFIG: () => DEFAULT_DEVICE_CONFIG,
36
+ DEFAULT_SCREENSHOT_STUDIO_CONFIG: () => DEFAULT_SCREENSHOT_STUDIO_CONFIG,
37
+ DEVICE_DIMENSIONS: () => DEVICE_DIMENSIONS,
38
+ DeviceConfigService: () => DeviceConfigService,
39
+ PatternRenderer: () => PatternRenderer,
40
+ ReportService: () => ReportService,
41
+ ScreenshotRenderer: () => ScreenshotRenderer,
42
+ ScreenshotStudioService: () => ScreenshotStudioService,
43
+ useCanvasExport: () => useCanvasExport,
44
+ useFreehandCanvas: () => useFreehandCanvas,
45
+ useReportExport: () => useReportExport,
46
+ useScreenshotStudio: () => useScreenshotStudio
47
+ });
48
+ module.exports = __toCommonJS(index_exports);
49
+
50
+ // src/domain/config/CanvasConfig.ts
51
+ var DEFAULT_CANVAS_CONFIG = {
52
+ defaultFormat: "png",
53
+ defaultQuality: 0.9,
54
+ maxDimension: 4096,
55
+ scale: 2,
56
+ backgroundColor: "#ffffff",
57
+ useCORS: true
58
+ };
59
+
60
+ // src/domain/config/DeviceConfig.ts
61
+ var DEVICE_DIMENSIONS = {
62
+ "iphone-15-pro": { width: 1290, height: 2796 },
63
+ "iphone-15": { width: 1179, height: 2556 },
64
+ "iphone-se": { width: 1080, height: 2340 },
65
+ "ipad-pro": { width: 2048, height: 2732 },
66
+ "ipad": { width: 1620, height: 2160 },
67
+ "android-phone": { width: 1440, height: 3120 },
68
+ "android-tablet": { width: 2e3, height: 2200 },
69
+ "desktop": { width: 1920, height: 1080 }
70
+ };
71
+ var DEFAULT_DEVICE_CONFIG = {
72
+ devices: Object.keys(DEVICE_DIMENSIONS),
73
+ defaultDevice: "iphone-15-pro",
74
+ showFrame: true,
75
+ frameColor: "#1f2937",
76
+ frameThickness: 8
77
+ };
78
+
79
+ // src/domain/config/ScreenshotStudioConfig.ts
80
+ var DEFAULT_SCREENSHOT_STUDIO_CONFIG = {
81
+ canvas: {
82
+ defaultFormat: "png",
83
+ defaultQuality: 0.9,
84
+ scale: 2,
85
+ useCORS: true
86
+ },
87
+ device: {
88
+ devices: ["iphone-15-pro"],
89
+ defaultDevice: "iphone-15-pro",
90
+ showFrame: true,
91
+ frameColor: "#1f2937",
92
+ frameThickness: 8
93
+ },
94
+ export: {
95
+ includeMetadata: true,
96
+ folderStructure: "by-language",
97
+ defaultFormat: "png"
98
+ },
99
+ ai: {
100
+ enabled: true,
101
+ provider: "gemini"
102
+ },
103
+ languages: ["en-US", "tr-TR", "de-DE", "fr-FR", "es-ES", "ja-JP"],
104
+ defaultLanguage: "en-US"
105
+ };
106
+
107
+ // src/infrastructure/renderers/CanvasRenderer.ts
108
+ var CanvasRenderer = class {
109
+ constructor(config = DEFAULT_CANVAS_CONFIG) {
110
+ this.config = { ...DEFAULT_CANVAS_CONFIG, ...config };
111
+ this.imageCache = /* @__PURE__ */ new Map();
112
+ }
113
+ /**
114
+ * Create a new canvas element
115
+ */
116
+ createCanvas(dimensions) {
117
+ const canvas = document.createElement("canvas");
118
+ canvas.width = dimensions.width;
119
+ canvas.height = dimensions.height;
120
+ return canvas;
121
+ }
122
+ /**
123
+ * Get canvas 2D context
124
+ */
125
+ getContext(canvas) {
126
+ return canvas.getContext("2d");
127
+ }
128
+ /**
129
+ * Apply context configuration
130
+ */
131
+ configureContext(ctx, config) {
132
+ if (config.fillStyle !== void 0) {
133
+ ctx.fillStyle = config.fillStyle;
134
+ }
135
+ if (config.strokeStyle !== void 0) {
136
+ ctx.strokeStyle = config.strokeStyle;
137
+ }
138
+ if (config.lineWidth !== void 0) {
139
+ ctx.lineWidth = config.lineWidth;
140
+ }
141
+ if (config.lineCap !== void 0) {
142
+ ctx.lineCap = config.lineCap;
143
+ }
144
+ if (config.lineJoin !== void 0) {
145
+ ctx.lineJoin = config.lineJoin;
146
+ }
147
+ if (config.globalAlpha !== void 0) {
148
+ ctx.globalAlpha = config.globalAlpha;
149
+ }
150
+ if (config.font !== void 0) {
151
+ ctx.font = config.font;
152
+ }
153
+ if (config.textAlign !== void 0) {
154
+ ctx.textAlign = config.textAlign;
155
+ }
156
+ if (config.textBaseline !== void 0) {
157
+ ctx.textBaseline = config.textBaseline;
158
+ }
159
+ }
160
+ /**
161
+ * Clear canvas
162
+ */
163
+ clearCanvas(ctx, dimensions) {
164
+ ctx.clearRect(0, 0, dimensions.width, dimensions.height);
165
+ }
166
+ /**
167
+ * Fill rectangle
168
+ */
169
+ fillRect(ctx, x, y, width, height, color) {
170
+ if (color) {
171
+ ctx.fillStyle = color;
172
+ }
173
+ ctx.fillRect(x, y, width, height);
174
+ }
175
+ /**
176
+ * Stroke rectangle
177
+ */
178
+ strokeRect(ctx, x, y, width, height, color, lineWidth) {
179
+ if (color) {
180
+ ctx.strokeStyle = color;
181
+ }
182
+ if (lineWidth !== void 0) {
183
+ ctx.lineWidth = lineWidth;
184
+ }
185
+ ctx.strokeRect(x, y, width, height);
186
+ }
187
+ /**
188
+ * Draw text
189
+ */
190
+ drawText(ctx, text, x, y, config) {
191
+ if (config) {
192
+ this.configureContext(ctx, config);
193
+ }
194
+ ctx.fillText(text, x, y);
195
+ }
196
+ /**
197
+ * Draw image
198
+ */
199
+ async drawImage(ctx, imageSource, x, y, width, height) {
200
+ let img;
201
+ if (typeof imageSource === "string") {
202
+ img = await this.loadImage(imageSource);
203
+ } else {
204
+ img = imageSource;
205
+ }
206
+ if (width && height) {
207
+ ctx.drawImage(img, x, y, width, height);
208
+ } else {
209
+ ctx.drawImage(img, x, y);
210
+ }
211
+ }
212
+ /**
213
+ * Load image from URL with caching
214
+ */
215
+ loadImage(url) {
216
+ if (this.imageCache.has(url)) {
217
+ return Promise.resolve(this.imageCache.get(url));
218
+ }
219
+ return new Promise((resolve, reject) => {
220
+ const img = new Image();
221
+ if (this.config.useCORS) {
222
+ img.crossOrigin = "anonymous";
223
+ }
224
+ img.onload = () => {
225
+ this.imageCache.set(url, img);
226
+ resolve(img);
227
+ };
228
+ img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
229
+ img.src = url;
230
+ });
231
+ }
232
+ /**
233
+ * Clear image cache to free memory
234
+ */
235
+ clearImageCache() {
236
+ this.imageCache.clear();
237
+ }
238
+ /**
239
+ * Get cache size for monitoring
240
+ */
241
+ getCacheSize() {
242
+ return this.imageCache.size;
243
+ }
244
+ /**
245
+ * Create gradient
246
+ */
247
+ createLinearGradient(ctx, x1, y1, x2, y2, colorStops) {
248
+ const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
249
+ colorStops.forEach(({ offset, color }) => {
250
+ gradient.addColorStop(offset, color);
251
+ });
252
+ return gradient;
253
+ }
254
+ /**
255
+ * Export canvas to blob
256
+ */
257
+ async exportToBlob(canvas, options = {}) {
258
+ const format = options.format || this.config.defaultFormat;
259
+ const quality = options.quality || this.config.defaultQuality;
260
+ const scale = options.scale || this.config.scale;
261
+ const scaledCanvas = (scale || 1) !== 1 ? this.scaleCanvas(canvas, scale || 1) : canvas;
262
+ const mimeType = `image/${format || "png"}`;
263
+ const blob = await new Promise((resolve) => {
264
+ scaledCanvas.toBlob(
265
+ (result) => resolve(result),
266
+ mimeType,
267
+ quality
268
+ );
269
+ });
270
+ if (!blob) {
271
+ throw new Error("Failed to export canvas to blob");
272
+ }
273
+ const url = URL.createObjectURL(blob);
274
+ return {
275
+ blob,
276
+ url,
277
+ dimensions: {
278
+ width: scaledCanvas.width,
279
+ height: scaledCanvas.height
280
+ },
281
+ format: format || "png",
282
+ size: blob.size
283
+ };
284
+ }
285
+ /**
286
+ * Export canvas to data URL
287
+ */
288
+ exportToDataURL(canvas, format, quality) {
289
+ const mimeType = format || `image/${this.config.defaultFormat}`;
290
+ const q = quality || this.config.defaultQuality;
291
+ return canvas.toDataURL(mimeType, q);
292
+ }
293
+ /**
294
+ * Scale canvas
295
+ */
296
+ scaleCanvas(canvas, scale) {
297
+ const scaled = document.createElement("canvas");
298
+ scaled.width = canvas.width * scale;
299
+ scaled.height = canvas.height * scale;
300
+ const ctx = scaled.getContext("2d");
301
+ if (ctx) {
302
+ ctx.imageSmoothingEnabled = true;
303
+ ctx.imageSmoothingQuality = "high";
304
+ ctx.drawImage(canvas, 0, 0, scaled.width, scaled.height);
305
+ }
306
+ return scaled;
307
+ }
308
+ /**
309
+ * Clone canvas
310
+ */
311
+ cloneCanvas(canvas) {
312
+ const clone = document.createElement("canvas");
313
+ clone.width = canvas.width;
314
+ clone.height = canvas.height;
315
+ const ctx = clone.getContext("2d");
316
+ if (ctx) {
317
+ ctx.drawImage(canvas, 0, 0);
318
+ }
319
+ return clone;
320
+ }
321
+ };
322
+
323
+ // src/infrastructure/renderers/PatternRenderer.ts
324
+ var PatternRenderer = class {
325
+ constructor() {
326
+ this.patternCanvasCache = /* @__PURE__ */ new Map();
327
+ this.MAX_CACHE_SIZE = 10;
328
+ }
329
+ /**
330
+ * Render pattern on canvas
331
+ */
332
+ renderPattern(ctx, pattern, dimensions, color = "#000000") {
333
+ if (pattern === "none") return;
334
+ ctx.save();
335
+ ctx.globalAlpha = 0.1;
336
+ switch (pattern) {
337
+ case "dots":
338
+ this.renderDotPattern(ctx, dimensions, color);
339
+ break;
340
+ case "grid":
341
+ this.renderGridPattern(ctx, dimensions, color);
342
+ break;
343
+ case "lines":
344
+ this.renderLinePattern(ctx, dimensions, color);
345
+ break;
346
+ case "waves":
347
+ this.renderWavePattern(ctx, dimensions, color);
348
+ break;
349
+ case "checkers":
350
+ this.renderCheckerPattern(ctx, dimensions, color);
351
+ break;
352
+ }
353
+ ctx.restore();
354
+ }
355
+ /**
356
+ * Clear pattern cache to free memory
357
+ */
358
+ clearCache() {
359
+ this.patternCanvasCache.clear();
360
+ }
361
+ /**
362
+ * Render dot pattern - optimized with batch rendering
363
+ */
364
+ renderDotPattern(ctx, dimensions, color) {
365
+ ctx.fillStyle = color;
366
+ const spacing = 30;
367
+ const radius = 2;
368
+ ctx.beginPath();
369
+ for (let x = spacing; x < dimensions.width; x += spacing) {
370
+ for (let y = spacing; y < dimensions.height; y += spacing) {
371
+ ctx.moveTo(x + radius, y);
372
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
373
+ }
374
+ }
375
+ ctx.fill();
376
+ }
377
+ /**
378
+ * Render grid pattern - optimized with single path
379
+ */
380
+ renderGridPattern(ctx, dimensions, color) {
381
+ ctx.strokeStyle = color;
382
+ ctx.lineWidth = 1;
383
+ const spacing = 30;
384
+ ctx.beginPath();
385
+ for (let x = 0; x <= dimensions.width; x += spacing) {
386
+ ctx.moveTo(x, 0);
387
+ ctx.lineTo(x, dimensions.height);
388
+ }
389
+ for (let y = 0; y <= dimensions.height; y += spacing) {
390
+ ctx.moveTo(0, y);
391
+ ctx.lineTo(dimensions.width, y);
392
+ }
393
+ ctx.stroke();
394
+ }
395
+ /**
396
+ * Render line pattern - optimized with single path
397
+ */
398
+ renderLinePattern(ctx, dimensions, color) {
399
+ ctx.strokeStyle = color;
400
+ ctx.lineWidth = 2;
401
+ const spacing = 20;
402
+ ctx.beginPath();
403
+ for (let y = spacing; y < dimensions.height; y += spacing) {
404
+ ctx.moveTo(0, y);
405
+ ctx.lineTo(dimensions.width, y);
406
+ }
407
+ ctx.stroke();
408
+ }
409
+ /**
410
+ * Render wave pattern - optimized with pre-calculated values
411
+ */
412
+ renderWavePattern(ctx, dimensions, color) {
413
+ ctx.strokeStyle = color;
414
+ ctx.lineWidth = 2;
415
+ const amplitude = 10;
416
+ const frequency = 0.1;
417
+ const spacing = 40;
418
+ const step = 5;
419
+ for (let y = spacing; y < dimensions.height; y += spacing) {
420
+ ctx.beginPath();
421
+ const startX = 0;
422
+ const startY = y + Math.sin(startX * frequency) * amplitude;
423
+ ctx.moveTo(startX, startY);
424
+ for (let x = step; x <= dimensions.width; x += step) {
425
+ const waveY = y + Math.sin(x * frequency) * amplitude;
426
+ ctx.lineTo(x, waveY);
427
+ }
428
+ ctx.stroke();
429
+ }
430
+ }
431
+ /**
432
+ * Render checker pattern
433
+ */
434
+ renderCheckerPattern(ctx, dimensions, color) {
435
+ ctx.fillStyle = color;
436
+ const size = 20;
437
+ for (let x = 0; x < dimensions.width; x += size * 2) {
438
+ for (let y = 0; y < dimensions.height; y += size * 2) {
439
+ ctx.fillRect(x, y, size, size);
440
+ ctx.fillRect(x + size, y + size, size, size);
441
+ }
442
+ }
443
+ }
444
+ };
445
+
446
+ // src/infrastructure/services/DeviceConfigService.ts
447
+ var DeviceConfigService = class _DeviceConfigService {
448
+ constructor() {
449
+ this.phonesCache = null;
450
+ this.tabletsCache = null;
451
+ this.allDevicesCache = null;
452
+ this.devices = /* @__PURE__ */ new Map();
453
+ this.initializeDevices();
454
+ }
455
+ static getInstance() {
456
+ if (!_DeviceConfigService.instance) {
457
+ _DeviceConfigService.instance = new _DeviceConfigService();
458
+ }
459
+ return _DeviceConfigService.instance;
460
+ }
461
+ initializeDevices() {
462
+ const deviceConfigs = [
463
+ {
464
+ name: "iPhone 15 Pro",
465
+ dimensions: { width: 1290, height: 2796, scale: 3 },
466
+ isPhone: true,
467
+ hasNotch: true,
468
+ cornerRadius: 20,
469
+ frameThickness: 8
470
+ },
471
+ {
472
+ name: "iPhone 15",
473
+ dimensions: { width: 1179, height: 2556, scale: 3 },
474
+ isPhone: true,
475
+ hasNotch: true,
476
+ cornerRadius: 20,
477
+ frameThickness: 8
478
+ },
479
+ {
480
+ name: "iPhone SE",
481
+ dimensions: { width: 1080, height: 2340, scale: 3 },
482
+ isPhone: true,
483
+ hasNotch: false,
484
+ cornerRadius: 16,
485
+ frameThickness: 6
486
+ },
487
+ {
488
+ name: "iPad Pro",
489
+ dimensions: { width: 2048, height: 2732, scale: 2 },
490
+ isTablet: true,
491
+ hasNotch: false,
492
+ cornerRadius: 24,
493
+ frameThickness: 10
494
+ },
495
+ {
496
+ name: "iPad",
497
+ dimensions: { width: 1620, height: 2160, scale: 2 },
498
+ isTablet: true,
499
+ hasNotch: false,
500
+ cornerRadius: 20,
501
+ frameThickness: 10
502
+ },
503
+ {
504
+ name: "Android Phone",
505
+ dimensions: { width: 1440, height: 3120, scale: 3 },
506
+ isPhone: true,
507
+ hasNotch: false,
508
+ cornerRadius: 24,
509
+ frameThickness: 8
510
+ },
511
+ {
512
+ name: "Android Tablet",
513
+ dimensions: { width: 2e3, height: 2200, scale: 2 },
514
+ isTablet: true,
515
+ hasNotch: false,
516
+ cornerRadius: 24,
517
+ frameThickness: 10
518
+ },
519
+ {
520
+ name: "Desktop",
521
+ dimensions: { width: 1920, height: 1080, scale: 1 },
522
+ isPhone: false,
523
+ isTablet: false,
524
+ hasNotch: false,
525
+ cornerRadius: 0,
526
+ frameThickness: 0
527
+ }
528
+ ];
529
+ Object.entries(DEVICE_DIMENSIONS).forEach(([type, dimensions]) => {
530
+ const deviceType = type;
531
+ const config = deviceConfigs.find((_, index) => {
532
+ const types = Object.keys(DEVICE_DIMENSIONS);
533
+ return types[index] === type;
534
+ });
535
+ if (config) {
536
+ this.devices.set(deviceType, {
537
+ type: deviceType,
538
+ ...config,
539
+ dimensions
540
+ });
541
+ }
542
+ });
543
+ }
544
+ getDeviceConfig(deviceType) {
545
+ return this.devices.get(deviceType);
546
+ }
547
+ getAllDevices() {
548
+ if (!this.allDevicesCache) {
549
+ this.allDevicesCache = Array.from(this.devices.values());
550
+ }
551
+ return this.allDevicesCache;
552
+ }
553
+ getPhones() {
554
+ if (!this.phonesCache) {
555
+ this.phonesCache = this.getAllDevices().filter((d) => d.isPhone);
556
+ }
557
+ return this.phonesCache;
558
+ }
559
+ getTablets() {
560
+ if (!this.tabletsCache) {
561
+ this.tabletsCache = this.getAllDevices().filter((d) => d.isTablet);
562
+ }
563
+ return this.tabletsCache;
564
+ }
565
+ isPhone(deviceType) {
566
+ const config = this.getDeviceConfig(deviceType);
567
+ return config?.isPhone || false;
568
+ }
569
+ isTablet(deviceType) {
570
+ const config = this.getDeviceConfig(deviceType);
571
+ return config?.isTablet || false;
572
+ }
573
+ hasNotch(deviceType) {
574
+ const config = this.getDeviceConfig(deviceType);
575
+ return config?.hasNotch || false;
576
+ }
577
+ };
578
+
579
+ // src/infrastructure/renderers/ScreenshotRenderer.ts
580
+ var ScreenshotRenderer = class {
581
+ constructor() {
582
+ this.renderCache = /* @__PURE__ */ new Map();
583
+ this.MAX_CACHE_SIZE = 5;
584
+ this.canvasRenderer = new CanvasRenderer();
585
+ this.patternRenderer = new PatternRenderer();
586
+ this.deviceConfigService = DeviceConfigService.getInstance();
587
+ }
588
+ /**
589
+ * Clear render cache to free memory
590
+ */
591
+ clearCache() {
592
+ this.renderCache.clear();
593
+ this.canvasRenderer.clearImageCache?.();
594
+ this.patternRenderer.clearCache?.();
595
+ }
596
+ /**
597
+ * Render device frame
598
+ */
599
+ renderDeviceFrame(ctx, dimensions, deviceType, frameStyle, frameColor) {
600
+ const deviceConfig = this.deviceConfigService.getDeviceConfig(deviceType);
601
+ if (!deviceConfig) return;
602
+ if (frameStyle === "none") return;
603
+ const isPhone = this.deviceConfigService.isPhone(deviceType);
604
+ const isTablet = this.deviceConfigService.isTablet(deviceType);
605
+ if (!isPhone && !isTablet) return;
606
+ const frameThickness = deviceConfig.frameThickness || 8;
607
+ if (frameStyle === "realistic") {
608
+ this.renderRealisticFrame(ctx, dimensions, frameColor, frameThickness);
609
+ if (isPhone && this.deviceConfigService.hasNotch(deviceType)) {
610
+ this.renderNotch(ctx, dimensions);
611
+ }
612
+ } else if (frameStyle === "minimal") {
613
+ this.renderMinimalFrame(ctx, dimensions, frameColor, frameThickness);
614
+ }
615
+ }
616
+ /**
617
+ * Render realistic device frame
618
+ */
619
+ renderRealisticFrame(ctx, dimensions, color, thickness) {
620
+ ctx.strokeStyle = color;
621
+ ctx.lineWidth = thickness;
622
+ ctx.strokeRect(
623
+ thickness / 2,
624
+ thickness / 2,
625
+ dimensions.width - thickness,
626
+ dimensions.height - thickness
627
+ );
628
+ }
629
+ /**
630
+ * Render minimal device frame
631
+ */
632
+ renderMinimalFrame(ctx, dimensions, color, thickness) {
633
+ ctx.strokeStyle = color;
634
+ ctx.lineWidth = thickness;
635
+ ctx.strokeRect(0, 0, dimensions.width, dimensions.height);
636
+ }
637
+ /**
638
+ * Render device notch
639
+ */
640
+ renderNotch(ctx, dimensions) {
641
+ ctx.fillStyle = "#1f2937";
642
+ ctx.beginPath();
643
+ ctx.arc(dimensions.width / 2, 20, 10, 0, Math.PI * 2);
644
+ ctx.fill();
645
+ }
646
+ /**
647
+ * Render background - optimized
648
+ */
649
+ renderBackground(ctx, design, dimensions) {
650
+ if (design.backgroundGradientEnd) {
651
+ const gradient = this.canvasRenderer.createLinearGradient(
652
+ ctx,
653
+ 0,
654
+ 0,
655
+ dimensions.width,
656
+ dimensions.height,
657
+ [
658
+ { offset: 0, color: design.backgroundColor },
659
+ { offset: 1, color: design.backgroundGradientEnd }
660
+ ]
661
+ );
662
+ ctx.fillStyle = gradient;
663
+ } else {
664
+ ctx.fillStyle = design.backgroundColor;
665
+ }
666
+ ctx.fillRect(0, 0, dimensions.width, dimensions.height);
667
+ if (design.backgroundPattern && design.backgroundPattern !== "none") {
668
+ this.patternRenderer.renderPattern(
669
+ ctx,
670
+ design.backgroundPattern,
671
+ dimensions
672
+ );
673
+ }
674
+ }
675
+ /**
676
+ * Render text content
677
+ */
678
+ renderTextContent(ctx, content, design, dimensions) {
679
+ const titleConfig = {
680
+ fillStyle: design.textColor,
681
+ font: `bold ${design.titleFontSize}px ${design.fontFamily}`,
682
+ textAlign: design.textAlign
683
+ };
684
+ const subtitleConfig = {
685
+ fillStyle: design.textColor,
686
+ font: `${design.subtitleFontSize}px ${design.fontFamily}`,
687
+ textAlign: design.textAlign
688
+ };
689
+ const titleY = design.layout === "text-bottom" ? dimensions.height - 150 : 100 + (design.offsetY || 0);
690
+ this.canvasRenderer.drawText(
691
+ ctx,
692
+ this.formatText(content.title, design.textCase),
693
+ dimensions.width / 2,
694
+ titleY,
695
+ titleConfig
696
+ );
697
+ const subtitleY = titleY + design.titleFontSize + (design.gap || 20);
698
+ this.canvasRenderer.drawText(
699
+ ctx,
700
+ this.formatText(content.subtitle, design.textCase),
701
+ dimensions.width / 2,
702
+ subtitleY,
703
+ subtitleConfig
704
+ );
705
+ }
706
+ /**
707
+ * Format text case
708
+ */
709
+ formatText(text, textCase = "none") {
710
+ switch (textCase) {
711
+ case "uppercase":
712
+ return text.toUpperCase();
713
+ case "lowercase":
714
+ return text.toLowerCase();
715
+ default:
716
+ return text;
717
+ }
718
+ }
719
+ /**
720
+ * Render screenshot image
721
+ */
722
+ async renderScreenshotImage(ctx, imageUrl, design, dimensions) {
723
+ if (!imageUrl) return;
724
+ const deviceConfig = this.deviceConfigService.getDeviceConfig(
725
+ design.deviceType
726
+ );
727
+ if (!deviceConfig) return;
728
+ const frameThickness = deviceConfig.frameThickness || 8;
729
+ const padding = 60 + frameThickness;
730
+ const screenshotWidth = dimensions.width - padding * 2;
731
+ const screenshotHeight = dimensions.height - padding * 2;
732
+ try {
733
+ await this.canvasRenderer.drawImage(
734
+ ctx,
735
+ imageUrl,
736
+ padding,
737
+ padding,
738
+ screenshotWidth,
739
+ screenshotHeight
740
+ );
741
+ } catch (error) {
742
+ console.error("Failed to render screenshot image:", error);
743
+ }
744
+ }
745
+ /**
746
+ * Render complete screenshot
747
+ */
748
+ async renderDesignedScreenshot(content, design) {
749
+ const deviceConfig = this.deviceConfigService.getDeviceConfig(design.deviceType);
750
+ if (!deviceConfig) {
751
+ throw new Error(`Device config not found: ${design.deviceType}`);
752
+ }
753
+ const dimensions = deviceConfig.dimensions;
754
+ const canvas = this.canvasRenderer.createCanvas(dimensions);
755
+ const ctx = canvas.getContext("2d");
756
+ if (!ctx) {
757
+ throw new Error("Failed to get canvas context");
758
+ }
759
+ this.renderBackground(ctx, design, dimensions);
760
+ if (content.screenshot) {
761
+ await this.renderScreenshotImage(ctx, content.screenshot, design, dimensions);
762
+ }
763
+ this.renderTextContent(ctx, content, design, dimensions);
764
+ this.renderDeviceFrame(
765
+ ctx,
766
+ dimensions,
767
+ design.deviceType,
768
+ design.frameStyle,
769
+ design.deviceColor
770
+ );
771
+ return canvas;
772
+ }
773
+ /**
774
+ * Render placeholder content
775
+ */
776
+ renderPlaceholderContent(ctx, dimensions, deviceType) {
777
+ this.canvasRenderer.fillRect(ctx, 0, 0, dimensions.width, dimensions.height, "#ffffff");
778
+ this.canvasRenderer.fillRect(ctx, 0, 0, dimensions.width, 60, "#f3f4f6");
779
+ this.canvasRenderer.fillRect(
780
+ ctx,
781
+ 0,
782
+ dimensions.height - 80,
783
+ dimensions.width,
784
+ 80,
785
+ "#f3f4f6"
786
+ );
787
+ ctx.fillStyle = "#e5e7eb";
788
+ for (let i = 0; i < 5; i++) {
789
+ const y = 80 + i * 120;
790
+ const height = Math.min(100, dimensions.height - y - 100);
791
+ if (y < dimensions.height - 100) {
792
+ this.canvasRenderer.fillRect(ctx, 20, y, dimensions.width - 40, height);
793
+ }
794
+ }
795
+ const deviceConfig = this.deviceConfigService.getDeviceConfig(deviceType);
796
+ if (deviceConfig) {
797
+ this.renderDeviceFrame(
798
+ ctx,
799
+ dimensions,
800
+ deviceType,
801
+ "realistic",
802
+ "#1f2937"
803
+ );
804
+ }
805
+ }
806
+ };
807
+
808
+ // src/application/services/ScreenshotStudioService.ts
809
+ var ScreenshotStudioService = class {
810
+ constructor(config) {
811
+ this.config = {
812
+ ...DEFAULT_SCREENSHOT_STUDIO_CONFIG,
813
+ ...config
814
+ };
815
+ this.renderer = new ScreenshotRenderer();
816
+ }
817
+ /**
818
+ * Generate screenshot
819
+ */
820
+ async generateScreenshot(options) {
821
+ const deviceConfig = this.config.device?.devices.find(
822
+ (d) => d === options.deviceType
823
+ );
824
+ if (!deviceConfig) {
825
+ throw new Error(`Device configuration not found for ${options.deviceType}`);
826
+ }
827
+ const canvas = this.renderer["canvasRenderer"].createCanvas(
828
+ options.customDimensions || { width: 1290, height: 2796 }
829
+ );
830
+ const ctx = canvas.getContext("2d");
831
+ if (!ctx) {
832
+ throw new Error("Failed to get canvas context");
833
+ }
834
+ this.renderer["renderPlaceholderContent"](
835
+ ctx,
836
+ { width: canvas.width, height: canvas.height },
837
+ options.deviceType
838
+ );
839
+ const blob = await this.renderer["canvasRenderer"].exportToBlob(canvas, {
840
+ format: options.format || this.config.export?.defaultFormat || "png",
841
+ quality: options.quality || this.config.canvas?.defaultQuality || 0.9
842
+ });
843
+ return {
844
+ id: this.generateId(),
845
+ deviceType: options.deviceType,
846
+ dimensions: { width: canvas.width, height: canvas.height },
847
+ format: options.format || this.config.export?.defaultFormat || "png",
848
+ timestamp: /* @__PURE__ */ new Date(),
849
+ fileSize: blob.blob.size,
850
+ url: blob.url
851
+ };
852
+ }
853
+ /**
854
+ * Generate batch screenshots
855
+ */
856
+ async generateBatchScreenshots(deviceTypes) {
857
+ const BATCH_SIZE = 3;
858
+ const results = [];
859
+ for (let i = 0; i < deviceTypes.length; i += BATCH_SIZE) {
860
+ const batch = deviceTypes.slice(i, i + BATCH_SIZE);
861
+ const batchPromises = batch.map(
862
+ (deviceType) => this.generateScreenshot({ deviceType })
863
+ );
864
+ const batchResults = await Promise.all(batchPromises);
865
+ results.push(...batchResults);
866
+ }
867
+ return results;
868
+ }
869
+ /**
870
+ * Generate designed screenshot
871
+ */
872
+ async generateDesignedScreenshot(content, design) {
873
+ const canvas = await this.renderer.renderDesignedScreenshot(content, design);
874
+ const blob = await this.renderer["canvasRenderer"].exportToBlob(canvas, {
875
+ format: "png"
876
+ });
877
+ return {
878
+ id: this.generateId(),
879
+ deviceType: design.deviceType,
880
+ dimensions: { width: canvas.width, height: canvas.height },
881
+ format: "png",
882
+ timestamp: /* @__PURE__ */ new Date(),
883
+ fileSize: blob.blob.size,
884
+ url: blob.url
885
+ };
886
+ }
887
+ /**
888
+ * Export project as ZIP
889
+ */
890
+ async exportProjectAsZip(project) {
891
+ const JSZip = (await import("jszip")).default;
892
+ const zip = new JSZip();
893
+ const folder = zip.folder(project.currentLanguage);
894
+ if (!folder) {
895
+ throw new Error("Failed to create folder in ZIP");
896
+ }
897
+ if (this.config.export?.includeMetadata) {
898
+ const metadataText = `APP NAME: ${project.appMetadata.appName}
899
+ SUBTITLE: ${project.appMetadata.subtitle}
900
+ DESCRIPTION:
901
+ ${project.appMetadata.description}
902
+
903
+ KEYWORDS: ${project.appMetadata.keywords}
904
+ WHATS NEW:
905
+ ${project.appMetadata.whatsNew}`;
906
+ folder.file("aso_metadata.txt", metadataText);
907
+ }
908
+ const screenshotPromises = project.screens.map(
909
+ (screen, index) => this.generateDesignedScreenshot(screen, project.design).then((screenshot) => ({ screenshot, index }))
910
+ );
911
+ const screenshots = await Promise.all(screenshotPromises);
912
+ screenshots.forEach(({ screenshot, index }) => {
913
+ const base64Data = screenshot.url.split(",")[1];
914
+ folder.file(
915
+ `screen_${index + 1}_${project.design.deviceType}.png`,
916
+ base64Data,
917
+ { base64: true }
918
+ );
919
+ });
920
+ return await zip.generateAsync({ type: "blob" });
921
+ }
922
+ /**
923
+ * Create project
924
+ */
925
+ createProject(name) {
926
+ return {
927
+ id: this.generateId(),
928
+ name,
929
+ design: this.getDefaultDesign(),
930
+ screens: [this.getDefaultScreen()],
931
+ currentLanguage: this.config.defaultLanguage || "en-US",
932
+ selectedScreenId: "1",
933
+ appMetadata: {
934
+ appName: "My App",
935
+ subtitle: "App Description",
936
+ promotionalText: "Promotional text",
937
+ keywords: "keyword1, keyword2",
938
+ description: "App description",
939
+ whatsNew: "What is new in this version"
940
+ },
941
+ appIcon: null,
942
+ createdAt: /* @__PURE__ */ new Date(),
943
+ updatedAt: /* @__PURE__ */ new Date()
944
+ };
945
+ }
946
+ /**
947
+ * Get default design
948
+ */
949
+ getDefaultDesign() {
950
+ return {
951
+ backgroundColor: "#6366f1",
952
+ backgroundGradientEnd: "#a855f7",
953
+ backgroundImage: null,
954
+ backgroundBlur: 0,
955
+ backgroundOverlayOpacity: 0,
956
+ backgroundPattern: "none",
957
+ textColor: "#ffffff",
958
+ highlightColor: "#fbbf24",
959
+ textAlign: "center",
960
+ deviceColor: "#1f2937",
961
+ deviceType: this.config.device?.defaultDevice || "iphone-15-pro",
962
+ frameStyle: "realistic",
963
+ layout: "text-top",
964
+ scale: 0.85,
965
+ fontFamily: "Inter",
966
+ titleFontSize: 60,
967
+ subtitleFontSize: 24,
968
+ titleFontWeight: "900",
969
+ subtitleFontWeight: "500",
970
+ textCase: "uppercase",
971
+ lineHeight: 0.9,
972
+ letterSpacing: -1,
973
+ rotation: 0,
974
+ rotationX: 0,
975
+ rotationY: 0,
976
+ rotationZ: 0,
977
+ offsetY: 0,
978
+ shadowIntensity: 0.5,
979
+ padding: 40,
980
+ gap: 20,
981
+ isPanoramic: false
982
+ };
983
+ }
984
+ /**
985
+ * Get default screen
986
+ */
987
+ getDefaultScreen() {
988
+ return {
989
+ id: "1",
990
+ title: "MY *APP*",
991
+ subtitle: "Your app description here",
992
+ screenshot: null,
993
+ translations: {
994
+ "en-US": { title: "MY *APP*", subtitle: "Your app description here" }
995
+ },
996
+ imageFit: "cover",
997
+ imageZoom: 1,
998
+ imageAnchorX: 50,
999
+ imageAnchorY: 50
1000
+ };
1001
+ }
1002
+ /**
1003
+ * Generate unique ID - optimized version
1004
+ */
1005
+ generateId() {
1006
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
1007
+ return crypto.randomUUID();
1008
+ }
1009
+ return `${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 9)}`;
1010
+ }
1011
+ };
1012
+
1013
+ // src/application/services/ReportService.ts
1014
+ var ReportService = class _ReportService {
1015
+ static getInstance() {
1016
+ if (!_ReportService.instance) {
1017
+ _ReportService.instance = new _ReportService();
1018
+ }
1019
+ return _ReportService.instance;
1020
+ }
1021
+ /**
1022
+ * Export DOM element to PDF
1023
+ */
1024
+ async exportToPDF(elementId, options = {}) {
1025
+ const html2canvas = (await import("html2canvas")).default;
1026
+ const jsPDF = (await import("jspdf")).default;
1027
+ const element = document.getElementById(elementId);
1028
+ if (!element) {
1029
+ throw new Error("Report element not found");
1030
+ }
1031
+ const canvas = await html2canvas(element, {
1032
+ scale: options.scale || 2,
1033
+ useCORS: options.useCORS !== false,
1034
+ backgroundColor: options.backgroundColor || "#ffffff"
1035
+ });
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 || "report.pdf");
1042
+ }
1043
+ /**
1044
+ * Export canvas to PDF
1045
+ */
1046
+ async exportCanvasToPDF(canvas, options = {}) {
1047
+ const jsPDF = (await import("jspdf")).default;
1048
+ const imgData = canvas.toDataURL("image/png");
1049
+ const pdf = new jsPDF("p", "mm", "a4");
1050
+ const imgWidth = 210;
1051
+ const imgHeight = canvas.height * imgWidth / canvas.width;
1052
+ pdf.addImage(imgData, "PNG", 0, 0, imgWidth, imgHeight);
1053
+ pdf.save(options.filename || "canvas-export.pdf");
1054
+ }
1055
+ /**
1056
+ * Export data to Excel
1057
+ */
1058
+ async exportToExcel(data, options = {}) {
1059
+ const XLSX = await import("xlsx");
1060
+ const wb = XLSX.utils.book_new();
1061
+ const apps = data.items.filter((i) => i.type === "app").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
1062
+ const campaigns = data.items.filter((i) => i.type === "campaign").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
1063
+ const posts = data.items.filter((i) => i.type === "post").map((i) => ({ Name: i.name, ...i.metrics, ...i.details }));
1064
+ if (apps.length > 0) {
1065
+ XLSX.utils.book_append_sheet(
1066
+ wb,
1067
+ XLSX.utils.json_to_sheet(apps),
1068
+ options.sheetName || "Apps"
1069
+ );
1070
+ }
1071
+ if (campaigns.length > 0) {
1072
+ XLSX.utils.book_append_sheet(
1073
+ wb,
1074
+ XLSX.utils.json_to_sheet(campaigns),
1075
+ "Campaigns"
1076
+ );
1077
+ }
1078
+ if (posts.length > 0) {
1079
+ XLSX.utils.book_append_sheet(
1080
+ wb,
1081
+ XLSX.utils.json_to_sheet(posts),
1082
+ "Posts"
1083
+ );
1084
+ }
1085
+ if (options.includeSummary !== false) {
1086
+ const summary = [
1087
+ { Metric: "Total Reach", Value: data.summary.totalReach },
1088
+ {
1089
+ Metric: "Total Engagement",
1090
+ Value: data.summary.totalEngagement
1091
+ },
1092
+ {
1093
+ Metric: "Average Score",
1094
+ Value: data.summary.averageScore
1095
+ },
1096
+ {
1097
+ Metric: "Export Date",
1098
+ Value: (/* @__PURE__ */ new Date()).toLocaleDateString()
1099
+ }
1100
+ ];
1101
+ XLSX.utils.book_append_sheet(
1102
+ wb,
1103
+ XLSX.utils.json_to_sheet(summary),
1104
+ "Summary"
1105
+ );
1106
+ }
1107
+ XLSX.writeFile(wb, options.filename || "report.xlsx");
1108
+ }
1109
+ /**
1110
+ * Export data to CSV
1111
+ */
1112
+ exportToCSV(data, filename = "report.csv", delimiter = ",") {
1113
+ const headers = ["Type", "Name", "Metrics"];
1114
+ const rows = data.items.map((item) => [
1115
+ item.type,
1116
+ item.name,
1117
+ JSON.stringify(item.metrics)
1118
+ ]);
1119
+ const csvContent = [headers, ...rows].map((row) => row.map((cell) => `"${cell}"`).join(delimiter)).join("\n");
1120
+ const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
1121
+ const url = URL.createObjectURL(blob);
1122
+ const link = document.createElement("a");
1123
+ link.href = url;
1124
+ link.download = filename;
1125
+ link.click();
1126
+ URL.revokeObjectURL(url);
1127
+ }
1128
+ /**
1129
+ * Export multiple elements to PDF
1130
+ */
1131
+ async exportMultipleToPDF(elementIds, options = {}) {
1132
+ const html2canvas = (await import("html2canvas")).default;
1133
+ const jsPDF = (await import("jspdf")).default;
1134
+ const pdf = new jsPDF("p", "mm", "a4");
1135
+ for (let i = 0; i < elementIds.length; i++) {
1136
+ const element = document.getElementById(elementIds[i]);
1137
+ if (!element) continue;
1138
+ const canvas = await html2canvas(element, {
1139
+ scale: options.scale || 2,
1140
+ useCORS: options.useCORS !== false,
1141
+ backgroundColor: options.backgroundColor || "#ffffff"
1142
+ });
1143
+ const imgData = canvas.toDataURL("image/png");
1144
+ const imgWidth = 210;
1145
+ const imgHeight = canvas.height * imgWidth / canvas.width;
1146
+ if (i > 0) {
1147
+ pdf.addPage();
1148
+ }
1149
+ pdf.addImage(imgData, "PNG", 0, 0, imgWidth, imgHeight);
1150
+ }
1151
+ pdf.save(options.filename || "multi-page-report.pdf");
1152
+ }
1153
+ };
1154
+
1155
+ // src/presentation/hooks/useScreenshotStudio.ts
1156
+ var import_react = require("react");
1157
+ var DEFAULT_SCREENS = [
1158
+ {
1159
+ id: "1",
1160
+ title: "APPGROWTHFACTORY *MODERN*",
1161
+ subtitle: "Create content with AI power",
1162
+ screenshot: null,
1163
+ translations: {
1164
+ "en-US": {
1165
+ title: "APPGROWTHFACTORY *MODERN*",
1166
+ subtitle: "Create content with AI power"
1167
+ },
1168
+ "tr-TR": {
1169
+ title: "APPGROWTHFACTORY *MODERN*",
1170
+ subtitle: "Yapay zeka g\xFCc\xFCyle i\xE7erik \xFCret"
1171
+ }
1172
+ },
1173
+ imageFit: "cover",
1174
+ imageZoom: 1,
1175
+ imageAnchorX: 50,
1176
+ imageAnchorY: 50
1177
+ },
1178
+ {
1179
+ id: "2",
1180
+ title: "CONNECT *GLOBAL*",
1181
+ subtitle: "Post to all platforms instantly",
1182
+ screenshot: null,
1183
+ translations: {
1184
+ "en-US": {
1185
+ title: "CONNECT *GLOBAL*",
1186
+ subtitle: "Post to all platforms instantly"
1187
+ },
1188
+ "tr-TR": {
1189
+ title: "BA\u011ELAN *K\xDCRESEL*",
1190
+ subtitle: "T\xFCm platformlara an\u0131nda g\xF6nder"
1191
+ }
1192
+ },
1193
+ imageFit: "cover",
1194
+ imageZoom: 1,
1195
+ imageAnchorX: 50,
1196
+ imageAnchorY: 50
1197
+ }
1198
+ ];
1199
+ var DEFAULT_DESIGN = {
1200
+ backgroundColor: "#6366f1",
1201
+ backgroundGradientEnd: "#a855f7",
1202
+ backgroundImage: null,
1203
+ backgroundBlur: 0,
1204
+ backgroundOverlayOpacity: 0,
1205
+ backgroundPattern: "none",
1206
+ textColor: "#ffffff",
1207
+ highlightColor: "#fbbf24",
1208
+ textAlign: "center",
1209
+ deviceColor: "#1f2937",
1210
+ deviceType: "iphone-15-pro",
1211
+ frameStyle: "realistic",
1212
+ layout: "text-top",
1213
+ scale: 0.85,
1214
+ fontFamily: "Inter",
1215
+ titleFontSize: 60,
1216
+ subtitleFontSize: 24,
1217
+ titleFontWeight: "900",
1218
+ subtitleFontWeight: "500",
1219
+ textCase: "uppercase",
1220
+ lineHeight: 0.9,
1221
+ letterSpacing: -1,
1222
+ rotation: 0,
1223
+ rotationX: 0,
1224
+ rotationY: 0,
1225
+ rotationZ: 0,
1226
+ offsetY: 0,
1227
+ shadowIntensity: 0.5,
1228
+ padding: 40,
1229
+ gap: 20,
1230
+ isPanoramic: false
1231
+ };
1232
+ var DEFAULT_APP_METADATA = {
1233
+ appName: "Growdo",
1234
+ subtitle: "AI Content Generator",
1235
+ promotionalText: "Unlock your creative potential",
1236
+ keywords: "ai, content, marketing, automation, generator",
1237
+ description: "The ultimate tool for AI-powered marketing content.",
1238
+ whatsNew: "New Design Studio integration!"
1239
+ };
1240
+ function useScreenshotStudio(options = {}) {
1241
+ const {
1242
+ config = {},
1243
+ onShowToast = () => {
1244
+ }
1245
+ } = options;
1246
+ const fullConfig = {
1247
+ ...DEFAULT_SCREENSHOT_STUDIO_CONFIG,
1248
+ ...config
1249
+ };
1250
+ const serviceRef = (0, import_react.useRef)(null);
1251
+ if (!serviceRef.current) {
1252
+ serviceRef.current = new ScreenshotStudioService(fullConfig);
1253
+ }
1254
+ (0, import_react.useEffect)(() => {
1255
+ return () => {
1256
+ if (serviceRef.current) {
1257
+ }
1258
+ };
1259
+ }, []);
1260
+ const [screens, setScreens] = (0, import_react.useState)(DEFAULT_SCREENS);
1261
+ const [design, setDesign] = (0, import_react.useState)(DEFAULT_DESIGN);
1262
+ const [currentLanguage, setCurrentLanguage] = (0, import_react.useState)(
1263
+ fullConfig.defaultLanguage || "en-US"
1264
+ );
1265
+ const [selectedScreenId, setSelectedScreenId] = (0, import_react.useState)("1");
1266
+ const [isProcessing, setIsProcessing] = (0, import_react.useState)(false);
1267
+ const [isAnalyzing, setIsAnalyzing] = (0, import_react.useState)(false);
1268
+ const [appMetadata, setAppMetadata] = (0, import_react.useState)(
1269
+ DEFAULT_APP_METADATA
1270
+ );
1271
+ const [appIcon, setAppIcon] = (0, import_react.useState)(null);
1272
+ const [projectId, setProjectId] = (0, import_react.useState)(null);
1273
+ const activeScreen = screens.find((s) => s.id === selectedScreenId) || screens[0];
1274
+ const updateDesign = (0, import_react.useCallback)((updates) => {
1275
+ setDesign((prev) => ({ ...prev, ...updates }));
1276
+ }, []);
1277
+ const updateActiveScreen = (0, import_react.useCallback)(
1278
+ (updates) => {
1279
+ setScreens(
1280
+ (prev) => prev.map((screen) => {
1281
+ if (screen.id !== selectedScreenId) return screen;
1282
+ const updatedScreen = { ...screen, ...updates };
1283
+ if (updates.title || updates.subtitle) {
1284
+ updatedScreen.translations = {
1285
+ ...updatedScreen.translations,
1286
+ [currentLanguage]: {
1287
+ title: updatedScreen.title,
1288
+ subtitle: updatedScreen.subtitle
1289
+ }
1290
+ };
1291
+ }
1292
+ return updatedScreen;
1293
+ })
1294
+ );
1295
+ },
1296
+ [selectedScreenId, currentLanguage]
1297
+ );
1298
+ const changeLanguage = (0, import_react.useCallback)(
1299
+ (newLanguage) => {
1300
+ setCurrentLanguage(newLanguage);
1301
+ setScreens(
1302
+ (prev) => prev.map((screen) => {
1303
+ const translation = screen.translations[newLanguage] || screen.translations["en-US"] || { title: screen.title, subtitle: screen.subtitle };
1304
+ return {
1305
+ ...screen,
1306
+ title: translation.title,
1307
+ subtitle: translation.subtitle
1308
+ };
1309
+ })
1310
+ );
1311
+ onShowToast("Language Changed", `Switched to ${newLanguage}`);
1312
+ },
1313
+ [onShowToast]
1314
+ );
1315
+ const addScreen = (0, import_react.useCallback)(() => {
1316
+ const newScreen = {
1317
+ id: Date.now().toString(),
1318
+ title: "NEW *POST*",
1319
+ subtitle: "Engage your audience",
1320
+ screenshot: null,
1321
+ translations: {
1322
+ [currentLanguage]: {
1323
+ title: "NEW *POST*",
1324
+ subtitle: "Engage your audience"
1325
+ }
1326
+ },
1327
+ imageFit: "cover",
1328
+ imageZoom: 1,
1329
+ imageAnchorX: 50,
1330
+ imageAnchorY: 50
1331
+ };
1332
+ setScreens((prev) => [...prev, newScreen]);
1333
+ setSelectedScreenId(newScreen.id);
1334
+ onShowToast("Success", "New screen added");
1335
+ }, [currentLanguage, onShowToast]);
1336
+ const removeScreen = (0, import_react.useCallback)(
1337
+ (screenId) => {
1338
+ if (screens.length <= 1) return;
1339
+ setScreens((prev) => {
1340
+ const newScreens = prev.filter((s) => s.id !== screenId);
1341
+ if (selectedScreenId === screenId) {
1342
+ setSelectedScreenId(newScreens[0].id);
1343
+ }
1344
+ return newScreens;
1345
+ });
1346
+ onShowToast("Info", "Screen removed");
1347
+ },
1348
+ [screens.length, selectedScreenId, onShowToast]
1349
+ );
1350
+ const duplicateScreen = (0, import_react.useCallback)(
1351
+ (screenId) => {
1352
+ const screenToDuplicate = screens.find((s) => s.id === screenId);
1353
+ if (!screenToDuplicate) return;
1354
+ const newScreen = {
1355
+ ...screenToDuplicate,
1356
+ id: Date.now().toString(),
1357
+ title: `${screenToDuplicate.title} (Copy)`,
1358
+ translations: { ...screenToDuplicate.translations }
1359
+ };
1360
+ setScreens((prev) => {
1361
+ const index = prev.findIndex((s) => s.id === screenId);
1362
+ const newScreens = [...prev];
1363
+ newScreens.splice(index + 1, 0, newScreen);
1364
+ return newScreens;
1365
+ });
1366
+ setSelectedScreenId(newScreen.id);
1367
+ onShowToast("Success", "Screen duplicated");
1368
+ },
1369
+ [screens, onShowToast]
1370
+ );
1371
+ const applyTemplate = (0, import_react.useCallback)(
1372
+ (template) => {
1373
+ setDesign((prev) => ({ ...prev, ...template.design }));
1374
+ onShowToast("Template Applied", template.name);
1375
+ },
1376
+ [onShowToast]
1377
+ );
1378
+ const generateDesignedScreenshot = (0, import_react.useCallback)(
1379
+ async (screenId) => {
1380
+ if (!serviceRef.current) return;
1381
+ const screen = screens.find((s) => s.id === screenId);
1382
+ if (!screen) return;
1383
+ setIsProcessing(true);
1384
+ try {
1385
+ const result = await serviceRef.current.generateDesignedScreenshot(
1386
+ screen,
1387
+ design
1388
+ );
1389
+ onShowToast("Success", "Screenshot generated");
1390
+ return { url: result.url, id: result.id };
1391
+ } catch {
1392
+ onShowToast("Error", "Screenshot generation failed");
1393
+ } finally {
1394
+ setIsProcessing(false);
1395
+ }
1396
+ },
1397
+ [screens, design, onShowToast]
1398
+ );
1399
+ const exportAllScreensAsZip = (0, import_react.useCallback)(
1400
+ async (format = "png") => {
1401
+ if (!serviceRef.current) return;
1402
+ setIsProcessing(true);
1403
+ let url = null;
1404
+ try {
1405
+ const project = {
1406
+ id: projectId || "default-project",
1407
+ name: "Export Project",
1408
+ design,
1409
+ screens,
1410
+ currentLanguage,
1411
+ selectedScreenId,
1412
+ appMetadata,
1413
+ appIcon,
1414
+ createdAt: /* @__PURE__ */ new Date(),
1415
+ updatedAt: /* @__PURE__ */ new Date()
1416
+ };
1417
+ const blob = await serviceRef.current.exportProjectAsZip(project);
1418
+ url = URL.createObjectURL(blob);
1419
+ const link = document.createElement("a");
1420
+ link.href = url;
1421
+ link.download = `Studio_${currentLanguage}_${Date.now()}.zip`;
1422
+ link.click();
1423
+ onShowToast(
1424
+ "Success",
1425
+ `Exported ${screens.length} screens in ZIP archive.`
1426
+ );
1427
+ } catch {
1428
+ onShowToast("Error", "ZIP export failed");
1429
+ } finally {
1430
+ setIsProcessing(false);
1431
+ if (url) {
1432
+ setTimeout(() => {
1433
+ try {
1434
+ URL.revokeObjectURL(url);
1435
+ } catch (e) {
1436
+ }
1437
+ }, 100);
1438
+ }
1439
+ }
1440
+ },
1441
+ [
1442
+ screens,
1443
+ design,
1444
+ currentLanguage,
1445
+ appMetadata,
1446
+ appIcon,
1447
+ projectId,
1448
+ selectedScreenId,
1449
+ onShowToast
1450
+ ]
1451
+ );
1452
+ const _getProjectState = (0, import_react.useCallback)(() => {
1453
+ return {
1454
+ id: projectId || "appgrowthfactory_main_project",
1455
+ name: "Default Project",
1456
+ design,
1457
+ screens,
1458
+ currentLanguage,
1459
+ selectedScreenId,
1460
+ appMetadata,
1461
+ appIcon,
1462
+ createdAt: /* @__PURE__ */ new Date(),
1463
+ updatedAt: /* @__PURE__ */ new Date()
1464
+ };
1465
+ }, [
1466
+ design,
1467
+ screens,
1468
+ currentLanguage,
1469
+ selectedScreenId,
1470
+ appMetadata,
1471
+ appIcon,
1472
+ projectId
1473
+ ]);
1474
+ const _loadProjectState = (0, import_react.useCallback)(
1475
+ (state) => {
1476
+ if (!state) return;
1477
+ if (state.screens) setScreens(state.screens);
1478
+ if (state.design) setDesign(state.design);
1479
+ if (state.currentLanguage) setCurrentLanguage(state.currentLanguage);
1480
+ if (state.selectedScreenId && state.screens?.find((s) => s.id === state.selectedScreenId)) {
1481
+ setSelectedScreenId(state.selectedScreenId);
1482
+ }
1483
+ if (state.appMetadata) setAppMetadata(state.appMetadata);
1484
+ if (state.appIcon !== void 0) setAppIcon(state.appIcon);
1485
+ },
1486
+ []
1487
+ );
1488
+ return {
1489
+ screens,
1490
+ design,
1491
+ currentLanguage,
1492
+ selectedScreenId,
1493
+ activeScreen,
1494
+ isProcessing,
1495
+ isAnalyzing,
1496
+ appMetadata,
1497
+ appIcon,
1498
+ projectId,
1499
+ updateDesign,
1500
+ updateActiveScreen,
1501
+ setSelectedScreenId,
1502
+ changeLanguage,
1503
+ applyTemplate,
1504
+ addScreen,
1505
+ removeScreen,
1506
+ duplicateScreen,
1507
+ generateDesignedScreenshot,
1508
+ exportAllScreensAsZip,
1509
+ setAppMetadata,
1510
+ setAppIcon,
1511
+ setProjectId,
1512
+ _getProjectState,
1513
+ _loadProjectState
1514
+ };
1515
+ }
1516
+
1517
+ // src/presentation/hooks/useCanvasExport.ts
1518
+ var import_react2 = require("react");
1519
+ function useCanvasExport(options = {}) {
1520
+ const { config = DEFAULT_CANVAS_CONFIG } = options;
1521
+ const [isExporting, setIsExporting] = (0, import_react2.useState)(false);
1522
+ const [progress, setProgress] = (0, import_react2.useState)(0);
1523
+ const [error, setError] = (0, import_react2.useState)(null);
1524
+ const rendererRef = (0, import_react2.useRef)(null);
1525
+ const urlCleanupRef = (0, import_react2.useRef)([]);
1526
+ if (!rendererRef.current) {
1527
+ rendererRef.current = new CanvasRenderer(config);
1528
+ }
1529
+ (0, import_react2.useEffect)(() => {
1530
+ return () => {
1531
+ urlCleanupRef.current.forEach((url) => {
1532
+ try {
1533
+ URL.revokeObjectURL(url);
1534
+ } catch (e) {
1535
+ }
1536
+ });
1537
+ urlCleanupRef.current = [];
1538
+ };
1539
+ }, []);
1540
+ const exportToBlob = (0, import_react2.useCallback)(
1541
+ async (canvas, options2) => {
1542
+ if (!rendererRef.current) return null;
1543
+ setIsExporting(true);
1544
+ setProgress(0);
1545
+ setError(null);
1546
+ try {
1547
+ const result = await rendererRef.current.exportToBlob(canvas, options2);
1548
+ setProgress(100);
1549
+ if (result?.url) {
1550
+ urlCleanupRef.current.push(result.url);
1551
+ }
1552
+ return result;
1553
+ } catch (err) {
1554
+ const errorMessage = err instanceof Error ? err.message : "Export failed";
1555
+ setError(errorMessage);
1556
+ return null;
1557
+ } finally {
1558
+ setIsExporting(false);
1559
+ }
1560
+ },
1561
+ []
1562
+ );
1563
+ const exportToDataURL = (0, import_react2.useCallback)(
1564
+ (canvas, format, quality) => {
1565
+ if (!rendererRef.current) return null;
1566
+ try {
1567
+ return rendererRef.current.exportToDataURL(canvas, format, quality);
1568
+ } catch (err) {
1569
+ const errorMessage = err instanceof Error ? err.message : "Export failed";
1570
+ setError(errorMessage);
1571
+ return null;
1572
+ }
1573
+ },
1574
+ []
1575
+ );
1576
+ const downloadCanvas = (0, import_react2.useCallback)(
1577
+ async (canvas, filename, options2) => {
1578
+ setIsExporting(true);
1579
+ setProgress(0);
1580
+ setError(null);
1581
+ try {
1582
+ const result = await exportToBlob(canvas, options2);
1583
+ if (!result) {
1584
+ throw new Error("Export failed");
1585
+ }
1586
+ setProgress(50);
1587
+ const link = document.createElement("a");
1588
+ link.href = result.url;
1589
+ link.download = filename;
1590
+ link.click();
1591
+ setProgress(100);
1592
+ setTimeout(() => {
1593
+ URL.revokeObjectURL(result.url);
1594
+ const index = urlCleanupRef.current.indexOf(result.url);
1595
+ if (index > -1) {
1596
+ urlCleanupRef.current.splice(index, 1);
1597
+ }
1598
+ }, 100);
1599
+ } catch (err) {
1600
+ const errorMessage = err instanceof Error ? err.message : "Download failed";
1601
+ setError(errorMessage);
1602
+ } finally {
1603
+ setIsExporting(false);
1604
+ }
1605
+ },
1606
+ [exportToBlob]
1607
+ );
1608
+ const cloneCanvas = (0, import_react2.useCallback)(
1609
+ (canvas) => {
1610
+ if (!rendererRef.current) return null;
1611
+ try {
1612
+ return rendererRef.current.cloneCanvas(canvas);
1613
+ } catch (err) {
1614
+ const errorMessage = err instanceof Error ? err.message : "Clone failed";
1615
+ setError(errorMessage);
1616
+ return null;
1617
+ }
1618
+ },
1619
+ []
1620
+ );
1621
+ return {
1622
+ isExporting,
1623
+ progress,
1624
+ error,
1625
+ exportToBlob,
1626
+ exportToDataURL,
1627
+ downloadCanvas,
1628
+ cloneCanvas
1629
+ };
1630
+ }
1631
+
1632
+ // src/presentation/hooks/useReportExport.ts
1633
+ var import_react3 = require("react");
1634
+ function useReportExport() {
1635
+ const [isExporting, setIsExporting] = (0, import_react3.useState)(false);
1636
+ const [error, setError] = (0, import_react3.useState)(null);
1637
+ const reportServiceRef = (0, import_react3.useRef)(null);
1638
+ if (!reportServiceRef.current) {
1639
+ reportServiceRef.current = ReportService.getInstance();
1640
+ }
1641
+ const exportToPDF = (0, import_react3.useCallback)(
1642
+ async (elementId, options) => {
1643
+ if (!reportServiceRef.current) {
1644
+ throw new Error("ReportService not initialized");
1645
+ }
1646
+ setIsExporting(true);
1647
+ setError(null);
1648
+ try {
1649
+ await reportServiceRef.current.exportToPDF(elementId, options);
1650
+ } catch (err) {
1651
+ const errorMessage = err instanceof Error ? err.message : "PDF export failed";
1652
+ setError(errorMessage);
1653
+ throw err;
1654
+ } finally {
1655
+ setIsExporting(false);
1656
+ }
1657
+ },
1658
+ []
1659
+ );
1660
+ const exportCanvasToPDF = (0, import_react3.useCallback)(
1661
+ async (canvas, options) => {
1662
+ if (!reportServiceRef.current) {
1663
+ throw new Error("ReportService not initialized");
1664
+ }
1665
+ setIsExporting(true);
1666
+ setError(null);
1667
+ try {
1668
+ await reportServiceRef.current.exportCanvasToPDF(canvas, options);
1669
+ } catch (err) {
1670
+ const errorMessage = err instanceof Error ? err.message : "Canvas PDF export failed";
1671
+ setError(errorMessage);
1672
+ throw err;
1673
+ } finally {
1674
+ setIsExporting(false);
1675
+ }
1676
+ },
1677
+ []
1678
+ );
1679
+ const exportMultipleToPDF = (0, import_react3.useCallback)(
1680
+ async (elementIds, options) => {
1681
+ if (!reportServiceRef.current) {
1682
+ throw new Error("ReportService not initialized");
1683
+ }
1684
+ setIsExporting(true);
1685
+ setError(null);
1686
+ try {
1687
+ await reportServiceRef.current.exportMultipleToPDF(elementIds, options);
1688
+ } catch (err) {
1689
+ const errorMessage = err instanceof Error ? err.message : "Multi-page PDF export failed";
1690
+ setError(errorMessage);
1691
+ throw err;
1692
+ } finally {
1693
+ setIsExporting(false);
1694
+ }
1695
+ },
1696
+ []
1697
+ );
1698
+ const exportToExcel = (0, import_react3.useCallback)(
1699
+ async (data, options) => {
1700
+ if (!reportServiceRef.current) {
1701
+ throw new Error("ReportService not initialized");
1702
+ }
1703
+ setIsExporting(true);
1704
+ setError(null);
1705
+ try {
1706
+ await reportServiceRef.current.exportToExcel(data, options);
1707
+ } catch (err) {
1708
+ const errorMessage = err instanceof Error ? err.message : "Excel export failed";
1709
+ setError(errorMessage);
1710
+ throw err;
1711
+ } finally {
1712
+ setIsExporting(false);
1713
+ }
1714
+ },
1715
+ []
1716
+ );
1717
+ const exportToCSV = (0, import_react3.useCallback)(
1718
+ (data, filename, delimiter) => {
1719
+ if (!reportServiceRef.current) {
1720
+ throw new Error("ReportService not initialized");
1721
+ }
1722
+ try {
1723
+ reportServiceRef.current.exportToCSV(data, filename, delimiter);
1724
+ } catch (err) {
1725
+ const errorMessage = err instanceof Error ? err.message : "CSV export failed";
1726
+ setError(errorMessage);
1727
+ throw err;
1728
+ }
1729
+ },
1730
+ []
1731
+ );
1732
+ return {
1733
+ isExporting,
1734
+ error,
1735
+ exportToPDF,
1736
+ exportCanvasToPDF,
1737
+ exportMultipleToPDF,
1738
+ exportToExcel,
1739
+ exportToCSV
1740
+ };
1741
+ }
1742
+
1743
+ // src/presentation/hooks/useFreehandCanvas.ts
1744
+ var import_react4 = require("react");
1745
+ var DEFAULT_BRUSH_SIZE = 3;
1746
+ var DEFAULT_COLOR = "#1a1a1a";
1747
+ function widthFor(stroke, point) {
1748
+ if (!stroke.dynamic) return stroke.size;
1749
+ return stroke.size * (0.5 + Math.min(1, Math.max(0, point.pressure)));
1750
+ }
1751
+ function useFreehandCanvas(options = {}) {
1752
+ const { brushSize = DEFAULT_BRUSH_SIZE, color = DEFAULT_COLOR, pressureEnabled = true } = options;
1753
+ const optionsRef = (0, import_react4.useRef)({ brushSize, color, pressureEnabled, onStrokeEnd: options.onStrokeEnd });
1754
+ optionsRef.current = { brushSize, color, pressureEnabled, onStrokeEnd: options.onStrokeEnd };
1755
+ const canvasRef = (0, import_react4.useRef)(null);
1756
+ const strokesRef = (0, import_react4.useRef)([]);
1757
+ const activeStrokeRef = (0, import_react4.useRef)(null);
1758
+ const dprRef = (0, import_react4.useRef)(1);
1759
+ const [strokeCount, setStrokeCount] = (0, import_react4.useState)(0);
1760
+ const redraw = (0, import_react4.useCallback)(() => {
1761
+ const canvas = canvasRef.current;
1762
+ const ctx = canvas?.getContext("2d");
1763
+ if (!canvas || !ctx) return;
1764
+ const dpr = dprRef.current;
1765
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
1766
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1767
+ ctx.scale(dpr, dpr);
1768
+ const all = activeStrokeRef.current ? [...strokesRef.current, activeStrokeRef.current] : strokesRef.current;
1769
+ ctx.lineCap = "round";
1770
+ ctx.lineJoin = "round";
1771
+ for (const stroke of all) {
1772
+ ctx.strokeStyle = stroke.color;
1773
+ ctx.fillStyle = stroke.color;
1774
+ if (stroke.points.length === 1) {
1775
+ const point = stroke.points[0];
1776
+ ctx.beginPath();
1777
+ ctx.arc(point.x, point.y, widthFor(stroke, point) / 2, 0, Math.PI * 2);
1778
+ ctx.fill();
1779
+ continue;
1780
+ }
1781
+ for (let i = 1; i < stroke.points.length; i += 1) {
1782
+ const from = stroke.points[i - 1];
1783
+ const to = stroke.points[i];
1784
+ ctx.beginPath();
1785
+ ctx.lineWidth = (widthFor(stroke, from) + widthFor(stroke, to)) / 2;
1786
+ ctx.moveTo(from.x, from.y);
1787
+ ctx.lineTo(to.x, to.y);
1788
+ ctx.stroke();
1789
+ }
1790
+ }
1791
+ }, []);
1792
+ const syncDimensions = (0, import_react4.useCallback)(() => {
1793
+ const canvas = canvasRef.current;
1794
+ if (!canvas) return false;
1795
+ const dpr = window.devicePixelRatio || 1;
1796
+ const width = Math.max(1, Math.round(canvas.clientWidth * dpr));
1797
+ const height = Math.max(1, Math.round(canvas.clientHeight * dpr));
1798
+ if (canvas.width === width && canvas.height === height && dprRef.current === dpr) return false;
1799
+ canvas.width = width;
1800
+ canvas.height = height;
1801
+ dprRef.current = dpr;
1802
+ return true;
1803
+ }, []);
1804
+ (0, import_react4.useEffect)(() => {
1805
+ const canvas = canvasRef.current;
1806
+ if (!canvas) return;
1807
+ const el = canvas;
1808
+ el.style.touchAction = "none";
1809
+ syncDimensions();
1810
+ redraw();
1811
+ const observer = new ResizeObserver(() => {
1812
+ if (syncDimensions()) redraw();
1813
+ });
1814
+ observer.observe(canvas);
1815
+ function pointFrom(event) {
1816
+ const rect = el.getBoundingClientRect();
1817
+ return {
1818
+ x: event.clientX - rect.left,
1819
+ y: event.clientY - rect.top,
1820
+ pressure: event.pressure > 0 ? event.pressure : 0.5
1821
+ };
1822
+ }
1823
+ function drawSegment(stroke, from, to) {
1824
+ const ctx = el.getContext("2d");
1825
+ if (!ctx) return;
1826
+ ctx.lineCap = "round";
1827
+ ctx.lineJoin = "round";
1828
+ ctx.strokeStyle = stroke.color;
1829
+ ctx.lineWidth = (widthFor(stroke, from) + widthFor(stroke, to)) / 2;
1830
+ ctx.beginPath();
1831
+ ctx.moveTo(from.x, from.y);
1832
+ ctx.lineTo(to.x, to.y);
1833
+ ctx.stroke();
1834
+ }
1835
+ function drawDot(stroke, point) {
1836
+ const ctx = el.getContext("2d");
1837
+ if (!ctx) return;
1838
+ ctx.fillStyle = stroke.color;
1839
+ ctx.beginPath();
1840
+ ctx.arc(point.x, point.y, widthFor(stroke, point) / 2, 0, Math.PI * 2);
1841
+ ctx.fill();
1842
+ }
1843
+ function onPointerDown(event) {
1844
+ if (event.button !== 0 && event.pointerType === "mouse") return;
1845
+ event.preventDefault();
1846
+ el.setPointerCapture(event.pointerId);
1847
+ const { brushSize: size, color: ink, pressureEnabled: dynamic } = optionsRef.current;
1848
+ const stroke = {
1849
+ size,
1850
+ color: ink,
1851
+ dynamic: dynamic && event.pointerType === "pen",
1852
+ points: [pointFrom(event)]
1853
+ };
1854
+ activeStrokeRef.current = stroke;
1855
+ drawDot(stroke, stroke.points[0]);
1856
+ }
1857
+ function onPointerMove(event) {
1858
+ const stroke = activeStrokeRef.current;
1859
+ if (!stroke) return;
1860
+ event.preventDefault();
1861
+ const point = pointFrom(event);
1862
+ const previous = stroke.points[stroke.points.length - 1];
1863
+ stroke.points.push(point);
1864
+ drawSegment(stroke, previous, point);
1865
+ }
1866
+ function commit(event) {
1867
+ const stroke = activeStrokeRef.current;
1868
+ if (!stroke) return;
1869
+ activeStrokeRef.current = null;
1870
+ if (el.hasPointerCapture(event.pointerId)) {
1871
+ el.releasePointerCapture(event.pointerId);
1872
+ }
1873
+ strokesRef.current = [...strokesRef.current, stroke];
1874
+ setStrokeCount(strokesRef.current.length);
1875
+ optionsRef.current.onStrokeEnd?.(stroke);
1876
+ }
1877
+ el.addEventListener("pointerdown", onPointerDown);
1878
+ el.addEventListener("pointermove", onPointerMove);
1879
+ el.addEventListener("pointerup", commit);
1880
+ el.addEventListener("pointercancel", commit);
1881
+ return () => {
1882
+ observer.disconnect();
1883
+ el.removeEventListener("pointerdown", onPointerDown);
1884
+ el.removeEventListener("pointermove", onPointerMove);
1885
+ el.removeEventListener("pointerup", commit);
1886
+ el.removeEventListener("pointercancel", commit);
1887
+ };
1888
+ }, [redraw, syncDimensions]);
1889
+ const undo = (0, import_react4.useCallback)(() => {
1890
+ if (strokesRef.current.length === 0) return;
1891
+ strokesRef.current = strokesRef.current.slice(0, -1);
1892
+ setStrokeCount(strokesRef.current.length);
1893
+ redraw();
1894
+ }, [redraw]);
1895
+ const clear = (0, import_react4.useCallback)(() => {
1896
+ if (strokesRef.current.length === 0) return;
1897
+ strokesRef.current = [];
1898
+ setStrokeCount(0);
1899
+ redraw();
1900
+ }, [redraw]);
1901
+ const getStrokes = (0, import_react4.useCallback)(() => strokesRef.current.map((stroke) => ({ ...stroke, points: stroke.points.map((point) => ({ ...point })) })), []);
1902
+ return {
1903
+ canvasRef,
1904
+ undo,
1905
+ clear,
1906
+ strokeCount,
1907
+ isEmpty: strokeCount === 0,
1908
+ getStrokes
1909
+ };
1910
+ }
1911
+ // Annotate the CommonJS export names for ESM import in node:
1912
+ 0 && (module.exports = {
1913
+ CanvasRenderer,
1914
+ DEFAULT_CANVAS_CONFIG,
1915
+ DEFAULT_DEVICE_CONFIG,
1916
+ DEFAULT_SCREENSHOT_STUDIO_CONFIG,
1917
+ DEVICE_DIMENSIONS,
1918
+ DeviceConfigService,
1919
+ PatternRenderer,
1920
+ ReportService,
1921
+ ScreenshotRenderer,
1922
+ ScreenshotStudioService,
1923
+ useCanvasExport,
1924
+ useFreehandCanvas,
1925
+ useReportExport,
1926
+ useScreenshotStudio
1927
+ });