canvas
canvas 让 guest 在宿主拥有的真实绘图面上呈现内容。它提供受限 2D 指令和完整软件像素帧两条路径,但不把 DOM、浏览器 Canvas context 或像素读回能力交给 guest。
适用场景
| 场景 | 建议路径 |
|---|---|
| 图表、表单、棋盘和简单 2D UI | draw 批量提交绘图指令 |
| 模拟器、像素编辑器和软件渲染器 | present 提交一帧 RGBA 像素 |
| 自己做文本布局但使用宿主字体 | measureText |
| 跟随容器尺寸或屏幕密度变化 | 监听 canvas.resize |
不适合 3D、自定义着色器、大规模场景树或需要读回渲染结果的工作流。这些需求应使用未来 GPU 能力或 guest 库,而不是继续扩张 canvas。
能力声明
应用只声明实际使用的方法:
{
"capabilities": [
"host:canvas.getInfo",
"host:canvas.draw",
"host:canvas.measureText"
]
}
声明 canvas.draw 不会自动获得 canvas.present。事件随对应方法和绘图面的生命周期送达,不另设通配订阅能力。
Reference
数据类型
CanvasInfo {
width: u32 // 逻辑像素
height: u32
dpr: f32 // 已按信任档位限制精度
mode: "draw" | "pixels" | "hybrid"
}
FrameInfo {
width: u32
height: u32
stride: u32
format: "rgba8"
sequence: u64
}
canvasId 由应用壳层创建并传给 guest。它只标识本应用的绘图面,不是可跨应用复用的资源句柄。
方法
canvas.getInfo
canvas.getInfo(canvasId) -> CanvasInfo | null
返回绘图面的当前尺寸。绘图面不存在或已经销毁时返回 null;guest 不应缓存结果跨越 canvas.resize。
canvas.draw
canvas.draw(canvasId, operations[]) -> { accepted: u32, dropped: u32 }
一次提交一批 v0.1 白名单 2D 指令。指令至少覆盖变换栈、矩形、路径、裁剪、纯色、包内图片和文本。未知指令、非有限数值、越界资源引用和超预算批次必须被拒绝,不能透传给浏览器绘图对象。
资源参数使用 manifest 中的 assetId 或 Handle<font, use, app-context, runtime>,不接受 URL、路径或文件 token。
canvas.present
canvas.present(canvasId, pixels: ByteSource, frame: FrameInfo) -> void
提交一帧完整 rgba8 像素。宿主验证 width × 4 <= stride、总字节数、绘图面尺寸和序号。调用完成只表示宿主接收了帧,不保证显示设备已经扫描输出。
canvas.measureText
canvas.measureText(canvasId, texts[], font?) -> TextMetrics[]
批量测量文本。返回结果与输入顺序一一对应;字体句柄缺失时使用绘图面的默认字体。该方法只做度量,不进行段落排版、换行或富文本布局。
事件
event canvas.frame {
canvasId,
sequence: u64,
timeMs: f64
}
event canvas.resize {
canvasId,
width: u32,
height: u32,
dpr: f32
}
canvas.frame 是下一帧提示,不是高精度计时器。timeMs 使用平台统一的粗化、相对、非递减时间。宿主可以合并积压帧提示。
JS 工作流示例
下面的计数器每帧只提交一次批量绘制:
const canvasId = 'main';
let count = 0;
host.events.on('canvas.frame', ({ canvasId: id }) => {
if (id !== canvasId) return;
host.canvas.draw(canvasId, [
{ op: 'clear', color: '#101418' },
{ op: 'fillRect', x: 24, y: 24, width: 180, height: 56, color: '#2f81f7' },
{ op: 'fillText', x: 40, y: 60, text: `Frame ${count++}`, color: '#ffffff' },
]);
});
软件渲染器应复用像素缓冲,并按最新 resize 结果调整:
const info = await host.canvas.getInfo('main');
if (!info) throw new Error('canvas unavailable');
const pixels = renderFrame(info.width, info.height);
await host.canvas.present('main', pixels, {
width: info.width,
height: info.height,
stride: info.width * 4,
format: 'rgba8',
sequence: 1n,
});
原生 WASM 绑定示例
原生 guest 把已分配线性内存区域绑定为 ByteSource,但指针不进入逻辑 API:
CanvasInfo info = host_canvas_get_info(MAIN_CANVAS);
uint32_t byte_len = info.width * info.height * 4;
uint8_t *pixels = guest_alloc(byte_len);
render_rgba8(pixels, info.width, info.height);
host_canvas_present(
MAIN_CANVAS,
byte_source(pixels, byte_len),
(FrameInfo){ info.width, info.height, info.width * 4, RGBA8, 1 }
);
绑定层必须先验证内存范围,再复制或借用字节;宿主不得在调用返回后保留未经声明的 guest 指针。
生命周期与背压
- 绘图面由壳层创建,随应用视图销毁。
resize后旧尺寸帧可以被拒绝为stale-frame。- 宿主只保留有限数量待呈现帧;新帧可以替换尚未显示的旧帧。
draw返回的dropped使被预算丢弃的指令可观察。- guest 暂停或不可见时,宿主可以停止发送
frame。
错误
| code | 含义 | retryable |
|---|---|---|
not-found |
canvasId 不存在 |
false |
invalid-operation |
指令不在白名单或字段非法 | false |
invalid-frame |
尺寸、步长、格式或字节数不匹配 | false |
stale-frame |
帧基于过期尺寸 | true |
limit-exceeded |
批次、像素或帧率超过预算 | true |
安全与预算
- 信任档位:green。
- 不提供像素读回、屏幕内容、DOM 节点或真实 context。
- 单次预算至少覆盖指令数、路径点数、文本总长度和像素字节数。
- 时间窗预算覆盖
draw/present次数和总提交字节。 - 图像和字体只能来自本应用包内资源或类型化句柄。
- 宿主必须把非有限浮点数、极端变换矩阵和病态路径挡在渲染后端之前。
不属于本 API
场景树、动画系统、材质、灯光、物理、布局、命中测试和游戏对象属于 L2 guest 库。GPU 命令、着色器和显存资源属于尚未准入 v0.1 的独立 L1 提议。
一致性测试
宿主至少验证:批量指令顺序、尺寸变化竞态、无效浮点数、超大路径、错误字节长度、帧替换、隐藏页面节流、字体句柄失效,以及 JS/原生 guest 对同一输入得到等价结果。