页面与交互
渲染布局与 Canvas
用只读响应式页面和完整 Canvas renderer API 构建场景。
页面与交互 · AI 路径
HTML 负责页面,Canvas 负责连续绘制
渲染布局同步返回一份响应式页面;Canvas renderer 增加精灵、镜头、粒子、碰撞和连续输入。两者只消费状态快照,玩家存档仍只能由 Worker 动作结算。
# 选择页面技术
| 需求 | 负责人 |
|---|---|
| 状态栏、卡片、列表、表单、弹窗、响应式排版 | 渲染布局 HTML/CSS |
| 精灵、连续移动、命中检测、局部高频图形 | Canvas renderer |
| 三维模型、透视相机、灯光、3D 动画与拾取 | type=3d Three.js Canvas renderer |
| 点击、输入、付费或插件动作 | 已存在的控件 binding |
| 属性、对象、存档、AI 或联网结算 | Worker 控件/事件脚本 |
| Canvas 外的临时覆盖层 | 客户端特效脚本 |
不要为了一个普通按钮使用 Canvas,也不要为了移动精灵逐帧重建整页 HTML。
# 渲染布局契约
脚本同步返回 HTML 字符串。它可以读取允许的 player、objects 和 tools 代理,但不能修改状态,也不能使用 async、await、Promise、DOM、网络或定时器。
return `
<main lp-key="camp-root" class="min-h-dvh bg-slate-950 p-4 text-white">
<h1>` + tools.tr("冒险营地") + `</h1>
<p>` + player.getString("当前提示") + `</p>
<button lp-control="explore-control-id" lp-key="explore-action">
` + tools.tr("开始探索") + `
</button>
</main>
`;
玩家输入、AI 输出和联网文本都是不可信展示数据,应转义或安全绑定,不能直接插入可执行 HTML。
# 绑定协议
| 属性 | 作用 |
|---|---|
lp-control |
绑定已有控件及其 Worker 规则 |
lp-repeat-control |
展开对象/插件重复行并保留实例上下文 |
lp-for |
从 JSON 数组或非负整数展开模板,最多 100 行;支持 {{entry.id}} 和 {{index}} |
lp-if |
按条件挂载或卸载节点及其绑定,支持 ! 取反 |
lp-args |
在控件事件根节点传递 JSON 参数,脚本通过 tools.kwargs.params 读取 |
lp-events |
声明控件事件绑定 |
lp-model |
绑定数值属性或可写动态字符串 |
lp-key |
更新时保留稳定节点身份 |
lp-canvas |
挂载已配置的 Canvas renderer |
binding 使用稳定资源 ID,节点使用稳定业务 key。随机 key 会破坏输入、焦点和 renderer 连续性。
重复按钮可共用一个控件:
<button lp-for='entry in [{"id":"attack","label":"攻击"},{"id":"defend","label":"防御"}]'
lp-control="动作控件ID" lp-events="js_g" lp-args='{"action":"{{entry.id}}"}'>{{entry.label}}</button>
控件的 js_g 脚本读取 tools.kwargs.params.action。lp-for 不执行 JS 表达式;数组可以由布局脚本同步生成,动态数据可用返回 JSON 的占位符。数值和对象数据直接插入 HTML 属性前须正确转义。
动态字符串脚本可以同步返回 JSON.stringify([{ id: 'attack', label: '攻击' }]),然后将数据源写为 lp-for="entry in <|动态字符串ID|>",或等价的 lp-for="entry in player.getString('动态字符串ID')"。引擎在展开时读取当前字符串并解析 JSON,玩家数据变化后更新列表;这里不执行任意 JS 表达式。不要在布局脚本中把 player.getString(...) 的结果直接拼入 HTML 属性:数据中的引号可能破坏标记结构。
真实游戏的价格、倍率等配置放在表格,玩家专属数值放在属性中。动态字符串脚本只读取表格并组织 JSON,不要在脚本中重复写数值。例如 const table = player.getTable('动作配置表ID') 后,从 table.rowNames 取稳定 ID,用 table.getData(id, '名称') 和 table.getData(id, '消耗') 组装行。点击后的控件脚本应按 ID 重新从表格读取价格,而非信任 HTML 参数。
状态变化时会重新计算并比较列表数据,但不会整个重建 DOM:只有同一位置内容发生变化的行会重建,其余行继续复用。列表最多 100 行;调整排序可能使多个位置变化,建议保持 ID 和顺序稳定,避免用高频状态每帧改动列表。
条件渲染与输入框可组合使用:
<section lp-if="<{面板开关属性ID}>">
<input type="number" lp-model="数量属性ID">
<textarea lp-model="备注字符串ID"></textarea>
</section>
lp-if 可用属性 <{属性ID}>、动态字符串 <|条件ID|> 或循环行的 {{entry.visible}},前缀 ! 取反;空值、0、false、null、undefined、NaN 为假。复杂条件让动态字符串脚本返回 'true' 或 'false',不执行任意 JS 表达式。隐藏会卸载子树,重显时重新挂载:输入焦点和未提交草稿可能丢失;若只需视觉隐藏,请改用动态 style。
lp-model 当前只支持 input[type=number] 绑定普通数值属性,以及 textarea 绑定开启 editable 且没有脚本的动态字符串;文本 input、select、checkbox 不支持。输入发送给 Worker 校验写入,再以权威状态同步回表单。用于生成列表 JSON 的脚本动态字符串不能同时当作可编辑字段;需要编辑时另建可编辑源字符串。置于对象列表内时,输入会继承当前对象上下文。
# 挂载 Canvas renderer
在场景 Canvas 配置中创建唯一 renderer ID,并给宿主明确的响应式尺寸:
<canvas
lp-canvas="world"
lp-key="world-canvas"
class="block h-[70dvh] w-full"
></canvas>
renderer 脚本获得 player、objects 和 tools.canvas,并且必须同步返回 frame 函数或生命周期对象:
| 回调 | 执行时机 | 输入 |
|---|---|---|
mount(ctx) |
Pixi 和宿主准备完成后一次 | 初始快照 |
update(ctx) |
运行时快照改变时 | 新的冻结快照 |
frame(ctx) |
每个活动动画帧 | deltaMs 与 elapsedMs |
bindingResult(result) |
Canvas binding 请求结算后 | request/binding/accepted/reason |
dispose(ctx) |
renderer 移除、场景切换或替换 | 最终清理上下文 |
回调不能是 async,也不能返回 Promise。允许的资源 Promise 用 .then/.catch 启动;高级 API 还应维护 disposed 标记。
# 状态快照 API
生命周期 ctx 与 tools.canvas 都提供冻结状态:
| 值 | 内容 |
|---|---|
ctx.revision / canvas.state.revision |
当前快照修订号 |
ctx.state / canvas.state.snapshot |
{ properties, dynamicStrings },按稳定 ID 索引 |
ctx.objects / canvas.objects.snapshot |
按类/实例组织的对象快照 |
ctx.scene / canvas.scene |
id、逻辑 width、height、dpr |
ctx.input / canvas.input |
指针和键盘快照 |
ctx.deltaMs / ctx.elapsedMs |
截断后的帧间隔与运行时间 |
快照只读。本地动画不能证明 Worker 动作成功,应等待下一份状态快照或 bindingResult。
# 完整 canvas API
本节绘制/碰撞/资源示例为缺省 type=2d 的 Pixi 接口。type=3d 复用生命周期、输入、绑定和源码模块,原生场景及绘制接口见 Three.js 三维 Canvas。两种引擎分别按需导入;实际发布分包和网络下载仍需按目标构建验收。
# 绘制
| 方法 | 作用 |
|---|---|
canvas.draw.sprite(key, options) |
创建/更新已配置图片、帧或动画 |
canvas.draw.rect(key, options) |
创建/更新矩形或圆角矩形 |
canvas.draw.text(key, options) |
创建/更新 Pixi 文本 |
canvas.draw.bounds(key) |
返回 { x, y, width, height } 或 undefined |
canvas.draw.remove(key) |
删除并销毁一个托管节点,返回 boolean |
canvas.draw.clear() |
删除全部托管节点,只在整体替换/销毁时使用 |
三个 draw 方法都是 upsert,必须复用稳定且唯一的 key。通用 options 包括 x、y、width、height、scale、scaleX、scaleY、rotation、alpha、visible、zIndex、bindings、objectId 和可序列化 payload。
- Sprite 额外支持
asset、frame、animation、anchorX、anchorY。 - Rect 额外支持
color、fillAlpha、stroke、strokeAlpha、lineWidth、radius。 - Text 必须传
text;Pixi 字体字段放在style,可传anchorX/anchorY。
const canvas = tools.canvas;
let x = 80;
return {
frame(ctx) {
if (canvas.input.isKeyDown("ArrowRight")) {
x += 180 * ctx.deltaMs / 1000;
}
if (canvas.input.wasKeyPressed("Space")) {
canvas.emitBinding("jump", { x });
}
canvas.draw.rect("player", {
x, y: 120, width: 32, height: 32,
radius: 16, color: "#22d3ee"
});
},
dispose() {
canvas.draw.clear();
}
};
# 资源
| 方法 | 返回 |
|---|---|
canvas.assets.resolve(assetId, objectId?) |
{ id, source, state } |
canvas.assets.state(assetId) |
idle、loading、ready 或 error |
canvas.assets.preload(assetId, objectId?) |
Promise<boolean> |
资源必须先存在于 renderer 配置中,不能编造 URL。preload 是上下文中返回 Promise 的例外,但生命周期仍需同步返回:不要 await,通过 state/update 在资源就绪后绘制。
# 输入
canvas.input.pointer 包含 x、y、down、pressed、released、pointerType、button、buttons、movementX、movementY、wheelX 和 wheelY。
canvas.input.keys、canvas.input.pressedKeys 和 canvas.input.releasedKeys 使用 KeyboardEvent.code。Canvas 获得焦点后才接收键盘输入。
| 方法 | 含义 |
|---|---|
canvas.input.isKeyDown(code) / canvas.input.wasKeyPressed(code) / canvas.input.wasKeyReleased(code) |
持续按下、本帧按下、本帧释放 |
canvas.input.isButtonDown(button?) / canvas.input.wasButtonPressed(button?) / canvas.input.wasButtonReleased(button?) |
指针按钮状态,默认 button 为 0 |
# 碰撞与边界
| 方法 | 返回 |
|---|---|
canvas.collision.intersects(leftKey, rightKey) |
两个托管节点边界是否重叠 |
canvas.collision.contains(key, x, y) |
点是否在托管节点内部 |
canvas.collision.clamp(key, bounds?) |
把节点移入边界,返回是否成功 |
clamp 默认边界是 Canvas 逻辑区域。碰撞使用当前轴对齐渲染边界,不是自定义物理形状。
# 动作绑定
canvas.emitBinding(name, payload?, objectId?) 立即返回请求 ID。name 必须映射已配置的 action binding,其中使用真实控件 ID 和 js_g、js_press、js_release、js_move 之一。
托管节点可声明 bindings: { click, press, release, move },同时携带 payload 和 objectId。move binding 应配置节流,不能逐帧向 Worker 发动作;Worker 脚本从 tools.kwargs.payload 读取可序列化参数。
bindingResult 表示传输/执行链是否接受请求,不表示业务一定成功。权威成功结果仍来自后续状态快照。
# 模块
canvas.modules.load(key, source) 同步返回作者源码导出的函数或对象。同一 key/source 每个 renderer session 只执行一次;源码变化时旧模块先清理再替换;session 销毁会调用模块导出的 dispose(ctx)。
模块源码只能来自作者维护的只读动态字符串或静态表格。禁止执行玩家输入、动态表格、弱联网结果或运行时 AI 输出。
# 高级 PixiJS
canvas.pixi 开放 PixiJS 8 模块命名空间,canvas.app 开放宿主拥有的 Application。高级 renderer 可创建 Container、Graphics、Sprite、Text、滤镜、mask、shader、mesh、粒子和 ticker 回调。
不能创建/销毁宿主 Application、替换 renderer、销毁 app.ticker 或把 app.canvas 移到其他 DOM。canvas.draw 不管理自行创建的对象;必须在 dispose 中移除监听并销毁资源。
# 预算与失败
运行时会限制托管节点、纹理内存/并发、模块数量/深度/key 长度和模块源码总量。连续三次 frame 报错会暂停 frame 程序并显示 Canvas 诊断。复用稳定节点、预加载有限资源、避免每帧全量扫描,并优先修复第一条诊断。
# 纯 AI 任务合同
向编辑器 AI 提供场景、页面区域、数据 ID、控件/action binding、renderer ID、资源清单、目标宽度、视觉参考、清理规则和验收步骤。要求它保留 Worker 规则,只替换表现层。
验收首帧、状态更新、键盘/触摸焦点、尺寸/DPR、资源失败、binding 拒绝、重复输入、对象定位、场景清理和窄屏无重叠。
下一步:tools 运行时 API、客户端视觉特效和指针脚本。