From f22319737eadade20ad9b12d71c8c4699e7920c6 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 1 Dec 2025 10:58:43 -0500 Subject: [PATCH] Fix organic exports shadows and file textures --- script.js | 61 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/script.js b/script.js index 1afa77c..c5844d0 100644 --- a/script.js +++ b/script.js @@ -1429,15 +1429,24 @@ async function imageUrlToDataUrl(src) { if (!src || src.startsWith('data:')) return src; if (DATA_URL_CACHE.has(src)) return DATA_URL_CACHE.get(src); + const abs = (() => { try { return new URL(src, window.location.href).href; } catch { return src; } })(); + const urlObj = (() => { try { return new URL(abs); } catch { return null; } })(); + const isFile = urlObj?.protocol === 'file:'; + if (isFile) { // file:// cannot be safely read for data URLs in-browser + DATA_URL_CACHE.set(src, null); + return null; + } + const cachedImg = IMG_CACHE.get(src); const cachedUrl = imageToDataUrl(cachedImg); if (cachedUrl) { DATA_URL_CACHE.set(src, cachedUrl); return cachedUrl; } - const abs = (() => { try { return new URL(src, window.location.href).href; } catch { return src; } })(); + let dataUrl = null; try { + if (isFile) throw new Error('Skip fetch for file://'); const resp = await fetch(abs); if (!resp.ok) throw new Error(`Status ${resp.status}`); dataUrl = await blobToDataUrl(await resp.blob()); @@ -1446,7 +1455,7 @@ // Fallback: draw to a canvas to capture even when fetch is blocked (e.g., file://) dataUrl = await new Promise(resolve => { const img = new Image(); - img.crossOrigin = 'anonymous'; + if (!isFile) img.crossOrigin = 'anonymous'; img.onload = () => { try { const c = document.createElement('canvas'); @@ -1491,7 +1500,7 @@ await Promise.all(uniqueImageUrls.map(async (url) => dataUrlMap.set(url, await imageUrlToDataUrl(url)))); const bounds = balloonsBounds(); - const pad = 20; + const pad = 50; // extra room to avoid clipping drop-shadows const width = bounds.w + pad * 2; const height = bounds.h + pad * 2; const vb = [bounds.minX - pad, bounds.minY - pad, width, height].join(' '); @@ -1499,12 +1508,33 @@ let defs = ''; let elements = ''; const patterns = new Map(); + const shadowFilters = new Map(); + + const ensureShadowFilter = (dx, dy, blurPx, alpha) => { + const key = `${dx}|${dy}|${blurPx}|${alpha}`; + if (!shadowFilters.has(key)) { + const id = `shadow-${shadowFilters.size}`; + const stdDev = Math.max(0.01, blurPx * 0.5); + const clampedAlpha = clamp01(alpha); + const flood = ``; + const blur = ``; + const offset = ``; + const composite = ``; + const merge = ``; + defs += `${flood}${blur}${offset}${composite}${merge}`; + shadowFilters.set(key, id); + } + return shadowFilters.get(key); + }; + const shineShadowId = ensureShadowFilter(0, 0, 3, 0.1); balloons.forEach(b => { + const meta = FLAT_COLORS[b.colorIdx] || {}; let fill = b.color; if (b.image) { const patternKey = `${b.colorIdx}|${b.image}`; - if (!patterns.has(patternKey)) { + const imageHref = dataUrlMap.get(b.image); + if (!patterns.has(patternKey) && imageHref) { const patternId = `p${patterns.size}`; patterns.set(patternKey, patternId); const meta = FLAT_COLORS[b.colorIdx] || {}; @@ -1514,15 +1544,29 @@ const imgW = zoom, imgH = zoom; const imgX = 0.5 - (fx * zoom); const imgY = 0.5 - (fy * zoom); - const imageHref = dataUrlMap.get(b.image) || b.image; defs += ` `; } - fill = `url(#${patterns.get(patternKey)})`; + if (patterns.has(patternKey)) fill = `url(#${patterns.get(patternKey)})`; + } + let filterAttr = ''; + if (b.image) { + const lum = luminance(meta.hex || b.color); + if (lum > 0.6) { + const strength = clamp01((lum - 0.6) / 0.4); + const blur = 4 + 4 * strength; + const offsetY = 1 + 2 * strength; + const alpha = 0.05 + 0.07 * strength; + const id = ensureShadowFilter(0, offsetY, blur, alpha); + filterAttr = ` filter="url(#${id})"`; + } + } else { + const id = ensureShadowFilter(0, 0, 10, 0.2); + filterAttr = ` filter="url(#${id})"`; } const strokeAttr = isBorderEnabled ? ` stroke="#111827" stroke-width="0.5"` : ` stroke="none" stroke-width="0"`; - elements += ``; + elements += ``; if (isShineEnabled) { const sx = b.x - b.radius * SHINE_OFFSET; @@ -1531,7 +1575,8 @@ const ry = b.radius * SHINE_RY; const { fill: shineFill, stroke: shineStroke } = shineStyle(b.color); const stroke = shineStroke ? ` stroke="${shineStroke}" stroke-width="1"` : ''; - elements += ``; + const shineFilter = shineShadowId ? ` filter="url(#${shineShadowId})"` : ''; + elements += ``; } });