@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.
- package/dist/CanvasConfig-pzoE0RHD.d.mts +310 -0
- package/dist/CanvasConfig-pzoE0RHD.d.ts +310 -0
- package/dist/application/index.d.mts +78 -0
- package/dist/application/index.d.ts +78 -0
- package/dist/application/index.js +1083 -0
- package/dist/application/index.mjs +11 -0
- package/dist/chunk-BN7JTXL7.mjs +346 -0
- package/dist/chunk-MC6PNV2F.mjs +34 -0
- package/dist/chunk-SF6WND7M.mjs +547 -0
- package/dist/chunk-TZGTTYTZ.mjs +0 -0
- package/dist/chunk-WMKPCJFI.mjs +661 -0
- package/dist/chunk-Z6IB4BQV.mjs +31 -0
- package/dist/domain/config/index.d.mts +87 -0
- package/dist/domain/config/index.d.ts +87 -0
- package/dist/domain/config/index.js +92 -0
- package/dist/domain/config/index.mjs +15 -0
- package/dist/domain/index.d.mts +79 -0
- package/dist/domain/index.d.ts +79 -0
- package/dist/domain/index.js +137 -0
- package/dist/domain/index.mjs +55 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1633 -0
- package/dist/index.mjs +38 -0
- package/dist/infrastructure/index.d.mts +174 -0
- package/dist/infrastructure/index.d.ts +174 -0
- package/dist/infrastructure/index.js +714 -0
- package/dist/infrastructure/index.mjs +13 -0
- package/dist/presentation/index.d.mts +108 -0
- package/dist/presentation/index.d.ts +108 -0
- package/dist/presentation/index.js +1617 -0
- package/dist/presentation/index.mjs +14 -0
- package/package.json +86 -0
- package/src/application/index.ts +7 -0
- package/src/application/services/ReportService.ts +200 -0
- package/src/application/services/ScreenshotStudioService.ts +243 -0
- package/src/domain/config/CanvasConfig.ts +36 -0
- package/src/domain/config/DeviceConfig.ts +47 -0
- package/src/domain/config/ScreenshotStudioConfig.ts +82 -0
- package/src/domain/config/index.ts +8 -0
- package/src/domain/index.ts +7 -0
- package/src/domain/types/canvas.types.ts +85 -0
- package/src/domain/types/index.ts +8 -0
- package/src/domain/types/report.types.ts +83 -0
- package/src/domain/types/screenshot.types.ts +238 -0
- package/src/index.ts +102 -0
- package/src/infrastructure/index.ts +9 -0
- package/src/infrastructure/renderers/CanvasRenderer.ts +287 -0
- package/src/infrastructure/renderers/PatternRenderer.ts +160 -0
- package/src/infrastructure/renderers/ScreenshotRenderer.ts +331 -0
- package/src/infrastructure/services/DeviceConfigService.ts +144 -0
- package/src/presentation/hooks/useCanvasExport.ts +162 -0
- package/src/presentation/hooks/useReportExport.ts +161 -0
- package/src/presentation/hooks/useScreenshotStudio.ts +480 -0
- 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
|
+
}
|