@umituz/web-canvas 1.0.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 (55) hide show
  1. package/dist/CanvasConfig-pzoE0RHD.d.mts +310 -0
  2. package/dist/CanvasConfig-pzoE0RHD.d.ts +310 -0
  3. package/dist/application/index.d.mts +78 -0
  4. package/dist/application/index.d.ts +78 -0
  5. package/dist/application/index.js +1083 -0
  6. package/dist/application/index.mjs +11 -0
  7. package/dist/chunk-BN7JTXL7.mjs +346 -0
  8. package/dist/chunk-MC6PNV2F.mjs +34 -0
  9. package/dist/chunk-SF6WND7M.mjs +547 -0
  10. package/dist/chunk-TZGTTYTZ.mjs +0 -0
  11. package/dist/chunk-WMKPCJFI.mjs +661 -0
  12. package/dist/chunk-Z6IB4BQV.mjs +31 -0
  13. package/dist/domain/config/index.d.mts +87 -0
  14. package/dist/domain/config/index.d.ts +87 -0
  15. package/dist/domain/config/index.js +92 -0
  16. package/dist/domain/config/index.mjs +15 -0
  17. package/dist/domain/index.d.mts +79 -0
  18. package/dist/domain/index.d.ts +79 -0
  19. package/dist/domain/index.js +137 -0
  20. package/dist/domain/index.mjs +55 -0
  21. package/dist/index.d.mts +6 -0
  22. package/dist/index.d.ts +6 -0
  23. package/dist/index.js +1633 -0
  24. package/dist/index.mjs +38 -0
  25. package/dist/infrastructure/index.d.mts +174 -0
  26. package/dist/infrastructure/index.d.ts +174 -0
  27. package/dist/infrastructure/index.js +714 -0
  28. package/dist/infrastructure/index.mjs +13 -0
  29. package/dist/presentation/index.d.mts +108 -0
  30. package/dist/presentation/index.d.ts +108 -0
  31. package/dist/presentation/index.js +1617 -0
  32. package/dist/presentation/index.mjs +14 -0
  33. package/package.json +86 -0
  34. package/src/application/index.ts +7 -0
  35. package/src/application/services/ReportService.ts +200 -0
  36. package/src/application/services/ScreenshotStudioService.ts +243 -0
  37. package/src/domain/config/CanvasConfig.ts +36 -0
  38. package/src/domain/config/DeviceConfig.ts +47 -0
  39. package/src/domain/config/ScreenshotStudioConfig.ts +82 -0
  40. package/src/domain/config/index.ts +8 -0
  41. package/src/domain/index.ts +7 -0
  42. package/src/domain/types/canvas.types.ts +85 -0
  43. package/src/domain/types/index.ts +8 -0
  44. package/src/domain/types/report.types.ts +83 -0
  45. package/src/domain/types/screenshot.types.ts +238 -0
  46. package/src/index.ts +102 -0
  47. package/src/infrastructure/index.ts +9 -0
  48. package/src/infrastructure/renderers/CanvasRenderer.ts +287 -0
  49. package/src/infrastructure/renderers/PatternRenderer.ts +160 -0
  50. package/src/infrastructure/renderers/ScreenshotRenderer.ts +331 -0
  51. package/src/infrastructure/services/DeviceConfigService.ts +144 -0
  52. package/src/presentation/hooks/useCanvasExport.ts +162 -0
  53. package/src/presentation/hooks/useReportExport.ts +161 -0
  54. package/src/presentation/hooks/useScreenshotStudio.ts +480 -0
  55. package/src/presentation/index.ts +8 -0
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Screenshot Renderer
3
+ * Device frame and screenshot rendering operations
4
+ */
5
+
6
+ import type {
7
+ CanvasDimensions,
8
+ CanvasContextConfig,
9
+ } from '../../domain/types/canvas.types';
10
+ import type {
11
+ DeviceType,
12
+ ScreenshotContent,
13
+ ScreenshotDesign,
14
+ FrameStyle,
15
+ LayoutStyle,
16
+ } from '../../domain/types/screenshot.types';
17
+ import { CanvasRenderer } from './CanvasRenderer';
18
+ import { PatternRenderer } from './PatternRenderer';
19
+ import { DeviceConfigService } from '../services/DeviceConfigService';
20
+
21
+ export class ScreenshotRenderer {
22
+ private canvasRenderer: CanvasRenderer;
23
+ private patternRenderer: PatternRenderer;
24
+ private deviceConfigService: DeviceConfigService;
25
+
26
+ constructor() {
27
+ this.canvasRenderer = new CanvasRenderer();
28
+ this.patternRenderer = new PatternRenderer();
29
+ this.deviceConfigService = DeviceConfigService.getInstance();
30
+ }
31
+
32
+ /**
33
+ * Render device frame
34
+ */
35
+ public renderDeviceFrame(
36
+ ctx: CanvasRenderingContext2D,
37
+ dimensions: CanvasDimensions,
38
+ deviceType: DeviceType,
39
+ frameStyle: FrameStyle,
40
+ frameColor: string
41
+ ): void {
42
+ const deviceConfig = this.deviceConfigService.getDeviceConfig(deviceType);
43
+ if (!deviceConfig) return;
44
+
45
+ if (frameStyle === 'none') return;
46
+
47
+ const isPhone = this.deviceConfigService.isPhone(deviceType);
48
+ const isTablet = this.deviceConfigService.isTablet(deviceType);
49
+
50
+ if (!isPhone && !isTablet) return;
51
+
52
+ const frameThickness = deviceConfig.frameThickness || 8;
53
+
54
+ if (frameStyle === 'realistic') {
55
+ this.renderRealisticFrame(ctx, dimensions, frameColor, frameThickness);
56
+ if (isPhone && this.deviceConfigService.hasNotch(deviceType)) {
57
+ this.renderNotch(ctx, dimensions);
58
+ }
59
+ } else if (frameStyle === 'minimal') {
60
+ this.renderMinimalFrame(ctx, dimensions, frameColor, frameThickness);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Render realistic device frame
66
+ */
67
+ private renderRealisticFrame(
68
+ ctx: CanvasRenderingContext2D,
69
+ dimensions: CanvasDimensions,
70
+ color: string,
71
+ thickness: number
72
+ ): void {
73
+ ctx.strokeStyle = color;
74
+ ctx.lineWidth = thickness;
75
+ ctx.strokeRect(
76
+ thickness / 2,
77
+ thickness / 2,
78
+ dimensions.width - thickness,
79
+ dimensions.height - thickness
80
+ );
81
+ }
82
+
83
+ /**
84
+ * Render minimal device frame
85
+ */
86
+ private renderMinimalFrame(
87
+ ctx: CanvasRenderingContext2D,
88
+ dimensions: CanvasDimensions,
89
+ color: string,
90
+ thickness: number
91
+ ): void {
92
+ ctx.strokeStyle = color;
93
+ ctx.lineWidth = thickness;
94
+ ctx.strokeRect(0, 0, dimensions.width, dimensions.height);
95
+ }
96
+
97
+ /**
98
+ * Render device notch
99
+ */
100
+ private renderNotch(
101
+ ctx: CanvasRenderingContext2D,
102
+ dimensions: CanvasDimensions
103
+ ): void {
104
+ ctx.fillStyle = '#1f2937';
105
+ ctx.beginPath();
106
+ ctx.arc(dimensions.width / 2, 20, 10, 0, Math.PI * 2);
107
+ ctx.fill();
108
+ }
109
+
110
+ /**
111
+ * Render background
112
+ */
113
+ public renderBackground(
114
+ ctx: CanvasRenderingContext2D,
115
+ design: ScreenshotDesign,
116
+ dimensions: CanvasDimensions
117
+ ): void {
118
+ if (design.backgroundGradientEnd) {
119
+ const gradient = this.canvasRenderer.createLinearGradient(
120
+ ctx,
121
+ 0,
122
+ 0,
123
+ dimensions.width,
124
+ dimensions.height,
125
+ [
126
+ { offset: 0, color: design.backgroundColor },
127
+ { offset: 1, color: design.backgroundGradientEnd },
128
+ ]
129
+ );
130
+ ctx.fillStyle = gradient;
131
+ } else {
132
+ ctx.fillStyle = design.backgroundColor;
133
+ }
134
+
135
+ ctx.fillRect(0, 0, dimensions.width, dimensions.height);
136
+
137
+ if (design.backgroundPattern && design.backgroundPattern !== 'none') {
138
+ this.patternRenderer.renderPattern(
139
+ ctx,
140
+ design.backgroundPattern,
141
+ dimensions
142
+ );
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Render text content
148
+ */
149
+ public renderTextContent(
150
+ ctx: CanvasRenderingContext2D,
151
+ content: ScreenshotContent,
152
+ design: ScreenshotDesign,
153
+ dimensions: CanvasDimensions
154
+ ): void {
155
+ const titleConfig: CanvasContextConfig = {
156
+ fillStyle: design.textColor,
157
+ font: `bold ${design.titleFontSize}px ${design.fontFamily}`,
158
+ textAlign: design.textAlign,
159
+ };
160
+
161
+ const subtitleConfig: CanvasContextConfig = {
162
+ fillStyle: design.textColor,
163
+ font: `${design.subtitleFontSize}px ${design.fontFamily}`,
164
+ textAlign: design.textAlign,
165
+ };
166
+
167
+ const titleY =
168
+ design.layout === 'text-bottom'
169
+ ? dimensions.height - 150
170
+ : 100 + (design.offsetY || 0);
171
+
172
+ this.canvasRenderer.drawText(
173
+ ctx,
174
+ this.formatText(content.title, design.textCase),
175
+ dimensions.width / 2,
176
+ titleY,
177
+ titleConfig
178
+ );
179
+
180
+ const subtitleY = titleY + design.titleFontSize + (design.gap || 20);
181
+ this.canvasRenderer.drawText(
182
+ ctx,
183
+ this.formatText(content.subtitle, design.textCase),
184
+ dimensions.width / 2,
185
+ subtitleY,
186
+ subtitleConfig
187
+ );
188
+ }
189
+
190
+ /**
191
+ * Format text case
192
+ */
193
+ private formatText(
194
+ text: string,
195
+ textCase: 'uppercase' | 'lowercase' | 'none' = 'none'
196
+ ): string {
197
+ switch (textCase) {
198
+ case 'uppercase':
199
+ return text.toUpperCase();
200
+ case 'lowercase':
201
+ return text.toLowerCase();
202
+ default:
203
+ return text;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Render screenshot image
209
+ */
210
+ public async renderScreenshotImage(
211
+ ctx: CanvasRenderingContext2D,
212
+ imageUrl: string,
213
+ design: ScreenshotDesign,
214
+ dimensions: CanvasDimensions
215
+ ): Promise<void> {
216
+ if (!imageUrl) return;
217
+
218
+ const deviceConfig = this.deviceConfigService.getDeviceConfig(
219
+ design.deviceType
220
+ );
221
+ if (!deviceConfig) return;
222
+
223
+ // Calculate screenshot area (inside device frame)
224
+ const frameThickness = deviceConfig.frameThickness || 8;
225
+ const padding = 60 + frameThickness;
226
+ const screenshotWidth = dimensions.width - padding * 2;
227
+ const screenshotHeight = dimensions.height - padding * 2;
228
+
229
+ try {
230
+ await this.canvasRenderer.drawImage(
231
+ ctx,
232
+ imageUrl,
233
+ padding,
234
+ padding,
235
+ screenshotWidth,
236
+ screenshotHeight
237
+ );
238
+ } catch (error) {
239
+ console.error('Failed to render screenshot image:', error);
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Render complete screenshot
245
+ */
246
+ public async renderDesignedScreenshot(
247
+ content: ScreenshotContent,
248
+ design: ScreenshotDesign
249
+ ): Promise<HTMLCanvasElement> {
250
+ const deviceConfig =
251
+ this.deviceConfigService.getDeviceConfig(design.deviceType);
252
+ if (!deviceConfig) {
253
+ throw new Error(`Device config not found: ${design.deviceType}`);
254
+ }
255
+
256
+ const dimensions = deviceConfig.dimensions;
257
+ const canvas = this.canvasRenderer.createCanvas(dimensions);
258
+ const ctx = canvas.getContext('2d');
259
+
260
+ if (!ctx) {
261
+ throw new Error('Failed to get canvas context');
262
+ }
263
+
264
+ // Render background
265
+ this.renderBackground(ctx, design, dimensions);
266
+
267
+ // Render screenshot image if available
268
+ if (content.screenshot) {
269
+ await this.renderScreenshotImage(ctx, content.screenshot, design, dimensions);
270
+ }
271
+
272
+ // Render text content
273
+ this.renderTextContent(ctx, content, design, dimensions);
274
+
275
+ // Render device frame
276
+ this.renderDeviceFrame(
277
+ ctx,
278
+ dimensions,
279
+ design.deviceType,
280
+ design.frameStyle,
281
+ design.deviceColor
282
+ );
283
+
284
+ return canvas;
285
+ }
286
+
287
+ /**
288
+ * Render placeholder content
289
+ */
290
+ public renderPlaceholderContent(
291
+ ctx: CanvasRenderingContext2D,
292
+ dimensions: CanvasDimensions,
293
+ deviceType: DeviceType
294
+ ): void {
295
+ // Background
296
+ this.canvasRenderer.fillRect(ctx, 0, 0, dimensions.width, dimensions.height, '#ffffff');
297
+
298
+ // Header area
299
+ this.canvasRenderer.fillRect(ctx, 0, 0, dimensions.width, 60, '#f3f4f6');
300
+ this.canvasRenderer.fillRect(
301
+ ctx,
302
+ 0,
303
+ dimensions.height - 80,
304
+ dimensions.width,
305
+ 80,
306
+ '#f3f4f6'
307
+ );
308
+
309
+ // Placeholder content blocks
310
+ ctx.fillStyle = '#e5e7eb';
311
+ for (let i = 0; i < 5; i++) {
312
+ const y = 80 + i * 120;
313
+ const height = Math.min(100, dimensions.height - y - 100);
314
+ if (y < dimensions.height - 100) {
315
+ this.canvasRenderer.fillRect(ctx, 20, y, dimensions.width - 40, height);
316
+ }
317
+ }
318
+
319
+ // Device frame
320
+ const deviceConfig = this.deviceConfigService.getDeviceConfig(deviceType);
321
+ if (deviceConfig) {
322
+ this.renderDeviceFrame(
323
+ ctx,
324
+ dimensions,
325
+ deviceType,
326
+ 'realistic',
327
+ '#1f2937'
328
+ );
329
+ }
330
+ }
331
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Device Configuration Service
3
+ * Manages device configurations and dimensions
4
+ */
5
+
6
+ import { DeviceType, DeviceConfig } from '../../domain/types/screenshot.types';
7
+ import { DEVICE_DIMENSIONS } from '../../domain/config/DeviceConfig';
8
+
9
+ /**
10
+ * Device configuration service
11
+ */
12
+ export class DeviceConfigService {
13
+ private static instance: DeviceConfigService;
14
+ private devices: Map<DeviceType, DeviceConfig>;
15
+
16
+ private constructor() {
17
+ this.devices = new Map();
18
+ this.initializeDevices();
19
+ }
20
+
21
+ public static getInstance(): DeviceConfigService {
22
+ if (!DeviceConfigService.instance) {
23
+ DeviceConfigService.instance = new DeviceConfigService();
24
+ }
25
+ return DeviceConfigService.instance;
26
+ }
27
+
28
+ private initializeDevices(): void {
29
+ const deviceConfigs: Omit<DeviceConfig, 'type'>[] = [
30
+ {
31
+ name: 'iPhone 15 Pro',
32
+ dimensions: { width: 1290, height: 2796, scale: 3 },
33
+ isPhone: true,
34
+ hasNotch: true,
35
+ cornerRadius: 20,
36
+ frameThickness: 8,
37
+ },
38
+ {
39
+ name: 'iPhone 15',
40
+ dimensions: { width: 1179, height: 2556, scale: 3 },
41
+ isPhone: true,
42
+ hasNotch: true,
43
+ cornerRadius: 20,
44
+ frameThickness: 8,
45
+ },
46
+ {
47
+ name: 'iPhone SE',
48
+ dimensions: { width: 1080, height: 2340, scale: 3 },
49
+ isPhone: true,
50
+ hasNotch: false,
51
+ cornerRadius: 16,
52
+ frameThickness: 6,
53
+ },
54
+ {
55
+ name: 'iPad Pro',
56
+ dimensions: { width: 2048, height: 2732, scale: 2 },
57
+ isTablet: true,
58
+ hasNotch: false,
59
+ cornerRadius: 24,
60
+ frameThickness: 10,
61
+ },
62
+ {
63
+ name: 'iPad',
64
+ dimensions: { width: 1620, height: 2160, scale: 2 },
65
+ isTablet: true,
66
+ hasNotch: false,
67
+ cornerRadius: 20,
68
+ frameThickness: 10,
69
+ },
70
+ {
71
+ name: 'Android Phone',
72
+ dimensions: { width: 1440, height: 3120, scale: 3 },
73
+ isPhone: true,
74
+ hasNotch: false,
75
+ cornerRadius: 24,
76
+ frameThickness: 8,
77
+ },
78
+ {
79
+ name: 'Android Tablet',
80
+ dimensions: { width: 2000, height: 2200, scale: 2 },
81
+ isTablet: true,
82
+ hasNotch: false,
83
+ cornerRadius: 24,
84
+ frameThickness: 10,
85
+ },
86
+ {
87
+ name: 'Desktop',
88
+ dimensions: { width: 1920, height: 1080, scale: 1 },
89
+ isPhone: false,
90
+ isTablet: false,
91
+ hasNotch: false,
92
+ cornerRadius: 0,
93
+ frameThickness: 0,
94
+ },
95
+ ];
96
+
97
+ Object.entries(DEVICE_DIMENSIONS).forEach(([type, dimensions]) => {
98
+ const deviceType = type as DeviceType;
99
+ const config = deviceConfigs.find((_, index) => {
100
+ const types = Object.keys(DEVICE_DIMENSIONS);
101
+ return types[index] === type;
102
+ });
103
+
104
+ if (config) {
105
+ this.devices.set(deviceType, {
106
+ type: deviceType,
107
+ ...config,
108
+ dimensions,
109
+ });
110
+ }
111
+ });
112
+ }
113
+
114
+ public getDeviceConfig(deviceType: DeviceType): DeviceConfig | undefined {
115
+ return this.devices.get(deviceType);
116
+ }
117
+
118
+ public getAllDevices(): DeviceConfig[] {
119
+ return Array.from(this.devices.values());
120
+ }
121
+
122
+ public getPhones(): DeviceConfig[] {
123
+ return this.getAllDevices().filter((d) => d.isPhone);
124
+ }
125
+
126
+ public getTablets(): DeviceConfig[] {
127
+ return this.getAllDevices().filter((d) => d.isTablet);
128
+ }
129
+
130
+ public isPhone(deviceType: DeviceType): boolean {
131
+ const config = this.getDeviceConfig(deviceType);
132
+ return config?.isPhone || false;
133
+ }
134
+
135
+ public isTablet(deviceType: DeviceType): boolean {
136
+ const config = this.getDeviceConfig(deviceType);
137
+ return config?.isTablet || false;
138
+ }
139
+
140
+ public hasNotch(deviceType: DeviceType): boolean {
141
+ const config = this.getDeviceConfig(deviceType);
142
+ return config?.hasNotch || false;
143
+ }
144
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * useCanvasExport Hook
3
+ * React hook for canvas export operations
4
+ */
5
+
6
+ import { useState, useCallback } from 'react';
7
+ import type { CanvasExportOptions, CanvasResult } from '../../domain/types/canvas.types';
8
+ import type { CanvasConfig } from '../../domain/config/CanvasConfig';
9
+ import { DEFAULT_CANVAS_CONFIG } from '../../domain/config/CanvasConfig';
10
+ import { CanvasRenderer } from '../../infrastructure/renderers/CanvasRenderer';
11
+
12
+ /**
13
+ * useCanvasExport hook options
14
+ */
15
+ export interface UseCanvasExportOptions {
16
+ config?: CanvasConfig;
17
+ }
18
+
19
+ /**
20
+ * useCanvasExport hook return
21
+ */
22
+ export interface UseCanvasExportReturn {
23
+ isExporting: boolean;
24
+ progress: number;
25
+ error: string | null;
26
+ exportToBlob: (
27
+ canvas: HTMLCanvasElement,
28
+ options?: CanvasExportOptions
29
+ ) => Promise<CanvasResult | null>;
30
+ exportToDataURL: (
31
+ canvas: HTMLCanvasElement,
32
+ format?: string,
33
+ quality?: number
34
+ ) => string | null;
35
+ downloadCanvas: (
36
+ canvas: HTMLCanvasElement,
37
+ filename: string,
38
+ options?: CanvasExportOptions
39
+ ) => Promise<void>;
40
+ cloneCanvas: (canvas: HTMLCanvasElement) => HTMLCanvasElement | null;
41
+ }
42
+
43
+ /**
44
+ * useCanvasExport hook
45
+ */
46
+ export function useCanvasExport(
47
+ options: UseCanvasExportOptions = {}
48
+ ): UseCanvasExportReturn {
49
+ const { config = DEFAULT_CANVAS_CONFIG } = options;
50
+
51
+ const [isExporting, setIsExporting] = useState(false);
52
+ const [progress, setProgress] = useState(0);
53
+ const [error, setError] = useState<string | null>(null);
54
+
55
+ const renderer = new CanvasRenderer(config);
56
+
57
+ const exportToBlob = useCallback(
58
+ async (
59
+ canvas: HTMLCanvasElement,
60
+ options?: CanvasExportOptions
61
+ ): Promise<CanvasResult | null> => {
62
+ setIsExporting(true);
63
+ setProgress(0);
64
+ setError(null);
65
+
66
+ try {
67
+ const result = await renderer.exportToBlob(canvas, options);
68
+ setProgress(100);
69
+ return result;
70
+ } catch (err) {
71
+ const errorMessage =
72
+ err instanceof Error ? err.message : 'Export failed';
73
+ setError(errorMessage);
74
+ return null;
75
+ } finally {
76
+ setIsExporting(false);
77
+ }
78
+ },
79
+ [renderer]
80
+ );
81
+
82
+ const exportToDataURL = useCallback(
83
+ (
84
+ canvas: HTMLCanvasElement,
85
+ format?: string,
86
+ quality?: number
87
+ ): string | null => {
88
+ try {
89
+ return renderer.exportToDataURL(canvas, format, quality);
90
+ } catch (err) {
91
+ const errorMessage =
92
+ err instanceof Error ? err.message : 'Export failed';
93
+ setError(errorMessage);
94
+ return null;
95
+ }
96
+ },
97
+ [renderer]
98
+ );
99
+
100
+ const downloadCanvas = useCallback(
101
+ async (
102
+ canvas: HTMLCanvasElement,
103
+ filename: string,
104
+ options?: CanvasExportOptions
105
+ ): Promise<void> => {
106
+ setIsExporting(true);
107
+ setProgress(0);
108
+ setError(null);
109
+
110
+ try {
111
+ const result = await exportToBlob(canvas, options);
112
+ if (!result) {
113
+ throw new Error('Export failed');
114
+ }
115
+
116
+ setProgress(50);
117
+
118
+ const link = document.createElement('a');
119
+ link.href = result.url;
120
+ link.download = filename;
121
+ link.click();
122
+
123
+ setProgress(100);
124
+
125
+ setTimeout(() => {
126
+ URL.revokeObjectURL(result.url);
127
+ }, 100);
128
+ } catch (err) {
129
+ const errorMessage =
130
+ err instanceof Error ? err.message : 'Download failed';
131
+ setError(errorMessage);
132
+ } finally {
133
+ setIsExporting(false);
134
+ }
135
+ },
136
+ [exportToBlob]
137
+ );
138
+
139
+ const cloneCanvas = useCallback(
140
+ (canvas: HTMLCanvasElement): HTMLCanvasElement | null => {
141
+ try {
142
+ return renderer.cloneCanvas(canvas);
143
+ } catch (err) {
144
+ const errorMessage =
145
+ err instanceof Error ? err.message : 'Clone failed';
146
+ setError(errorMessage);
147
+ return null;
148
+ }
149
+ },
150
+ [renderer]
151
+ );
152
+
153
+ return {
154
+ isExporting,
155
+ progress,
156
+ error,
157
+ exportToBlob,
158
+ exportToDataURL,
159
+ downloadCanvas,
160
+ cloneCanvas,
161
+ };
162
+ }