@overtone-art/canvas-editor-core 0.2.7 → 0.2.8

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,7 +1,7 @@
1
1
  import { FabricObject } from 'fabric';
2
2
 
3
3
  type Unit = 'px' | 'mm' | 'in';
4
- type LayerType = 'image' | 'text' | 'shape' | 'template' | 'group';
4
+ type LayerType = 'image' | 'mask' | 'text' | 'shape' | 'template' | 'group';
5
5
  interface LayerData {
6
6
  id: string;
7
7
  type: LayerType;
@@ -14,6 +14,11 @@ interface LayerData {
14
14
  }
15
15
  interface LayerMeta {
16
16
  pattern?: PatternState;
17
+ mask?: {
18
+ width: number;
19
+ height: number;
20
+ revision: number;
21
+ };
17
22
  [key: string]: unknown;
18
23
  }
19
24
  interface SerializedLayer extends LayerData {
@@ -30,6 +35,7 @@ interface EditorState {
30
35
  layers: SerializedLayer[];
31
36
  background?: string;
32
37
  backgroundImage?: Record<string, unknown> | null;
38
+ backgroundImageOptions?: SerializedBackgroundImageOptions | null;
33
39
  mockup?: MockupConfig | null;
34
40
  }
35
41
  interface EditorConfig {
@@ -54,6 +60,63 @@ interface ResizeOptions {
54
60
  interface BackgroundImageOptions {
55
61
  fit?: 'cover' | 'contain' | 'stretch';
56
62
  opacity?: number;
63
+ /** Fabric/HTML image loading mode for remote assets. */
64
+ crossOrigin?: '' | 'anonymous' | 'use-credentials' | null;
65
+ /** Cancels the current load; never serialized. */
66
+ signal?: AbortSignal;
67
+ }
68
+ type SerializedBackgroundImageOptions = Omit<BackgroundImageOptions, 'signal'>;
69
+ interface SemanticExportOptions extends PngLikeExportOptions {
70
+ /** Document keeps full-canvas coordinates; source returns native image pixels. */
71
+ resolution?: 'document' | 'source';
72
+ }
73
+ interface MaskPoint {
74
+ x: number;
75
+ y: number;
76
+ }
77
+ interface MaskBrushOptions {
78
+ mode: 'add' | 'subtract';
79
+ size: number;
80
+ /** Soft-edge inner radius ratio, from 0 (soft) to 1 (hard). */
81
+ hardness: number;
82
+ }
83
+ type MaskRefinementPrompt = {
84
+ type: 'point';
85
+ x: number;
86
+ y: number;
87
+ label: 'foreground' | 'background';
88
+ } | {
89
+ type: 'rectangle';
90
+ left: number;
91
+ top: number;
92
+ width: number;
93
+ height: number;
94
+ } | {
95
+ type: 'text';
96
+ value: string;
97
+ };
98
+ interface MaskRefinementRequest {
99
+ mask: string;
100
+ width: number;
101
+ height: number;
102
+ prompts: MaskRefinementPrompt[];
103
+ }
104
+ interface MaskRefinementResult {
105
+ dataUrl: string;
106
+ width?: number;
107
+ height?: number;
108
+ metadata?: Record<string, unknown>;
109
+ }
110
+ interface MaskRefinementProvider {
111
+ refine(request: MaskRefinementRequest, options?: {
112
+ signal?: AbortSignal;
113
+ onProgress?: (progress: number) => void;
114
+ }): Promise<MaskRefinementResult>;
115
+ }
116
+ /** Kept structural to avoid coupling the public types module to export.ts. */
117
+ interface PngLikeExportOptions {
118
+ multiplier?: number;
119
+ quality?: number;
57
120
  }
58
121
  interface DpiIssue {
59
122
  layerId: string;
@@ -303,6 +366,19 @@ interface MockupOverlay {
303
366
  blendMode?: MockupBlendMode;
304
367
  opacity?: number;
305
368
  }
369
+ type MockupDisplacementChannel = 'red' | 'green' | 'blue' | 'alpha';
370
+ interface MockupDisplacement {
371
+ /** Channel-map image cover-fitted to the mockup canvas. A value of 128 is neutral. */
372
+ image: string;
373
+ /** Maximum horizontal displacement in document pixels. Defaults to 10. */
374
+ scaleX?: number;
375
+ /** Maximum vertical displacement in document pixels. Defaults to 10. */
376
+ scaleY?: number;
377
+ /** Map channel controlling horizontal displacement. Defaults to red. */
378
+ channelX?: MockupDisplacementChannel;
379
+ /** Map channel controlling vertical displacement. Defaults to green. */
380
+ channelY?: MockupDisplacementChannel;
381
+ }
306
382
  interface MockupConfig {
307
383
  /** Garment/product image rendered behind the design. */
308
384
  image: string;
@@ -312,6 +388,8 @@ interface MockupConfig {
312
388
  clipToPrintArea?: boolean;
313
389
  designBlendMode?: MockupBlendMode;
314
390
  designOpacity?: number;
391
+ /** Optional channel-map warp applied to the design before compositing. */
392
+ displacement?: MockupDisplacement;
315
393
  /** Optional product lighting/shadow pass rendered after the design. */
316
394
  overlay?: MockupOverlay;
317
395
  }
@@ -380,4 +458,4 @@ interface EditorEvents {
380
458
  };
381
459
  }
382
460
 
383
- export { type SvgExportOptions as A, type BackgroundImageOptions as B, type CanvasSizePreset as C, DEFAULT_PATTERN_CONFIG as D, type EditorConfig as E, type FileAdapter as F, type TemplateParameter as G, type TileMode as H, type ImageAdjustments as I, type LayerData as L, type MockupBlendMode as M, type NormalizedLayerPosition as N, type PatternConfig as P, type ResizeOptions as R, type SerializedLayer as S, type TemplateDefinition as T, type Unit as U, type DpiIssue as a, type EditorEvents as b, type EditorState as c, type ExportFormat as d, type FontDefinition as e, type ImageAttribution as f, type ImageProvider as g, type ImageProviderResult as h, type ImageSearchOptions as i, type ImageSearchResult as j, type LayerMeta as k, type LayerType as l, type LicenseConfig as m, type LicensePayload as n, type LicenseStatus as o, type MockupConfig as p, type MockupOverlay as q, type MockupPrintArea as r, type PatternLocks as s, type PatternSourceResolver as t, type PatternState as u, type PositioningAdapter as v, type PrintifyPositioning as w, type ProjectPage as x, type ProjectState as y, type ShapePlugin as z };
461
+ export { type PatternLocks as A, type BackgroundImageOptions as B, type CanvasSizePreset as C, DEFAULT_PATTERN_CONFIG as D, type EditorConfig as E, type FileAdapter as F, type PatternSourceResolver as G, type PatternState as H, type ImageAdjustments as I, type PositioningAdapter as J, type PrintifyPositioning as K, type LayerData as L, type MaskBrushOptions as M, type NormalizedLayerPosition as N, type ProjectPage as O, type PatternConfig as P, type ProjectState as Q, type ResizeOptions as R, type SemanticExportOptions as S, type SerializedBackgroundImageOptions as T, type SerializedLayer as U, type ShapePlugin as V, type SvgExportOptions as W, type TemplateDefinition as X, type TemplateParameter as Y, type TileMode as Z, type Unit as _, type DpiIssue as a, type EditorEvents as b, type EditorState as c, type ExportFormat as d, type FontDefinition as e, type ImageAttribution as f, type ImageProvider as g, type ImageProviderResult as h, type ImageSearchOptions as i, type ImageSearchResult as j, type LayerMeta as k, type LayerType as l, type LicenseConfig as m, type LicensePayload as n, type LicenseStatus as o, type MaskPoint as p, type MaskRefinementPrompt as q, type MaskRefinementProvider as r, type MaskRefinementRequest as s, type MaskRefinementResult as t, type MockupBlendMode as u, type MockupConfig as v, type MockupDisplacement as w, type MockupDisplacementChannel as x, type MockupOverlay as y, type MockupPrintArea as z };
@@ -1,7 +1,7 @@
1
1
  import { FabricObject } from 'fabric';
2
2
 
3
3
  type Unit = 'px' | 'mm' | 'in';
4
- type LayerType = 'image' | 'text' | 'shape' | 'template' | 'group';
4
+ type LayerType = 'image' | 'mask' | 'text' | 'shape' | 'template' | 'group';
5
5
  interface LayerData {
6
6
  id: string;
7
7
  type: LayerType;
@@ -14,6 +14,11 @@ interface LayerData {
14
14
  }
15
15
  interface LayerMeta {
16
16
  pattern?: PatternState;
17
+ mask?: {
18
+ width: number;
19
+ height: number;
20
+ revision: number;
21
+ };
17
22
  [key: string]: unknown;
18
23
  }
19
24
  interface SerializedLayer extends LayerData {
@@ -30,6 +35,7 @@ interface EditorState {
30
35
  layers: SerializedLayer[];
31
36
  background?: string;
32
37
  backgroundImage?: Record<string, unknown> | null;
38
+ backgroundImageOptions?: SerializedBackgroundImageOptions | null;
33
39
  mockup?: MockupConfig | null;
34
40
  }
35
41
  interface EditorConfig {
@@ -54,6 +60,63 @@ interface ResizeOptions {
54
60
  interface BackgroundImageOptions {
55
61
  fit?: 'cover' | 'contain' | 'stretch';
56
62
  opacity?: number;
63
+ /** Fabric/HTML image loading mode for remote assets. */
64
+ crossOrigin?: '' | 'anonymous' | 'use-credentials' | null;
65
+ /** Cancels the current load; never serialized. */
66
+ signal?: AbortSignal;
67
+ }
68
+ type SerializedBackgroundImageOptions = Omit<BackgroundImageOptions, 'signal'>;
69
+ interface SemanticExportOptions extends PngLikeExportOptions {
70
+ /** Document keeps full-canvas coordinates; source returns native image pixels. */
71
+ resolution?: 'document' | 'source';
72
+ }
73
+ interface MaskPoint {
74
+ x: number;
75
+ y: number;
76
+ }
77
+ interface MaskBrushOptions {
78
+ mode: 'add' | 'subtract';
79
+ size: number;
80
+ /** Soft-edge inner radius ratio, from 0 (soft) to 1 (hard). */
81
+ hardness: number;
82
+ }
83
+ type MaskRefinementPrompt = {
84
+ type: 'point';
85
+ x: number;
86
+ y: number;
87
+ label: 'foreground' | 'background';
88
+ } | {
89
+ type: 'rectangle';
90
+ left: number;
91
+ top: number;
92
+ width: number;
93
+ height: number;
94
+ } | {
95
+ type: 'text';
96
+ value: string;
97
+ };
98
+ interface MaskRefinementRequest {
99
+ mask: string;
100
+ width: number;
101
+ height: number;
102
+ prompts: MaskRefinementPrompt[];
103
+ }
104
+ interface MaskRefinementResult {
105
+ dataUrl: string;
106
+ width?: number;
107
+ height?: number;
108
+ metadata?: Record<string, unknown>;
109
+ }
110
+ interface MaskRefinementProvider {
111
+ refine(request: MaskRefinementRequest, options?: {
112
+ signal?: AbortSignal;
113
+ onProgress?: (progress: number) => void;
114
+ }): Promise<MaskRefinementResult>;
115
+ }
116
+ /** Kept structural to avoid coupling the public types module to export.ts. */
117
+ interface PngLikeExportOptions {
118
+ multiplier?: number;
119
+ quality?: number;
57
120
  }
58
121
  interface DpiIssue {
59
122
  layerId: string;
@@ -303,6 +366,19 @@ interface MockupOverlay {
303
366
  blendMode?: MockupBlendMode;
304
367
  opacity?: number;
305
368
  }
369
+ type MockupDisplacementChannel = 'red' | 'green' | 'blue' | 'alpha';
370
+ interface MockupDisplacement {
371
+ /** Channel-map image cover-fitted to the mockup canvas. A value of 128 is neutral. */
372
+ image: string;
373
+ /** Maximum horizontal displacement in document pixels. Defaults to 10. */
374
+ scaleX?: number;
375
+ /** Maximum vertical displacement in document pixels. Defaults to 10. */
376
+ scaleY?: number;
377
+ /** Map channel controlling horizontal displacement. Defaults to red. */
378
+ channelX?: MockupDisplacementChannel;
379
+ /** Map channel controlling vertical displacement. Defaults to green. */
380
+ channelY?: MockupDisplacementChannel;
381
+ }
306
382
  interface MockupConfig {
307
383
  /** Garment/product image rendered behind the design. */
308
384
  image: string;
@@ -312,6 +388,8 @@ interface MockupConfig {
312
388
  clipToPrintArea?: boolean;
313
389
  designBlendMode?: MockupBlendMode;
314
390
  designOpacity?: number;
391
+ /** Optional channel-map warp applied to the design before compositing. */
392
+ displacement?: MockupDisplacement;
315
393
  /** Optional product lighting/shadow pass rendered after the design. */
316
394
  overlay?: MockupOverlay;
317
395
  }
@@ -380,4 +458,4 @@ interface EditorEvents {
380
458
  };
381
459
  }
382
460
 
383
- export { type SvgExportOptions as A, type BackgroundImageOptions as B, type CanvasSizePreset as C, DEFAULT_PATTERN_CONFIG as D, type EditorConfig as E, type FileAdapter as F, type TemplateParameter as G, type TileMode as H, type ImageAdjustments as I, type LayerData as L, type MockupBlendMode as M, type NormalizedLayerPosition as N, type PatternConfig as P, type ResizeOptions as R, type SerializedLayer as S, type TemplateDefinition as T, type Unit as U, type DpiIssue as a, type EditorEvents as b, type EditorState as c, type ExportFormat as d, type FontDefinition as e, type ImageAttribution as f, type ImageProvider as g, type ImageProviderResult as h, type ImageSearchOptions as i, type ImageSearchResult as j, type LayerMeta as k, type LayerType as l, type LicenseConfig as m, type LicensePayload as n, type LicenseStatus as o, type MockupConfig as p, type MockupOverlay as q, type MockupPrintArea as r, type PatternLocks as s, type PatternSourceResolver as t, type PatternState as u, type PositioningAdapter as v, type PrintifyPositioning as w, type ProjectPage as x, type ProjectState as y, type ShapePlugin as z };
461
+ export { type PatternLocks as A, type BackgroundImageOptions as B, type CanvasSizePreset as C, DEFAULT_PATTERN_CONFIG as D, type EditorConfig as E, type FileAdapter as F, type PatternSourceResolver as G, type PatternState as H, type ImageAdjustments as I, type PositioningAdapter as J, type PrintifyPositioning as K, type LayerData as L, type MaskBrushOptions as M, type NormalizedLayerPosition as N, type ProjectPage as O, type PatternConfig as P, type ProjectState as Q, type ResizeOptions as R, type SemanticExportOptions as S, type SerializedBackgroundImageOptions as T, type SerializedLayer as U, type ShapePlugin as V, type SvgExportOptions as W, type TemplateDefinition as X, type TemplateParameter as Y, type TileMode as Z, type Unit as _, type DpiIssue as a, type EditorEvents as b, type EditorState as c, type ExportFormat as d, type FontDefinition as e, type ImageAttribution as f, type ImageProvider as g, type ImageProviderResult as h, type ImageSearchOptions as i, type ImageSearchResult as j, type LayerMeta as k, type LayerType as l, type LicenseConfig as m, type LicensePayload as n, type LicenseStatus as o, type MaskPoint as p, type MaskRefinementPrompt as q, type MaskRefinementProvider as r, type MaskRefinementRequest as s, type MaskRefinementResult as t, type MockupBlendMode as u, type MockupConfig as v, type MockupDisplacement as w, type MockupDisplacementChannel as x, type MockupOverlay as y, type MockupPrintArea as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overtone-art/canvas-editor-core",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Abstract pluggable canvas editor built on Fabric.js",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -37,9 +37,43 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "fabric": "^7.4.0",
40
- "nanoid": "^5.1.5"
40
+ "nanoid": "^5.1.16"
41
+ },
42
+ "peerDependencies": {
43
+ "fontkit": "^2.0.4",
44
+ "pdf-lib": "^1.17.1",
45
+ "pdfkit": "^0.19.1",
46
+ "sharp": "^0.35.0",
47
+ "svg-to-pdfkit": "^0.1.8"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "fontkit": {
51
+ "optional": true
52
+ },
53
+ "pdf-lib": {
54
+ "optional": true
55
+ },
56
+ "pdfkit": {
57
+ "optional": true
58
+ },
59
+ "sharp": {
60
+ "optional": true
61
+ },
62
+ "svg-to-pdfkit": {
63
+ "optional": true
64
+ }
41
65
  },
42
66
  "devDependencies": {
67
+ "@types/fontkit": "^2.0.9",
68
+ "@types/opentype.js": "^1.3.10",
69
+ "@types/pdfkit": "^0.17.6",
70
+ "@types/svg-to-pdfkit": "^0.1.4",
71
+ "fontkit": "^2.0.4",
72
+ "opentype.js": "^1.3.4",
73
+ "pdf-lib": "^1.17.1",
74
+ "pdfkit": "^0.19.1",
75
+ "sharp": "^0.35.0",
76
+ "svg-to-pdfkit": "^0.1.8",
43
77
  "tsup": "^8.4.0",
44
78
  "typescript": "^5.8.2",
45
79
  "vitest": "^3.2.1",
@@ -52,6 +86,7 @@
52
86
  "lint": "eslint src/",
53
87
  "type-check": "tsc --noEmit",
54
88
  "test": "vitest run",
89
+ "test:coverage": "vitest run --coverage",
55
90
  "test:watch": "vitest"
56
91
  }
57
92
  }
@@ -1,104 +0,0 @@
1
- // src/export.ts
2
- function computePrintAreaClip(area, scaleX, scaleY, targetWidth, targetHeight) {
3
- const left = Math.max(0, Math.min(targetWidth, area.left * scaleX));
4
- const top = Math.max(0, Math.min(targetHeight, area.top * scaleY));
5
- const right = Math.max(left, Math.min(targetWidth, (area.left + area.width) * scaleX));
6
- const bottom = Math.max(top, Math.min(targetHeight, (area.top + area.height) * scaleY));
7
- return { left, top, width: right - left, height: bottom - top };
8
- }
9
- function computeCoverPlacement(sourceWidth, sourceHeight, targetWidth, targetHeight) {
10
- if (sourceWidth <= 0 || sourceHeight <= 0 || targetWidth <= 0 || targetHeight <= 0) {
11
- throw new Error("Cover dimensions must be positive");
12
- }
13
- const scale = Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);
14
- const width = sourceWidth * scale;
15
- const height = sourceHeight * scale;
16
- return {
17
- left: (targetWidth - width) / 2,
18
- top: (targetHeight - height) / 2,
19
- width,
20
- height
21
- };
22
- }
23
- function canvasElementToBlob(output, format, quality) {
24
- const mime = format === "jpeg" ? "image/jpeg" : `image/${format}`;
25
- return new Promise((resolve, reject) => {
26
- output.toBlob(
27
- (blob) => blob ? resolve(blob) : reject(new Error(`Failed to export ${format}`)),
28
- mime,
29
- quality
30
- );
31
- });
32
- }
33
- async function exportPNG(canvas, options = {}) {
34
- const { multiplier = 1, format = "png", quality = 1 } = options;
35
- const output = canvas.toCanvasElement(multiplier);
36
- return canvasElementToBlob(output, format, quality);
37
- }
38
- async function exportMockup(canvas, mockup, options = {}) {
39
- const { multiplier = 1, format = "png", quality = 1 } = options;
40
- const design = canvas.toCanvasElement(multiplier);
41
- const output = design.ownerDocument.createElement("canvas");
42
- output.width = design.width;
43
- output.height = design.height;
44
- const context = output.getContext("2d");
45
- if (!context) throw new Error("2D canvas context is unavailable");
46
- const loadImage = (url) => new Promise((resolve, reject) => {
47
- const element = new Image();
48
- element.crossOrigin = "anonymous";
49
- element.onload = () => resolve(element);
50
- element.onerror = () => reject(new Error(`Failed to load mockup image: ${url}`));
51
- element.src = url;
52
- });
53
- const drawCover = (image) => {
54
- const placement = computeCoverPlacement(
55
- image.naturalWidth || image.width,
56
- image.naturalHeight || image.height,
57
- output.width,
58
- output.height
59
- );
60
- context.drawImage(image, placement.left, placement.top, placement.width, placement.height);
61
- };
62
- drawCover(await loadImage(mockup.image));
63
- context.save();
64
- if (mockup.printArea && mockup.clipToPrintArea !== false) {
65
- const clip = computePrintAreaClip(
66
- mockup.printArea,
67
- output.width / canvas.getWidth(),
68
- output.height / canvas.getHeight(),
69
- output.width,
70
- output.height
71
- );
72
- context.beginPath();
73
- context.rect(clip.left, clip.top, clip.width, clip.height);
74
- context.clip();
75
- }
76
- context.globalAlpha = Math.max(0, Math.min(1, mockup.designOpacity ?? 1));
77
- context.globalCompositeOperation = !mockup.designBlendMode || mockup.designBlendMode === "normal" ? "source-over" : mockup.designBlendMode;
78
- context.drawImage(design, 0, 0);
79
- context.restore();
80
- if (mockup.overlay) {
81
- context.save();
82
- context.globalAlpha = Math.max(0, Math.min(1, mockup.overlay.opacity ?? 1));
83
- context.globalCompositeOperation = mockup.overlay.blendMode === "normal" ? "source-over" : mockup.overlay.blendMode ?? "multiply";
84
- drawCover(await loadImage(mockup.overlay.image));
85
- context.restore();
86
- }
87
- return canvasElementToBlob(output, format, quality);
88
- }
89
- function exportSVG(canvas) {
90
- return canvas.toSVG();
91
- }
92
- function exportDataURL(canvas, format = "png", multiplier = 1) {
93
- return canvas.toDataURL({ format, multiplier });
94
- }
95
-
96
- export {
97
- computePrintAreaClip,
98
- computeCoverPlacement,
99
- exportPNG,
100
- exportMockup,
101
- exportSVG,
102
- exportDataURL
103
- };
104
- //# sourceMappingURL=chunk-NINRTPOJ.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/export.ts"],"sourcesContent":["import type { Canvas, ImageFormat } from 'fabric';\nimport type { MockupConfig, MockupPrintArea } from './types';\n\nexport interface PngExportOptions {\n multiplier?: number;\n format?: ImageFormat;\n quality?: number;\n}\n\nexport interface CoverPlacement {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\nexport function computePrintAreaClip(\n area: MockupPrintArea,\n scaleX: number,\n scaleY: number,\n targetWidth: number,\n targetHeight: number,\n): MockupPrintArea {\n const left = Math.max(0, Math.min(targetWidth, area.left * scaleX));\n const top = Math.max(0, Math.min(targetHeight, area.top * scaleY));\n const right = Math.max(left, Math.min(targetWidth, (area.left + area.width) * scaleX));\n const bottom = Math.max(top, Math.min(targetHeight, (area.top + area.height) * scaleY));\n return { left, top, width: right - left, height: bottom - top };\n}\n\n/** Object-fit: cover geometry, exported for deterministic preview/composite tests. */\nexport function computeCoverPlacement(\n sourceWidth: number,\n sourceHeight: number,\n targetWidth: number,\n targetHeight: number,\n): CoverPlacement {\n if (sourceWidth <= 0 || sourceHeight <= 0 || targetWidth <= 0 || targetHeight <= 0) {\n throw new Error('Cover dimensions must be positive');\n }\n const scale = Math.max(targetWidth / sourceWidth, targetHeight / sourceHeight);\n const width = sourceWidth * scale;\n const height = sourceHeight * scale;\n return {\n left: (targetWidth - width) / 2,\n top: (targetHeight - height) / 2,\n width,\n height,\n };\n}\n\nfunction canvasElementToBlob(\n output: HTMLCanvasElement,\n format: ImageFormat,\n quality: number,\n): Promise<Blob> {\n const mime = format === 'jpeg' ? 'image/jpeg' : `image/${format}`;\n return new Promise<Blob>((resolve, reject) => {\n output.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error(`Failed to export ${format}`))),\n mime,\n quality,\n );\n });\n}\n\nexport async function exportPNG(canvas: Canvas, options: PngExportOptions = {}): Promise<Blob> {\n const { multiplier = 1, format = 'png' as ImageFormat, quality = 1 } = options;\n const output = canvas.toCanvasElement(multiplier);\n return canvasElementToBlob(output, format, quality);\n}\n\n/** Rasterize the browser mockup preview together with the transparent design. */\nexport async function exportMockup(\n canvas: Canvas,\n mockup: MockupConfig,\n options: PngExportOptions = {},\n): Promise<Blob> {\n const { multiplier = 1, format = 'png' as ImageFormat, quality = 1 } = options;\n const design = canvas.toCanvasElement(multiplier);\n const output = design.ownerDocument.createElement('canvas');\n output.width = design.width;\n output.height = design.height;\n const context = output.getContext('2d');\n if (!context) throw new Error('2D canvas context is unavailable');\n\n const loadImage = (url: string) =>\n new Promise<HTMLImageElement>((resolve, reject) => {\n const element = new Image();\n element.crossOrigin = 'anonymous';\n element.onload = () => resolve(element);\n element.onerror = () => reject(new Error(`Failed to load mockup image: ${url}`));\n element.src = url;\n });\n const drawCover = (image: HTMLImageElement) => {\n const placement = computeCoverPlacement(\n image.naturalWidth || image.width,\n image.naturalHeight || image.height,\n output.width,\n output.height,\n );\n context.drawImage(image, placement.left, placement.top, placement.width, placement.height);\n };\n\n drawCover(await loadImage(mockup.image));\n context.save();\n if (mockup.printArea && mockup.clipToPrintArea !== false) {\n const clip = computePrintAreaClip(\n mockup.printArea,\n output.width / canvas.getWidth(),\n output.height / canvas.getHeight(),\n output.width,\n output.height,\n );\n context.beginPath();\n context.rect(clip.left, clip.top, clip.width, clip.height);\n context.clip();\n }\n context.globalAlpha = Math.max(0, Math.min(1, mockup.designOpacity ?? 1));\n context.globalCompositeOperation =\n !mockup.designBlendMode || mockup.designBlendMode === 'normal'\n ? 'source-over'\n : mockup.designBlendMode;\n context.drawImage(design, 0, 0);\n context.restore();\n\n if (mockup.overlay) {\n context.save();\n context.globalAlpha = Math.max(0, Math.min(1, mockup.overlay.opacity ?? 1));\n context.globalCompositeOperation =\n mockup.overlay.blendMode === 'normal'\n ? 'source-over'\n : (mockup.overlay.blendMode ?? 'multiply');\n drawCover(await loadImage(mockup.overlay.image));\n context.restore();\n }\n return canvasElementToBlob(output, format, quality);\n}\n\nexport function exportSVG(canvas: Canvas): string {\n return canvas.toSVG();\n}\n\nexport function exportDataURL(canvas: Canvas, format: ImageFormat = 'png', multiplier = 1): string {\n return canvas.toDataURL({ format, multiplier });\n}\n"],"mappings":"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","names":[]}