页面与交互
渲染布局与 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-events |
声明控件事件绑定 |
lp-model |
绑定数值属性或可写动态字符串 |
lp-key |
更新时保留稳定节点身份 |
lp-canvas |
挂载已配置的 Canvas renderer |
binding 使用稳定资源 ID,节点使用稳定业务 key。随机 key 会破坏输入、焦点和 renderer 连续性。
# 挂载 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、客户端视觉特效和指针脚本。