对话框
对话框的组件、头像解析顺序、动画、换行与文字自适应缩放规则
Dialog 渲染对话框:角色名牌、对话正文,以及可选的头像
用自定义组件替换内置对话框,通过 game.configure({ dialog }) 传入。在组件内组合 Avatar、Nametag、Texts,或用 useDialog 读取当前台词
示例
- 导入组件
import { Avatar, Dialog, Nametag, Texts, useGame } from "narraleaf-react";- 组合 ADV 布局
function GameDialog() {
return (
<Dialog className="bg-white">
<div className="dialog-content flex items-start gap-4 w-full h-full">
<Avatar />
<div className="dialog-text-content min-w-0 flex-1">
<Nametag className="font-bold" />
<Texts className="text-lg" />
</div>
</div>
</Dialog>
);
}Avatar 是可选的。内置对话框使用这套布局
- 在游戏中注册
function App() {
const game = useGame();
useEffect(() => {
game.configure({
dialog: GameDialog,
});
}, []);
return /* ... */
}组件
Dialog
渲染对话框容器。子节点通常是 Avatar、Nametag、Texts,布局自定
children?: React.ReactNode- 对话框内容...props: HTMLMotionProps<"div">- 传给内部的motion.div,接受普通 div props 以及initial、animate、exit、transition、layout等 Motion props 和 Motion 事件处理器
视觉 transform 写在 Dialog 上,播放器会把它们应用到内部的 motion 元素
Nametag
渲染说话人名字,需放在 Dialog 内。未传 children 或 name 时显示当前说话人,未传 color 时使用当前角色颜色
character?: Character | null- 取名字的角色entry?: NvlDialogEntry- 取名字的 NVL 条目name?: React.ReactNode- 要显示的名字color?: Color- 文字颜色children?: React.ReactNode- 自定义名牌内容...props: Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "color">
Texts
渲染逐字显示的对话正文,需放在 Dialog 内。这些 props 设定默认外观,未设定的属性从 CSS 继承,Sentence 与 Word 上的样式覆盖两者
children?: neverdefaultColor?: Color- 句子和词都未设颜色时使用的文字颜色fontSize?: React.CSSProperties["fontSize"]fontWeight?: React.CSSProperties["fontWeight"]fontWeightBold?: React.CSSProperties["fontWeight"]- 句子或词为粗体时的字重fontFamily?: React.CSSProperties["fontFamily"]writingMode?: TextWritingMode- 文本框的排版流向。vertical-rl就是日文小说的经典设定:字列从上往下读、列向左推进。默认"horizontal-tb"。自0.26.0起textOrientation?: TextGlyphOrientation- 字形在竖列里怎么站。mixed让 CJK 立着、拉丁字母躺着。横排时忽略。默认"mixed"。自0.26.0起tateChuYoko?: TateChuYoko- 縦中横:把短的拉丁或数字串横着立在字列里,而不是让它躺着。true合并不超过两个字符的串,传数字则自定上限。横排时忽略。默认true。自0.26.0起autoFit?: boolean- 在逐字显示过程中缩小字号,使整行留在它所在的盒子内。默认true。自0.34.0起autoFitMinFontSize?: number- 缩放能设到的最小字号,单位 px。默认12。自0.34.0起...props: React.HTMLAttributes<HTMLDivElement>
Avatar
渲染当前台词解析出的头像;该行没有头像时不渲染任何节点
...props: React.ImgHTMLAttributes<HTMLImageElement>- 与默认图片样式合并
默认图片样式:
width/height:96objectFit:"cover"borderRadius:6flex:"0 0 auto"
TextsPreview
用与 Texts 相同的逐字机渲染一行文本,但与正在进行的对白脱钩。凡是需要在台词之外展示一段对白样例的地方都用它:设置页的文本速度那一行、字体预览、样式选择器
它读取 cps 与 gameSpeed 偏好,并在两者变化时重新播放——这正是它能给速度滑块做实时预览的原因。在 GameProviders 之外,它退回到 Game.DefaultPreference 与 Game.DefaultConfig,因此在没有游戏在跑的页面上同样能渲染
import { TextsPreview, Word } from "narraleaf-react";
<TextsPreview
text={["敏捷的棕色狐狸", new Word("跃过", { color: "#f00" }), "了懒狗。"]}
fontSize={18}
/>内容来源给一个就够。words 优先于 text,text 优先于 sentence
text?: TextsPreviewInput- 静态内容:一个StaticWord<string | Pausing>,或它们组成的数组sentence?: Sentence- 要预览的 Sentence。其中的动态词不会被求值,而是被丢弃;预览已经跑过的内容请改用words。句子自身的颜色、字体与粗体/斜体标志则照常生效words?: Word<Pausing | string | TextEvent>[]- 已经求值完毕的词useTypeEffect?: boolean- 逐字打出而不是整行直接显示。默认trueloop?: TextsPreviewLoop-false表示只播一次;{enabled?: boolean, delay?: number}可设定两轮之间的间隔(毫秒)。默认truerestartDelay?: number-loop为布尔值时,下一轮开始前的间隔(毫秒)。loop为带delay的对象时忽略。缺省取GameConfig.autoForwardDefaultPausecps?: number- 每秒字数。缺省取cps偏好gameSpeed?: number- 速度倍率。缺省取gameSpeed偏好pauseDuration?: number- 未自带时长的Pause停多久(毫秒)。缺省取GameConfig.autoForwardDefaultPausedefaultColor?: Color、fontSize、fontWeight、fontWeightBold、fontFamily- 与Texts上的同名 props 一致writingMode?: TextWritingMode、textOrientation?: TextGlyphOrientation、tateChuYoko?: TateChuYoko- 与Texts上的同名 props 一致。自0.26.0起onCompleted?: () => void- 每次一行播完时调用,因此循环播放的预览每轮都会调用一次...props: Omit<React.HTMLAttributes<HTMLDivElement>, "children">
预览没有副作用:内容里的 TextEvent 会被跳过而不触发,因此预览一行永远不会换立绘、也不会放音效。这里同样没有 autoFit——预览不在对话框内,没有可供适配的盒子
动画
向 Dialog 传入 Motion props。内置对话框没有进入和退出动画
import { Dialog, Nametag, Texts } from "narraleaf-react";
function GameDialog() {
return (
<Dialog
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.25, ease: "easeOut" }}
layout
>
<Nametag />
<Texts />
</Dialog>
);
}播放器在这些时机播放动画:
- 一句对话被另一句替换:对话框保持不动,不播放
exit,也不会因为正文或名字变化而重播进入动画 - 对话之后是非对话内容:
exit与剧情推进同时进行 - 上一次
exit尚未结束又出现新对话:两段动画同时播放 - 在对话框中显示的菜单 prompt:选项出现前采用同样的行为
正在退出的对话框忽略点击、跳过和自动播放
GameConfig.animationPropagate 作用于对话框的 AnimatePresence 边界。只有当自定义对话框内部嵌套了需要播放退出动画的 AnimatePresence 时,才设为 true
头像
Avatar 为当前说话的角色显示小头像。头像可以配置在角色上、绑定到角色的舞台立绘上,或单独某一行上
每一行按以下顺序解析头像:
- 旁白或无名角色:无头像
- 句子配置中的
avatar: false:无头像 - 句子级头像
- 绑定到该角色、最近显示且仍可见的立绘的头像
- 角色级头像
- 无头像
引擎不会把全身立绘裁切成头像。没有配置头像的角色不显示头像
角色级头像
台上台词与旁白使用同一张图
import { Character } from "narraleaf-react";
const alice = new Character("Alice", {
avatar: "/assets/alice/avatar-default.png",
});
alice.say("画外音时也可以使用角色级头像。");方法写法:
const alice = new Character("Alice")
.setAvatar("/assets/alice/avatar-default.png");
alice.say("本句使用默认头像。");舞台立绘
把舞台 Image 绑定到角色,由可见立绘决定头像
import { Character, Image } from "narraleaf-react";
const aliceBody = new Image({
name: "alice-body",
src: "/assets/alice/body-normal.png",
opacity: 1,
});
const alice = new Character("Alice", {
avatar: "/assets/alice/avatar-default.png",
portraits: [
{
image: aliceBody,
avatar: "/assets/alice/avatar-normal.png",
},
],
});aliceBody 在当前场景可见时使用立绘头像,否则使用角色级头像
const alice = new Character("Alice")
.setAvatar("/assets/alice/avatar-default.png")
.addPortrait(aliceBody, {
avatar: "/assets/alice/avatar-normal.png",
});按表情区分头像
立绘头像可以是一个函数,入参为当前立绘、currentSrc、tags、character、sentence、gameState,返回值为:
- 图片 URL 或
StaticImageData; null,不显示头像;undefined,继续按解析顺序进入下一步
const aliceBody = new Image({
name: "alice-body",
src: {
groups: [
["normal", "happy", "angry"],
["school", "casual"],
],
defaults: ["normal", "school"],
resolve: (emotion, outfit) => `/assets/alice/body-${emotion}-${outfit}.png`,
},
opacity: 1,
});
const alice = new Character("Alice", {
avatar: "/assets/alice/avatar-default.png",
portraits: [
{
image: aliceBody,
avatar: ({ tags }) => {
const emotion = tags?.[0] ?? "normal";
return `/assets/alice/avatar-${emotion}.png`;
},
},
],
});单行覆盖
alice.say("仅本行隐藏头像。", {
avatar: false,
});
alice.say("本行使用特殊切图。", {
avatar: "/assets/alice/avatar-special.png",
});
alice.say("本行使用解析函数。", {
avatar: ({ tags, currentSrc }) => {
if (tags?.includes("angry")) {
return "/assets/alice/avatar-angry-close.png";
}
return undefined;
},
});avatar: false 隐藏该行头像,其他值优先于立绘头像和角色级头像
多个立绘同时可见
同一角色有多个绑定立绘可见时,引擎按场景的显示顺序(从后到前)取最近显示的那一个。opacity 等效为 0 的显示对象会被忽略
依赖头像的布局
useAvatar() 返回 visible、src、character、portrait,可据此让网格或弹性布局在没有头像时改变。返回值结构与示例见 useAvatar
外观
容器样式写在 Dialog 上:
function GameDialog() {
return (
<Dialog
style={{
backgroundColor: "rgba(0, 0, 0, 0.5)",
borderRadius: "10px",
padding: "20px",
}}
>
{/* ... */}
</Dialog>
);
}名牌样式写在 Nametag 上:
function GameDialog() {
return (
<Dialog>
<Nametag
style={{
backgroundImage: "url('/path/to/image.png')",
backgroundSize: "cover",
width: "100%",
height: "100%",
}}
/>
</Dialog>
);
}断行
对话正文在横排与竖排下都按严格禁则排版。行首不会出现 、 。 」 ? !、小写假名与长音符,行尾不会出现 「 (。拉丁词不会被拆开,在任何宽度下都放不下的连续串(例如网址)会断开而不是溢出
严格禁则只在文档声明了语言时生效。在承载播放器的页面上设置 lang,取任意值均可;NarraLeaf Studio 构建出的外壳已经带有该属性
文本缩放
自 0.35.0 起可用
对白行在逐字显示的过程中缩小,以保持整行留在盒子内。起始字号为 fontSize 并在放得下时一直保持,因此短行按原字号显示。文本抵达盒子末端后,其后每多一个字都会重新测量,并按需要缩小到整行仍能容纳,最小到 autoFitMinFontSize。在最小字号下仍然溢出的行保持溢出
缩放默认开启。单行关闭用 autoFit={false},整个游戏关闭用 GameConfig.disableTextScaling
<Texts fontSize={24} autoFitMinFontSize={16} />Sentence 与 Word 上设定的字号随行一并缩放而不是被替换,相对大小关系在任何字号下都成立
盒子是容器的父元素。父元素自身没有高度时,行保持作者设定的字号
叠层
ADV 对话框自带一个叠层,它覆盖对话框并画在其上,且处于同一个缩放后的舞台内。凡是属于某一行、又放不进文本框的东西都放在那里:行内词的释义浮层、名字上的提示框。用 useDialogOverlay 取得它
叠层是内置 Dialog 组件的一部分,因此由 Dialog 组合而成的自定义对话框同样具备它。NVL 模式没有叠层
导出的类型
narraleaf-react 导出:
- 组件 props:
NametagProps、TextAppearanceProps、TextsProps、RawTextsProps、EntryTextsProps、TextsPreviewProps、TextsPreviewInput、TextsPreviewLoop - 头像来源:
DialogAvatarSource、DialogAvatarResolverContext、DialogAvatarResolver、DialogAvatar - 立绘:
CharacterPortraitConfig、DialogAvatarResolution - React:
Avatar、useAvatar,以及类型DialogAvatarContext
Character 提供 setAvatar、addPortrait、setPortraits。参见 Character、CharacterConfig,以及 SentenceUserConfig 和 SentenceConfig 的 avatar 字段