(() => { '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, }; }); })();