mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1495 字
4 分钟
6.UI 进阶指南 · 立绘、开场白、地图、交互与点击事件
2026-06-23

UI 进阶指南 · 立绘、开场白、地图、交互与点击事件#

进阶 UI 开发涉及更多前端技术与 RPH 平台交互机制。建议掌握 UI 基础后再阅读本文。


一、动态立绘#

1.1 变量定义#

在角色变量结构中增加 portraitTag 字段:

{
"girls": [
{
"name": "小满",
"portraitTag": "young adult woman, long black hair, loose ponytail, shy expression, fitted black dress, full body, standing, visual novel character sprite, simple background, no text"
}
]
}

1.2 AI 维护规则#

在变量说明或世界书中加入以下规则,指导 AI 每轮维护立绘提示词:

【AI动态立绘 UI · portraitTag 规则】
1. portraitTag 必须是英文绘图关键词,不要写中文
2. 只描述角色立绘,不描述剧情插图
3. 推荐包含:young adult woman, hair color, hair style, eye color, expression, outfit, full body, standing, visual novel character sprite, simple background, no text
4. 同一角色必须保持固定外貌一致
5. 普通对话不要频繁改变 portraitTag
6. 只有首次登场、换装、受伤、觉醒、身份变化、明显状态变化时才更新
7. 禁止使用 &、#、引号、尖括号,避免破坏图片地址

1.3 HTML 模板结构#

<div class="portrait-stage">
<div class="portrait-topline">
<span>AI GENERATED PORTRAIT</span>
<span class="portrait-badge">{{name}}</span>
</div>
<div class="portrait-frame">
<img class="portrait-img js-auto-portrait" data-tag="{{portraitTag}}" alt="{{name}}立绘">
<div class="portrait-loading">
正在调用 RP Hub 生图设置生成立绘<br>
如果一直不显示,请确认已在网站设置里填写自动生图密钥
</div>
</div>
</div>

1.4 CSS 样式#

.portrait-stage {
margin: 8px 0 12px;
border: 1px solid rgba(244,114,182,.28);
border-radius: 14px;
overflow: hidden;
background: radial-gradient(circle at 50% 12%, rgba(244,114,182,.18), rgba(2,6,23,.96) 68%);
}
.portrait-topline {
display: flex; justify-content: space-between;
padding: 7px 10px; font-size: 10px;
color: rgba(226,232,240,.72);
background: rgba(2,6,23,.72);
}
.portrait-badge { color: #f472b6; font-weight: 700; }
.portrait-frame {
position: relative; width: 100%;
aspect-ratio: 2 / 3; min-height: 260px;
display: flex; align-items: center; justify-content: center;
}
.portrait-img { display: none; width: 100%; height: auto; object-fit: contain; }
.portrait-frame.loaded .portrait-img { display: block; }
.portrait-loading {
position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
text-align: center; font-size: 12px; line-height: 1.7;
color: rgba(226,232,240,.72); padding: 18px;
}
.portrait-frame.loaded .portrait-loading { display: none; }

1.5 JavaScript 脚本#

(function () {
const NEG = 'bad anatomy,bad hands,blurry,low quality,text,watermark,worst quality';
const ARTIST = 'visual novel character sprite, very aesthetic, masterpiece, no text';
async function readSettings() {
try {
const db = await new Promise((ok, bad) => {
const r = indexedDB.open('RPHubDB', 1);
r.onsuccess = () => ok(r.result);
r.onerror = () => bad(r.error);
});
for (const storeName of Array.from(db.objectStoreNames || [])) {
const data = await new Promise(resolve => {
try {
const req = db.transaction([storeName], 'readonly').objectStore(storeName).get('settings');
req.onsuccess = () => resolve(req.result);
req.onerror = () => resolve(null);
} catch (e) { resolve(null); }
});
const settings = data && data.value ? data.value : data;
if (settings && settings.imageGenKey) return settings;
}
} catch (e) {}
return {};
}
function cleanTag(tag) {
return String(tag || '').replace(/[<>"'&]/g, ' ').replace(/\s+/g, ' ').trim();
}
function buildUrl(tag, settings) {
const token = String(settings.imageGenKey || '').trim();
if (!token) return '';
const base = token.toUpperCase().startsWith('STA1N') ? 'https://nai.sta1n.cn' : 'https://std.loliyc.com';
const fullTag = cleanTag(tag) + ', solo, full body, standing, visual novel character sprite, simple background, no text';
return base + '/generate?tag=' + encodeURIComponent(fullTag)
+ '&token=' + encodeURIComponent(token)
+ '&model=nai-diffusion-4-5-full'
+ '&artist=' + encodeURIComponent(ARTIST)
+ '&size=' + encodeURIComponent(settings.imageSize || '竖图(-1)')
+ '&steps=40&scale=6&cfg=0&sampler=k_dpmpp_2m_sde'
+ '&negative=' + encodeURIComponent(NEG)
+ '&nocache=0&noise_schedule=karras';
}
async function renderPortraits() {
const settings = await readSettings();
document.querySelectorAll('.js-auto-portrait').forEach(img => {
const frame = img.closest('.portrait-frame');
const url = buildUrl(img.getAttribute('data-tag'), settings);
if (!url) return;
img.onload = () => frame && frame.classList.add('loaded');
img.onerror = () => {
const tip = frame && frame.querySelector('.portrait-loading');
if (tip) tip.innerHTML = '立绘生成失败<br>请检查密钥、额度或提示词';
};
img.src = url;
});
}
renderPortraits();
})();

在向 AI 描述 UI 模板时,确保 initialVariableState 中包含 portraitTaghtmlTemplate 中包含上述 HTML + CSS,script 中包含上述读取密钥和生成图片的 JS。


二、全屏演出动画开场白#

2.1 实现原理#

使用短暗号触发正则替换,将暗号替换为完整的 HTML 开场白页面,以 iframe 形式嵌入正文。

2.2 基础模板#

<div class="rp-open">
<style>
.rp-open{
position:relative; left:50%; transform:translateX(-50%);
width:min(96vw,1360px); height:min(72vh,680px); min-height:480px;
margin:8px 0; overflow:hidden; border-radius:18px;
background:#080b12; color:#f4e7c2;
font-family:"Microsoft YaHei","Noto Serif SC",serif;
border:1px solid rgba(230,205,130,.28);
box-shadow:0 18px 55px rgba(0,0,0,.42);
}
.rp-open *{box-sizing:border-box}
.rp-open input{position:absolute; opacity:0; pointer-events:none}
/* ... 更多样式请按需从源文件复制 ... */
</style>
<input type="radio" name="rp-open-state" id="rp-s1" checked>
<input type="radio" name="rp-open-state" id="rp-s2">
<!-- 继续添加场景所需的页面数量 -->
<div class="rp-bg">
<section class="rp-page p1">
<div class="rp-card">
<div class="rp-small">THE STORY BEGINS</div>
<div class="rp-title">灰冠学院</div>
<div class="rp-sub">旧王冠沉没后的第四百一十七年。</div>
<label class="rp-btn" for="rp-s2">继续</label>
</div>
</section>
<!-- 添加更多场景页面... -->
</div>
</div>

2.3 核心结构说明#

元素作用
<input type="radio">控制页面切换,每个场景一个 radio
label[for="rp-sX"]点击跳转到对应场景页
.rp-page每个场景的内容页面
.rp-line.lX逐字打字机效果的文字行
.rp-btn-row多按钮选择行
.rp-bg背景层,含渐变和网格装饰
.rp-card内容卡片容器

2.4 动画效果#

动画说明
rpFade页面切换时的淡入上滑效果
rpCaret打字机光标闪烁效果
rpTypeX逐字显示动画,每个场景独立控制速度

2.5 正则替换配置#

使用短暗号(如 <<<intro>>>)确保不会在正文中误触发,正则替换为完整的开场白 HTML。

如果需要 iframe 形式隔离,可使用以下 JS 结构:

(function(){
const html = `<!DOCTYPE html><html>...完整HTML...</html>`;
const frame = document.currentScript.previousElementSibling;
frame.srcdoc = html;
})();

三、实时地图 UI 模板#

3.1 地图三要素#

要素说明
地点节点地图上有哪些地方
节点位置这些地方摆在地图哪里
状态变量每个地方当前的状态(当前位置、可前往、锁定、危险、已完成)

3.2 通用模板#

<div class="rp-map">
<style>
.rp-map{
--bg:#101722; --panel:#182131; --gold:#d8bd76;
--text:#eee3c8; --muted:#9faabd; --danger:#b85656;
--open:#6f9f74; --locked:#596171;
width:min(96vw,980px); margin:12px auto; padding:16px;
border-radius:18px;
background:linear-gradient(135deg,#101722,#20293b);
border:1px solid rgba(216,189,118,.35);
color:var(--text);
font-family:"Microsoft YaHei","Noto Serif SC",serif;
box-shadow:0 16px 42px rgba(0,0,0,.35);
}
/* 完整样式请参考实时地图UI简易模板教程.txt */
</style>
<div class="map-head">
<div>
<div class="map-title">{{currentRegion}}</div>
<div class="map-sub">当前位置:{{currentLocation}} · 地图状态:{{mapStatus}}</div>
</div>
<div class="map-danger">危险等级:{{dangerLevel}}</div>
</div>
<div class="map-body">
<!-- 节点 -->
<div class="map-node node-start {{nodeStartClass}}">
<div class="node-name">入口</div>
<div class="node-state">{{nodeStartStatus}}</div>
</div>
<div class="map-node node-center {{nodeCenterClass}}">
<div class="node-name">中央区域</div>
<div class="node-state">{{nodeCenterStatus}}</div>
</div>
<!-- 更多节点... -->
</div>
<div class="map-info">
<div class="info-card">
<div class="info-label">可前往路线</div>
<div class="info-text">{{availableRoutes}}</div>
</div>
<div class="info-card">
<div class="info-label">最近事件</div>
<div class="info-text">{{latestEvent}}</div>
</div>
</div>
</div>

3.3 变量示例#

{
"currentRegion": "起始区域",
"currentLocation": "入口",
"mapStatus": "初始探索",
"dangerLevel": "低",
"nodeStartClass": "active",
"nodeStartStatus": "当前所在地",
"nodeCenterClass": "open",
"nodeCenterStatus": "可前往",
"nodeEastClass": "locked",
"nodeEastStatus": "尚未解锁",
"nodeWestClass": "locked",
"nodeWestStatus": "尚未解锁",
"availableRoutes": "中央区域",
"latestEvent": "玩家抵达入口,地图开始记录当前探索状态。"
}

3.4 节点状态#

类名视觉效果含义
active高亮显示当前所在地
open正常显示已开放,可以前往
locked变灰/锁定未解锁
danger警戒效果危险区域
cleared已标记已完成区域

3.5 变量说明(给 AI 看)#

currentRegion:当前大区域名称
currentLocation:玩家当前所在地点
mapStatus:当前地图阶段(初始探索、调查中、战斗后、封锁中)
dangerLevel:危险等级(低、中、高、极高)
nodeXXClass:控制地图节点样式,可填 active/open/locked/danger/cleared
nodeXXStatus:显示地点下面的小字状态
availableRoutes:当前可前往地点
latestEvent:最近一次地图变化或剧情事件

3.6 世界书更新规则#

【实时地图更新规则】
- 每次剧情推进后,必须更新地图变量
- currentLocation 必须等于玩家当前实际所在地点
- 玩家进入某地点后,该地点 nodeClass 改为 active,其他地点根据情况改为 open/locked/danger/cleared
- 如果玩家解锁新地点,将对应 nodeClass 从 locked 改为 open
- 如果某地点发生危险事件,将对应 nodeClass 改为 danger
- availableRoutes 只显示当前剧情下玩家合理可前往的地点
- latestEvent 用一句话总结本次地图变化
- 不得让 UI 地图显示与正文剧情冲突

3.7 节点位置修改#

在 CSS 中调整百分比值:

.node-start { left: 12%; top: 42%; }
.node-center { left: 42%; top: 28%; }
.node-east { right: 13%; top: 42%; }
.node-west { left: 50%; bottom: 13%; }
/* 移动端适配 */
@media (max-width: 720px) {
.node-start { left: 8%; top: 42%; }
.node-center { left: 36%; top: 18%; }
.node-east { right: 6%; top: 45%; }
.node-west { left: 34%; bottom: 12%; }
}

四、全息地图(同心圆环交互)#

4.1 设计理念#

以同心圆环形式展示多层区域地图,每一层可独立点击并显示详情。适合岛屿、城市圈、秘境等层级式地图。

4.2 核心结构#

<div class="radar-ring-container">
<!-- 外层:边缘浅滩 -->
<div class="ring-layer ring-shallows" data-region="边缘浅滩"
data-desc="暗礁密布的珊瑚浅滩与红树林带..."
data-risk="潮汐异动 · 灵汐族圣临之地">
</div>
<!-- 第二层:翠叶密林 -->
<div class="ring-layer ring-jungle" data-region="翠叶密林"
data-desc="占岛屿面积60%的原始密林..."
data-risk="猛兽出没 · 远古守护">
</div>
<!-- 第三层:岩脊高地 -->
<div class="ring-layer ring-highland" data-region="岩脊高地"
data-desc="险峻的岩壁与铁皮竹林..."
data-risk="落石险径 · 灵压紊乱">
</div>
<!-- 核心:灵泉盆地 -->
<div class="ring-layer ring-core" data-region="灵泉盆地 · Elyria之冠"
data-desc="生命之泉与母树 Elyria 所在..."
data-risk="能量衰微 · 命运枢纽">
</div>
</div>
<!-- 右侧详情展示区 -->
<div class="info-pavilion" id="infoPanel">
<div class="region-name">
<span>🌊</span>
<span id="regionNameDisplay">边缘浅滩</span>
</div>
<div class="region-desc" id="regionDescDisplay">...</div>
<div class="region-tag" id="regionRiskDisplay">⚠️ 潮汐异动</div>
</div>

4.3 交互逻辑#

  • 点击任意环层 → 该层高亮 → 右侧面板更新为该区域的名称、描述和风险标记
  • JS 使用 dataset 读取每个环层的 data-regiondata-descdata-risk
  • 包含波纹点击反馈效果

4.4 完整代码参考#

完整实现请参见 全息地图.txt 源文件,内含全部 CSS 样式、JS 交互逻辑以及层级响应式适配。


五、适配边缘地图#

5.1 说明#

这是一个完整的 RPG 风格地图 HTML 页面,使用 CSS 网格 + SVG 路线绘制,包含已解锁/未锁定两种状态标记,适用于幻想风格角色卡 UI。

5.2 核心特征#

特征说明
地图容器.map-container 居中布局,仿古卷轴风格
SVG 路线使用 SVG path 绘制地点间连接线
地点标记.map-marker 包含 .unlocked.locked 两种状态
提示框点击 triggerSlash('/命令') 或弹出未解锁提示
图例显示”已解锁 / 未解锁 / 当前进度”

5.3 代码框架#

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>幻想郷地図</title>
<style>
/* 完整样式请参考适配边缘地图.txt */
</style>
</head>
<body>
<div class="map-container">
<button class="close-btn" onclick="handleClose()">✕</button>
<div class="title-section">
<h2>幻想郷地図</h2>
<span class="subtitle">RPG Map</span>
</div>
<div class="map-area">
<div class="map-grid">
<svg class="map-svg" viewBox="0 0 400 400">
<!-- SVG 路线 -->
<path class="map-path" d="M200 100 Q250 140 280 180 T320 240"></path>
</svg>
<div class="map-locations">
<!-- 地点标记 -->
<div class="map-marker unlocked" style="top:10%;left:48%">
<div class="marker-dot">1</div>
<div class="marker-name">神社</div>
</div>
<div class="map-marker locked" style="top:55%;left:70%">
<div class="marker-dot">5</div>
<div class="marker-name">香霖堂</div>
</div>
</div>
</div>
</div>
<div class="legend">
<!-- 图例 -->
</div>
</div>
<script>
function handleClose() {
if (window.triggerSlash) window.triggerSlash('/主页');
}
function handleMarkerClick(number) {
const commands = { '1': '/前往神社', '2': '/回家' };
if (window.triggerSlash) window.triggerSlash(commands[number]);
}
</script>
</body>
</html>

完整实现请参考 适配边缘地图.txt 源文件。


六、暂存点击事件#

6.1 说明#

这是一个表单交互 UI,玩家可选择食材、放油量等选项,点击提交后通过 triggerSlash 将选择发送给 AI。

6.2 核心交互模式#

  • 使用 .toggle-btn 切换按钮组实现选项选择
  • 使用 +/- 按钮控制油量数值
  • 提交时将所有选择汇总为 JSON 字符串,通过 triggerSlash 发送

6.3 关键代码#

(function() {
var state = { food: 'meat', useOil: 'yes', oilMl: 0 };
var submitBtn = document.getElementById('submitBtn');
function syncToButton() {
submitBtn.dataset.food = state.food;
submitBtn.dataset.oil = state.useOil;
submitBtn.dataset.ml = state.oilMl;
}
// 提交时触发
document.getElementById('submitBtn').onclick = function() {
var p = {
food: this.dataset.food === 'meat' ? '肉' : '菜',
oil: this.dataset.oil === 'yes' ? '放油' : '不放油',
ml: parseInt(this.dataset.ml)
};
try { window.triggerSlash(JSON.stringify(p)); }
catch(e) { alert(p.food + ' | ' + p.oil + ' | ' + p.ml + 'ml'); }
};
})();

完整实现请参考 暂存点击事件.txt 源文件。


七、正文悬浮球正则卡#

工具型卡片:通过正则追加完整 HTML loader,由 RP-Hub 包装成可执行 iframe,向父页面注入可拖动悬浮球菜单,在卡片卸载时自动清理。

7.1 正则配置#

{
"name": "正文悬浮球菜单 父页面注入自动清理版",
"regex": "^(?![\\s\\S]*rp-parent-float-owner-v2)([\\s\\S]*)$",
"flags": "g",
"replacement": "$1\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <style>html,body{margin:0;padding:0;width:1px;height:1px;background:transparent;overflow:hidden;font-size:0}</style>\n</head>\n<body>\n<script>\n(function(){\n // ...完整脚本见 正文悬浮球正则卡.json 源文件...\n})();\n<\/script>\n</body>\n</html>",
"placement": [2],
"markdownOnly": true,
"promptOnly": false,
"runOnEdit": true,
"scope": "character",
"disabled": false
}

7.2 功能说明#

  • 检测页面上是否已注入悬浮球(通过 rp-parent-float-owner-v2 标记)
  • 未注入时,追加完整的 HTML 文档触发 createExecutableHtmlIframe
  • 向父页面注入可拖动的圆形悬浮球按钮
  • 点击悬浮球展开快捷菜单
  • 菜单包含多个功能按钮,点击触发 triggerSlash
  • 页面卸载时自动清理 DOM 元素

7.3 完整代码#

完整实现请参考 正文悬浮球正则卡.json 源文件,内含:

  • 悬浮球 CSS 样式(圆球 + 阴影 + 渐变)
  • 菜单 CSS 样式(弹出动画 + 半透明背景)
  • 拖动功能(mousedown/mousemove/mouseup
  • 存储位置到 localStorage
  • 菜单按钮功能映射
  • pagehide/beforeunload 自动清理

八、人物部位点击说明#

8.1 说明#

在角色立绘上放置透明热区按钮,玩家点击不同部位时在右侧面板显示对应描述。适用于角色介绍、服装展示、身体状态说明等场景。

8.2 HTML 结构#

<div class="viewer">
<section class="image-card">
<div class="image-map-wrap">
<img class="character-image" src="人物图片URL" alt="人物示意图">
<!-- 透明热区按钮 -->
<button class="hotspot"
style="left: 47.77%; top: 19.65%; width: 19.07%; height: 11.79%;"
data-name="头部" data-desc="这是头部的描述。"></button>
<button class="hotspot"
style="left: 48.39%; top: 31.43%; width: 17.84%; height: 15.23%;"
data-name="躯干" data-desc="这是躯干的描述。"></button>
<!-- 更多热区... -->
</div>
</section>
<aside class="info-card">
<h1 class="info-title" id="part-name">请选择部位</h1>
<p class="info-desc" id="part-desc">点击图片中的区域查看说明。</p>
</aside>
</div>

8.3 CSS 要点#

  • .hotspot 完全透明(opacity: 0
  • 热区位置和大小通过 lefttopwidthheight 百分比定位
  • 使用 aspect-ratio: 3/4 保持图片比例

8.4 JavaScript#

const hotspots = document.querySelectorAll('.hotspot');
hotspots.forEach(function(hotspot) {
hotspot.addEventListener('click', function() {
document.getElementById('part-name').textContent = hotspot.dataset.name;
document.getElementById('part-desc').textContent = hotspot.dataset.desc;
});
});

8.5 如何适配自己的角色#

  1. 替换 imgsrc 为自己的角色图片
  2. 调整每个 .hotspotleft/top/width/height 百分比使其匹配图片上的身体部位
  3. 修改 data-namedata-desc 为对应部位的名称和描述
  4. 可在 info-card 中加入部位列表芯片,展示全部可选部位

完整实现请参考 人物部位点击说明.html 源文件。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

6.UI 进阶指南 · 立绘、开场白、地图、交互与点击事件
https://646677.xyz/posts/rpcard/6ui2/
作者
Charlotte
发布于
2026-06-23
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录