From f8bf7f56c464550a7246ab1ec3e0096f2783b2a1 Mon Sep 17 00:00:00 2001 From: SLP-DEV1 Date: Thu, 27 Aug 2026 01:11:17 +0200 Subject: [PATCH 1/5] fix: clip cover-mode artwork to target area --- src/lib/renderImageMockup.ts | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/lib/renderImageMockup.ts b/src/lib/renderImageMockup.ts index 7863369..e234032 100644 --- a/src/lib/renderImageMockup.ts +++ b/src/lib/renderImageMockup.ts @@ -1,7 +1,13 @@ import { loadImageFromFile } from "./images"; -import { computePlacementMetrics, clamp } from "./renderPlacement"; +import { computePlacementMetrics, clamp, type PlacementMetrics } from "./renderPlacement"; import type { MockupSettings } from "../types"; +interface ClipContext { + beginPath(): void; + rect(x: number, y: number, width: number, height: number): void; + clip(): void; +} + function canvasToPngBlob(canvas: HTMLCanvasElement): Promise { return new Promise((resolve, reject) => { canvas.toBlob((blob) => { @@ -11,6 +17,26 @@ function canvasToPngBlob(canvas: HTMLCanvasElement): Promise { }); } +export function clipCoverPlacement( + ctx: ClipContext, + settings: MockupSettings, + metrics: PlacementMetrics, + canvasWidth: number, + canvasHeight: number, +): boolean { + if (settings.fitMode !== "cover") return false; + + const clipX = (metrics.targetLeft / 100) * canvasWidth; + const clipY = (metrics.targetTop / 100) * canvasHeight; + const clipW = (metrics.targetWidth / 100) * canvasWidth; + const clipH = (metrics.targetHeight / 100) * canvasHeight; + + ctx.beginPath(); + ctx.rect(clipX, clipY, clipW, clipH); + ctx.clip(); + return true; +} + export async function renderImageMockup(mockup: File, design: File, settings: MockupSettings): Promise { const [mockupImage, designImage] = await Promise.all([ loadImageFromFile(mockup), @@ -40,6 +66,7 @@ export async function renderImageMockup(mockup: File, design: File, settings: Mo const drawH = (metrics.displayHeight / 100) * height; ctx.save(); + clipCoverPlacement(ctx, settings, metrics, width, height); ctx.globalAlpha = clamp(settings.opacity, 0, 100) / 100; ctx.translate(drawX + drawW / 2, drawY + drawH / 2); ctx.rotate((settings.rotation * Math.PI) / 180); From 79e5c34421fab8a540ffa8a341e2bc6fbd01dae0 Mon Sep 17 00:00:00 2001 From: SLP-DEV1 Date: Thu, 27 Aug 2026 01:11:28 +0200 Subject: [PATCH 2/5] test: cover-mode clip rectangle --- src/lib/renderImageMockup.test.ts | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/lib/renderImageMockup.test.ts diff --git a/src/lib/renderImageMockup.test.ts b/src/lib/renderImageMockup.test.ts new file mode 100644 index 0000000..dea084b --- /dev/null +++ b/src/lib/renderImageMockup.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it, vi } from "vitest"; +import type { MockupSettings } from "../types"; +import { computePlacementMetrics } from "./renderPlacement"; +import { clipCoverPlacement } from "./renderImageMockup"; + +const settings: MockupSettings = { + smartObjectName: "Image Canvas", + left: 0, + top: 0, + width: 100, + height: 100, + areaLeftPercent: 10, + areaTopPercent: 20, + areaWidthPercent: 40, + areaHeightPercent: 20, + rotation: 15, + opacity: 100, + fitMode: "cover", + anchor: "center", +}; + +describe("cover-mode clipping", () => { + it("clips to the target rectangle before the rotated design is drawn", () => { + const metrics = computePlacementMetrics(settings, { width: 1000, height: 2000 }); + const ctx = { + beginPath: vi.fn(), + rect: vi.fn(), + clip: vi.fn(), + }; + + expect(clipCoverPlacement(ctx, settings, metrics, 1000, 500)).toBe(true); + expect(ctx.beginPath).toHaveBeenCalledTimes(1); + expect(ctx.rect).toHaveBeenCalledWith(100, 100, 400, 100); + expect(ctx.clip).toHaveBeenCalledTimes(1); + }); + + it("does not clip contain mode", () => { + const containSettings = { ...settings, fitMode: "contain" as const }; + const metrics = computePlacementMetrics(containSettings, { width: 1000, height: 2000 }); + const ctx = { + beginPath: vi.fn(), + rect: vi.fn(), + clip: vi.fn(), + }; + + expect(clipCoverPlacement(ctx, containSettings, metrics, 1000, 500)).toBe(false); + expect(ctx.rect).not.toHaveBeenCalled(); + expect(ctx.clip).not.toHaveBeenCalled(); + }); +}); From 79207605a9319df9cdae98598bb91f2da199961d Mon Sep 17 00:00:00 2001 From: SLP-DEV1 Date: Thu, 27 Aug 2026 01:12:00 +0200 Subject: [PATCH 3/5] fix: mirror cover clipping in visual editor --- src/components/VisualPlacementEditor.tsx | 37 +++++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/components/VisualPlacementEditor.tsx b/src/components/VisualPlacementEditor.tsx index bf6e740..a8eb9ce 100644 --- a/src/components/VisualPlacementEditor.tsx +++ b/src/components/VisualPlacementEditor.tsx @@ -49,6 +49,9 @@ export function VisualPlacementEditor({ documentSize, design, mockup, settings, const aspect = documentSize.width > 0 && documentSize.height > 0 ? documentSize.width / documentSize.height : 1.5; const metrics = useMemo(() => computePlacementMetrics(settings, designSize), [designSize, settings]); + const usesCoverClip = mode === "image" && settings.fitMode === "cover" && Boolean(design); + const targetWidth = Math.max(0.001, metrics.targetWidth); + const targetHeight = Math.max(0.001, metrics.targetHeight); function pointerPercent(event: React.PointerEvent): { x: number; y: number } | null { const element = ref.current; @@ -177,6 +180,32 @@ export function VisualPlacementEditor({ documentSize, design, mockup, settings, {slotLabel} + {usesCoverClip && design ? ( +
+
+ +
+
+ ) : null} +
beginDrag(event, "design-move")} title="Drag design" > - {design ? ( + {design && !usesCoverClip ? ( {design.file.name} - ) : ( + ) : !design ? ( No design - )} + ) : null}