From 52615a5086bb804165e298e843d7868de21d9dab Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 12:19:57 +0000 Subject: [PATCH] kasuti: download custom motif as PNG Adds a "PNG" button beside "JSON" in the pattern editor footer. It rasterises the normalised motif (solid black strokes on a white background, line width scaled to the cell size, ~800 px on the longer axis) to a canvas, pulls a PNG blob via toBlob, and triggers a download with the same safe-name slug as the JSON export. The existing "Download" button is relabelled "JSON" so the two outputs are distinguishable. https://claude.ai/code/session_01KNdXjQczMCRGMK7K3Qderw --- .../interactives/KasutiEmbroidery.tsx | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/src/components/interactives/KasutiEmbroidery.tsx b/src/components/interactives/KasutiEmbroidery.tsx index f1b3437..f5d912e 100644 --- a/src/components/interactives/KasutiEmbroidery.tsx +++ b/src/components/interactives/KasutiEmbroidery.tsx @@ -2146,6 +2146,48 @@ const PatternEditor: React.FC = ({ open, onOpenChange, initi URL.revokeObjectURL(url); }, [edgeList, title]); + const handleDownloadPng = useCallback(() => { + const pattern = normalizePattern(edgeList); + if (!pattern) return; + const trimmed = title.trim() || 'Custom'; + const safeName = + trimmed.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'motif'; + const [rows, cols] = pattern.bbox; + const span = Math.max(rows, cols, 1); + const cellPx = 800 / span; + const pad = 40; + const W = Math.round(cols * cellPx + pad * 2); + const H = Math.round(rows * cellPx + pad * 2); + const canvas = document.createElement('canvas'); + canvas.width = W; + canvas.height = H; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, W, H); + ctx.strokeStyle = '#171717'; + ctx.lineWidth = Math.max(4, cellPx * 0.16); + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + for (const [a, b] of pattern.edges) { + ctx.beginPath(); + ctx.moveTo(pad + a[1] * cellPx, pad + a[0] * cellPx); + ctx.lineTo(pad + b[1] * cellPx, pad + b[0] * cellPx); + ctx.stroke(); + } + canvas.toBlob((blob) => { + if (!blob) return; + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `kasuti-${safeName}-${pattern.edges.length}.png`; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); + }, 'image/png'); + }, [edgeList, title]); + const handleUpload = useCallback(() => { fileInputRef.current?.click(); }, []); @@ -2312,7 +2354,7 @@ const PatternEditor: React.FC = ({ open, onOpenChange, initi /> -
+
+