/* One Piece Wiki — original, procedurally generated portrait/icon artwork. Every character gets a role-themed line-art medallion; every Devil Fruit gets a stylized fruit-shape icon colored by type. All shapes are drawn from scratch here — no external art or copyrighted imagery is used anywhere in this file. */ function radialLines(cx, cy, rInner, rOuter, count, strokeWidth) { let out = ''; for (let i = 0; i < count; i++) { const a = (Math.PI * 2 * i) / count; const x1 = cx + rInner * Math.cos(a), y1 = cy + rInner * Math.sin(a); const x2 = cx + rOuter * Math.cos(a), y2 = cy + rOuter * Math.sin(a); out += ``; } return out; } function starPoints(cx, cy, rOuter, rInner, count) { const pts = []; for (let i = 0; i < count * 2; i++) { const r = i % 2 === 0 ? rOuter : rInner; const a = (Math.PI * i) / count - Math.PI / 2; pts.push(`${(cx + r * Math.cos(a)).toFixed(1)},${(cy + r * Math.sin(a)).toFixed(1)}`); } return pts.join(' '); } /* Simple original line-art built from primitive shapes (circles/lines/polygons) — one per archetype. */ const ROLE_ICONS = { sword: '', compass: `${radialLines(32, 32, 15, 18, 12, 2)}`, target: '', cross: '', scroll: '', gear: `${radialLines(32, 32, 12, 18, 8, 3)}`, note: '', chefhat: '', flask: '', anchor: '', crown: '', tiara: '', flag: '', dagger: '', skull: '', paw: '', wing: '', star: ``, }; const ROLE_ICON_RULES = [ [/navigat/, 'compass'], [/sniper|marksman|sharpshoot/, 'target'], [/doctor|medic|surgeon/, 'cross'], [/archaeolog|scholar/, 'scroll'], [/shipwright|engineer|mechanic/, 'gear'], [/musician|singer/, 'note'], [/cook|chef/, 'chefhat'], [/scientist/, 'flask'], [/helmsman|admiral|marine|navy|warden/, 'anchor'], [/king|emperor|empress|yonko|shogun/, 'crown'], [/princess|prince/, 'tiara'], [/revolutionary/, 'flag'], [/agent|spy|officer|assassin|cp0|cp9/, 'dagger'], [/mink|beast|tiger|lion|leopard|wolf|dragon/, 'paw'], [/phoenix|bird|wing|flight/, 'wing'], [/swordsm|santoryu|blade|commander|combatant/, 'sword'], [/captain|warlord|pirate/, 'skull'], ]; function iconKeyForCharacter(c) { const haystack = `${c.role} ${c.epithet || ''} ${c.affiliation}`.toLowerCase(); for (const [re, key] of ROLE_ICON_RULES) { if (re.test(haystack)) return key; } return 'star'; } function characterImageTitle(c) { return c.imageTitle || c.name; } function fruitImageTitle(f) { if (f.imageTitle) return f.imageTitle; if (f.id === 'gomu-gomu-no-mi') return 'Hito Hito no Mi, Model: Nika'; return f.name; } function properImageHTML(kind, title, label, fallback) { return ` ${fallback} `; } function characterPortraitFallbackSVG(c) { const accent = accentFor(c); const hue2 = (hashHue(c.id + 'x') + 45) % 360; const icon = ROLE_ICONS[iconKeyForCharacter(c)] || ROLE_ICONS.star; const uid = c.id.replace(/[^a-z0-9]/g, ''); const rotation = hashHue(c.name) % 90; const fruit = c.devilFruit ? findFruit(c.devilFruit) : null; return ` ${icon} ${fruit ? `${fruit.icon}` : ''} `; } const CATEGORY_HEX = { Paramecia: '#a06cd5', Zoan: '#4c9a5a', Logia: '#3f9bd0' }; function fruitPortraitFallbackSVG(f) { const color = CATEGORY_HEX[f.category] || '#e3b04b'; const glow = f.subtype && /mythical|special/i.test(f.subtype); const uid = f.id.replace(/[^a-z0-9]/g, ''); return ` ${f.icon} `; } function characterPortraitSVG(c) { return properImageHTML('character', characterImageTitle(c), `${c.name} portrait`, characterPortraitFallbackSVG(c)); } function fruitPortraitSVG(f) { return properImageHTML('fruit', fruitImageTitle(f), `${f.name} fruit`, fruitPortraitFallbackSVG(f)); } const IMAGE_CACHE_KEY = 'opwikiProperImages:v1'; const IMAGE_API_ENDPOINT = 'https://onepiece.fandom.com/api.php'; let properImageCache = null; function getProperImageCache() { if (properImageCache) return properImageCache; try { properImageCache = JSON.parse(localStorage.getItem(IMAGE_CACHE_KEY) || '{}'); } catch (_) { properImageCache = {}; } return properImageCache; } function saveProperImageCache() { try { localStorage.setItem(IMAGE_CACHE_KEY, JSON.stringify(getProperImageCache())); } catch (_) { // Non-critical: images still load for the current page if storage is unavailable. } } function wikiThumbnailUrl(title, size = 700) { const params = new URLSearchParams({ action: 'query', prop: 'pageimages', format: 'json', piprop: 'thumbnail', pithumbsize: String(size), redirects: '1', origin: '*', titles: title, }); return `${IMAGE_API_ENDPOINT}?${params.toString()}`; } async function resolveProperImage(title) { const cache = getProperImageCache(); if (Object.prototype.hasOwnProperty.call(cache, title)) return cache[title]; const response = await fetch(wikiThumbnailUrl(title)); if (!response.ok) throw new Error(`Image lookup failed for ${title}`); const data = await response.json(); const page = Object.values(data.query?.pages || {})[0]; const source = page?.thumbnail?.source || ''; cache[title] = source; saveProperImageCache(); return source; } function hydrateProperImages(root = document) { const holders = [...root.querySelectorAll('.proper-image[data-image-title]')]; holders.forEach(async (holder) => { const img = holder.querySelector('.proper-image__img'); if (!img || holder.dataset.loaded) return; holder.dataset.loaded = 'pending'; try { const source = await resolveProperImage(holder.dataset.imageTitle); if (!source) throw new Error('No thumbnail returned'); img.addEventListener('load', () => { holder.classList.add('is-loaded'); img.hidden = false; }, { once: true }); img.src = source; holder.dataset.loaded = 'true'; } catch (_) { holder.dataset.loaded = 'fallback'; } }); }