@umituz/web-canvas 1.0.5 → 1.0.7

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.
@@ -1,712 +0,0 @@
1
- import {
2
- DEFAULT_CANVAS_CONFIG,
3
- DEVICE_DIMENSIONS
4
- } from "./chunk-MC6PNV2F.mjs";
5
-
6
- // src/infrastructure/renderers/CanvasRenderer.ts
7
- var CanvasRenderer = class {
8
- constructor(config = DEFAULT_CANVAS_CONFIG) {
9
- this.config = { ...DEFAULT_CANVAS_CONFIG, ...config };
10
- this.imageCache = /* @__PURE__ */ new Map();
11
- }
12
- /**
13
- * Create a new canvas element
14
- */
15
- createCanvas(dimensions) {
16
- const canvas = document.createElement("canvas");
17
- canvas.width = dimensions.width;
18
- canvas.height = dimensions.height;
19
- return canvas;
20
- }
21
- /**
22
- * Get canvas 2D context
23
- */
24
- getContext(canvas) {
25
- return canvas.getContext("2d");
26
- }
27
- /**
28
- * Apply context configuration
29
- */
30
- configureContext(ctx, config) {
31
- if (config.fillStyle !== void 0) {
32
- ctx.fillStyle = config.fillStyle;
33
- }
34
- if (config.strokeStyle !== void 0) {
35
- ctx.strokeStyle = config.strokeStyle;
36
- }
37
- if (config.lineWidth !== void 0) {
38
- ctx.lineWidth = config.lineWidth;
39
- }
40
- if (config.lineCap !== void 0) {
41
- ctx.lineCap = config.lineCap;
42
- }
43
- if (config.lineJoin !== void 0) {
44
- ctx.lineJoin = config.lineJoin;
45
- }
46
- if (config.globalAlpha !== void 0) {
47
- ctx.globalAlpha = config.globalAlpha;
48
- }
49
- if (config.font !== void 0) {
50
- ctx.font = config.font;
51
- }
52
- if (config.textAlign !== void 0) {
53
- ctx.textAlign = config.textAlign;
54
- }
55
- if (config.textBaseline !== void 0) {
56
- ctx.textBaseline = config.textBaseline;
57
- }
58
- }
59
- /**
60
- * Clear canvas
61
- */
62
- clearCanvas(ctx, dimensions) {
63
- ctx.clearRect(0, 0, dimensions.width, dimensions.height);
64
- }
65
- /**
66
- * Fill rectangle
67
- */
68
- fillRect(ctx, x, y, width, height, color) {
69
- if (color) {
70
- ctx.fillStyle = color;
71
- }
72
- ctx.fillRect(x, y, width, height);
73
- }
74
- /**
75
- * Stroke rectangle
76
- */
77
- strokeRect(ctx, x, y, width, height, color, lineWidth) {
78
- if (color) {
79
- ctx.strokeStyle = color;
80
- }
81
- if (lineWidth !== void 0) {
82
- ctx.lineWidth = lineWidth;
83
- }
84
- ctx.strokeRect(x, y, width, height);
85
- }
86
- /**
87
- * Draw text
88
- */
89
- drawText(ctx, text, x, y, config) {
90
- if (config) {
91
- this.configureContext(ctx, config);
92
- }
93
- ctx.fillText(text, x, y);
94
- }
95
- /**
96
- * Draw image
97
- */
98
- async drawImage(ctx, imageSource, x, y, width, height) {
99
- let img;
100
- if (typeof imageSource === "string") {
101
- img = await this.loadImage(imageSource);
102
- } else {
103
- img = imageSource;
104
- }
105
- if (width && height) {
106
- ctx.drawImage(img, x, y, width, height);
107
- } else {
108
- ctx.drawImage(img, x, y);
109
- }
110
- }
111
- /**
112
- * Load image from URL with caching
113
- */
114
- loadImage(url) {
115
- if (this.imageCache.has(url)) {
116
- return Promise.resolve(this.imageCache.get(url));
117
- }
118
- return new Promise((resolve, reject) => {
119
- const img = new Image();
120
- if (this.config.useCORS) {
121
- img.crossOrigin = "anonymous";
122
- }
123
- img.onload = () => {
124
- this.imageCache.set(url, img);
125
- resolve(img);
126
- };
127
- img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
128
- img.src = url;
129
- });
130
- }
131
- /**
132
- * Clear image cache to free memory
133
- */
134
- clearImageCache() {
135
- this.imageCache.clear();
136
- }
137
- /**
138
- * Get cache size for monitoring
139
- */
140
- getCacheSize() {
141
- return this.imageCache.size;
142
- }
143
- /**
144
- * Create gradient
145
- */
146
- createLinearGradient(ctx, x1, y1, x2, y2, colorStops) {
147
- const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
148
- colorStops.forEach(({ offset, color }) => {
149
- gradient.addColorStop(offset, color);
150
- });
151
- return gradient;
152
- }
153
- /**
154
- * Export canvas to blob
155
- */
156
- async exportToBlob(canvas, options = {}) {
157
- const format = options.format || this.config.defaultFormat;
158
- const quality = options.quality || this.config.defaultQuality;
159
- const scale = options.scale || this.config.scale;
160
- const scaledCanvas = (scale || 1) !== 1 ? this.scaleCanvas(canvas, scale || 1) : canvas;
161
- const mimeType = `image/${format || "png"}`;
162
- const blob = await new Promise((resolve) => {
163
- scaledCanvas.toBlob(
164
- (result) => resolve(result),
165
- mimeType,
166
- quality
167
- );
168
- });
169
- if (!blob) {
170
- throw new Error("Failed to export canvas to blob");
171
- }
172
- const url = URL.createObjectURL(blob);
173
- return {
174
- blob,
175
- url,
176
- dimensions: {
177
- width: scaledCanvas.width,
178
- height: scaledCanvas.height
179
- },
180
- format: format || "png",
181
- size: blob.size
182
- };
183
- }
184
- /**
185
- * Export canvas to data URL
186
- */
187
- exportToDataURL(canvas, format, quality) {
188
- const mimeType = format || `image/${this.config.defaultFormat}`;
189
- const q = quality || this.config.defaultQuality;
190
- return canvas.toDataURL(mimeType, q);
191
- }
192
- /**
193
- * Scale canvas
194
- */
195
- scaleCanvas(canvas, scale) {
196
- const scaled = document.createElement("canvas");
197
- scaled.width = canvas.width * scale;
198
- scaled.height = canvas.height * scale;
199
- const ctx = scaled.getContext("2d");
200
- if (ctx) {
201
- ctx.imageSmoothingEnabled = true;
202
- ctx.imageSmoothingQuality = "high";
203
- ctx.drawImage(canvas, 0, 0, scaled.width, scaled.height);
204
- }
205
- return scaled;
206
- }
207
- /**
208
- * Clone canvas
209
- */
210
- cloneCanvas(canvas) {
211
- const clone = document.createElement("canvas");
212
- clone.width = canvas.width;
213
- clone.height = canvas.height;
214
- const ctx = clone.getContext("2d");
215
- if (ctx) {
216
- ctx.drawImage(canvas, 0, 0);
217
- }
218
- return clone;
219
- }
220
- };
221
-
222
- // src/infrastructure/renderers/PatternRenderer.ts
223
- var PatternRenderer = class {
224
- constructor() {
225
- this.patternCanvasCache = /* @__PURE__ */ new Map();
226
- this.MAX_CACHE_SIZE = 10;
227
- }
228
- /**
229
- * Render pattern on canvas
230
- */
231
- renderPattern(ctx, pattern, dimensions, color = "#000000") {
232
- if (pattern === "none") return;
233
- ctx.save();
234
- ctx.globalAlpha = 0.1;
235
- switch (pattern) {
236
- case "dots":
237
- this.renderDotPattern(ctx, dimensions, color);
238
- break;
239
- case "grid":
240
- this.renderGridPattern(ctx, dimensions, color);
241
- break;
242
- case "lines":
243
- this.renderLinePattern(ctx, dimensions, color);
244
- break;
245
- case "waves":
246
- this.renderWavePattern(ctx, dimensions, color);
247
- break;
248
- case "checkers":
249
- this.renderCheckerPattern(ctx, dimensions, color);
250
- break;
251
- }
252
- ctx.restore();
253
- }
254
- /**
255
- * Clear pattern cache to free memory
256
- */
257
- clearCache() {
258
- this.patternCanvasCache.clear();
259
- }
260
- /**
261
- * Render dot pattern - optimized with batch rendering
262
- */
263
- renderDotPattern(ctx, dimensions, color) {
264
- ctx.fillStyle = color;
265
- const spacing = 30;
266
- const radius = 2;
267
- ctx.beginPath();
268
- for (let x = spacing; x < dimensions.width; x += spacing) {
269
- for (let y = spacing; y < dimensions.height; y += spacing) {
270
- ctx.moveTo(x + radius, y);
271
- ctx.arc(x, y, radius, 0, Math.PI * 2);
272
- }
273
- }
274
- ctx.fill();
275
- }
276
- /**
277
- * Render grid pattern - optimized with single path
278
- */
279
- renderGridPattern(ctx, dimensions, color) {
280
- ctx.strokeStyle = color;
281
- ctx.lineWidth = 1;
282
- const spacing = 30;
283
- ctx.beginPath();
284
- for (let x = 0; x <= dimensions.width; x += spacing) {
285
- ctx.moveTo(x, 0);
286
- ctx.lineTo(x, dimensions.height);
287
- }
288
- for (let y = 0; y <= dimensions.height; y += spacing) {
289
- ctx.moveTo(0, y);
290
- ctx.lineTo(dimensions.width, y);
291
- }
292
- ctx.stroke();
293
- }
294
- /**
295
- * Render line pattern - optimized with single path
296
- */
297
- renderLinePattern(ctx, dimensions, color) {
298
- ctx.strokeStyle = color;
299
- ctx.lineWidth = 2;
300
- const spacing = 20;
301
- ctx.beginPath();
302
- for (let y = spacing; y < dimensions.height; y += spacing) {
303
- ctx.moveTo(0, y);
304
- ctx.lineTo(dimensions.width, y);
305
- }
306
- ctx.stroke();
307
- }
308
- /**
309
- * Render wave pattern - optimized with pre-calculated values
310
- */
311
- renderWavePattern(ctx, dimensions, color) {
312
- ctx.strokeStyle = color;
313
- ctx.lineWidth = 2;
314
- const amplitude = 10;
315
- const frequency = 0.1;
316
- const spacing = 40;
317
- const step = 5;
318
- for (let y = spacing; y < dimensions.height; y += spacing) {
319
- ctx.beginPath();
320
- const startX = 0;
321
- const startY = y + Math.sin(startX * frequency) * amplitude;
322
- ctx.moveTo(startX, startY);
323
- for (let x = step; x <= dimensions.width; x += step) {
324
- const waveY = y + Math.sin(x * frequency) * amplitude;
325
- ctx.lineTo(x, waveY);
326
- }
327
- ctx.stroke();
328
- }
329
- }
330
- /**
331
- * Render checker pattern
332
- */
333
- renderCheckerPattern(ctx, dimensions, color) {
334
- ctx.fillStyle = color;
335
- const size = 20;
336
- for (let x = 0; x < dimensions.width; x += size * 2) {
337
- for (let y = 0; y < dimensions.height; y += size * 2) {
338
- ctx.fillRect(x, y, size, size);
339
- ctx.fillRect(x + size, y + size, size, size);
340
- }
341
- }
342
- }
343
- };
344
-
345
- // src/infrastructure/services/DeviceConfigService.ts
346
- var DeviceConfigService = class _DeviceConfigService {
347
- constructor() {
348
- this.phonesCache = null;
349
- this.tabletsCache = null;
350
- this.allDevicesCache = null;
351
- this.devices = /* @__PURE__ */ new Map();
352
- this.initializeDevices();
353
- }
354
- static getInstance() {
355
- if (!_DeviceConfigService.instance) {
356
- _DeviceConfigService.instance = new _DeviceConfigService();
357
- }
358
- return _DeviceConfigService.instance;
359
- }
360
- initializeDevices() {
361
- const deviceConfigs = [
362
- {
363
- name: "iPhone 15 Pro",
364
- dimensions: { width: 1290, height: 2796, scale: 3 },
365
- isPhone: true,
366
- hasNotch: true,
367
- cornerRadius: 20,
368
- frameThickness: 8
369
- },
370
- {
371
- name: "iPhone 15",
372
- dimensions: { width: 1179, height: 2556, scale: 3 },
373
- isPhone: true,
374
- hasNotch: true,
375
- cornerRadius: 20,
376
- frameThickness: 8
377
- },
378
- {
379
- name: "iPhone SE",
380
- dimensions: { width: 1080, height: 2340, scale: 3 },
381
- isPhone: true,
382
- hasNotch: false,
383
- cornerRadius: 16,
384
- frameThickness: 6
385
- },
386
- {
387
- name: "iPad Pro",
388
- dimensions: { width: 2048, height: 2732, scale: 2 },
389
- isTablet: true,
390
- hasNotch: false,
391
- cornerRadius: 24,
392
- frameThickness: 10
393
- },
394
- {
395
- name: "iPad",
396
- dimensions: { width: 1620, height: 2160, scale: 2 },
397
- isTablet: true,
398
- hasNotch: false,
399
- cornerRadius: 20,
400
- frameThickness: 10
401
- },
402
- {
403
- name: "Android Phone",
404
- dimensions: { width: 1440, height: 3120, scale: 3 },
405
- isPhone: true,
406
- hasNotch: false,
407
- cornerRadius: 24,
408
- frameThickness: 8
409
- },
410
- {
411
- name: "Android Tablet",
412
- dimensions: { width: 2e3, height: 2200, scale: 2 },
413
- isTablet: true,
414
- hasNotch: false,
415
- cornerRadius: 24,
416
- frameThickness: 10
417
- },
418
- {
419
- name: "Desktop",
420
- dimensions: { width: 1920, height: 1080, scale: 1 },
421
- isPhone: false,
422
- isTablet: false,
423
- hasNotch: false,
424
- cornerRadius: 0,
425
- frameThickness: 0
426
- }
427
- ];
428
- Object.entries(DEVICE_DIMENSIONS).forEach(([type, dimensions]) => {
429
- const deviceType = type;
430
- const config = deviceConfigs.find((_, index) => {
431
- const types = Object.keys(DEVICE_DIMENSIONS);
432
- return types[index] === type;
433
- });
434
- if (config) {
435
- this.devices.set(deviceType, {
436
- type: deviceType,
437
- ...config,
438
- dimensions
439
- });
440
- }
441
- });
442
- }
443
- getDeviceConfig(deviceType) {
444
- return this.devices.get(deviceType);
445
- }
446
- getAllDevices() {
447
- if (!this.allDevicesCache) {
448
- this.allDevicesCache = Array.from(this.devices.values());
449
- }
450
- return this.allDevicesCache;
451
- }
452
- getPhones() {
453
- if (!this.phonesCache) {
454
- this.phonesCache = this.getAllDevices().filter((d) => d.isPhone);
455
- }
456
- return this.phonesCache;
457
- }
458
- getTablets() {
459
- if (!this.tabletsCache) {
460
- this.tabletsCache = this.getAllDevices().filter((d) => d.isTablet);
461
- }
462
- return this.tabletsCache;
463
- }
464
- isPhone(deviceType) {
465
- const config = this.getDeviceConfig(deviceType);
466
- return config?.isPhone || false;
467
- }
468
- isTablet(deviceType) {
469
- const config = this.getDeviceConfig(deviceType);
470
- return config?.isTablet || false;
471
- }
472
- hasNotch(deviceType) {
473
- const config = this.getDeviceConfig(deviceType);
474
- return config?.hasNotch || false;
475
- }
476
- };
477
-
478
- // src/infrastructure/renderers/ScreenshotRenderer.ts
479
- var ScreenshotRenderer = class {
480
- constructor() {
481
- this.renderCache = /* @__PURE__ */ new Map();
482
- this.MAX_CACHE_SIZE = 5;
483
- this.canvasRenderer = new CanvasRenderer();
484
- this.patternRenderer = new PatternRenderer();
485
- this.deviceConfigService = DeviceConfigService.getInstance();
486
- }
487
- /**
488
- * Clear render cache to free memory
489
- */
490
- clearCache() {
491
- this.renderCache.clear();
492
- this.canvasRenderer.clearImageCache?.();
493
- this.patternRenderer.clearCache?.();
494
- }
495
- /**
496
- * Render device frame
497
- */
498
- renderDeviceFrame(ctx, dimensions, deviceType, frameStyle, frameColor) {
499
- const deviceConfig = this.deviceConfigService.getDeviceConfig(deviceType);
500
- if (!deviceConfig) return;
501
- if (frameStyle === "none") return;
502
- const isPhone = this.deviceConfigService.isPhone(deviceType);
503
- const isTablet = this.deviceConfigService.isTablet(deviceType);
504
- if (!isPhone && !isTablet) return;
505
- const frameThickness = deviceConfig.frameThickness || 8;
506
- if (frameStyle === "realistic") {
507
- this.renderRealisticFrame(ctx, dimensions, frameColor, frameThickness);
508
- if (isPhone && this.deviceConfigService.hasNotch(deviceType)) {
509
- this.renderNotch(ctx, dimensions);
510
- }
511
- } else if (frameStyle === "minimal") {
512
- this.renderMinimalFrame(ctx, dimensions, frameColor, frameThickness);
513
- }
514
- }
515
- /**
516
- * Render realistic device frame
517
- */
518
- renderRealisticFrame(ctx, dimensions, color, thickness) {
519
- ctx.strokeStyle = color;
520
- ctx.lineWidth = thickness;
521
- ctx.strokeRect(
522
- thickness / 2,
523
- thickness / 2,
524
- dimensions.width - thickness,
525
- dimensions.height - thickness
526
- );
527
- }
528
- /**
529
- * Render minimal device frame
530
- */
531
- renderMinimalFrame(ctx, dimensions, color, thickness) {
532
- ctx.strokeStyle = color;
533
- ctx.lineWidth = thickness;
534
- ctx.strokeRect(0, 0, dimensions.width, dimensions.height);
535
- }
536
- /**
537
- * Render device notch
538
- */
539
- renderNotch(ctx, dimensions) {
540
- ctx.fillStyle = "#1f2937";
541
- ctx.beginPath();
542
- ctx.arc(dimensions.width / 2, 20, 10, 0, Math.PI * 2);
543
- ctx.fill();
544
- }
545
- /**
546
- * Render background - optimized
547
- */
548
- renderBackground(ctx, design, dimensions) {
549
- if (design.backgroundGradientEnd) {
550
- const gradient = this.canvasRenderer.createLinearGradient(
551
- ctx,
552
- 0,
553
- 0,
554
- dimensions.width,
555
- dimensions.height,
556
- [
557
- { offset: 0, color: design.backgroundColor },
558
- { offset: 1, color: design.backgroundGradientEnd }
559
- ]
560
- );
561
- ctx.fillStyle = gradient;
562
- } else {
563
- ctx.fillStyle = design.backgroundColor;
564
- }
565
- ctx.fillRect(0, 0, dimensions.width, dimensions.height);
566
- if (design.backgroundPattern && design.backgroundPattern !== "none") {
567
- this.patternRenderer.renderPattern(
568
- ctx,
569
- design.backgroundPattern,
570
- dimensions
571
- );
572
- }
573
- }
574
- /**
575
- * Render text content
576
- */
577
- renderTextContent(ctx, content, design, dimensions) {
578
- const titleConfig = {
579
- fillStyle: design.textColor,
580
- font: `bold ${design.titleFontSize}px ${design.fontFamily}`,
581
- textAlign: design.textAlign
582
- };
583
- const subtitleConfig = {
584
- fillStyle: design.textColor,
585
- font: `${design.subtitleFontSize}px ${design.fontFamily}`,
586
- textAlign: design.textAlign
587
- };
588
- const titleY = design.layout === "text-bottom" ? dimensions.height - 150 : 100 + (design.offsetY || 0);
589
- this.canvasRenderer.drawText(
590
- ctx,
591
- this.formatText(content.title, design.textCase),
592
- dimensions.width / 2,
593
- titleY,
594
- titleConfig
595
- );
596
- const subtitleY = titleY + design.titleFontSize + (design.gap || 20);
597
- this.canvasRenderer.drawText(
598
- ctx,
599
- this.formatText(content.subtitle, design.textCase),
600
- dimensions.width / 2,
601
- subtitleY,
602
- subtitleConfig
603
- );
604
- }
605
- /**
606
- * Format text case
607
- */
608
- formatText(text, textCase = "none") {
609
- switch (textCase) {
610
- case "uppercase":
611
- return text.toUpperCase();
612
- case "lowercase":
613
- return text.toLowerCase();
614
- default:
615
- return text;
616
- }
617
- }
618
- /**
619
- * Render screenshot image
620
- */
621
- async renderScreenshotImage(ctx, imageUrl, design, dimensions) {
622
- if (!imageUrl) return;
623
- const deviceConfig = this.deviceConfigService.getDeviceConfig(
624
- design.deviceType
625
- );
626
- if (!deviceConfig) return;
627
- const frameThickness = deviceConfig.frameThickness || 8;
628
- const padding = 60 + frameThickness;
629
- const screenshotWidth = dimensions.width - padding * 2;
630
- const screenshotHeight = dimensions.height - padding * 2;
631
- try {
632
- await this.canvasRenderer.drawImage(
633
- ctx,
634
- imageUrl,
635
- padding,
636
- padding,
637
- screenshotWidth,
638
- screenshotHeight
639
- );
640
- } catch (error) {
641
- console.error("Failed to render screenshot image:", error);
642
- }
643
- }
644
- /**
645
- * Render complete screenshot
646
- */
647
- async renderDesignedScreenshot(content, design) {
648
- const deviceConfig = this.deviceConfigService.getDeviceConfig(design.deviceType);
649
- if (!deviceConfig) {
650
- throw new Error(`Device config not found: ${design.deviceType}`);
651
- }
652
- const dimensions = deviceConfig.dimensions;
653
- const canvas = this.canvasRenderer.createCanvas(dimensions);
654
- const ctx = canvas.getContext("2d");
655
- if (!ctx) {
656
- throw new Error("Failed to get canvas context");
657
- }
658
- this.renderBackground(ctx, design, dimensions);
659
- if (content.screenshot) {
660
- await this.renderScreenshotImage(ctx, content.screenshot, design, dimensions);
661
- }
662
- this.renderTextContent(ctx, content, design, dimensions);
663
- this.renderDeviceFrame(
664
- ctx,
665
- dimensions,
666
- design.deviceType,
667
- design.frameStyle,
668
- design.deviceColor
669
- );
670
- return canvas;
671
- }
672
- /**
673
- * Render placeholder content
674
- */
675
- renderPlaceholderContent(ctx, dimensions, deviceType) {
676
- this.canvasRenderer.fillRect(ctx, 0, 0, dimensions.width, dimensions.height, "#ffffff");
677
- this.canvasRenderer.fillRect(ctx, 0, 0, dimensions.width, 60, "#f3f4f6");
678
- this.canvasRenderer.fillRect(
679
- ctx,
680
- 0,
681
- dimensions.height - 80,
682
- dimensions.width,
683
- 80,
684
- "#f3f4f6"
685
- );
686
- ctx.fillStyle = "#e5e7eb";
687
- for (let i = 0; i < 5; i++) {
688
- const y = 80 + i * 120;
689
- const height = Math.min(100, dimensions.height - y - 100);
690
- if (y < dimensions.height - 100) {
691
- this.canvasRenderer.fillRect(ctx, 20, y, dimensions.width - 40, height);
692
- }
693
- }
694
- const deviceConfig = this.deviceConfigService.getDeviceConfig(deviceType);
695
- if (deviceConfig) {
696
- this.renderDeviceFrame(
697
- ctx,
698
- dimensions,
699
- deviceType,
700
- "realistic",
701
- "#1f2937"
702
- );
703
- }
704
- }
705
- };
706
-
707
- export {
708
- CanvasRenderer,
709
- PatternRenderer,
710
- DeviceConfigService,
711
- ScreenshotRenderer
712
- };