Fix organic exports shadows and file textures
This commit is contained in:
parent
7a2583c06a
commit
f22319737e
61
script.js
61
script.js
@ -1429,15 +1429,24 @@
|
|||||||
async function imageUrlToDataUrl(src) {
|
async function imageUrlToDataUrl(src) {
|
||||||
if (!src || src.startsWith('data:')) return src;
|
if (!src || src.startsWith('data:')) return src;
|
||||||
if (DATA_URL_CACHE.has(src)) return DATA_URL_CACHE.get(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 cachedImg = IMG_CACHE.get(src);
|
||||||
const cachedUrl = imageToDataUrl(cachedImg);
|
const cachedUrl = imageToDataUrl(cachedImg);
|
||||||
if (cachedUrl) {
|
if (cachedUrl) {
|
||||||
DATA_URL_CACHE.set(src, cachedUrl);
|
DATA_URL_CACHE.set(src, cachedUrl);
|
||||||
return cachedUrl;
|
return cachedUrl;
|
||||||
}
|
}
|
||||||
const abs = (() => { try { return new URL(src, window.location.href).href; } catch { return src; } })();
|
|
||||||
let dataUrl = null;
|
let dataUrl = null;
|
||||||
try {
|
try {
|
||||||
|
if (isFile) throw new Error('Skip fetch for file://');
|
||||||
const resp = await fetch(abs);
|
const resp = await fetch(abs);
|
||||||
if (!resp.ok) throw new Error(`Status ${resp.status}`);
|
if (!resp.ok) throw new Error(`Status ${resp.status}`);
|
||||||
dataUrl = await blobToDataUrl(await resp.blob());
|
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://)
|
// Fallback: draw to a canvas to capture even when fetch is blocked (e.g., file://)
|
||||||
dataUrl = await new Promise(resolve => {
|
dataUrl = await new Promise(resolve => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.crossOrigin = 'anonymous';
|
if (!isFile) img.crossOrigin = 'anonymous';
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
try {
|
try {
|
||||||
const c = document.createElement('canvas');
|
const c = document.createElement('canvas');
|
||||||
@ -1491,7 +1500,7 @@
|
|||||||
await Promise.all(uniqueImageUrls.map(async (url) => dataUrlMap.set(url, await imageUrlToDataUrl(url))));
|
await Promise.all(uniqueImageUrls.map(async (url) => dataUrlMap.set(url, await imageUrlToDataUrl(url))));
|
||||||
|
|
||||||
const bounds = balloonsBounds();
|
const bounds = balloonsBounds();
|
||||||
const pad = 20;
|
const pad = 50; // extra room to avoid clipping drop-shadows
|
||||||
const width = bounds.w + pad * 2;
|
const width = bounds.w + pad * 2;
|
||||||
const height = bounds.h + pad * 2;
|
const height = bounds.h + pad * 2;
|
||||||
const vb = [bounds.minX - pad, bounds.minY - pad, width, height].join(' ');
|
const vb = [bounds.minX - pad, bounds.minY - pad, width, height].join(' ');
|
||||||
@ -1499,12 +1508,33 @@
|
|||||||
let defs = '';
|
let defs = '';
|
||||||
let elements = '';
|
let elements = '';
|
||||||
const patterns = new Map();
|
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 = `<feFlood flood-color="#000000" flood-opacity="${clampedAlpha}" />`;
|
||||||
|
const blur = `<feGaussianBlur in="SourceAlpha" stdDeviation="${stdDev}" result="blur" />`;
|
||||||
|
const offset = `<feOffset dx="${dx}" dy="${dy}" in="blur" result="shadow" />`;
|
||||||
|
const composite = `<feComposite in="shadow" in2="SourceAlpha" operator="in" result="shadow" />`;
|
||||||
|
const merge = `<feMerge><feMergeNode in="shadow" /><feMergeNode in="SourceGraphic" /></feMerge>`;
|
||||||
|
defs += `<filter id="${id}" x="-50%" y="-50%" width="200%" height="200%" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">${flood}${blur}${offset}${composite}${merge}</filter>`;
|
||||||
|
shadowFilters.set(key, id);
|
||||||
|
}
|
||||||
|
return shadowFilters.get(key);
|
||||||
|
};
|
||||||
|
const shineShadowId = ensureShadowFilter(0, 0, 3, 0.1);
|
||||||
|
|
||||||
balloons.forEach(b => {
|
balloons.forEach(b => {
|
||||||
|
const meta = FLAT_COLORS[b.colorIdx] || {};
|
||||||
let fill = b.color;
|
let fill = b.color;
|
||||||
if (b.image) {
|
if (b.image) {
|
||||||
const patternKey = `${b.colorIdx}|${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}`;
|
const patternId = `p${patterns.size}`;
|
||||||
patterns.set(patternKey, patternId);
|
patterns.set(patternKey, patternId);
|
||||||
const meta = FLAT_COLORS[b.colorIdx] || {};
|
const meta = FLAT_COLORS[b.colorIdx] || {};
|
||||||
@ -1514,15 +1544,29 @@
|
|||||||
const imgW = zoom, imgH = zoom;
|
const imgW = zoom, imgH = zoom;
|
||||||
const imgX = 0.5 - (fx * zoom);
|
const imgX = 0.5 - (fx * zoom);
|
||||||
const imgY = 0.5 - (fy * zoom);
|
const imgY = 0.5 - (fy * zoom);
|
||||||
const imageHref = dataUrlMap.get(b.image) || b.image;
|
|
||||||
defs += `<pattern id="${patternId}" patternContentUnits="objectBoundingBox" width="1" height="1">
|
defs += `<pattern id="${patternId}" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||||
<image href="${imageHref}" x="${imgX}" y="${imgY}" width="${imgW}" height="${imgH}" preserveAspectRatio="xMidYMid slice" />
|
<image href="${imageHref}" x="${imgX}" y="${imgY}" width="${imgW}" height="${imgH}" preserveAspectRatio="xMidYMid slice" />
|
||||||
</pattern>`;
|
</pattern>`;
|
||||||
}
|
}
|
||||||
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"`;
|
const strokeAttr = isBorderEnabled ? ` stroke="#111827" stroke-width="0.5"` : ` stroke="none" stroke-width="0"`;
|
||||||
elements += `<circle cx="${b.x}" cy="${b.y}" r="${b.radius}" fill="${fill}"${strokeAttr} />`;
|
elements += `<circle cx="${b.x}" cy="${b.y}" r="${b.radius}" fill="${fill}"${strokeAttr}${filterAttr} />`;
|
||||||
|
|
||||||
if (isShineEnabled) {
|
if (isShineEnabled) {
|
||||||
const sx = b.x - b.radius * SHINE_OFFSET;
|
const sx = b.x - b.radius * SHINE_OFFSET;
|
||||||
@ -1531,7 +1575,8 @@
|
|||||||
const ry = b.radius * SHINE_RY;
|
const ry = b.radius * SHINE_RY;
|
||||||
const { fill: shineFill, stroke: shineStroke } = shineStyle(b.color);
|
const { fill: shineFill, stroke: shineStroke } = shineStyle(b.color);
|
||||||
const stroke = shineStroke ? ` stroke="${shineStroke}" stroke-width="1"` : '';
|
const stroke = shineStroke ? ` stroke="${shineStroke}" stroke-width="1"` : '';
|
||||||
elements += `<ellipse cx="${sx}" cy="${sy}" rx="${rx}" ry="${ry}" fill="${shineFill}"${stroke} transform="rotate(${SHINE_ROT} ${sx} ${sy})" />`;
|
const shineFilter = shineShadowId ? ` filter="url(#${shineShadowId})"` : '';
|
||||||
|
elements += `<ellipse cx="${sx}" cy="${sy}" rx="${rx}" ry="${ry}" fill="${shineFill}"${stroke}${shineFilter} transform="rotate(${SHINE_ROT} ${sx} ${sy})" />`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user