212 lines
7.2 KiB
JavaScript
212 lines
7.2 KiB
JavaScript
(() => {
|
|
'use strict';
|
|
|
|
// Shared helpers & palette flattening
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const PX_PER_INCH = 4;
|
|
const SIZE_PRESETS = [24, 18, 11, 9, 5];
|
|
const TEXTURE_ZOOM_DEFAULT = 1.8;
|
|
const TEXTURE_FOCUS_DEFAULT = { x: 0.5, y: 0.5 };
|
|
const SWATCH_TEXTURE_ZOOM = 2.5;
|
|
const PNG_EXPORT_SCALE = 3;
|
|
|
|
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
|
const clamp01 = v => clamp(v, 0, 1);
|
|
const normalizeHex = h => (h || '').toLowerCase();
|
|
function hexToRgb(hex) {
|
|
const h = normalizeHex(hex).replace('#','');
|
|
if (h.length === 3) {
|
|
const r = parseInt(h[0] + h[0], 16);
|
|
const g = parseInt(h[1] + h[1], 16);
|
|
const b = parseInt(h[2] + h[2], 16);
|
|
return { r, g, b };
|
|
}
|
|
if (h.length === 6) {
|
|
const r = parseInt(h.slice(0,2), 16);
|
|
const g = parseInt(h.slice(2,4), 16);
|
|
const b = parseInt(h.slice(4,6), 16);
|
|
return { r, g, b };
|
|
}
|
|
return { r: 0, g: 0, b: 0 };
|
|
}
|
|
function luminance(hex) {
|
|
const { r, g, b } = hexToRgb(hex || '#000');
|
|
const norm = [r,g,b].map(v => {
|
|
const c = v / 255;
|
|
return c <= 0.03928 ? c/12.92 : Math.pow((c+0.055)/1.055, 2.4);
|
|
});
|
|
return 0.2126*norm[0] + 0.7152*norm[1] + 0.0722*norm[2];
|
|
}
|
|
function shineStyle(colorHex) {
|
|
const hex = normalizeHex(colorHex);
|
|
const isRetroWhite = hex === '#e8e3d9';
|
|
const isPureWhite = hex === '#ffffff';
|
|
const lum = luminance(hex);
|
|
if (isPureWhite || isRetroWhite) {
|
|
return { fill: 'rgba(220,220,220,0.22)', stroke: null };
|
|
}
|
|
if (lum > 0.7) {
|
|
const t = clamp01((lum - 0.7) / 0.3);
|
|
const fillAlpha = 0.08 + (0.04 - 0.08) * t;
|
|
return { fill: `rgba(0,0,0,${fillAlpha})`, stroke: null };
|
|
}
|
|
const base = 0.20;
|
|
const softened = lum > 0.4 ? base * 0.7 : base;
|
|
const finalAlpha = isRetroWhite ? softened * 0.6 : softened;
|
|
return { fill: `rgba(255,255,255,${finalAlpha})`, stroke: null };
|
|
}
|
|
|
|
const FLAT_COLORS = [];
|
|
const NAME_BY_HEX = new Map();
|
|
const HEX_TO_FIRST_IDX = new Map();
|
|
const allowedSet = new Set();
|
|
|
|
(function buildFlat() {
|
|
if (!Array.isArray(window.PALETTE)) return;
|
|
window.PALETTE.forEach(group => {
|
|
(group.colors || []).forEach(c => {
|
|
if (!c?.hex) return;
|
|
const item = { ...c, family: group.family };
|
|
item.imageZoom = Number.isFinite(c.imageZoom) ? Math.max(1, c.imageZoom) : TEXTURE_ZOOM_DEFAULT;
|
|
item.imageFocus = {
|
|
x: clamp01(c.imageFocusX ?? c.imageFocus?.x ?? TEXTURE_FOCUS_DEFAULT.x),
|
|
y: clamp01(c.imageFocusY ?? c.imageFocus?.y ?? TEXTURE_FOCUS_DEFAULT.y)
|
|
};
|
|
item._idx = FLAT_COLORS.length;
|
|
FLAT_COLORS.push(item);
|
|
|
|
const key = (c.hex || '').toLowerCase();
|
|
if (!NAME_BY_HEX.has(key)) NAME_BY_HEX.set(key, c.name);
|
|
if (!HEX_TO_FIRST_IDX.has(key)) HEX_TO_FIRST_IDX.set(key, item._idx);
|
|
allowedSet.add(key);
|
|
});
|
|
});
|
|
})();
|
|
|
|
const IMG_CACHE = new Map();
|
|
function getImage(path, onLoad) {
|
|
if (!path) return null;
|
|
let img = IMG_CACHE.get(path);
|
|
if (!img) {
|
|
img = new Image();
|
|
// Avoid CORS issues on file:// by only setting crossOrigin for http/https
|
|
const href = (() => { try { return new URL(path, window.location.href); } catch { return null; } })();
|
|
const isFile = href?.protocol === 'file:' || window.location.protocol === 'file:';
|
|
if (!isFile) img.crossOrigin = 'anonymous';
|
|
img.decoding = 'async';
|
|
img.loading = 'eager';
|
|
img.src = path;
|
|
if (onLoad) img.onload = onLoad;
|
|
IMG_CACHE.set(path, img);
|
|
}
|
|
return img;
|
|
}
|
|
|
|
const DATA_URL_CACHE = new Map();
|
|
const XLINK_NS = 'http://www.w3.org/1999/xlink';
|
|
const blobToDataUrl = blob => new Promise((resolve, reject) => {
|
|
const r = new FileReader();
|
|
r.onloadend = () => resolve(r.result);
|
|
r.onerror = reject;
|
|
r.readAsDataURL(blob);
|
|
});
|
|
function imageToDataUrl(img) {
|
|
if (!img || !img.complete || img.naturalWidth === 0) return null;
|
|
// On file:// origins, drawing may be blocked; return null to fall back to original href.
|
|
if (window.location.protocol === 'file:') return null;
|
|
try {
|
|
const c = document.createElement('canvas');
|
|
c.width = img.naturalWidth;
|
|
c.height = img.naturalHeight;
|
|
c.getContext('2d').drawImage(img, 0, 0);
|
|
return c.toDataURL('image/png');
|
|
} catch (err) {
|
|
console.warn('[Export] imageToDataUrl failed:', err);
|
|
return null;
|
|
}
|
|
}
|
|
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:';
|
|
|
|
const cachedImg = IMG_CACHE.get(src);
|
|
const cachedUrl = imageToDataUrl(cachedImg);
|
|
if (cachedUrl) { DATA_URL_CACHE.set(src, cachedUrl); return cachedUrl; }
|
|
|
|
let dataUrl = null;
|
|
try {
|
|
if (isFile) {
|
|
// On file:// we cannot safely read pixels; return the original path.
|
|
dataUrl = src;
|
|
} else {
|
|
const resp = await fetch(abs);
|
|
if (!resp.ok) throw new Error(`Status ${resp.status}`);
|
|
dataUrl = await blobToDataUrl(await resp.blob());
|
|
}
|
|
} catch (err) {
|
|
if (!isFile) console.warn('[Export] Fetch failed for', abs, err);
|
|
dataUrl = await new Promise(resolve => {
|
|
const img = new Image();
|
|
if (!isFile) img.crossOrigin = 'anonymous';
|
|
img.onload = () => {
|
|
try {
|
|
const c = document.createElement('canvas');
|
|
c.width = img.naturalWidth || 1;
|
|
c.height = img.naturalHeight || 1;
|
|
c.getContext('2d').drawImage(img, 0, 0);
|
|
resolve(c.toDataURL('image/png'));
|
|
} catch (e) {
|
|
console.error('[Export] Canvas fallback failed for', abs, e);
|
|
resolve(null);
|
|
}
|
|
};
|
|
img.onerror = () => resolve(null);
|
|
img.src = abs;
|
|
});
|
|
}
|
|
if (!dataUrl) dataUrl = abs;
|
|
DATA_URL_CACHE.set(src, dataUrl);
|
|
return dataUrl;
|
|
}
|
|
|
|
function download(href, suggestedFilename) {
|
|
const a = document.createElement('a');
|
|
a.href = href;
|
|
a.download = suggestedFilename || 'download';
|
|
a.rel = 'noopener';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
}
|
|
|
|
window.shared = {
|
|
PX_PER_INCH,
|
|
SIZE_PRESETS,
|
|
TEXTURE_ZOOM_DEFAULT,
|
|
TEXTURE_FOCUS_DEFAULT,
|
|
SWATCH_TEXTURE_ZOOM,
|
|
PNG_EXPORT_SCALE,
|
|
clamp,
|
|
clamp01,
|
|
normalizeHex,
|
|
hexToRgb,
|
|
shineStyle,
|
|
luminance,
|
|
FLAT_COLORS,
|
|
NAME_BY_HEX,
|
|
HEX_TO_FIRST_IDX,
|
|
allowedSet,
|
|
getImage,
|
|
DATA_URL_CACHE,
|
|
XLINK_NS,
|
|
blobToDataUrl,
|
|
imageToDataUrl,
|
|
imageUrlToDataUrl,
|
|
download,
|
|
};
|
|
});
|
|
})();
|