From c645ee09d2fa83784022bb21ea0583cba131422a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 23:47:39 +0000 Subject: [PATCH] Add original generated portrait/icon artwork for every entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds js/portraits.js: procedurally generated, original SVG artwork for all 101 characters (a role-themed line-art medallion — sword for swordsmen, crown for royalty, compass for navigators, etc., derived from each character's role/epithet/affiliation) and all 58 Devil Fruits (a stylized swirled-fruit icon colored by category with the fruit's symbolic emoji at center). Official One Piece artwork is copyrighted (Oda/Shueisha/Toei), so rather than scraping and re-hosting character art from the web, every image here is drawn from scratch with basic SVG primitives — no external assets, no copyright risk, fully offline-capable, and crisp at any size since it's vector. Wires the new generators into every card and detail view: character cards/profile header, Devil Fruit cards/detail header, the Devil-Fruit callout on character pages, and user chips on Devil Fruit pages. Removes the now-unused plain-letter initialFor() avatar helper. --- character.html | 1 + css/styles.css | 53 ++++++++++---------- fruit.html | 1 + fruits.html | 1 + index.html | 1 + js/app.js | 9 +--- js/character.js | 4 +- js/fruit.js | 4 +- js/portraits.js | 128 ++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 166 insertions(+), 36 deletions(-) create mode 100644 js/portraits.js diff --git a/character.html b/character.html index 7d815cc..84a600c 100644 --- a/character.html +++ b/character.html @@ -34,6 +34,7 @@ + diff --git a/css/styles.css b/css/styles.css index f0a6c3f..d690c2e 100644 --- a/css/styles.css +++ b/css/styles.css @@ -367,18 +367,18 @@ p { margin: 0 0 1em; color: var(--text-muted); } margin-bottom: 0.5rem; } +.portrait-svg { + display: block; + width: 100%; + height: 100%; +} + .char-card__avatar { width: 84px; height: 84px; margin: 0 auto 0.5rem; border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-family: var(--font-display); - font-size: 2.1rem; - color: #fff; - background: linear-gradient(145deg, var(--accent, var(--gold-deep)), color-mix(in srgb, var(--accent, var(--gold-deep)) 60%, black)); + overflow: hidden; border: 3px solid var(--ink); box-shadow: inset 0 0 0 2px rgba(255,255,255,0.25); } @@ -464,8 +464,12 @@ p { margin: 0 0 1em; color: var(--text-muted); } .fruit-card__inner { padding: 1.1rem 1.15rem; } .fruit-card__icon { - font-size: 2rem; - margin-bottom: 0.6rem; + width: 56px; + height: 56px; + border-radius: 50%; + overflow: hidden; + margin-bottom: 0.7rem; + box-shadow: 0 0 0 1px var(--surface-border); } .fruit-card__inner h3 { @@ -536,13 +540,7 @@ p { margin: 0 0 1em; color: var(--text-muted); } height: 110px; flex-shrink: 0; border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-family: var(--font-display); - font-size: 2.6rem; - color: #fff; - background: linear-gradient(145deg, var(--accent, var(--gold)), color-mix(in srgb, var(--accent, var(--gold)) 55%, black)); + overflow: hidden; border: 4px solid var(--bg); box-shadow: 0 0 0 3px var(--accent, var(--gold)), var(--shadow-sm); } @@ -625,7 +623,14 @@ p { margin: 0 0 1em; color: var(--text-muted); } .fruit-callout:hover { transform: translateX(2px); border-color: var(--gold); } -.fruit-callout__icon { font-size: 2.4rem; flex-shrink: 0; } +.fruit-callout__icon { + width: 68px; + height: 68px; + flex-shrink: 0; + border-radius: 50%; + overflow: hidden; + box-shadow: 0 0 0 1px var(--surface-border-strong); +} .fruit-callout__label { font-size: 0.72rem; @@ -708,13 +713,7 @@ p { margin: 0 0 1em; color: var(--text-muted); } width: 38px; height: 38px; border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-family: var(--font-display); - font-size: 1.1rem; - color: #fff; - background: linear-gradient(145deg, var(--accent, var(--gold)), color-mix(in srgb, var(--accent, var(--gold)) 55%, black)); + overflow: hidden; flex-shrink: 0; } @@ -735,8 +734,12 @@ p { margin: 0 0 1em; color: var(--text-muted); } } .fruit-detail-header__icon { - font-size: 4rem; + width: 100px; + height: 100px; flex-shrink: 0; + border-radius: 50%; + overflow: hidden; + box-shadow: 0 0 0 3px var(--cat-color, var(--gold)), var(--shadow-sm); } .fruit-detail-header h1 { margin-bottom: 0.2rem; } diff --git a/fruit.html b/fruit.html index 7d4dda3..04e62e4 100644 --- a/fruit.html +++ b/fruit.html @@ -34,6 +34,7 @@ + diff --git a/fruits.html b/fruits.html index 153de1c..30e11a4 100644 --- a/fruits.html +++ b/fruits.html @@ -60,6 +60,7 @@ + diff --git a/index.html b/index.html index 1ace47b..6813642 100644 --- a/index.html +++ b/index.html @@ -62,6 +62,7 @@ + diff --git a/js/app.js b/js/app.js index 1a9a6d5..041ab15 100644 --- a/js/app.js +++ b/js/app.js @@ -36,11 +36,6 @@ function accentFor(entity) { return `hsl(${hashHue(entity.name || key)}, 58%, 48%)`; } -function initialFor(name) { - const match = (name || '?').match(/[A-Za-z]/); - return match ? match[0].toUpperCase() : '?'; -} - function formatBounty(amount) { if (amount === null || amount === undefined) return 'Unknown'; return '฿' + amount.toLocaleString('en-US'); @@ -89,7 +84,7 @@ function characterCardHTML(c) {
${deadRibbon} WANTED -
${initialFor(c.name)}
+
${characterPortraitSVG(c)}

${escapeHtml(c.name)}

@@ -112,7 +107,7 @@ function fruitCardHTML(f) {
-
${f.icon}
+
${fruitPortraitSVG(f)}

${escapeHtml(f.name)}

${escapeHtml(f.category)}${f.subtype ? ' · ' + escapeHtml(f.subtype.split(' —')[0].split(' (')[0]) : ''}

${escapeHtml(f.tagline)}

diff --git a/js/character.js b/js/character.js index 7ccdba3..c141418 100644 --- a/js/character.js +++ b/js/character.js @@ -35,7 +35,7 @@ root.innerHTML = `
-
${initialFor(character.name)}
+
${characterPortraitSVG(character)}

${escapeHtml(character.affiliation)} · ${escapeHtml(character.role)}

${escapeHtml(character.name)}

@@ -52,7 +52,7 @@
${fruit ? ` -
${fruit.icon}
+
${fruitPortraitSVG(fruit)}

${escapeHtml(fruitRelation ? fruitRelation.status : 'Devil Fruit')}

${escapeHtml(fruit.name)}

diff --git a/js/fruit.js b/js/fruit.js index a70810c..ea9b321 100644 --- a/js/fruit.js +++ b/js/fruit.js @@ -25,7 +25,7 @@ if (!c) return ''; return `
-
${initialFor(c.name)}
+
${characterPortraitSVG(c)}
${escapeHtml(c.name)}
${escapeHtml(u.status)}
@@ -36,7 +36,7 @@ root.innerHTML = `
-
${fruit.icon}
+
${fruitPortraitSVG(fruit)}

${escapeHtml(fruit.category)}${fruit.subtype ? ' · ' + escapeHtml(fruit.subtype) : ''}${fruit.awakened ? ' · Awakened' : ''}

${escapeHtml(fruit.name)}

diff --git a/js/portraits.js b/js/portraits.js new file mode 100644 index 0000000..e4ef12c --- /dev/null +++ b/js/portraits.js @@ -0,0 +1,128 @@ +/* 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 characterPortraitSVG(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 fruitPortraitSVG(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} + + `; +}