feat: 视觉方向与背景图素材(章节场景图、重构 CSS、界面布局)

This commit is contained in:
Jayway Liu
2026-10-04 06:57:33 +08:00
parent 9b279f0cf8
commit 1a2f6606fc
9 changed files with 172 additions and 450 deletions
+20
View File
@@ -0,0 +1,20 @@
# 视觉方向与影像来源
## 当前界面
剧情采用“考古档案影像”作为视觉语言:深墨绿背景、青铜绿的档案标注、朱红的主操作,宋体标题与清晰的正文字体。图片铺满场景,文本区保持不透明度与对比度,供长篇阅读和选择操作。手机上内容自然滚动,不截断长文本。
当前四张照片均为**殷墟相关历史参考影像**,不是虚构“苍梧”遗址、人物或剧情现场照片。画面内标注“影像参考 / 殷墟考古资料”。这是可用的临时影像方案,不应作为正式章节插画向用户宣称。正式美术需求见 [插图生成清单](illustration-prompts.md),其 57 张插画尚未生成;后续应按剧情节点及分支逐张替换,不可用同一张史料图误导性地代表所有分支。
## 已加载的图片
图片置于 `public/images/`,均取自 Wikimedia Commons 的 CC0 文件页面;下载的是缩放至 1280px 宽的 JPEG,应用从本地路径加载,不依赖运行时第三方请求。
| 本地路径 | 原始文件页面 | 用途 |
|---|---|---|
| `ruins.jpg` | [Yinxu Palace Foundation](https://commons.wikimedia.org/wiki/File:Yinxu_Palace_Foundation_(45144971564).jpg) | 第一章背景 |
| `excavation.jpg` | [Sacrificial Pits of the Palace-Temple Complex of Yinxu](https://commons.wikimedia.org/wiki/File:Sacrificial_Pits_of_the_Palace-Temple_Complex_of_Yinxu_(45144967314).jpg) | 第二章背景 |
| `bronze.jpg` | [Shang Bronze Gui, Yinxu](https://commons.wikimedia.org/wiki/File:Shang_Bronze_Gui,_Yinxu.jpg) | 过场与特定剧情节点 |
| `inscription.jpg` | [Ancient Chinese Writing on Ox Scapula](https://commons.wikimedia.org/wiki/File:Ancient_Chinese_Writing_on_Ox_Scapula,_Shang_Dynasty_Oracle_Bone,_Yinxu,_Anyang.jpg) | 第三章与特定剧情节点 |
正式发布前应再次核对各来源页面的授权与署名元数据,并让 UI 的图片来源说明可直接访问。现阶段 UI 使用概括性说明,此表提供精确来源。
Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

+88 -373
View File
@@ -1,374 +1,89 @@
.app {
max-width: 720px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* ---- 影游场景 ---- */
.scene {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 24px;
background: linear-gradient(160deg, #14101e 30%, #2a2338 100%);
border-radius: 12px;
min-height: 80vh;
transition: background 0.8s ease;
}
.scene-header {
position: fixed;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
padding: 12px 20px;
font-size: 13px;
color: #b0a490;
background: rgba(20, 16, 30, 0.8);
}
.variant-tag {
color: #d4a96a;
}
.dialogue-box {
display: flex;
gap: 16px;
background: rgba(20, 16, 30, 0.85);
border: 1px solid #3a3248;
border-radius: 12px;
padding: 20px;
cursor: pointer;
}
.sprite {
flex-shrink: 0;
width: 72px;
height: 72px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: #f0e6d2;
}
.speaker-name {
font-weight: 700;
color: #d4a96a;
margin-bottom: 6px;
}
.dialogue-text {
line-height: 1.8;
margin: 0 0 8px;
}
.hint {
font-size: 12px;
color: #8a7f6a;
}
/* ---- 选择点 ---- */
.choice-box,
.narration-box {
background: rgba(20, 16, 30, 0.85);
border: 1px solid #3a3248;
border-radius: 12px;
padding: 20px;
}
.choice-prompt,
.narration-prompt {
font-size: 17px;
line-height: 1.7;
color: #f0e6d2;
}
.choice-options {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 12px;
}
.choice-btn {
text-align: left;
padding: 12px 16px;
border-radius: 8px;
border: 1px solid #4a3f5a;
background: #241e33;
color: #e8dcc8;
font-size: 15px;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
}
.choice-btn:hover {
border-color: #d4a96a;
background: #2e2642;
}
/* ---- 讲述点 ---- */
.mechanism-tag {
display: inline-block;
font-size: 12px;
color: #1a1626;
background: #d4a96a;
border-radius: 4px;
padding: 2px 8px;
margin-bottom: 10px;
}
.hold-btn {
width: 100%;
margin-top: 12px;
padding: 18px;
border-radius: 999px;
border: 2px solid #d4a96a;
background: transparent;
color: #d4a96a;
font-size: 16px;
cursor: pointer;
user-select: none;
touch-action: none;
}
.hold-btn.holding {
background: #d4a96a;
color: #1a1626;
}
/* ---- 过场 ---- */
.cutscene {
text-align: center;
cursor: pointer;
}
.cutscene-placeholder {
border: 2px dashed #4a3f5a;
border-radius: 12px;
padding: 80px 20px;
font-size: 20px;
color: #8a7f6a;
margin-bottom: 12px;
}
/* ---- 通用按钮 ---- */
button.primary,
button.secondary {
margin-top: 16px;
padding: 12px 24px;
border-radius: 8px;
font-size: 15px;
cursor: pointer;
}
button.primary {
border: none;
background: #d4a96a;
color: #1a1626;
font-weight: 700;
}
button.secondary {
border: 1px solid #4a3f5a;
background: transparent;
color: #b0a490;
width: 100%;
margin-top: 10px;
}
.chapter-end {
text-align: center;
padding: 40px 0;
}
/* ---- 画像报告 ---- */
.report {
padding: 32px 24px;
}
.report h1 {
color: #d4a96a;
}
.badge-banner {
display: flex;
gap: 20px;
align-items: center;
background: rgba(212, 169, 106, 0.08);
border: 1px solid #d4a96a;
border-radius: 12px;
padding: 20px;
margin: 16px 0 24px;
}
.badge-medal {
width: 72px;
height: 72px;
border-radius: 50%;
background: #d4a96a;
color: #1a1626;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
font-weight: 700;
flex-shrink: 0;
}
.badge-note {
font-size: 13px;
color: #8a7f6a;
}
.dimension-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 28px;
}
.dimension-row {
display: grid;
grid-template-columns: 140px 1fr 190px;
align-items: center;
gap: 12px;
}
.dimension-label {
color: #e8dcc8;
}
.dimension-bar {
height: 14px;
background: #2a2338;
border-radius: 7px;
overflow: hidden;
}
.dimension-fill {
height: 100%;
background: #d4a96a;
}
.dimension-value {
font-size: 13px;
color: #b0a490;
}
/* ---- 分享卡 ---- */
.share-card {
text-align: center;
margin-bottom: 16px;
}
.share-canvas {
max-width: 320px;
width: 100%;
border-radius: 12px;
border: 1px solid #3a3248;
display: block;
margin: 0 auto 12px;
}
/* ---- 语音输入与身份登记 ---- */
.voice-input {
margin-top: 12px;
}
.voice-live {
margin-top: 10px;
padding: 10px 14px;
background: rgba(212, 169, 106, 0.1);
border-left: 3px solid #d4a96a;
border-radius: 4px;
color: #e8dcc8;
line-height: 1.7;
}
.voice-textarea {
width: 100%;
box-sizing: border-box;
padding: 12px 14px;
border-radius: 8px;
border: 1px solid #4a3f5a;
background: #241e33;
color: #e8dcc8;
font-size: 15px;
line-height: 1.7;
resize: vertical;
margin-bottom: 8px;
}
.identity-box {
background: rgba(20, 16, 30, 0.85);
border: 1px solid #3a3248;
border-radius: 12px;
padding: 20px;
}
.identity-field {
display: grid;
grid-template-columns: 80px 1fr;
align-items: center;
gap: 12px;
margin: 10px 0;
color: #d8ccb8;
}
.identity-field input {
padding: 10px 12px;
border-radius: 8px;
border: 1px solid #4a3f5a;
background: #241e33;
color: #e8dcc8;
font-size: 15px;
}
/* ---- 同意门(ADR-0004) ---- */
.consent-box {
background: rgba(20, 16, 30, 0.85);
border: 1px solid #3a3248;
border-radius: 12px;
padding: 24px;
max-width: 560px;
margin: 48px auto;
}
.consent-box h2 {
color: #f0e6d2;
font-size: 20px;
margin: 8px 0 16px;
}
.consent-list {
color: #d8ccb8;
font-size: 15px;
line-height: 1.8;
padding-left: 20px;
margin-bottom: 20px;
}
.consent-list li {
margin-bottom: 8px;
}
.consent-note {
color: #d8ccb8;
font-size: 15px;
line-height: 1.7;
margin-bottom: 16px;
}
.consent-box .primary,
.consent-box .secondary {
margin-right: 10px;
margin-bottom: 10px;
.app { min-height: 100svh; display: flex; flex-direction: column; }
.scene { position: relative; isolation: isolate; min-height: 100svh; display: flex; flex-direction: column; overflow: hidden; background: #17201d; }
.scene-media, .scene-shade { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.scene-media { object-fit: cover; object-position: center; z-index: -2; filter: saturate(.75) contrast(1.06); }
.scene-shade { z-index: -1; background: linear-gradient(90deg, rgba(11,22,20,.72), rgba(11,22,20,.14) 80%), linear-gradient(0deg, #101917 0%, rgba(16,25,23,.9) 20%, rgba(16,25,23,.08) 72%); }
.scene-content { width: min(100% - 64px, 1100px); min-height: 100svh; margin-inline: auto; display: flex; flex-direction: column; padding-bottom: clamp(28px,5vh,64px); }
.scene-header { display: flex; align-items: center; justify-content: space-between; gap: 24px; border-bottom: 1px solid rgba(233,231,223,.34); padding-block: 20px; font-size: 13px; color: #e8e8dd; }
.scene-brand { font-family: 'Noto Serif CJK SC','Songti SC',serif; font-size: 18px; white-space: nowrap; }
.scene-brand small { display: inline-block; margin-left: 16px; font: 11px/1.5 'Noto Sans CJK SC','PingFang SC',sans-serif; color: #b7c8bb; }
.scene-chapter { color: #d6d3c8; text-align: right; }
.scene-stage { margin-top: auto; margin-bottom: 32px; }
.scene-index, .scene-credit { font-size: 12px; color: #b9d2c6; }
.scene-index { letter-spacing: 1px; }
.scene-stage h1 { margin: 6px 0 10px; font-size: clamp(36px,5vw,68px); line-height: 1.2; color: #f5f0e4; text-shadow: 0 2px 24px rgba(0,0,0,.38); }
.scene-credit { display: block; margin-top: 8px; }
.dialogue-box, .choice-box, .narration-box, .identity-box, .cutscene, .chapter-end { width: min(100%,760px); padding: 24px 28px; border: 1px solid rgba(201,219,203,.34); border-left: 3px solid #a8bda5; border-radius: 3px; background: rgba(13,25,23,.94); box-shadow: 0 16px 40px rgba(0,0,0,.2); backdrop-filter: blur(12px); }
.dialogue-box { display: flex; align-items: flex-start; gap: 18px; text-align: left; color: inherit; }
.dialogue-box:hover { border-color: #d9bc94; }
.speaker-mark { flex: 0 0 46px; width: 46px; height: 46px; display: grid; place-items: center; border: 1px solid #8ca696; border-radius: 50%; font-family: 'Noto Serif CJK SC','Songti SC',serif; color: #d2d7c8; }
.dialogue-body { display: block; min-width: 0; flex: 1; }
.speaker-name { display: block; margin-bottom: 8px; color: #d8b58a; font-weight: 600; }
.dialogue-text { display: block; line-height: 1.9; white-space: pre-line; }
.continue-hint { display: block; margin-top: 16px; color: #adc5b4; font-size: 12px; text-align: right; }
.choice-prompt, .narration-prompt, .cutscene p { color: #f2f0e7; font-size: 18px; line-height: 1.75; }
.choice-options { display: grid; gap: 9px; margin-top: 18px; }
.choice-btn { width: 100%; padding: 14px 18px; border: 1px solid #546d60; border-radius: 3px; background: rgba(34,54,47,.7); color: #f1eee4; text-align: left; transition: background .2s,border-color .2s; }
.choice-btn:hover { background: #344d40; border-color: #d8b58a; }
.mechanism-tag { display: inline-block; margin-bottom: 12px; padding: 3px 8px; border: 1px solid #799b82; color: #c2d8c5; font-size: 12px; }
.hint { color: #aabbb0; font-size: 12px; line-height: 1.6; }
button.primary, button.secondary, .hold-btn { min-height: 44px; padding: 10px 20px; border-radius: 3px; font-size: 15px; transition: background .2s,border-color .2s; }
button.primary { border: 1px solid #c36e55; background: #a94732; color: #fff9ed; font-weight: 600; }
button.primary:hover { background: #c05d43; }
button.secondary { border: 1px solid #829989; background: transparent; color: #e6eae0; }
button.secondary:hover { border-color: #d6b48b; background: rgba(168,189,165,.12); }
.cutscene .primary, .chapter-end .primary { margin-top: 12px; }
.voice-input { margin-top: 16px; }
.voice-controls { max-width: 280px; }
.hold-btn { width: 100%; border: 1px solid #91ae99; background: #294638; color: #edf1e7; touch-action: none; }
.hold-btn.holding { background: #a94732; border-color: #c36e55; }
.voice-live { padding: 10px 14px; border-left: 2px solid #b2c6ac; color: #d8e3d8; background: rgba(74,106,85,.23); }
.voice-textarea, .identity-field input { width: 100%; padding: 12px 14px; border: 1px solid #718f7c; border-radius: 3px; background: #172b25; color: #f0f0e7; line-height: 1.6; }
.voice-textarea { display: block; margin-bottom: 12px; resize: vertical; }
.voice-textarea::placeholder, .identity-field input::placeholder { color: #9eb1a3; }
.voice-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.voice-actions > button:not(.primary) { border: 0; background: transparent; color: #c5d2c2; text-decoration: underline; }
.identity-field { display: grid; grid-template-columns: 80px minmax(0,1fr); align-items: center; gap: 12px; margin: 10px 0; }
.identity-box .primary, .narration-box .secondary { margin-top: 12px; }
.consent-box { width: min(100% - 40px,700px); margin: auto; padding: clamp(24px,5vw,56px); border-top: 2px solid #9bb69b; background: #1b2a24; box-shadow: 0 16px 60px rgba(0,0,0,.25); }
.app:has(.consent-box) { padding-block: 32px; background: radial-gradient(ellipse at 80% 0%,#335444,#101917 65%); }
.consent-box h2 { font-size: clamp(24px,3vw,36px); margin: 8px 0 20px; }
.consent-list { padding-left: 22px; line-height: 1.8; color: #d7e0d6; }
.consent-list li { margin-bottom: 10px; }
.consent-note { color: #d7e0d6; }
.consent-box .primary { margin-right: 12px; }
.consent-box .hint { margin-top: 20px; }
.report { width: min(100% - 40px,880px); margin-inline: auto; padding: clamp(32px,5vw,72px) 0; }
.report h1 { font-size: clamp(27px,4vw,42px); color: #f1eee3; }
.badge-banner { display: flex; align-items: center; gap: 24px; margin: 28px 0 36px; padding: 22px 0; border-block: 1px solid #5a7762; }
.badge-banner h2 { color: #e7c297; margin-bottom: 8px; }
.badge-banner p { margin-bottom: 6px; }
.badge-medal { flex: 0 0 70px; height: 70px; display: grid; place-items: center; border: 2px solid #bd9570; border-radius: 50%; color: #eed7af; font: 34px 'Noto Serif CJK SC','Songti SC',serif; }
.badge-note { color: #a9baad; font-size: 13px; }
.dimension-list { display: grid; gap: 18px; margin-bottom: 42px; }
.dimension-row { display: grid; grid-template-columns: 140px minmax(60px,1fr) 185px; align-items: center; gap: 14px; }
.dimension-label { color: #e6ede1; }
.dimension-bar { height: 8px; background: #35463b; }
.dimension-fill { height: 100%; background: #a8c1a3; }
.dimension-value { color: #bdcfc0; font-size: 13px; text-align: right; }
.share-card { text-align: center; margin-bottom: 28px; }
.share-canvas { display: block; width: min(100%,320px); height: auto; margin: 0 auto 16px; border: 1px solid #698573; }
.report > .secondary { display: block; margin: auto; }
@media (max-width: 640px) {
.scene-content { width: min(100% - 32px,480px); padding-bottom: 20px; }
.scene-header { padding-block: 14px; align-items: flex-start; }
.scene-brand { font-size: 15px; }
.scene-brand small { display: block; margin-left: 0; margin-top: 3px; font-size: 10px; }
.scene-chapter { font-size: 11px; }
.scene-stage { margin-bottom: 20px; }
.scene-stage h1 { font-size: 38px; }
.dialogue-box, .choice-box, .narration-box, .identity-box, .cutscene, .chapter-end { padding: 18px; }
.speaker-mark { flex-basis: 36px; width: 36px; height: 36px; }
.dialogue-box { gap: 12px; }
.choice-prompt, .narration-prompt { font-size: 16px; }
.dimension-row { grid-template-columns: minmax(0,1fr) auto; gap: 4px 10px; }
.dimension-bar { grid-column: 1 / -1; grid-row: 2; }
.dimension-value { font-size: 12px; }
}
@media (max-height: 700px) { .scene-stage { margin-top: 48px; } }
@media (prefers-reduced-motion: reduce) { .choice-btn, button.primary, button.secondary, .hold-btn { transition: none; } }
+14 -24
View File
@@ -1,31 +1,21 @@
:root {
font: 16px/1.6 system-ui, 'PingFang SC', 'Segoe UI', sans-serif;
color: #d8ccb8;
background: #14101e;
font: 16px/1.6 'Noto Sans CJK SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
color: #e9e7df;
background: #111a19;
color-scheme: dark;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
}
#root {
min-height: 100svh;
}
h1 {
font-size: 28px;
}
h2 {
font-size: 20px;
margin: 0 0 4px;
}
p {
margin: 0 0 6px;
}
* { box-sizing: border-box; }
body { margin: 0; min-width: 320px; }
#root { min-height: 100svh; }
button, input, textarea { font: inherit; }
button { cursor: pointer; }
button:disabled { opacity: .48; cursor: not-allowed; }
:focus-visible { outline: 2px solid #dfad78; outline-offset: 3px; }
h1, h2, p { margin-top: 0; }
h1, h2 { font-family: 'Noto Serif CJK SC', 'Songti SC', 'STSong', serif; font-weight: 500; }
p { margin-bottom: .65em; }
::selection { background: #a44735; color: #fff; }
+34 -36
View File
@@ -13,14 +13,10 @@ const FOLLOW_UP_LABELS: Record<FollowUpDimension, string> = {
emotion: '情绪',
};
/** speaker/sprite → 立绘占位色(无图片素材,CSS 色块) */
const SPRITE_COLORS: Record<string, string> = {
curator: '#6a5a8a',
shenduo: '#4a6a8a',
atang: '#c46a5a',
wenjian: '#7a9a8a',
laogui: '#8a7a4a',
scene: '#3a3a4a',
const CHAPTER_IMAGES: Record<string, string> = {
chapter1: '/images/ruins.jpg',
chapter2: '/images/excavation.jpg',
chapter3: '/images/inscription.jpg',
};
interface Props {
@@ -28,7 +24,7 @@ interface Props {
onFinish: () => void;
}
/** 最简影游界面:对话框 / 选项按钮 / 讲述点语音输入 / 身份登记 / 场景变体渐变 */
/** 对话 / 选择 / 讲述 / 身份登记与章节过场 */
export function StoryPlayer({ engine, onFinish }: Props) {
// tick 只是触发重渲染;引擎是唯一事实源
const [tick, setTick] = useState(0);
@@ -37,33 +33,34 @@ export function StoryPlayer({ engine, onFinish }: Props) {
const node = engine.currentNode();
const chapter = engine.currentChapter();
const variant = engine.state.activeVariant;
const image = node.type === 'cutscene' || (chapter.id === 'chapter2' && node.id === 'c2-05')
? '/images/bronze.jpg'
: node.id === 'c1-05' ? '/images/inscription.jpg' : CHAPTER_IMAGES[chapter.id];
return (
<div
className="scene"
style={
variant
? { background: `linear-gradient(160deg, #1a1626 30%, ${variant.tint} 130%)` }
: undefined
}
>
<header className="scene-header">
<span>{chapter.title}</span>
{variant && <span className="variant-tag">场景变体:{variant.label}</span>}
</header>
<main className="scene">
<img className="scene-media" src={image} alt="" aria-hidden="true" />
<div className="scene-shade" />
<div className="scene-content">
<header className="scene-header">
<span className="scene-brand">苍梧疑冢 <small>时空档案馆 / ACT {chapter.act}</small></span>
<span className="scene-chapter">{chapter.title}</span>
</header>
<div className="scene-stage">
<span className="scene-index">ARCHIVE / 0{chapter.act}</span>
<h1>{chapter.title}</h1>
<span className="scene-credit">影像参考 / 殷墟考古资料</span>
</div>
{node.type === 'dialogue' && (
<div className="dialogue-box" onClick={() => { engine.advance(); if (engine.state.finished) onFinish(); else rerender(); }}>
<div className="sprite" style={{ background: SPRITE_COLORS[node.sprite ?? 'scene'] }}>
{node.speaker.slice(0, 1)}
</div>
<div>
<div className="speaker-name">{node.speaker}</div>
<p className="dialogue-text">{engine.renderText(node.text)}</p>
<span className="hint">点击继续 ▸</span>
</div>
</div>
<button type="button" className="dialogue-box" onClick={() => { engine.advance(); if (engine.state.finished) onFinish(); else rerender(); }}>
<span className="speaker-mark" aria-hidden="true">{node.speaker.slice(0, 1)}</span>
<span className="dialogue-body">
<span className="speaker-name">{node.speaker}</span>
<span className="dialogue-text">{engine.renderText(node.text)}</span>
<span className="continue-hint">继续 <span aria-hidden="true">→</span></span>
</span>
</button>
)}
{node.type === 'choice' && (
@@ -84,10 +81,10 @@ export function StoryPlayer({ engine, onFinish }: Props) {
{node.type === 'identity' && <IdentityView node={node} engine={engine} onDone={rerender} />}
{node.type === 'cutscene' && (
<div className="cutscene" onClick={() => { engine.advance(); rerender(); }}>
<div className="cutscene-placeholder">▶ {node.video}</div>
<div className="cutscene">
<span className="mechanism-tag">场景记录 / {chapter.act}</span>
<p>{engine.renderText(node.caption)}</p>
<span className="hint">点击继续 ▸</span>
<button className="primary" onClick={() => { engine.advance(); rerender(); }}>继续 →</button>
</div>
)}
@@ -106,7 +103,8 @@ export function StoryPlayer({ engine, onFinish }: Props) {
</button>
</div>
)}
</div>
</div>
</main>
);
}
+16 -17
View File
@@ -27,31 +27,30 @@ export function ShareCard({ badgeDimension, scores }: Props) {
const badge = BADGE_MAP[badgeDimension] ?? { title: '档案采集员', flavor: '' };
const dim = PROFILE_DIMENSIONS.find((d) => d.id === badgeDimension);
// 背景渐变(美术占位)
const bg = ctx.createLinearGradient(0, 0, W, H);
bg.addColorStop(0, '#1a1626');
bg.addColorStop(1, '#3a2a4a');
ctx.fillStyle = bg;
ctx.fillStyle = '#14231d';
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = '#8ba58e';
ctx.lineWidth = 2;
ctx.strokeRect(32, 32, W - 64, H - 64);
ctx.textAlign = 'center';
ctx.fillStyle = '#c9b48a';
ctx.fillStyle = '#d0dfcd';
ctx.font = '28px serif';
ctx.fillText('苍梧疑冢 · 人格画像', W / 2, 80);
// 勋章圆
ctx.beginPath();
ctx.arc(W / 2, 220, 70, 0, Math.PI * 2);
ctx.fillStyle = '#d4a96a';
ctx.fill();
ctx.fillStyle = '#1a1626';
ctx.strokeStyle = '#bfa47c';
ctx.lineWidth = 3;
ctx.stroke();
ctx.fillStyle = '#eed6aa';
ctx.font = 'bold 44px serif';
ctx.fillText('勋', W / 2, 236);
ctx.fillStyle = '#f0e6d2';
ctx.fillStyle = '#f1eee4';
ctx.font = 'bold 40px serif';
ctx.fillText(badge.title, W / 2, 360);
ctx.fillStyle = '#b0a490';
ctx.fillStyle = '#b3c6b4';
ctx.font = '22px serif';
ctx.fillText(badge.flavor, W / 2, 400);
if (dim) {
@@ -63,19 +62,19 @@ export function ShareCard({ badgeDimension, scores }: Props) {
ctx.textAlign = 'left';
scores.forEach((s, i) => {
const y = 510 + i * 56;
ctx.fillStyle = '#d8ccb8';
ctx.fillStyle = '#dce7d9';
ctx.font = '22px serif';
ctx.fillText(s.label, 80, y);
ctx.fillStyle = '#4a3f5a';
ctx.fillStyle = '#385044';
ctx.fillRect(260, y - 16, 240, 18);
ctx.fillStyle = '#d4a96a';
ctx.fillStyle = '#a8c1a3';
ctx.fillRect(260, y - 16, (240 * s.score) / s.max, 18);
ctx.fillStyle = '#d8ccb8';
ctx.fillStyle = '#dce7d9';
ctx.fillText(s.score.toFixed(1), 520, y);
});
ctx.textAlign = 'center';
ctx.fillStyle = '#8a7f6a';
ctx.fillStyle = '#acc0af';
ctx.font = '18px serif';
ctx.fillText('每个选择都是实测,没有自报。', W / 2, H - 40);
return canvas;