From a95d4831280a1adb7df136a67e551dffdb5fffa3 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:30:36 +0000 Subject: [PATCH] feat: trim transparent space around exported items Added `getPathBoundingBox` helper function in `assets/app.js` to compute the bounding box of traced items. Modified the SVG and PNG export logic to use the bounding box dimensions instead of the original image dimensions. Scaled and translated the traced path points by offsetting them based on the minimum bounding box coordinates. Adjusted custom texture offsets for both single image and pattern repeats to accurately place the fill inside the trimmed exports. Removed the white SVG `` background from the SVG export so that transparent space is maintained. Co-authored-by: TechJeeper <5033913+TechJeeper@users.noreply.github.com> --- assets/app.js | 56 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/assets/app.js b/assets/app.js index 27b9867..2ebfc4c 100644 --- a/assets/app.js +++ b/assets/app.js @@ -1005,6 +1005,23 @@ self.onmessage = function(e) { showHowtoIfNeeded(); // --- Exporters --- + const getPathBoundingBox = (paths, sx, sy) => { + if (!paths || paths.length === 0) return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const path of paths) { + for (const pt of path) { + const x = pt.x * sx; + const y = pt.y * sy; + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + } + if (minX === Infinity) return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; + return { minX, minY, maxX, maxY }; + }; + const formatSvgNumber = (value, precision = 2) => Number.isFinite(value) ? Number(value.toFixed(precision)).toString() : '0'; const getSvgPathString = (pts, closed, precision = 2) => { @@ -1069,12 +1086,14 @@ self.onmessage = function(e) { els.btnExportSvg.addEventListener('click', () => { if (state.paths.length === 0) return; - const outputW = state.originalWidth || state.imageObj.width; - const outputH = state.originalHeight || state.imageObj.height; const sx = state.traceScaleX || 1; const sy = state.traceScaleY || 1; - const scalePts = (pts, stride = 1) => thinSvgPoints(pts, stride).map(pt => ({ x: pt.x * sx, y: pt.y * sy })); + const bbox = getPathBoundingBox(state.paths, sx, sy); + const trimmedW = Math.max(1, bbox.maxX - bbox.minX); + const trimmedH = Math.max(1, bbox.maxY - bbox.minY); + + const scalePts = (pts, stride = 1) => thinSvgPoints(pts, stride).map(pt => ({ x: pt.x * sx - bbox.minX, y: pt.y * sy - bbox.minY })); const makeSvgPath = (precision = 2, stride = 1) => state.paths.map(p => getSvgPathString(scalePts(p, stride), true, precision)).join(' '); const fullSvgPath = makeSvgPath(); @@ -1083,15 +1102,15 @@ self.onmessage = function(e) { if (textureDataUrl) { if (state.fillMode === 'pattern') { const pWidth = state.fillImageObj.width * state.fillScale * sx, pHeight = state.fillImageObj.height * state.fillScale * sy; - svgContent = ``; + svgContent = ``; } else { const imgWidth = state.fillImageObj.width * state.fillScale * sx, imgHeight = state.fillImageObj.height * state.fillScale * sy; - svgContent = ``; + svgContent = ``; } } else { svgContent = ` `; } - return `${svgContent}`; + return `${svgContent}`; }; let blob = svgBlobFromString(buildSvg()); @@ -1137,16 +1156,19 @@ self.onmessage = function(e) { els.btnExportPng.addEventListener('click', () => { if (state.paths.length === 0) return; - const outputW = state.originalWidth || state.imageObj.width; - const outputH = state.originalHeight || state.imageObj.height; const sx = state.traceScaleX || 1; const sy = state.traceScaleY || 1; - const pngScale = getPngExportScale(outputW, outputH); - const exportW = Math.max(1, Math.round(outputW * pngScale)); - const exportH = Math.max(1, Math.round(outputH * pngScale)); - const ex = exportW / outputW; - const ey = exportH / outputH; - const scalePts = (pts) => pts.map(pt => ({ x: pt.x * sx * ex, y: pt.y * sy * ey })); + + const bbox = getPathBoundingBox(state.paths, sx, sy); + const trimmedW = Math.max(1, bbox.maxX - bbox.minX); + const trimmedH = Math.max(1, bbox.maxY - bbox.minY); + + const pngScale = getPngExportScale(trimmedW, trimmedH); + const exportW = Math.max(1, Math.round(trimmedW * pngScale)); + const exportH = Math.max(1, Math.round(trimmedH * pngScale)); + const ex = exportW / trimmedW; + const ey = exportH / trimmedH; + const scalePts = (pts) => pts.map(pt => ({ x: (pt.x * sx - bbox.minX) * ex, y: (pt.y * sy - bbox.minY) * ey })); const expCanvas = document.createElement('canvas'); expCanvas.width = exportW; @@ -1166,7 +1188,7 @@ self.onmessage = function(e) { enableHighQualitySmoothing(pCtx); pCtx.drawImage(state.fillImageObj, 0, 0, pCanvas.width, pCanvas.height); const pattern = eCtx.createPattern(pCanvas, 'repeat'); - pattern.setTransform(new DOMMatrix().translate(state.fillOffsetX * sx * ex, state.fillOffsetY * sy * ey)); + pattern.setTransform(new DOMMatrix().translate((state.fillOffsetX * sx - bbox.minX) * ex, (state.fillOffsetY * sy - bbox.minY) * ey)); eCtx.fillStyle = pattern; eCtx.fill('evenodd'); } else { @@ -1174,8 +1196,8 @@ self.onmessage = function(e) { eCtx.clip('evenodd'); eCtx.drawImage( state.fillImageObj, - state.fillOffsetX * sx * ex, - state.fillOffsetY * sy * ey, + (state.fillOffsetX * sx - bbox.minX) * ex, + (state.fillOffsetY * sy - bbox.minY) * ey, Math.max(1, Math.round(state.fillImageObj.width * state.fillScale * sx * ex)), Math.max(1, Math.round(state.fillImageObj.height * state.fillScale * sy * ey)) );